@needle-tools/engine 4.9.0-next.43185 → 4.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/components.needle.json +1 -1
  4. package/dist/{needle-engine.bundle-CZSmogSi.min.js → needle-engine.bundle-B1gr_nQ0.min.js} +119 -119
  5. package/dist/{needle-engine.bundle-Bs33m9iI.js → needle-engine.bundle-BikYBC35.js} +5444 -5405
  6. package/dist/{needle-engine.bundle-Bf4EhG38.umd.cjs → needle-engine.bundle-DrlDKOar.umd.cjs} +128 -128
  7. package/dist/needle-engine.js +2 -2
  8. package/dist/needle-engine.min.js +1 -1
  9. package/dist/needle-engine.umd.cjs +1 -1
  10. package/lib/engine-components/Component.d.ts +8 -5
  11. package/lib/engine-components/Component.js +8 -5
  12. package/lib/engine-components/Component.js.map +1 -1
  13. package/lib/engine-components/physics/Attractor.d.ts +12 -0
  14. package/lib/engine-components/physics/Attractor.js +14 -2
  15. package/lib/engine-components/physics/Attractor.js.map +1 -1
  16. package/lib/engine-components/web/Clickthrough.d.ts +1 -0
  17. package/lib/engine-components/web/Clickthrough.js +4 -2
  18. package/lib/engine-components/web/Clickthrough.js.map +1 -1
  19. package/lib/engine-components/web/CursorFollow.d.ts +9 -0
  20. package/lib/engine-components/web/CursorFollow.js +17 -0
  21. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  22. package/lib/engine-components/web/ScrollFollow.d.ts +29 -3
  23. package/lib/engine-components/web/ScrollFollow.js +91 -20
  24. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  25. package/package.json +2 -2
  26. package/src/engine-components/Component.ts +8 -5
  27. package/src/engine-components/physics/Attractor.ts +14 -4
  28. package/src/engine-components/web/Clickthrough.ts +6 -2
  29. package/src/engine-components/web/CursorFollow.ts +17 -2
  30. package/src/engine-components/web/ScrollFollow.ts +93 -23
@@ -1,11 +1,16 @@
1
- import { Object3D } from "three";
2
- import { serializable } from "../../engine/engine_serialization.js";
1
+ import { Box3, Object3D } from "three";
2
+
3
3
  import { Mathf } from "../../engine/engine_math.js";
4
- import { Behaviour } from "../Component.js";
5
- import { PlayableDirector } from "../timeline/PlayableDirector.js";
6
- import { EventList } from "../EventList.js";
4
+ import { serializable } from "../../engine/engine_serialization.js";
5
+ import { getBoundingBox } from "../../engine/engine_three_utils.js";
7
6
  import { Animation } from "../Animation.js";
7
+ import { Animator } from "../Animator.js";
8
8
  import { AudioSource } from "../AudioSource.js";
9
+ import { Behaviour } from "../Component.js";
10
+ import { EventList } from "../EventList.js";
11
+ import { Light } from "../Light.js";
12
+ import { SplineWalker } from "../splines/SplineWalker.js";
13
+ import { PlayableDirector } from "../timeline/PlayableDirector.js";
9
14
 
