@needle-tools/engine 4.9.3-next.0facab6 → 4.10.0-beta

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 (34) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/{needle-engine.bundle-DAo7BPxQ.umd.cjs → needle-engine.bundle-42AmEGfk.umd.cjs} +127 -126
  3. package/dist/{needle-engine.bundle-DP2gYtOQ.min.js → needle-engine.bundle-C6zhyLF5.min.js} +128 -127
  4. package/dist/{needle-engine.bundle-TvT7wv7z.js → needle-engine.bundle-Dj6faVbC.js} +4967 -4864
  5. package/dist/needle-engine.js +414 -413
  6. package/dist/needle-engine.min.js +1 -1
  7. package/dist/needle-engine.umd.cjs +1 -1
  8. package/lib/engine/codegen/register_types.js +2 -0
  9. package/lib/engine/codegen/register_types.js.map +1 -1
  10. package/lib/engine-components/Renderer.d.ts +2 -2
  11. package/lib/engine-components/Renderer.js +6 -4
  12. package/lib/engine-components/Renderer.js.map +1 -1
  13. package/lib/engine-components/codegen/components.d.ts +1 -0
  14. package/lib/engine-components/codegen/components.js +1 -0
  15. package/lib/engine-components/codegen/components.js.map +1 -1
  16. package/lib/engine-components/timeline/PlayableDirector.d.ts +28 -6
  17. package/lib/engine-components/timeline/PlayableDirector.js +60 -26
  18. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  19. package/lib/engine-components/timeline/TimelineModels.d.ts +3 -0
  20. package/lib/engine-components/timeline/TimelineModels.js.map +1 -1
  21. package/lib/engine-components/timeline/TimelineTracks.d.ts +7 -0
  22. package/lib/engine-components/timeline/TimelineTracks.js +19 -0
  23. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  24. package/lib/engine-components/web/ScrollFollow.d.ts +14 -4
  25. package/lib/engine-components/web/ScrollFollow.js +139 -25
  26. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  27. package/package.json +2 -2
  28. package/src/engine/codegen/register_types.ts +2 -0
  29. package/src/engine-components/Renderer.ts +6 -4
  30. package/src/engine-components/codegen/components.ts +1 -0
  31. package/src/engine-components/timeline/PlayableDirector.ts +79 -34
  32. package/src/engine-components/timeline/TimelineModels.ts +3 -0
  33. package/src/engine-components/timeline/TimelineTracks.ts +22 -0
  34. package/src/engine-components/web/ScrollFollow.ts +177 -24
@@ -1,8 +1,11 @@
1
1
  import { Box3, Object3D } from "three";
2
+ import { element } from "three/src/nodes/TSL.js";
3
+ import { Context } from "../../engine/engine_context.js";
2
4
 
3
5
  import { Mathf } from "../../engine/engine_math.js";
4
6
  import { serializable } from "../../engine/engine_serialization.js";
5
7
  import { getBoundingBox } from "../../engine/engine_three_utils.js";
8
+ import { getParam } from "../../engine/engine_utils.js";
6
9
  import { Animation } from "../Animation.js";
7
10
  import { Animator } from "../Animator.js";
8
11
  import { AudioSource } from "../AudioSource.js";
@@ -11,6 +14,9 @@ import { EventList } from "../EventList.js";
11
14
  import { Light } from "../Light.js";
12
15
  import { SplineWalker } from "../splines/SplineWalker.js";
13
16
  import { PlayableDirector } from "../timeline/PlayableDirector.js";
17
+ import { ScrollMarkerModel } from "../timeline/TimelineModels.js";
18
+
19
+ const debug = getParam("debugscroll");
14
20
 
