@needle-tools/engine 4.10.0-next.4f9d92a → 4.10.0-next.870425c

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 (96) hide show
  1. package/CHANGELOG.md +8 -3
  2. package/README.md +2 -1
  3. package/dist/{needle-engine.bundle-D4dO0t5I.umd.cjs → needle-engine.bundle-0b6rexDr.umd.cjs} +139 -137
  4. package/dist/{needle-engine.bundle-BeZ_xmJa.js → needle-engine.bundle-B5GtGvbq.js} +7750 -7653
  5. package/dist/needle-engine.bundle-CicGQeCY.min.js +1652 -0
  6. package/dist/needle-engine.js +15 -14
  7. package/dist/needle-engine.min.js +1 -1
  8. package/dist/needle-engine.umd.cjs +1 -1
  9. package/dist/vendor-CPuBPspY.umd.cjs +1121 -0
  10. package/dist/vendor-DPCU8cUF.min.js +1121 -0
  11. package/dist/vendor-MBoqSyFm.js +16240 -0
  12. package/lib/engine/engine_camera.js +5 -5
  13. package/lib/engine/engine_camera.js.map +1 -1
  14. package/lib/engine/engine_gizmos.d.ts +11 -10
  15. package/lib/engine/engine_gizmos.js +24 -10
  16. package/lib/engine/engine_gizmos.js.map +1 -1
  17. package/lib/engine/engine_license.js +1 -1
  18. package/lib/engine/engine_license.js.map +1 -1
  19. package/lib/engine/engine_lightdata.d.ts +3 -3
  20. package/lib/engine/engine_lightdata.js +10 -10
  21. package/lib/engine/engine_lightdata.js.map +1 -1
  22. package/lib/engine/engine_physics_rapier.js +4 -0
  23. package/lib/engine/engine_physics_rapier.js.map +1 -1
  24. package/lib/engine/engine_scenelighting.d.ts +1 -1
  25. package/lib/engine/engine_scenelighting.js +4 -5
  26. package/lib/engine/engine_scenelighting.js.map +1 -1
  27. package/lib/engine/engine_utils.d.ts +3 -1
  28. package/lib/engine/engine_utils.js +11 -0
  29. package/lib/engine/engine_utils.js.map +1 -1
  30. package/lib/engine/extensions/extension_utils.js +1 -1
  31. package/lib/engine/extensions/extension_utils.js.map +1 -1
  32. package/lib/engine/webcomponents/needle-engine.js +22 -0
  33. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  34. package/lib/engine/xr/NeedleXRController.d.ts +3 -3
  35. package/lib/engine/xr/NeedleXRController.js +28 -0
  36. package/lib/engine/xr/NeedleXRController.js.map +1 -1
  37. package/lib/engine-components/Camera.d.ts +1 -1
  38. package/lib/engine-components/Camera.js +1 -1
  39. package/lib/engine-components/CameraUtils.js +2 -1
  40. package/lib/engine-components/CameraUtils.js.map +1 -1
  41. package/lib/engine-components/CharacterController.d.ts +2 -2
  42. package/lib/engine-components/CharacterController.js +2 -2
  43. package/lib/engine-components/OrbitControls.d.ts +1 -1
  44. package/lib/engine-components/OrbitControls.js +1 -1
  45. package/lib/engine-components/Skybox.js +22 -4
  46. package/lib/engine-components/Skybox.js.map +1 -1
  47. package/lib/engine-components/debug/LogStats.d.ts +1 -0
  48. package/lib/engine-components/debug/LogStats.js +1 -0
  49. package/lib/engine-components/debug/LogStats.js.map +1 -1
  50. package/lib/engine-components/timeline/PlayableDirector.js +1 -1
  51. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  52. package/lib/engine-components/timeline/TimelineModels.d.ts +37 -2
  53. package/lib/engine-components/timeline/TimelineModels.js +6 -0
  54. package/lib/engine-components/timeline/TimelineModels.js.map +1 -1
  55. package/lib/engine-components/timeline/TimelineTracks.d.ts +2 -1
  56. package/lib/engine-components/timeline/TimelineTracks.js +26 -23
  57. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  58. package/lib/engine-components/web/ScrollFollow.d.ts +2 -0
  59. package/lib/engine-components/web/ScrollFollow.js +114 -99
  60. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  61. package/lib/engine-components/web/ViewBox.d.ts +33 -3
  62. package/lib/engine-components/web/ViewBox.js +133 -49
  63. package/lib/engine-components/web/ViewBox.js.map +1 -1
  64. package/lib/engine-components/webxr/WebARSessionRoot.js +1 -0
  65. package/lib/engine-components/webxr/WebARSessionRoot.js.map +1 -1
  66. package/lib/engine-components-experimental/Presentation.d.ts +1 -0
  67. package/lib/engine-components-experimental/Presentation.js +1 -0
  68. package/lib/engine-components-experimental/Presentation.js.map +1 -1
  69. package/package.json +2 -1
  70. package/src/engine/engine_camera.ts +5 -7
  71. package/src/engine/engine_gizmos.ts +37 -23
  72. package/src/engine/engine_license.ts +1 -1
  73. package/src/engine/engine_lightdata.ts +11 -11
  74. package/src/engine/engine_physics_rapier.ts +3 -0
  75. package/src/engine/engine_scenelighting.ts +5 -6
  76. package/src/engine/engine_utils.ts +12 -0
  77. package/src/engine/extensions/extension_utils.ts +1 -1
  78. package/src/engine/webcomponents/needle-engine.ts +33 -6
  79. package/src/engine/xr/NeedleXRController.ts +36 -4
  80. package/src/engine-components/Camera.ts +1 -1
  81. package/src/engine-components/CameraUtils.ts +1 -1
  82. package/src/engine-components/CharacterController.ts +2 -2
  83. package/src/engine-components/OrbitControls.ts +1 -1
  84. package/src/engine-components/Skybox.ts +26 -7
  85. package/src/engine-components/debug/LogStats.ts +1 -0
  86. package/src/engine-components/timeline/PlayableDirector.ts +1 -1
  87. package/src/engine-components/timeline/TimelineModels.ts +37 -3
  88. package/src/engine-components/timeline/TimelineTracks.ts +26 -23
  89. package/src/engine-components/web/ScrollFollow.ts +121 -103
  90. package/src/engine-components/web/ViewBox.ts +140 -50
  91. package/src/engine-components/webxr/WebARSessionRoot.ts +1 -0
  92. package/src/engine-components-experimental/Presentation.ts +1 -0
  93. package/dist/needle-engine.bundle-C3bpSNYu.min.js +0 -1650
  94. package/dist/vendor-D0Yvltn9.umd.cjs +0 -1121
  95. package/dist/vendor-DU8tJyl_.js +0 -14366
  96. package/dist/vendor-JyrX4DVM.min.js +0 -1121