10
15
  type ScrollFollowEvent = {
11
16
  /** Event type */
@@ -19,10 +24,23 @@ type ScrollFollowEvent = {
19
24
  defaultPrevented: boolean,
20
25
  }
21
26
 
27
+ /**
28
+ * The ScrollFollow component allows you to link the scroll position of the page (or a specific element) to one or more target objects.
29
+ * This can be used to create scroll-based animations, audio playback, or other effects. For example you can link the scroll position to a timeline (PlayableDirector) to create scroll-based storytelling effects or to an Animator component to change the animation state based on scroll.
30
+ *
31
+ * @link Example at https://scrollytelling-2-z23hmxby7c6x-u30ld.needle.run/
32
+ */
22
33
  export class ScrollFollow extends Behaviour {
23
34
 
24
35
  /**
25
36
  * Target object(s) to follow the scroll position of the page. If null, the main camera is used.
37
+ *
38
+ * Supported target types:
39
+ * - PlayableDirector (timeline), the scroll position will be mapped to the timeline time
40
+ * - Animator, the scroll position will be set to a float parameter named "scroll"
41
+ * - Animation, the scroll position will be mapped to the animation time
42
+ * - AudioSource, the scroll position will be mapped to the audio time
43
+ * - Any object with a `scroll` property (number or function)
26
44
  */
27
45
  @serializable([Behaviour, Object3D])
28
46
  target: object[] | object | null = null;
@@ -34,8 +52,23 @@ export class ScrollFollow extends Behaviour {
34
52
  @serializable()
35
53
  damping: number = 0;
36
54
 
55
+ /**
56
+ * If true, the scroll value will be inverted (e.g. scrolling down will result in a value of 0)
57
+ * @default false
58
+ */
59
+ @serializable()
60
+ invert: boolean = false;
61
+
62
+ /**
63
+ * If set, the scroll position will be read from the specified element instead of the window.
64
+ * Use a CSS selector to specify the element, e.g. `#my-scrollable-div` or `.scroll-container`.
65
+ * @default null
66
+ */
67
+ @serializable()
68
+ htmlSelector: string | null = null;
69
+
37
70
  @serializable()
38
- mode: "window" = "window";
71
+ private mode: "window" = "window";
39
72
 
40
73
  /**
41
74
  * Event fired when the scroll position changes
@@ -56,23 +89,27 @@ export class ScrollFollow extends Behaviour {
56
89
 
57
90
  /** @internal */
58
91
  onEnable() {
59
- window.addEventListener("wheel", this.updateCurrentScroll, { passive: true });
92
+ window.addEventListener("wheel", this.updateCurrentScrollValue, { passive: true });
60
93
  this.applied_value = -1;
61
- this.updateCurrentScroll();
62
94
  }
63
95
 
64
96
  /** @internal */
65
97
  onDisable() {
66
- window.removeEventListener("wheel", this.updateCurrentScroll);
98
+ window.removeEventListener("wheel", this.updateCurrentScrollValue);
67
99
  }
68
100
 
69
101
  /** @internal */
70
102
  lateUpdate() {
71
103
 
104
+ this.updateCurrentScrollValue();
105
+
72
106
  // apply damping if any
73
107
  if (this.damping > 0) {
74
108
  this.current_value = Mathf.lerp(this.current_value, this.target_value, this.context.time.deltaTime / this.damping);
75
109
  }
110
+ else {
111
+ this.current_value = this.target_value;
112
+ }
76
113
 
77
114
  if (this.current_value !== this.applied_value) {
78
115
  this.applied_value = this.current_value;
@@ -94,55 +131,88 @@ export class ScrollFollow extends Behaviour {
94
131
  // if not prevented apply scroll
95
132
  if (!defaultPrevented) {
96
133
 
134
+ const value = this.invert ? 1 - this.current_value : this.current_value;
135
+
97
136
  // apply scroll to target(s)
98
137
  if (Array.isArray(this.target)) {
99
- this.target.forEach(t => t && this.applyScroll(t));
138
+ this.target.forEach(t => t && ScrollFollow.applyScroll(t, value));
100
139
  }
101
140
  else if (this.target) {
102
- this.applyScroll(this.target);
141
+ ScrollFollow.applyScroll(this.target, value);
103
142
  }
104
143
  }
105
144
  }
106
145
  }
107
146
 
147
+ private _lastSelectorValue: string | null = null;
148
+ private _lastSelectorElement: Element | null = null;
108
149
 
109
- private updateCurrentScroll = () => {
150
+ private updateCurrentScrollValue = () => {
110
151
 
111
152
  switch (this.mode) {
112
153
  case "window":
113
- this.target_value = window.scrollY / (document.body.scrollHeight - window.innerHeight);
154
+ if (this.htmlSelector?.length) {
155
+ if (this.htmlSelector !== this._lastSelectorValue) {
156
+ this._lastSelectorElement = document.querySelector(this.htmlSelector);
157
+ this._lastSelectorValue = this.htmlSelector;
158
+ }
159
+ if (this._lastSelectorElement) {
160
+ const rect = this._lastSelectorElement.getBoundingClientRect();
161
+ this.target_value = -rect.top / (rect.height - window.innerHeight);
162
+ if (isNaN(this.target_value) || !isFinite(this.target_value)) this.target_value = 0;
163
+ break;
164
+ }
165
+ }
166
+ else {
167
+ this.target_value = window.scrollY / (document.body.scrollHeight - window.innerHeight);
168
+ }
114
169
  if (isNaN(this.target_value) || !isFinite(this.target_value)) this.target_value = 0;
115
170
  break;
116
171
  }
117
172
 
118
- if (this.damping <= 0) {
119
- this.current_value = this.target_value;
120
- }
121
-
122
173
  }
123
174
 
124
175
 
125
- private applyScroll(target: object) {
176
+ private static applyScroll(target: object, value: number) {
126
177
 
127
178
  if (!target) return;
128
179
 
129
180
  if (target instanceof PlayableDirector) {
130
- target.time = this.current_value * target.duration;
181
+ target.time = value * target.duration;
131
182
  if (!target.isPlaying) target.evaluate();
132
183
  }
184
+ else if (target instanceof Animator) {
185
+ target.setFloat("scroll", value);
186
+ }
133
187
  else if (target instanceof Animation) {
134
- target.time = this.current_value * target.duration;
188
+ target.time = value * target.duration;
135
189
  }
136
190
  else if (target instanceof AudioSource) {
137
191
  if (!target.duration) return;
138
- target.time = this.current_value * target.duration;
192
+ target.time = value * target.duration;
193
+ }
194
+ else if (target instanceof SplineWalker) {
195
+ target.position01 = value;
196
+ }
197
+ else if (target instanceof Light) {
198
+ target.intensity = value;
199
+ }
200
+ else if (target instanceof Object3D) {
201
+ // When objects are assigned they're expected to move vertically based on scroll
202
+ if (target["needle:scrollbounds"] === undefined) {
203
+ target["needle:scrollbounds"] = getBoundingBox(target) || null;
204
+ }
205
+ const bounds = target["needle:scrollbounds"] as Box3;
206
+ if (bounds) {
207
+ target.position.y = -bounds.min.y - value * (bounds.max.y - bounds.min.y);
208
+ }
139
209
  }
140
210
  else if ("scroll" in target) {
141
211
  if (typeof target.scroll === "number") {
142
- target.scroll = this.current_value;
212
+ target.scroll = value;
143
213
  }
144
214
  else if (typeof target.scroll === "function") {
145
- target.scroll(this.current_value);
215
+ target.scroll(value);
146
216
  }
147
217
  }
148
218
  }