15
21
  type ScrollFollowEvent = {
16
22
  /** Event type */
@@ -81,7 +87,8 @@ export class ScrollFollow extends Behaviour {
81
87
  @serializable()
82
88
  invert: boolean = false;
83
89
 
84
- /**
90
+ /**
91
+ * **Experimental - might change in future updates**
85
92
  * If set, the scroll position will be read from the specified element instead of the window.
86
93
  * Use a CSS selector to specify the element, e.g. `#my-scrollable-div` or `.scroll-container`.
87
94
  * @default null
@@ -102,17 +109,23 @@ export class ScrollFollow extends Behaviour {
102
109
  * Current scroll value in "pages" (0 = top of page, 1 = bottom of page)
103
110
  */
104
111
  get currentValue() {
105
- return this.current_value;
112
+ return this._current_value;
106
113
  }
107
114
 
108
- private current_value: number = 0;
109
- private target_value: number = 0;
110
- private applied_value: number = -1;
115
+ private _current_value: number = 0;
116
+ private _target_value: number = 0;
117
+ private _appliedValue: number = -1;
118
+
119
+
120
+ private _scrollStart: number = 0;
121
+ private _scrollEnd: number = 0;
122
+ private _scrollValue: number = 0;
123
+ private _scrollContainerHeight: number = 0;
111
124
 
112
125
  /** @internal */
113
126
  onEnable() {
114
127
  window.addEventListener("wheel", this.updateCurrentScrollValue, { passive: true });
115
- this.applied_value = -1;
128
+ this._appliedValue = -1;
116
129
  }
117
130
 
118
131
  /** @internal */
@@ -125,23 +138,27 @@ export class ScrollFollow extends Behaviour {
125
138
 
126
139
  this.updateCurrentScrollValue();
127
140
 
128
- // apply damping if any
129
- if (this.damping > 0) {
130
- this.current_value = Mathf.lerp(this.current_value, this.target_value, this.context.time.deltaTime / this.damping);
131
- }
132
- else {
133
- this.current_value = this.target_value;
141
+ if (this._target_value >= 0) {
142
+ if (this.damping > 0) { // apply damping
143
+ this._current_value = Mathf.lerp(this._current_value, this._target_value, this.context.time.deltaTime / this.damping);
144
+ if (Math.abs(this._current_value - this._target_value) < 0.001) {
145
+ this._current_value = this._target_value;
146
+ }
147
+ }
148
+ else {
149
+ this._current_value = this._target_value;
150
+ }
134
151
  }
135
152
 
136
- if (this.current_value !== this.applied_value) {
137
- this.applied_value = this.current_value;
153
+ if (this._current_value !== this._appliedValue) {
154
+ this._appliedValue = this._current_value;
138
155
 
139
156
  let defaultPrevented = false;
140
157
  if (this.changed.listenerCount > 0) {
141
158
  // fire change event
142
159
  const event: ScrollFollowEvent = {
143
160
  type: "change",
144
- value: this.current_value,
161
+ value: this._current_value,
145
162
  component: this,
146
163
  preventDefault: () => { event.defaultPrevented = true; },
147
164
  defaultPrevented: false,
@@ -153,14 +170,21 @@ export class ScrollFollow extends Behaviour {
153
170
  // if not prevented apply scroll
154
171
  if (!defaultPrevented) {
155
172
 
156
- const value = this.invert ? 1 - this.current_value : this.current_value;
173
+ const value = this.invert ? 1 - this._current_value : this._current_value;
174
+
175
+ const height = this._rangeEndValue - this._rangeStartValue;
176
+ const pixelValue = this._rangeStartValue + value * height;
157
177
 
158
178
  // apply scroll to target(s)
159
179
  if (Array.isArray(this.target)) {
160
- this.target.forEach(t => t && ScrollFollow.applyScroll(t, value));
180
+ this.target.forEach(t => t && this.applyScroll(t, value));
161
181
  }
162
182
  else if (this.target) {
163
- ScrollFollow.applyScroll(this.target, value);
183
+ this.applyScroll(this.target, value);
184
+ }
185
+
186
+ if (debug && this.context.time.frame % 30 === 0) {
187
+ console.debug(`[ScrollFollow] ${this._current_value.toFixed(5)} — ${(this._target_value * 100).toFixed(0)}%`);
164
188
  }
165
189
  }
166
190
  }
@@ -168,11 +192,16 @@ export class ScrollFollow extends Behaviour {
168
192
 
169
193
  private _lastSelectorValue: string | null = null;
170
194
  private _lastSelectorElement: Element | null = null;
195
+ /** Top y */
196
+ private _rangeStartValue: number = 0;
197
+ /** Bottom y */
198
+ private _rangeEndValue: number = 0;
171
199
 
172
200
  private updateCurrentScrollValue = () => {
173
201
 
174
202
  switch (this.mode) {
175
203
  case "window":
204
+
176
205
  if (this.htmlSelector?.length) {
177
206
  if (this.htmlSelector !== this._lastSelectorValue) {
178
207
  this._lastSelectorElement = document.querySelector(this.htmlSelector);
@@ -180,27 +209,39 @@ export class ScrollFollow extends Behaviour {
180
209
  }
181
210
  if (this._lastSelectorElement) {
182
211
  const rect = this._lastSelectorElement.getBoundingClientRect();
183
- this.target_value = -rect.top / (rect.height - window.innerHeight);
184
- if (isNaN(this.target_value) || !isFinite(this.target_value)) this.target_value = 0;
212
+
213
+ this._scrollStart = rect.top + window.scrollY;
214
+ this._scrollEnd = rect.height - window.innerHeight;
215
+ this._scrollValue = -rect.top;
216
+ this._target_value = -rect.top / (rect.height - window.innerHeight);
217
+ this._rangeStartValue = rect.top + window.scrollY;
218
+ this._rangeEndValue = this._rangeStartValue + rect.height - window.innerHeight;
219
+ this._scrollContainerHeight = rect.height;
185
220
  break;
186
221
  }
187
222
  }
188
223
  else {
189
- this.target_value = window.scrollY / (document.body.scrollHeight - window.innerHeight);
224
+ this._scrollStart = 0;
225
+ this._scrollEnd = window.document.body.scrollHeight - window.innerHeight;
226
+ this._scrollValue = window.scrollY;
227
+ this._target_value = this._scrollValue / (this._scrollEnd || 1);
228
+ this._rangeStartValue = 0;
229
+ this._rangeEndValue = document.body.scrollHeight;
230
+ this._scrollContainerHeight = window.innerHeight;
190
231
  }
191
- if (isNaN(this.target_value) || !isFinite(this.target_value)) this.target_value = 0;
192
232
  break;
193
233
  }
194
234
 
235
+ if (isNaN(this._target_value) || !isFinite(this._target_value)) this._target_value = -1;
195
236
  }
196
237
 
197
238
 
198
- private static applyScroll(target: object, value: number) {
239
+ private applyScroll(target: object, value: number) {
199
240
 
200
241
  if (!target) return;
201
242
 
202
243
  if (target instanceof PlayableDirector) {
203
- target.time = value * target.duration;
244
+ this.handleTimelineTarget(target, value);
204
245
  if (!target.isPlaying) target.evaluate();
205
246
  }
206
247
  else if (target instanceof Animator) {
@@ -240,4 +281,116 @@ export class ScrollFollow extends Behaviour {
240
281
  }
241
282
  }
242
283
 
284
+
285
+
286
+ private handleTimelineTarget(director: PlayableDirector, value: number) {
287
+
288
+ const duration = director.duration;
289
+
290
+
291
+ let scrollRegionStart = Infinity;
292
+ let scrollRegionEnd = 0;
293
+ markersArray.length = 0;
294
+
295
+ for (const marker of director.foreachMarker<ScrollMarkerModel & { element?: HTMLElement | null, needsUpdate?: boolean }>("ScrollMarker")) {
296
+
297
+ // Get marker elements from DOM
298
+ if (marker.selector?.length && (marker.element === undefined || marker.needsUpdate === true || /** element is not in DOM anymore? */ (!marker.element?.parentNode))) {
299
+ marker.needsUpdate = false;
300
+ try {
301
+ marker.element = document.querySelector<HTMLElement>(marker.selector) || null;
302
+ if (debug) console.debug("ScrollMarker found on page", marker.element, marker.selector);
303
+ }
304
+ catch (error) {
305
+ marker.element = null;
306
+ console.error("ScrollMarker selector is not valid: " + marker.selector + "\n", error);
307
+ }
308
+ }
309
+
310
+ // skip markers without element (e.g. if the selector didn't return any element)
311
+ if (!marker.element) continue;
312
+
313
+ markersArray.push(marker);
314
+
315
+ const top = marker.element.offsetTop;
316
+ const height = marker.element.offsetHeight;
317
+ const bottom = top + height;
318
+ if (top < scrollRegionStart) {
319
+ scrollRegionStart = top;
320
+ }
321
+ if (bottom > scrollRegionEnd) {
322
+ scrollRegionEnd = bottom;
323
+ }
324
+ }
325
+
326
+
327
+
328
+ const currentTop = this._scrollValue;
329
+ const currentBottom = currentTop + this._scrollContainerHeight;
330
+
331
+ weightsArray.length = 0;
332
+ let sum = 0;
333
+
334
+ let markerCount = 0;
335
+ for (const marker of markersArray) {
336
+
337
+ if (!marker.element) continue;
338
+
339
+ const top = marker.element.offsetTop;
340
+ const height = marker.element.offsetHeight;
341
+ const bottom = top + height;
342
+ let overlap = 0;
343
+
344
+ // TODO: if we have two marker sections where no HTML overlaps (vor example because some large section is between them) we probably want to still virtually interpolate between them slowly in that region
345
+
346
+ if (bottom < currentTop) {
347
+ // marker is above scroll region
348
+ overlap = 0;
349
+ }
350
+ else if (top > currentBottom) {
351
+ // marker is below scroll region
352
+ overlap = 0;
353
+ }
354
+ else {
355
+ // calculate overlap in pixels
356
+ const overlapTop = Math.max(top, currentTop);
357
+ const overlapBottom = Math.min(bottom, currentBottom);
358
+ overlap = Math.max(0, overlapBottom - overlapTop);
359
+ // console.log(marker.element.className, overlap)
360
+ }
361
+
362
+ markerCount += 1;
363
+
364
+ if (overlap > 0) {
365
+ weightsArray.push({ time: marker.time, weight: overlap });
366
+ sum += overlap;
367
+ }
368
+ }
369
+
370
+ if (weightsArray.length <= 0 && markerCount <= 0) {
371
+ director.time = value * duration;
372
+ }
373
+ else if (weightsArray.length > 0) {
374
+ // normalize and calculate weighted time
375
+ let time = weightsArray[0].time;
376
+ for (const o of weightsArray) {
377
+ const weight = o.weight / Math.max(0.00001, sum);
378
+ // lerp time based on weight
379
+ const diff = Math.abs(o.time - time);
380
+ time += diff * weight;
381
+ }
382
+ director.time = time;
383
+ }
384
+ }
385
+
386
+ }
387
+
388
+ const weightsArray: OverlapInfo[] = [];
389
+ const markersArray: (ScrollMarkerModel & { element?: HTMLElement | null })[] = [];
390
+
391
+ type OverlapInfo = {
392
+ /** Marker time */
393
+ time: number,
394
+ /** Overlap in pixels */
395
+ weight: number,
243
396
  }