@@ -8,7 +8,7 @@ import { syncField } from "../engine/engine_networking_auto.js";
8
8
  import { loadPMREM } from "../engine/engine_pmrem.js";
9
9
  import { serializable } from "../engine/engine_serialization_decorator.js";
10
10
  import { type IContext } from "../engine/engine_types.js";
11
- import { addAttributeChangeCallback, getParam, PromiseAllWithErrors, removeAttributeChangeCallback } from "../engine/engine_utils.js";
11
+ import { addAttributeChangeCallback, getParam, PromiseAllWithErrors, removeAttributeChangeCallback, toSourceId } from "../engine/engine_utils.js";
12
12
  import { registerObservableAttribute } from "../engine/webcomponents/needle-engine.extras.js";
13
13
  import { Camera, ClearFlags } from "./Camera.js";
14
14
  import { Behaviour, GameObject } from "./Component.js";
@@ -27,15 +27,34 @@ function createRemoteSkyboxComponent(context: IContext, url: string, skybox: boo
27
27
  }
28
28
 
29
29
  const remote = new RemoteSkybox();
30
+ remote.sourceId = toSourceId(url);
30
31
  remote.allowDrop = false;
31
32
  remote.allowNetworking = false;
32
33
  remote.background = skybox;
33
34
  remote.environment = environment;
34
35
  GameObject.addComponent(context.scene, remote);
35
36
  const urlChanged = newValue => {
36
- if (typeof newValue !== "string") return;
37
- if (debug) console.log(attribute, "CHANGED TO", newValue)
38
- remote.setSkybox(newValue);
37
+ if (debug) console.log(attribute, "CHANGED TO", newValue);
38
+ if (newValue) {
39
+ if (typeof newValue !== "string") {
40
+ console.warn("Invalid attribute value for " + attribute);
41
+ return;
42
+ }
43
+ remote.setSkybox(newValue);
44
+ }
45
+ else {
46
+ if (remote.sourceId) {
47
+ if (environment) {
48
+ if (!context.sceneLighting.internalEnableReflection(remote.sourceId)) {
49
+ context.scene.environment = null;
50
+ }
51
+ }
52
+ if (skybox) {
53
+ const skybox = context.lightmaps.tryGetSkybox(remote.sourceId);
54
+ context.scene.background = skybox;
55
+ }
56
+ }
57
+ }
39
58
  };
