@needle-tools/engine 4.10.0-next.f0ec242 → 4.10.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 (124) hide show
  1. package/CHANGELOG.md +7 -3
  2. package/README.md +2 -1
  3. package/components.needle.json +1 -1
  4. package/dist/needle-engine.bundle-BSq-d_16.min.js +1652 -0
  5. package/dist/{needle-engine.bundle-dgNq9Vsa.umd.cjs → needle-engine.bundle-C2kVfQq6.umd.cjs} +153 -140
  6. package/dist/{needle-engine.bundle-BC-0Ex9m.js → needle-engine.bundle-CIuhf7-t.js} +7388 -7113
  7. package/dist/needle-engine.d.ts +15 -15
  8. package/dist/needle-engine.js +259 -257
  9. package/dist/needle-engine.min.js +1 -1
  10. package/dist/needle-engine.umd.cjs +1 -1
  11. package/dist/vendor-CPuBPspY.umd.cjs +1121 -0
  12. package/dist/vendor-DPCU8cUF.min.js +1121 -0
  13. package/dist/vendor-MBoqSyFm.js +16240 -0
  14. package/lib/engine/codegen/register_types.js +2 -0
  15. package/lib/engine/codegen/register_types.js.map +1 -1
  16. package/lib/engine/engine_camera.d.ts +7 -1
  17. package/lib/engine/engine_camera.js +46 -6
  18. package/lib/engine/engine_camera.js.map +1 -1
  19. package/lib/engine/engine_context.d.ts +6 -0
  20. package/lib/engine/engine_context.js +48 -9
  21. package/lib/engine/engine_context.js.map +1 -1
  22. package/lib/engine/engine_gizmos.d.ts +11 -10
  23. package/lib/engine/engine_gizmos.js +24 -10
  24. package/lib/engine/engine_gizmos.js.map +1 -1
  25. package/lib/engine/engine_license.js +1 -1
  26. package/lib/engine/engine_license.js.map +1 -1
  27. package/lib/engine/engine_lightdata.d.ts +3 -3
  28. package/lib/engine/engine_lightdata.js +10 -10
  29. package/lib/engine/engine_lightdata.js.map +1 -1
  30. package/lib/engine/engine_physics_rapier.js +4 -0
  31. package/lib/engine/engine_physics_rapier.js.map +1 -1
  32. package/lib/engine/engine_scenelighting.d.ts +1 -1
  33. package/lib/engine/engine_scenelighting.js +4 -5
  34. package/lib/engine/engine_scenelighting.js.map +1 -1
  35. package/lib/engine/engine_utils.d.ts +3 -1
  36. package/lib/engine/engine_utils.js +11 -0
  37. package/lib/engine/engine_utils.js.map +1 -1
  38. package/lib/engine/extensions/NEEDLE_lightmaps.js +1 -1
  39. package/lib/engine/extensions/NEEDLE_lightmaps.js.map +1 -1
  40. package/lib/engine/extensions/extension_utils.js +1 -1
  41. package/lib/engine/extensions/extension_utils.js.map +1 -1
  42. package/lib/engine/webcomponents/logo-element.d.ts +1 -1
  43. package/lib/engine/webcomponents/logo-element.js +29 -5
  44. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  45. package/lib/engine/webcomponents/needle menu/needle-menu.js +4 -3
  46. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  47. package/lib/engine/webcomponents/needle-engine.js +22 -0
  48. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  49. package/lib/engine/webcomponents/needle-engine.loading.d.ts +0 -1
  50. package/lib/engine/webcomponents/needle-engine.loading.js +3 -36
  51. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  52. package/lib/engine/xr/NeedleXRController.d.ts +3 -3
  53. package/lib/engine/xr/NeedleXRController.js +28 -0
  54. package/lib/engine/xr/NeedleXRController.js.map +1 -1
  55. package/lib/engine-components/CameraUtils.js +2 -1
  56. package/lib/engine-components/CameraUtils.js.map +1 -1
  57. package/lib/engine-components/Renderer.js +6 -1
  58. package/lib/engine-components/Renderer.js.map +1 -1
  59. package/lib/engine-components/Skybox.js +22 -4
  60. package/lib/engine-components/Skybox.js.map +1 -1
  61. package/lib/engine-components/codegen/components.d.ts +1 -0
  62. package/lib/engine-components/codegen/components.js +1 -0
  63. package/lib/engine-components/codegen/components.js.map +1 -1
  64. package/lib/engine-components/debug/LogStats.d.ts +1 -0
  65. package/lib/engine-components/debug/LogStats.js +1 -0
  66. package/lib/engine-components/debug/LogStats.js.map +1 -1
  67. package/lib/engine-components/timeline/PlayableDirector.d.ts +7 -0
  68. package/lib/engine-components/timeline/PlayableDirector.js +8 -1
  69. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  70. package/lib/engine-components/timeline/TimelineModels.d.ts +11 -1
  71. package/lib/engine-components/timeline/TimelineTracks.d.ts +2 -1
  72. package/lib/engine-components/timeline/TimelineTracks.js +30 -25
  73. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  74. package/lib/engine-components/utils/LookAt.js +5 -1
  75. package/lib/engine-components/utils/LookAt.js.map +1 -1
  76. package/lib/engine-components/web/Clickthrough.js +10 -2
  77. package/lib/engine-components/web/Clickthrough.js.map +1 -1
  78. package/lib/engine-components/web/ScrollFollow.d.ts +24 -0
  79. package/lib/engine-components/web/ScrollFollow.js +169 -42
  80. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  81. package/lib/engine-components/web/ViewBox.d.ts +43 -0
  82. package/lib/engine-components/web/ViewBox.js +258 -0
  83. package/lib/engine-components/web/ViewBox.js.map +1 -0
  84. package/lib/engine-components/web/index.d.ts +1 -0
  85. package/lib/engine-components/web/index.js +1 -0
  86. package/lib/engine-components/web/index.js.map +1 -1
  87. package/lib/engine-components-experimental/Presentation.d.ts +1 -0
  88. package/lib/engine-components-experimental/Presentation.js +1 -0
  89. package/lib/engine-components-experimental/Presentation.js.map +1 -1
  90. package/package.json +3 -2
  91. package/src/engine/codegen/register_types.ts +2 -0
  92. package/src/engine/engine_camera.ts +61 -9
  93. package/src/engine/engine_context.ts +50 -10
  94. package/src/engine/engine_gizmos.ts +37 -23
  95. package/src/engine/engine_license.ts +1 -1
  96. package/src/engine/engine_lightdata.ts +11 -11
  97. package/src/engine/engine_physics_rapier.ts +3 -0
  98. package/src/engine/engine_scenelighting.ts +5 -6
  99. package/src/engine/engine_utils.ts +12 -0
  100. package/src/engine/extensions/NEEDLE_lightmaps.ts +1 -1
  101. package/src/engine/extensions/extension_utils.ts +1 -1
  102. package/src/engine/webcomponents/logo-element.ts +29 -4
  103. package/src/engine/webcomponents/needle menu/needle-menu.ts +4 -3
  104. package/src/engine/webcomponents/needle-engine.loading.ts +32 -32
  105. package/src/engine/webcomponents/needle-engine.ts +33 -6
  106. package/src/engine/xr/NeedleXRController.ts +36 -4
  107. package/src/engine-components/CameraUtils.ts +1 -1
  108. package/src/engine-components/Renderer.ts +6 -1
  109. package/src/engine-components/Skybox.ts +26 -7
  110. package/src/engine-components/codegen/components.ts +1 -0
  111. package/src/engine-components/debug/LogStats.ts +1 -0
  112. package/src/engine-components/timeline/PlayableDirector.ts +10 -1
  113. package/src/engine-components/timeline/TimelineModels.ts +11 -1
  114. package/src/engine-components/timeline/TimelineTracks.ts +30 -25
  115. package/src/engine-components/utils/LookAt.ts +5 -1
  116. package/src/engine-components/web/Clickthrough.ts +11 -2
  117. package/src/engine-components/web/ScrollFollow.ts +205 -51
  118. package/src/engine-components/web/ViewBox.ts +278 -0
  119. package/src/engine-components/web/index.ts +2 -1
  120. package/src/engine-components-experimental/Presentation.ts +1 -0
  121. package/dist/needle-engine.bundle-BSh7dSEx.min.js +0 -1639
  122. package/dist/vendor-D0Yvltn9.umd.cjs +0 -1121
  123. package/dist/vendor-DU8tJyl_.js +0 -14366
  124. package/dist/vendor-JyrX4DVM.min.js +0 -1121