40
59
  addAttributeChangeCallback(context.domElement, attribute, urlChanged);
41
60
  remote.addEventListener("destroy", () => {
@@ -50,7 +69,7 @@ ContextRegistry.registerCallback(ContextEvent.ContextCreationStart, (args) => {
50
69
  const context = args.context;
51
70
  const backgroundImage = context.domElement.getAttribute("background-image");
52
71
  const environmentImage = context.domElement.getAttribute("environment-image");
53
-
72
+
54
73
  if (backgroundImage) {
55
74
  if (debug) console.log("Creating RemoteSkybox to load background " + backgroundImage);
56
75
  // if the user is loading a GLB without a camera then the CameraUtils (which creates the default camera)
@@ -246,8 +265,8 @@ export class RemoteSkybox extends Behaviour {
246
265
  envMap.needsUpdate = true;
247
266
  }
248
267
 
249
- if(this.destroyed) return;
250
- if(!this.context) {
268
+ if (this.destroyed) return;
269
+ if (!this.context) {
251
270
  console.warn("RemoteSkybox: Context is not available - can not apply skybox.");
252
271
  return;
253
272
  }
@@ -4,6 +4,7 @@ import { Behaviour } from "../../engine-components/Component.js";
4
4
 
5
5
  const debug = getParam("logstats");
6
6
 
7
+ /** @internal */
7
8
  export class LogStats extends Behaviour {
8
9
 
9
10
  onEnable(): void {
@@ -115,7 +115,7 @@ export class PlayableDirector extends Behaviour {
115
115
  /** @internal */
116
116
  awake(): void {
117
117
  if (debug)
118
- console.log(this, this.playableAsset?.tracks);
118
+ console.log(this, this.playableAsset);
119
119
 
120
120
  this.rebuildGraph();
121
121
 
@@ -1,11 +1,16 @@
1
1
  import { AnimationClip, Object3D, Quaternion, Vector3 } from "three";
2
- import { Behavior } from "three-mesh-ui";
3
2
 
3
+ /**
4
+ * @category Animation and Sequencing
5
+ */
4
6
  export declare type TimelineAssetModel = {
5
7
  name: string;
6
8
  tracks: TrackModel[];
7
9
  }
8
10
 
11
+ /**
12
+ * @category Animation and Sequencing
13
+ */
9
14
  export enum TrackType {
10
15
  Activation = "ActivationTrack",
11
16
  Animation = "AnimationTrack",
@@ -15,6 +20,9 @@ export enum TrackType {
15
20
  Signal = "SignalTrack",
16
21
  }
17
22
 
23
+ /**
24
+ * @category Animation and Sequencing
25
+ */
18
26
  export enum ClipExtrapolation {
19
27
  None = 0,
20
28
  Hold = 1,
@@ -23,6 +31,9 @@ export enum ClipExtrapolation {
23
31
  Continue = 4
24
32
  };
25
33
 
34
+ /**
35
+ * @category Animation and Sequencing
36
+ */
26
37
  export declare type TrackModel = {
27
38
  name: string;
28
39
  type: TrackType;
@@ -37,11 +48,17 @@ export declare type TrackModel = {
37
48
  declare type Vec3 = { x: number, y: number, z: number };
38
49
  declare type Quat = { x: number, y: number, z: number, w: number };
39
50
 
51
+ /**
52
+ * @category Animation and Sequencing
53
+ */
40
54
  export declare type TrackOffset = {
41
55
  position: Vec3 | Vector3;
42
56
  rotation: Quat | Quaternion;
43
57
  }
44
58
 
59
+ /**
60
+ * @category Animation and Sequencing
61
+ */
45
62
  export declare type ClipModel = {
46
63
  start: number;
47
64
  end: number;
@@ -56,6 +73,9 @@ export declare type ClipModel = {
56
73
  reversed?: boolean;
57
74
  }
58
75
 
76
+ /**
77
+ * @category Animation and Sequencing
78
+ */
59
79
  export declare type AnimationClipModel = {
60
80
  clip: string | number | AnimationClip;
61
81
  loop: boolean;
@@ -65,12 +85,18 @@ export declare type AnimationClipModel = {
65
85
  rotation?: Quat | Quaternion;
66
86
  }
67
87
 
88
+ /**
89
+ * @category Animation and Sequencing
90
+ */
68
91
  export declare type AudioClipModel = {
69
92
  clip: string;
70
93
  loop: boolean;
71
94
  volume: number;
72
95
  }
73
96
 
97
+ /**
98
+ * @category Animation and Sequencing
99
+ */
74
100
  export declare type ControlClipModel = {
75
101
  sourceObject: string | Object3D;
76
102
  controlActivation: boolean;
@@ -81,11 +107,16 @@ export enum MarkerType {
81
107
  Signal = "SignalEmitter",
82
108
  }
83
109
 
84
- export declare class MarkerModel {
110
+ /**
111
+ * @category Animation and Sequencing
112
+ */export declare class MarkerModel {
85
113
  type: MarkerType;
86
114
  time: number;
87
115
  }
88
116
 
117
+ /**
118
+ * @category Animation and Sequencing
119
+ */
89
120
  export declare class SignalMarkerModel extends MarkerModel {
90
121
  retroActive: boolean;
91
122
  emitOnce: boolean;
@@ -99,5 +130,8 @@ export declare class SignalMarkerModel extends MarkerModel {
99
130
  * ```html
100
131
  * <div data-timeline-marker>...</div>
101
132
  * ```
102
- */
133
+ *
134
+ * @link [Example Project using ScrollMarker](https://scrollytelling-bike-z23hmxb2gnu5a.needle.run/)
135
+ * @category Animation and Sequencing
136
+ */
103
137
  export type ScrollMarkerModel = MarkerModel & { name: string };
@@ -187,21 +187,22 @@ export class AnimationTrackHandler extends TrackHandler {
187
187
  return;
188
188
  }
189
189
  // we only want to hook into the binding of the root object
190
- // TODO: test with a clip with multiple roots
191
- const parts = clip.tracks[0].name.split(".");
192
- const rootName = parts[parts.length - 2];
193
- const positionTrackName = rootName + ".position";
194
- const rotationTrackName = rootName + ".quaternion";
195
190
  let foundPositionTrack: boolean = false;
196
191
  let foundRotationTrack: boolean = false;
197
- for (const t of clip.tracks) {
198
- if (t.name.endsWith(positionTrackName)) {
199
- foundPositionTrack = true;
200
- this.createPositionInterpolant(clip, clipModel, t);
201
- }
202
- else if (t.name.endsWith(rotationTrackName)) {
203
- foundRotationTrack = true;
204
- 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
+ }
205
206
  }
206
207
  }
207
208
 
@@ -226,16 +227,14 @@ export class AnimationTrackHandler extends TrackHandler {
226
227
  const trackName = baseName + ".position";
227
228
  if (debug) console.warn("Create position track", objName, targetObj);
228
229
  // apply initial local position so it doesnt get flipped or otherwise changed
229
- const pos = targetObj.position;
230
- 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]);
231
231
  clip.tracks.push(track);
232
232
  this.createPositionInterpolant(clip, clipModel, track);
233
233
  }
234
234
  else if (!foundRotationTrack) {
235
235
  const trackName = clip.tracks[0].name.substring(0, indexOfProperty) + ".quaternion";
236
236
  if (debug) console.warn("Create quaternion track", objName, targetObj);
237
- const rot = targetObj.quaternion;
238
- 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]);
239
238
  clip.tracks.push(track);
240
239
  this.createRotationInterpolant(clip, clipModel, track);
241
240
  }
@@ -834,23 +833,27 @@ export class AudioTrackHandler extends TrackHandler {
834
833
 
835
834
  export class MarkerTrackHandler extends TrackHandler {
836
835
  models: Array<Models.MarkerModel & Record<string, any>> = [];
837
- isDirty = true;
836
+ needsSorting = true;
838
837
 
839
838
  *foreachMarker<T>(type: string | null = null) {
839
+ if(this.needsSorting) this.sort();
840
840
  for (const model of this.models) {
841
841
  if (model && model.type === type) yield model as T;
842
842
  }
843
843
  }
844
844
 
845
845
  onEnable() {
846
- this.isDirty = true;
846
+ this.needsSorting = true;
847
847
  }
848
848
 
849
849
  evaluate(_time: number) {
850
- if (this.isDirty) {
851
- this.isDirty = false;
852
- this.models.sort((a, b) => a.time - b.time);
853
- }
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
+
854
857
  }
855
858
  }
856
859
 
@@ -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".
@@ -173,8 +176,8 @@ export class ScrollFollow extends Behaviour {
173
176
 
174
177
  const value = this.invert ? 1 - this._current_value : this._current_value;
175
178
 
176
- const height = this._rangeEndValue - this._rangeStartValue;
177
- const pixelValue = this._rangeStartValue + value * height;
179
+ // const height = this._rangeEndValue - this._rangeStartValue;
180
+ // const pixelValue = this._rangeStartValue + value * height;
178
181
 
179
182
  // apply scroll to target(s)
180
183
  if (Array.isArray(this.target)) {
@@ -185,7 +188,7 @@ export class ScrollFollow extends Behaviour {
185
188
  }
186
189
 
187
190
  if (debug && this.context.time.frame % 30 === 0) {
188
- 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}]`);
189
192
  }
190
193
  }
191
194
  }
@@ -303,22 +306,24 @@ export class ScrollFollow extends Behaviour {
303
306
  const index = markerIndex++;
304
307
 
305
308
  // Get marker elements from DOM
306
- if ((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))) {
307
310
  marker.needsUpdate = false;
308
311
  try {
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
309
313
  marker.element = tryGetElementsForSelector(index, marker.name) as HTMLElement | null;
310
- if (debug) console.debug("ScrollMarker found on page", marker.element, marker.name);
311
- // if (!marker.element) {
312
- // marker.timeline = undefined;
313
- // continue;
314
- // }
315
- // else {
316
- // /** @ts-ignore */
317
- // marker.timeline = new ViewTimeline({
318
- // subject: marker.element,
319
- // axis: 'block', // https://drafts.csswg.org/scroll-animations/#scroll-notation
320
- // });
321
- // }
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
+ }
322
327
  }
323
328
  catch (error) {
324
329
  marker.element = null;
@@ -349,57 +354,75 @@ export class ScrollFollow extends Behaviour {
349
354
 
350
355
  weightsArray.length = 0;
351
356
  let sum = 0;
357
+ const oneFrameTime = 1 / 60;
352
358
 
353
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
354
360
  let markerCount = 0;
355
- for (const marker of markersArray) {
356
-
361
+ for (let i = 0; i < markersArray.length; i++) {
362
+ const marker = markersArray[i];
357
363
  if (!marker.element) continue;
364
+ const nextMarker = markersArray[i + 1];
365
+
366
+ const nextTime = nextMarker
367
+ ? (nextMarker.time - oneFrameTime)
368
+ : duration;
358
369
 
359
370
  markerCount += 1;
360
371
 
361
- // const timeline = marker.timeline;
362
- // if (timeline) {
363
- // const time01 = calculateTimelinePositionNormalized(timeline);
364
- // if (time01 > 0 && time01 <= 1) {
365
- // const overlap = calculateTimelinePositionNormalized(timeline!);
366
- // const weight = overlap;
367
- // // console.log(marker.element.className, time01)
368
- // weightsArray.push({ time: marker.time, weight: weight });
369
- // sum += weight;
370
- // }
371
- // }
372
- // continue;
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
+ }
393
+ }
394
+ continue;
373
395
  // if(this.context.time.frame % 10 === 0) console.log(marker.element?.className, timeline, calculateTimelinePositionNormalized(timeline!));
374
396
 
375
- const top = marker.element.offsetTop;
376
- const height = marker.element.offsetHeight;
377
- const bottom = top + height;
378
- let overlap = 0;
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;
379
401
 
380
- // 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
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
381
403
 
382
- if (bottom < currentTop) {
383
- // marker is above scroll region
384
- overlap = 0;
385
- }
386
- else if (top > currentBottom) {
387
- // marker is below scroll region
388
- overlap = 0;
389
- }
390
- else {
391
- // calculate overlap in pixels
392
- const overlapTop = Math.max(top, currentTop);
393
- const overlapBottom = Math.min(bottom, currentBottom);
394
- overlap = Math.max(0, overlapBottom - overlapTop);
395
- }
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
+ // }
396
419
 
397
- markerCount += 1;
420
+ // // if(this.context.time.frame % 20 === 0) console.log(overlap)
398
421
 
399
- if (overlap > 0) {
400
- weightsArray.push({ time: marker.time, weight: overlap });
401
- sum += overlap;
402
- }
422
+ // if (overlap > 0) {
423
+ // weightsArray.push({ time: marker.time, weight: overlap });
424
+ // sum += overlap;
425
+ // }
403
426
  }
404
427
 
405
428
  if (weightsArray.length <= 0 && markerCount <= 0) {
@@ -407,19 +430,26 @@ export class ScrollFollow extends Behaviour {
407
430
  }
408
431
  else if (weightsArray.length > 0) {
409
432
  // normalize and calculate weighted time
410
- let time = 0;
411
- for (const entry of weightsArray) {
412
- const weight = entry.weight / Math.max(0.00001, sum);
413
- // console.log(weight.toFixed(2))
414
- // lerp time based on weight
415
- const diff = Math.abs(entry.time - time);
416
- 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
+ }
417
442
  }
418
- // console.log(time.toFixed(2), [...weightsArray])
419
- if (this.damping <= 0)
443
+ if (this.damping <= 0) {
420
444
  director.time = time;
421
- else
445
+ }
446
+ else {
422
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(", "));
452
+ }
423
453
  }
424
454
  }
425
455
 
@@ -428,9 +458,13 @@ export class ScrollFollow extends Behaviour {
428
458
 
429
459
 
430
460
  const weightsArray: OverlapInfo[] = [];
431
- const markersArray: (ScrollMarkerModel & { element?: HTMLElement | null, timeline?: ViewTimeline })[] = [];
461
+ const markersArray: Array<ScrollMarkerModel & {
462
+ element?: HTMLElement | null,
463
+ timeline?: ViewTimeline,
464
+ }> = [];
432
465
 
433
466
  type OverlapInfo = {
467
+ name: string,
434
468
  /** Marker time */
435
469
  time: number,
436
470
  /** Overlap in pixels */
@@ -446,17 +480,29 @@ type OverlapInfo = {
446
480
  // }
447
481
  // const querySelectorResults: Array<SelectorCache> = [];
448
482
 
449
- const needleScrollMarkerCacheKey = "data-timeline-marker";
450
483
  const needleScrollMarkerIndexCache = new Map<number, Element | null>();
451
484
  const needleScrollMarkerNameCache = new Map<string, Element | null>();
452
485
  let needsScrollMarkerRefresh = true;
453
486
 
454
- function tryGetElementsForSelector(index: number, name: string): Element | null {
487
+ function tryGetElementsForSelector(index: number, name: string, _cycle: number = 0): Element | null {
455
488
 
456
489
  if (!needsScrollMarkerRefresh) {
457
- let element = name?.length ? needleScrollMarkerNameCache.get(name) : null;
458
- if (element) return element;
459
- element = needleScrollMarkerIndexCache.get(index) || null;
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
+ // }
460
506
  return element;
461
507
  }
462
508
  needsScrollMarkerRefresh = false;
@@ -467,26 +513,8 @@ function tryGetElementsForSelector(index: number, name: string): Element | null
467
513
  const name = m.getAttribute("data-timeline-marker");
468
514
  if (name?.length) needleScrollMarkerNameCache.set(name, m);
469
515
  });
470
- const element = needleScrollMarkerIndexCache.get(index) || null;
471
- return element;
472
-
473
-
474
- /* e.g.
475
- <div class="section behind start" data-needle-scroll-marker>
476
- */
477
- // console.log(index, element)
478
- if (element) return element;
479
-
480
- // for (const entry of querySelectorResults) {
481
- // if (entry.selector === selector) {
482
- // const index = entry.usedElementCount++;
483
- // return entry.elements && index < entry.elements.length ? entry.elements[index] : null;
484
- // }
485
- // }
486
- // const elements = document.querySelectorAll(selector);
487
- // querySelectorResults.push({ selector, elements: Array.from(elements), usedElementCount: 1 });
488
- // if (elements.length > 0) return elements[0];
489
- return null;
516
+ needsScrollMarkerRefresh = false;
517
+ return tryGetElementsForSelector(index, name);
490
518
  }
491
519
 
492
520
 
@@ -506,16 +534,6 @@ function calculateTimelinePositionNormalized(timeline: ViewTimeline) {
506
534
  const t01 = currentTime.unit === "seconds" ? (currentTime.value / durationValue) : (currentTime.value / 100);
507
535
  return t01;
508
536
  }
509
- function calculateNormalizedOverlap(timeline: ViewTimeline) {
510
- if (!timeline.source) return 0;
511
- const start = timeline.startOffset;
512
- const end = timeline.endOffset;
513
- const total = start.value + end.value;
514
- if (total <= 0) return 1;
515
- const startNorm = start.value / total;
516
- const endNorm = end.value / total;
517
- return 1 - (startNorm + endNorm);
518
- }
519
537
 
520
538
 
521
539
  declare global {