@@ -175,8 +175,10 @@ export class AnimationTrackHandler extends TrackHandler {
175
175
  // which means we want to notify the object that it's not animated anymore
176
176
  // and the animator can then take over
177
177
  onStateChanged() {
178
- if (this._animator)
179
- setObjectAnimated(this._animator.gameObject, this, this.director.isPlaying);
178
+ if (this._animator) {
179
+ // We can not check the *isPlaying* state here because the timeline might be paused and evaluated by e.g. ScrollFollow
180
+ setObjectAnimated(this._animator.gameObject, this, this.director.enabled && this.director.weight > 0);
181
+ }
180
182
  }
181
183
 
182
184
  createHooks(clipModel: Models.AnimationClipModel, clip) {
@@ -185,21 +187,22 @@ export class AnimationTrackHandler extends TrackHandler {
185
187
  return;
186
188
  }
187
189
  // we only want to hook into the binding of the root object
188
- // TODO: test with a clip with multiple roots
189
- const parts = clip.tracks[0].name.split(".");
190
- const rootName = parts[parts.length - 2];
191
- const positionTrackName = rootName + ".position";
192
- const rotationTrackName = rootName + ".quaternion";
193
190
  let foundPositionTrack: boolean = false;
194
191
  let foundRotationTrack: boolean = false;
195
- for (const t of clip.tracks) {
196
- if (t.name.endsWith(positionTrackName)) {
197
- foundPositionTrack = true;
198
- this.createPositionInterpolant(clip, clipModel, t);
199
- }
200
- else if (t.name.endsWith(rotationTrackName)) {
201
- foundRotationTrack = true;
202
- this.createRotationInterpolant(clip, clipModel, t);
192
+ const parts = clip.tracks.find(t => t.name.includes(".position") || t.name.includes(".quaternion"))?.name.split(".");
193
+ if (parts) {
194
+ const rootName = parts[parts.length - 2];
195
+ const positionTrackName = rootName + ".position";
196
+ const rotationTrackName = rootName + ".quaternion";
197
+ for (const t of clip.tracks) {
198
+ if (!foundPositionTrack && t.name.endsWith(positionTrackName)) {
199
+ foundPositionTrack = true;
200
+ this.createPositionInterpolant(clip, clipModel, t);
201
+ }
202
+ else if (!foundRotationTrack && t.name.endsWith(rotationTrackName)) {
203
+ foundRotationTrack = true;
204
+ this.createRotationInterpolant(clip, clipModel, t);
205
+ }
203
206
  }
204
207
  }
205
208
 
@@ -224,16 +227,14 @@ export class AnimationTrackHandler extends TrackHandler {
224
227
  const trackName = baseName + ".position";
225
228
  if (debug) console.warn("Create position track", objName, targetObj);
226
229
  // apply initial local position so it doesnt get flipped or otherwise changed
227
- const pos = targetObj.position;
228
- const track = new VectorKeyframeTrack(trackName, [0, clip.duration], [pos.x, pos.y, pos.z, pos.x, pos.y, pos.z]);
230
+ const track = new VectorKeyframeTrack(trackName, [0, clip.duration], [0, 0, 0, 0, 0, 0]);
229
231
  clip.tracks.push(track);
230
232
  this.createPositionInterpolant(clip, clipModel, track);
231
233
  }
232
234
  else if (!foundRotationTrack) {
233
235
  const trackName = clip.tracks[0].name.substring(0, indexOfProperty) + ".quaternion";
234
236
  if (debug) console.warn("Create quaternion track", objName, targetObj);
235
- const rot = targetObj.quaternion;
236
- const track = new QuaternionKeyframeTrack(trackName, [0, clip.duration], [rot.x, rot.y, rot.z, rot.w, rot.x, rot.y, rot.z, rot.w]);
237
+ const track = new QuaternionKeyframeTrack(trackName, [0, clip.duration], [0, 0, 0, 1, 0, 0, 0, 1]);
237
238
  clip.tracks.push(track);
238
239
  this.createRotationInterpolant(clip, clipModel, track);
239
240
  }
@@ -832,23 +833,27 @@ export class AudioTrackHandler extends TrackHandler {
832
833
 
833
834
  export class MarkerTrackHandler extends TrackHandler {
834
835
  models: Array<Models.MarkerModel & Record<string, any>> = [];
835
- isDirty = true;
836
+ needsSorting = true;
836
837
 
837
838
  *foreachMarker<T>(type: string | null = null) {
839
+ if(this.needsSorting) this.sort();
838
840
  for (const model of this.models) {
839
841
  if (model && model.type === type) yield model as T;
840
842
  }
841
843
  }
842
844
 
843
845
  onEnable() {
844
- this.isDirty = true;
846
+ this.needsSorting = true;
845
847
  }
846
848
 
847
849
  evaluate(_time: number) {
848
- if (this.isDirty) {
849
- this.isDirty = false;
850
- this.models.sort((a, b) => a.time - b.time);
851
- }
850
+ if (this.needsSorting) this.sort();
851
+ }
852
+
853
+ private sort() {
854
+ this.needsSorting = false;
855
+ this.models.sort((a, b) => a.time - b.time);
856
+
852
857
  }
853
858
  }
854
859
 
@@ -1,5 +1,6 @@
1
1
  import { Matrix4, Object3D, Quaternion, Vector3 } from "three";
2
2
 
3
+ import { isDevEnvironment } from "../../engine/debug/index.js";
3
4
  import { serializable } from "../../engine/engine_serialization.js";
4
5
  import { lookAtObject } from "../../engine/engine_three_utils.js";
5
6
  import { type UsdzBehaviour } from "../../engine-components/export/usdz/extensions/behavior/Behaviour.js";
@@ -42,7 +43,10 @@ export class LookAt extends Behaviour implements UsdzBehaviour {
42
43
  /** @internal */
43
44
  onBeforeRender(): void {
44
45
  let target: Object3D | null | undefined = this.target;
45
- if (!target) target = this.context.mainCamera;
46
+ if (!target) {
47
+ target = this.context.mainCamera;
48
+ if (isDevEnvironment()) console.warn(`[LookAt] No target set on ${this.name}, using main camera as target.`);
49
+ }
46
50
  if (!target) return;
47
51
 
48
52
  let copyTargetRotation = this.copyTargetRotation;
@@ -1,12 +1,21 @@
1
1
  import { NEPointerEvent } from "../../engine/engine_input.js";
2
2
  import { onStart } from "../../engine/engine_lifecycle_api.js";
3
+ import { addAttributeChangeCallback } from "../../engine/engine_utils.js";
3
4
  import { Behaviour } from "../Component.js";
4
5
 
5
6
  // Automatically add ClickThrough component if "clickthrough" attribute is present on the needle-engine element
6
7
  onStart(ctx => {
7
8
  const attribute = ctx.domElement.getAttribute("clickthrough");
8
- if (attribute !== null && attribute !== "0" && attribute !== "false") {
9
- ctx.scene.addComponent(ClickThrough);
9
+ if (clickthroughEnabled(attribute)) {
10
+ const comp = ctx.scene.addComponent(ClickThrough);
11
+ addAttributeChangeCallback(ctx.domElement, "clickthrough", () => {
12
+ const attribute = ctx.domElement.getAttribute("clickthrough");
13
+ comp.enabled = clickthroughEnabled(attribute);
14
+ });
15
+ }
16
+
17
+ function clickthroughEnabled(val: string | null) {
18
+ return val !== null && val !== "0" && val !== "false";
10
19
  }
11
20
  });
12
21
 
@@ -1,10 +1,12 @@
1
+ // For firefox ViewTimeline support
2
+ import "scroll-timeline-polyfill/dist/scroll-timeline.js";
3
+
1
4
  import { Box3, Object3D } from "three";
2
- import { element } from "three/src/nodes/TSL.js";
3
5
 
4
- import { Context } from "../../engine/engine_context.js";
6
+ import { isDevEnvironment } from "../../engine/debug/debug.js";
5
7
  import { Mathf } from "../../engine/engine_math.js";
6
8
  import { serializable } from "../../engine/engine_serialization.js";
7
- import { getBoundingBox } from "../../engine/engine_three_utils.js";
9
+ import { getBoundingBox, setVisibleInCustomShadowRendering } from "../../engine/engine_three_utils.js";
8
10
  import { getParam } from "../../engine/engine_utils.js";
9
11
  import { Animation } from "../Animation.js";
10
12
  import { Animator } from "../Animator.js";
@@ -38,6 +40,7 @@ type ScrollFollowEvent = {
38
40
  *
39
41
  * @link Example at https://scrollytelling-2-z23hmxby7c6x-u30ld.needle.run/
40
42
  * @link Template at https://github.com/needle-engine/scrollytelling-template
43
+ * @link [Scrollytelling Bike Demo](https://scrollytelling-bike-z23hmxb2gnu5a.needle.run/)
41
44
  *
42
45
  * ## How to use with an Animator
43
46
  * 1. Create an Animator component and set up a float parameter named "scroll".
@@ -150,7 +153,8 @@ export class ScrollFollow extends Behaviour {
150
153
  }
151
154
  }
152
155
 
153
- if (this._current_value !== this._appliedValue) {
156
+ // if (this._current_value !== this._appliedValue)
157
+ {
154
158
  this._appliedValue = this._current_value;
155
159
 
156
160
  let defaultPrevented = false;
@@ -172,8 +176,8 @@ export class ScrollFollow extends Behaviour {
172
176
 
173
177
  const value = this.invert ? 1 - this._current_value : this._current_value;
174
178
 
175
- const height = this._rangeEndValue - this._rangeStartValue;
176
- const pixelValue = this._rangeStartValue + value * height;
179
+ // const height = this._rangeEndValue - this._rangeStartValue;
180
+ // const pixelValue = this._rangeStartValue + value * height;
177
181
 
178
182
  // apply scroll to target(s)
179
183
  if (Array.isArray(this.target)) {
@@ -184,7 +188,7 @@ export class ScrollFollow extends Behaviour {
184
188
  }
185
189
 
186
190
  if (debug && this.context.time.frame % 30 === 0) {
187
- console.debug(`[ScrollFollow] ${this._current_value.toFixed(5)} — ${(this._target_value * 100).toFixed(0)}%`);
191
+ console.debug(`[ScrollFollow] ${this._current_value.toFixed(5)} — ${(this._target_value * 100).toFixed(0)}%, targets [${Array.isArray(this.target) ? this.target.length : 1}]`);
188
192
  }
189
193
  }
190
194
  }
@@ -242,7 +246,8 @@ export class ScrollFollow extends Behaviour {
242
246
 
243
247
  if (target instanceof PlayableDirector) {
244
248
  this.handleTimelineTarget(target, value);
245
- if (!target.isPlaying) target.evaluate();
249
+ if (target.isPlaying) target.pause();
250
+ target.evaluate();
246
251
  }
247
252
  else if (target instanceof Animator) {
248
253
  target.setFloat("scroll", value);
@@ -292,18 +297,37 @@ export class ScrollFollow extends Behaviour {
292
297
  let scrollRegionEnd = 0;
293
298
  markersArray.length = 0;
294
299
 
295
- for (const marker of director.foreachMarker<ScrollMarkerModel & { element?: HTMLElement | null, needsUpdate?: boolean }>("ScrollMarker")) {
300
+ // querySelectorResults.length = 0;
301
+ let markerIndex = 0;
302
+
303
+ // https://scroll-driven-animations.style/tools/view-timeline/ranges
304
+ for (const marker of director.foreachMarker<ScrollMarkerModel & { element?: HTMLElement | null, needsUpdate?: boolean, timeline?: ViewTimeline }>("ScrollMarker")) {
305
+
306
+ const index = markerIndex++;
296
307
 
297
308
  // 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))) {
309
+ if ((marker.element === undefined || marker.needsUpdate === true || /** element is not in DOM anymore? */ (marker.element && !marker.element?.parentNode))) {
299
310
  marker.needsUpdate = false;
300
311
  try {
301
- marker.element = document.querySelector<HTMLElement>(marker.selector) || null;
302
- if (debug) console.debug("ScrollMarker found on page", marker.element, marker.selector);
312
+ // TODO: with this it's currently not possible to remap markers from HTML. For example if I have two sections and I want to now use the marker["center"] multiple times to stay at that marker for a longer time
313
+ marker.element = tryGetElementsForSelector(index, marker.name) as HTMLElement | null;
314
+ if (debug) console.debug(`ScrollMarker #${index} "${marker.name}" (${marker.time.toFixed(2)}) found`, marker.element);
315
+ if (!marker.element) {
316
+ marker.timeline = undefined;
317
+ if (debug || isDevEnvironment()) console.warn(`No HTML element found for ScrollMarker: ${marker.name} (index ${index})`);
318
+ continue;
319
+ }
320
+ else {
321
+ /** @ts-ignore */
322
+ marker.timeline = new ViewTimeline({
323
+ subject: marker.element,
324
+ axis: 'block', // https://drafts.csswg.org/scroll-animations/#scroll-notation
325
+ });
326
+ }
303
327
  }
304
328
  catch (error) {
305
329
  marker.element = null;
306
- console.error("ScrollMarker selector is not valid: " + marker.selector + "\n", error);
330
+ console.error("ScrollMarker selector is not valid: " + marker.name + "\n", error);
307
331
  }
308
332
  }
309
333
 
@@ -330,65 +354,195 @@ export class ScrollFollow extends Behaviour {
330
354
 
331
355
  weightsArray.length = 0;
332
356
  let sum = 0;
357
+ const oneFrameTime = 1 / 60;
333
358
 
359
+ // We keep a separate count here in case there are some markers that could not be resolved so point to *invalid* elements - the timeline should fallback to 0-1 scroll behaviour then
334
360
  let markerCount = 0;
335
- for (const marker of markersArray) {
336
-
361
+ for (let i = 0; i < markersArray.length; i++) {
362
+ const marker = markersArray[i];
337
363
  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
- if (overlap > 0) {
363
- weightsArray.push({ time: marker.time, weight: overlap });
364
- sum += overlap;
364
+ const nextMarker = markersArray[i + 1];
365
+
366
+ const nextTime = nextMarker
367
+ ? (nextMarker.time - oneFrameTime)
368
+ : duration;
369
+
370
+ markerCount += 1;
371
+
372
+ const timeline = marker.timeline;
373
+ if (timeline) {
374
+ const time01 = calculateTimelinePositionNormalized(timeline);
375
+ // remap 0-1 to 0 - 1 - 0 (full weight at center)
376
+ const weight = 1 - Math.abs(time01 - 0.5) * 2;
377
+ const name = marker.name || `marker${i}`;
378
+ if (time01 > 0 && time01 <= 1) {
379
+ const lerpTime = marker.time + (nextTime - marker.time) * time01;
380
+ weightsArray.push({ name, time: lerpTime, weight: weight });
381
+ sum += weight;
382
+ }
383
+ // Before the first marker is reached
384
+ else if (i === 0 && time01 <= 0) {
385
+ weightsArray.push({ name, time: 0, weight: 1 });
386
+ sum += 1;
387
+ }
388
+ // After the last marker is reached
389
+ else if (i === markersArray.length - 1 && time01 >= 1) {
390
+ weightsArray.push({ name, time: duration, weight: 1 });
391
+ sum += 1;
392
+ }
365
393
  }
394
+ continue;
395
+ // if(this.context.time.frame % 10 === 0) console.log(marker.element?.className, timeline, calculateTimelinePositionNormalized(timeline!));
396
+
397
+ // const top = marker.element.offsetTop - this._scrollContainerHeight;
398
+ // const height = marker.element.offsetHeight + this._scrollContainerHeight;
399
+ // const bottom = top + height;
400
+ // let overlap = 0;
401
+
402
+ // // 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
403
+
404
+ // if (bottom < currentTop) {
405
+ // // marker is above scroll region
406
+ // overlap = 0;
407
+ // }
408
+ // else if (top > currentBottom) {
409
+ // // marker is below scroll region
410
+ // overlap = 0;
411
+ // }
412
+ // else {
413
+ // // calculate overlap in pixels
414
+ // const overlapTop = Math.max(top, currentTop);
415
+ // const overlapBottom = Math.min(bottom, currentBottom);
416
+ // const height = Math.max(1, currentBottom - currentTop);
417
+ // overlap = Math.max(0, overlapBottom - overlapTop);
418
+ // }
419
+
420
+ // // if(this.context.time.frame % 20 === 0) console.log(overlap)
421
+
422
+ // if (overlap > 0) {
423
+ // weightsArray.push({ time: marker.time, weight: overlap });
424
+ // sum += overlap;
425
+ // }
366
426
  }
367
427
 
368
- if (weightsArray.length <= 0 && markersArray.length <= 0) {
428
+ if (weightsArray.length <= 0 && markerCount <= 0) {
369
429
  director.time = value * duration;
370
430
  }
371
431
  else if (weightsArray.length > 0) {
372
432
  // normalize and calculate weighted time
373
- let time = weightsArray[0].time;
374
- for (const o of weightsArray) {
375
- const weight = o.weight / Math.max(0.00001, sum);
376
- // lerp time based on weight
377
- const diff = Math.abs(o.time - time);
378
- time += diff * weight;
433
+ let time = weightsArray[0].time; // fallback to first time
434
+ if (weightsArray.length > 1) {
435
+ for (const entry of weightsArray) {
436
+ const weight = entry.weight / Math.max(0.00001, sum);
437
+ // console.log(weight.toFixed(2))
438
+ // lerp time based on weight
439
+ const diff = Math.abs(entry.time - time);
440
+ time += diff * weight;
441
+ }
442
+ }
443
+ if (this.damping <= 0) {
444
+ director.time = time;
445
+ }
446
+ else {
447
+ director.time = Mathf.lerp(director.time, time, this.context.time.deltaTime / this.damping);
448
+ }
449
+
450
+ if (debug && this.context.time.frame % 30 === 0) {
451
+ console.log(`[ScrollFollow ] Timeline ${director.name}: ${time.toFixed(3)}`, weightsArray.map(w => `[${w.name} ${(w.weight * 100).toFixed(0)}%]`).join(", "));
379
452
  }
380
- director.time = time;
381
453
  }
382
454
  }
383
455
 
384
456
  }
385
457
 
458
+
459
+
386
460
  const weightsArray: OverlapInfo[] = [];
387
- const markersArray: (ScrollMarkerModel & { element?: HTMLElement | null })[] = [];
461
+ const markersArray: Array<ScrollMarkerModel & {
462
+ element?: HTMLElement | null,
463
+ timeline?: ViewTimeline,
464
+ }> = [];
388
465
 
389
466
  type OverlapInfo = {
467
+ name: string,
390
468
  /** Marker time */
391
469
  time: number,
392
470
  /** Overlap in pixels */
393
471
  weight: number,
472
+ }
473
+
474
+
475
+ // type SelectorCache = {
476
+ // /** The selector used to query the *elements */
477
+ // selector: string,
478
+ // elements: Element[] | null,
479
+ // usedElementCount: number,
480
+ // }
481
+ // const querySelectorResults: Array<SelectorCache> = [];
482
+
483
+ const needleScrollMarkerIndexCache = new Map<number, Element | null>();
484
+ const needleScrollMarkerNameCache = new Map<string, Element | null>();
485
+ let needsScrollMarkerRefresh = true;
486
+
487
+ function tryGetElementsForSelector(index: number, name: string, _cycle: number = 0): Element | null {
488
+
489
+ if (!needsScrollMarkerRefresh) {
490
+ if (name?.length) {
491
+ const element = needleScrollMarkerNameCache.get(name) || null;
492
+ if (element) return element;
493
+ // const isNumber = !isNaN(Number(name));
494
+ // if (!isNumber) {
495
+ // }
496
+ }
497
+ const element = needleScrollMarkerIndexCache.get(index) || null;
498
+ const value = element?.getAttribute("data-timeline-marker");
499
+ // if (value?.length) {
500
+ // if (cycle === 0) {
501
+ // // if the HTML marker we found by index does define a different marker name we try to find the correct HTML element by name
502
+ // return tryGetElementsForSelector(index, value, 1);
503
+ // }
504
+ // if (isDevEnvironment()) console.warn(`ScrollMarker name mismatch: expected "${name}", got "${value}"`);
505
+ // }
506
+ return element;
507
+ }
508
+ needsScrollMarkerRefresh = false;
509
+ needleScrollMarkerIndexCache.clear();
510
+ const markers = document.querySelectorAll(`[data-timeline-marker]`);
511
+ markers.forEach((m, i) => {
512
+ needleScrollMarkerIndexCache.set(i, m);
513
+ const name = m.getAttribute("data-timeline-marker");
514
+ if (name?.length) needleScrollMarkerNameCache.set(name, m);
515
+ });
516
+ needsScrollMarkerRefresh = false;
517
+ return tryGetElementsForSelector(index, name);
518
+ }
519
+
520
+
521
+ // #region ScrollTimeline
522
+
523
+ function calculateTimelinePositionNormalized(timeline: ViewTimeline) {
524
+ if (!timeline.source) return 0;
525
+ const currentTime = timeline.currentTime;
526
+ const duration = timeline.duration;
527
+ let durationValue = 1;
528
+ if (duration.unit === "seconds") {
529
+ durationValue = duration.value;
530
+ }
531
+ else if (duration.unit === "percent") {
532
+ durationValue = duration.value;
533
+ }
534
+ const t01 = currentTime.unit === "seconds" ? (currentTime.value / durationValue) : (currentTime.value / 100);
535
+ return t01;
536
+ }
537
+
538
+
539
+ declare global {
540
+ interface ViewTimeline {
541
+ axis: 'block' | 'inline';
542
+ currentTime: { unit: 'seconds' | 'percent', value: number };
543
+ duration: { unit: 'seconds' | 'percent', value: number };
544
+ source: Element | null;
545
+ startOffset: { unit: 'px', value: number };
546
+ endOffset: { unit: 'px', value: number };
547
+ }
394
548
  }