@needle-tools/engine 4.9.3 → 4.10.0-beta.1

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 (133) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{gltf-progressive-DhE1A6hX.min.js → gltf-progressive-CoZbSfPR.min.js} +1 -1
  4. package/dist/{gltf-progressive-egsMzRdv.js → gltf-progressive-DUR9TuAH.js} +3 -3
  5. package/dist/{gltf-progressive-DWiyqrwB.umd.cjs → gltf-progressive-Iy7aSAPk.umd.cjs} +1 -1
  6. package/dist/{needle-engine.bundle-C7LSzO5L.umd.cjs → needle-engine.bundle-6so_os_w.umd.cjs} +179 -145
  7. package/dist/{needle-engine.bundle-BAsxNKpA.js → needle-engine.bundle-Dj2DYdMY.js} +7699 -7235
  8. package/dist/needle-engine.bundle-Djy6H4lx.min.js +1650 -0
  9. package/dist/needle-engine.js +460 -456
  10. package/dist/needle-engine.min.js +1 -1
  11. package/dist/needle-engine.umd.cjs +1 -1
  12. package/dist/{postprocessing-BZOSD1ln.min.js → postprocessing-BHMVuZQ1.min.js} +1 -1
  13. package/dist/{postprocessing-Bb5StX0o.umd.cjs → postprocessing-BsnRNRRS.umd.cjs} +1 -1
  14. package/dist/{postprocessing-BzFF7i-7.js → postprocessing-DQ2pynXW.js} +2 -2
  15. package/dist/{three-BK56xWDs.umd.cjs → three-B-jwTHao.umd.cjs} +11 -11
  16. package/dist/{three-CsHK73Zc.js → three-CJSAehtG.js} +1 -0
  17. package/dist/{three-examples-Bph291U2.min.js → three-examples-BivkhnvN.min.js} +1 -1
  18. package/dist/{three-examples-C9WfZu-X.umd.cjs → three-examples-Deqc1bNw.umd.cjs} +1 -1
  19. package/dist/{three-examples-BvMpKSun.js → three-examples-Doq0rvFU.js} +1 -1
  20. package/dist/{three-mesh-ui-CN6aRT7i.js → three-mesh-ui-CktOi6oI.js} +1 -1
  21. package/dist/{three-mesh-ui-DnxkZWNA.umd.cjs → three-mesh-ui-CsHwj9cJ.umd.cjs} +1 -1
  22. package/dist/{three-mesh-ui-n_qS2BM-.min.js → three-mesh-ui-DhYXcXZe.min.js} +1 -1
  23. package/dist/{three-TNFQHSFa.min.js → three-qw28ZtTy.min.js} +10 -10
  24. package/dist/{vendor-BtJpSuCj.umd.cjs → vendor-D0Yvltn9.umd.cjs} +1 -1
  25. package/dist/{vendor-k9i6CeGi.js → vendor-DU8tJyl_.js} +1 -1
  26. package/dist/{vendor-XJ9xiwrv.min.js → vendor-JyrX4DVM.min.js} +1 -1
  27. package/lib/engine/api.d.ts +1 -0
  28. package/lib/engine/api.js +1 -0
  29. package/lib/engine/api.js.map +1 -1
  30. package/lib/engine/codegen/register_types.js +6 -0
  31. package/lib/engine/codegen/register_types.js.map +1 -1
  32. package/lib/engine/engine_animation.d.ts +21 -1
  33. package/lib/engine/engine_animation.js +32 -1
  34. package/lib/engine/engine_animation.js.map +1 -1
  35. package/lib/engine/engine_camera.d.ts +7 -1
  36. package/lib/engine/engine_camera.fit.d.ts +68 -0
  37. package/lib/engine/engine_camera.fit.js +166 -0
  38. package/lib/engine/engine_camera.fit.js.map +1 -0
  39. package/lib/engine/engine_camera.js +46 -6
  40. package/lib/engine/engine_camera.js.map +1 -1
  41. package/lib/engine/engine_context.d.ts +6 -0
  42. package/lib/engine/engine_context.js +48 -9
  43. package/lib/engine/engine_context.js.map +1 -1
  44. package/lib/engine/engine_gizmos.d.ts +2 -2
  45. package/lib/engine/engine_gizmos.js +2 -2
  46. package/lib/engine/engine_physics.js +6 -3
  47. package/lib/engine/engine_physics.js.map +1 -1
  48. package/lib/engine/webcomponents/logo-element.d.ts +1 -1
  49. package/lib/engine/webcomponents/logo-element.js +29 -5
  50. package/lib/engine/webcomponents/logo-element.js.map +1 -1
  51. package/lib/engine/webcomponents/needle menu/needle-menu.js +4 -3
  52. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  53. package/lib/engine/webcomponents/needle-engine.d.ts +1 -0
  54. package/lib/engine/webcomponents/needle-engine.js +6 -0
  55. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  56. package/lib/engine/webcomponents/needle-engine.loading.d.ts +0 -1
  57. package/lib/engine/webcomponents/needle-engine.loading.js +62 -59
  58. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  59. package/lib/engine-components/AnimatorController.js +16 -0
  60. package/lib/engine-components/AnimatorController.js.map +1 -1
  61. package/lib/engine-components/CameraUtils.js +8 -9
  62. package/lib/engine-components/CameraUtils.js.map +1 -1
  63. package/lib/engine-components/OrbitControls.d.ts +7 -47
  64. package/lib/engine-components/OrbitControls.js +25 -149
  65. package/lib/engine-components/OrbitControls.js.map +1 -1
  66. package/lib/engine-components/Renderer.d.ts +2 -2
  67. package/lib/engine-components/Renderer.js +10 -5
  68. package/lib/engine-components/Renderer.js.map +1 -1
  69. package/lib/engine-components/api.d.ts +0 -1
  70. package/lib/engine-components/api.js.map +1 -1
  71. package/lib/engine-components/codegen/components.d.ts +3 -0
  72. package/lib/engine-components/codegen/components.js +3 -0
  73. package/lib/engine-components/codegen/components.js.map +1 -1
  74. package/lib/engine-components/timeline/PlayableDirector.d.ts +35 -6
  75. package/lib/engine-components/timeline/PlayableDirector.js +67 -26
  76. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  77. package/lib/engine-components/timeline/TimelineModels.d.ts +11 -0
  78. package/lib/engine-components/timeline/TimelineModels.js.map +1 -1
  79. package/lib/engine-components/timeline/TimelineTracks.d.ts +7 -0
  80. package/lib/engine-components/timeline/TimelineTracks.js +23 -2
  81. package/lib/engine-components/timeline/TimelineTracks.js.map +1 -1
  82. package/lib/engine-components/utils/LookAt.js +5 -1
  83. package/lib/engine-components/utils/LookAt.js.map +1 -1
  84. package/lib/engine-components/web/Clickthrough.d.ts +3 -0
  85. package/lib/engine-components/web/Clickthrough.js +13 -2
  86. package/lib/engine-components/web/Clickthrough.js.map +1 -1
  87. package/lib/engine-components/web/CursorFollow.d.ts +3 -0
  88. package/lib/engine-components/web/CursorFollow.js +3 -0
  89. package/lib/engine-components/web/CursorFollow.js.map +1 -1
  90. package/lib/engine-components/web/HoverAnimation.d.ts +44 -0
  91. package/lib/engine-components/web/HoverAnimation.js +105 -0
  92. package/lib/engine-components/web/HoverAnimation.js.map +1 -0
  93. package/lib/engine-components/web/ScrollFollow.d.ts +40 -4
  94. package/lib/engine-components/web/ScrollFollow.js +256 -27
  95. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  96. package/lib/engine-components/web/ViewBox.d.ts +16 -0
  97. package/lib/engine-components/web/ViewBox.js +183 -0
  98. package/lib/engine-components/web/ViewBox.js.map +1 -0
  99. package/lib/engine-components/web/index.d.ts +2 -0
  100. package/lib/engine-components/web/index.js +2 -0
  101. package/lib/engine-components/web/index.js.map +1 -1
  102. package/package.json +1 -1
  103. package/plugins/vite/alias.js +5 -3
  104. package/plugins/vite/poster-client.js +22 -21
  105. package/src/engine/api.ts +2 -1
  106. package/src/engine/codegen/register_types.ts +6 -0
  107. package/src/engine/engine_animation.ts +69 -1
  108. package/src/engine/engine_camera.fit.ts +258 -0
  109. package/src/engine/engine_camera.ts +62 -8
  110. package/src/engine/engine_context.ts +50 -10
  111. package/src/engine/engine_gizmos.ts +2 -2
  112. package/src/engine/engine_physics.ts +6 -3
  113. package/src/engine/webcomponents/logo-element.ts +29 -4
  114. package/src/engine/webcomponents/needle menu/needle-menu.ts +4 -3
  115. package/src/engine/webcomponents/needle-engine.loading.ts +95 -56
  116. package/src/engine/webcomponents/needle-engine.ts +6 -1
  117. package/src/engine-components/AnimatorController.ts +21 -2
  118. package/src/engine-components/CameraUtils.ts +8 -9
  119. package/src/engine-components/OrbitControls.ts +36 -206
  120. package/src/engine-components/Renderer.ts +10 -5
  121. package/src/engine-components/api.ts +0 -1
  122. package/src/engine-components/codegen/components.ts +3 -0
  123. package/src/engine-components/timeline/PlayableDirector.ts +88 -34
  124. package/src/engine-components/timeline/TimelineModels.ts +11 -0
  125. package/src/engine-components/timeline/TimelineTracks.ts +26 -2
  126. package/src/engine-components/utils/LookAt.ts +5 -1
  127. package/src/engine-components/web/Clickthrough.ts +14 -2
  128. package/src/engine-components/web/CursorFollow.ts +3 -0
  129. package/src/engine-components/web/HoverAnimation.ts +99 -0
  130. package/src/engine-components/web/ScrollFollow.ts +316 -25
  131. package/src/engine-components/web/ViewBox.ts +199 -0
  132. package/src/engine-components/web/index.ts +3 -1
  133. package/dist/needle-engine.bundle-ugr1bBtk.min.js +0 -1616
@@ -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
 
@@ -21,6 +30,9 @@ onStart(ctx => {
21
30
  * - Alternatively, add the `clickthrough` attribute to the `<needle-engine>` HTML element (e.g. `<needle-engine clickthrough></needle-engine>`).
22
31
  *
23
32
  * @link Example https://stackblitz.com/~/github.com/needle-engine/sample-3d-over-html
33
+ * @category Web
34
+ * @group Components
35
+ * @component
24
36
  */
25
37
  export class ClickThrough extends Behaviour {
26
38
 
@@ -4,6 +4,9 @@ import { Behaviour } from "../Component.js";
4
4
 
5
5
  /**
6
6
  * The CursorFollow component makes the object follow the cursor (or touch) position on screen.
7
+ * @category Web
8
+ * @group Components
9
+ * @component
7
10
  */
8
11
  export class CursorFollow extends Behaviour {
9
12
 
@@ -0,0 +1,99 @@
1
+ import { AnimationClip } from "three";
2
+
3
+ import { ScaleClipType } from "../../engine/engine_animation.js";
4
+ import { AnimationUtils } from "../../engine/engine_animation.js";
5
+ import { serializable } from "../../engine/engine_serialization_decorator.js";
6
+ import { registerType } from "../../engine/engine_typestore.js";
7
+ import { Animation } from "../Animation.js";
8
+ import { Behaviour } from "../Component.js";
9
+
10
+
11
+ /**
12
+ * @category Web
13
+ * @group Components
14
+ * @component
15
+ */
16
+ @registerType
17
+ export class HoverAnimation extends Behaviour {
18
+
19
+ /**
20
+ * Default hover animation type if no custom clip is provided.
21
+ * **Node**: This is only used if no custom hover animation clip is provided.
22
+ * @default "linear"
23
+ */
24
+ @serializable()
25
+ type: ScaleClipType = "linear";
26
+
27
+ /**
28
+ * Duration of the hover animation in seconds.
29
+ * **Node**: This is only used if no custom hover animation clip is provided.
30
+ * @default 0.1
31
+ */
32
+ @serializable()
33
+ duration: number = 0.1;
34
+
35
+ /**
36
+ * Scale factor to apply when hovering.
37
+ * **Node**: This is only used if no custom hover animation clip is provided.
38
+ * @default 1.1
39
+ */
40
+ @serializable()
41
+ scaleFactor: number = 1.1;
42
+
43
+
44
+ /**
45
+ * Animation clip to play when hovering. If null, a default scale-up animation is used.
46
+ */
47
+ @serializable(AnimationClip)
48
+ hovered: AnimationClip | null = null;
49
+
50
+ /**
51
+ * Animation clip to play when not hovering. If null, an empty clip is used.
52
+ */
53
+ @serializable(AnimationClip)
54
+ idle: AnimationClip | null = null;
55
+
56
+ private animation: Animation | null = null;
57
+
58
+ start() {
59
+ if (!this.idle) this.idle = AnimationUtils.emptyClip();
60
+
61
+ if (!this.hovered) {
62
+ this.hovered = AnimationUtils.createScaleClip({
63
+ type: "linear",
64
+ duration: this.duration || 0.1,
65
+ scale: this.gameObject.scale,
66
+ scaleFactor: this.scaleFactor || 1.1,
67
+ });
68
+ }
69
+
70
+ this.animation ??= this.gameObject.addComponent(Animation);
71
+ this.animation.playAutomatically = false;
72
+ this.playIdle();
73
+ }
74
+
75
+ onEnable() {
76
+ if (this.animation) this.animation.enabled = true;
77
+ this.playIdle();
78
+ }
79
+ onDisable() {
80
+ if (this.animation) this.animation.enabled = false;
81
+ this.playIdle();
82
+ }
83
+
84
+ onPointerEnter() {
85
+ this.playHover();
86
+ }
87
+
88
+ onPointerExit() {
89
+ this.playIdle();
90
+ }
91
+
92
+ private playIdle() {
93
+ if (this.idle) this.animation?.play(this.idle, { exclusive: true, fadeDuration: .1, loop: true });
94
+ }
95
+ private playHover() {
96
+ if (this.hovered) this.animation?.play(this.hovered, { exclusive: true, fadeDuration: .1, loop: false, clampWhenFinished: true });
97
+ }
98
+
99
+ }
@@ -1,8 +1,11 @@
1
1
  import { Box3, Object3D } from "three";
2
+ import { element } from "three/src/nodes/TSL.js";
2
3
 
4
+ import { Context } from "../../engine/engine_context.js";
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 */
@@ -44,6 +50,10 @@ type ScrollFollowEvent = {
44
50
  * 2. Add a ScrollFollow component to the same GameObject or another GameObject in the scene.
45
51
  * 3. Assign the PlayableDirector component to the ScrollFollow's target property.
46
52
  * 4. The timeline will now scrub based on the scroll position of the page.
53
+ *
54
+ * @category Web
55
+ * @group Components
56
+ * @component
47
57
  */
48
58
  export class ScrollFollow extends Behaviour {
49
59
 
@@ -77,7 +87,8 @@ export class ScrollFollow extends Behaviour {
77
87
  @serializable()
78
88
  invert: boolean = false;
79
89
 
80
- /**
90
+ /**
91
+ * **Experimental - might change in future updates**
81
92
  * If set, the scroll position will be read from the specified element instead of the window.
82
93
  * Use a CSS selector to specify the element, e.g. `#my-scrollable-div` or `.scroll-container`.
83
94
  * @default null
@@ -98,17 +109,23 @@ export class ScrollFollow extends Behaviour {
98
109
  * Current scroll value in "pages" (0 = top of page, 1 = bottom of page)
99
110
  */
100
111
  get currentValue() {
101
- return this.current_value;
112
+ return this._current_value;
102
113
  }
103
114
 
104
- private current_value: number = 0;
105
- private target_value: number = 0;
106
- 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;
107
124
 
108
125
  /** @internal */
109
126
  onEnable() {
110
127
  window.addEventListener("wheel", this.updateCurrentScrollValue, { passive: true });
111
- this.applied_value = -1;
128
+ this._appliedValue = -1;
112
129
  }
113
130
 
114
131
  /** @internal */
@@ -121,23 +138,28 @@ export class ScrollFollow extends Behaviour {
121
138
 
122
139
  this.updateCurrentScrollValue();
123
140
 
124
- // apply damping if any
125
- if (this.damping > 0) {
126
- this.current_value = Mathf.lerp(this.current_value, this.target_value, this.context.time.deltaTime / this.damping);
127
- }
128
- else {
129
- 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
+ }
130
151
  }
131
152
 
132
- if (this.current_value !== this.applied_value) {
133
- this.applied_value = this.current_value;
153
+ // if (this._current_value !== this._appliedValue)
154
+ {
155
+ this._appliedValue = this._current_value;
134
156
 
135
157
  let defaultPrevented = false;
136
158
  if (this.changed.listenerCount > 0) {
137
159
  // fire change event
138
160
  const event: ScrollFollowEvent = {
139
161
  type: "change",
140
- value: this.current_value,
162
+ value: this._current_value,
141
163
  component: this,
142
164
  preventDefault: () => { event.defaultPrevented = true; },
143
165
  defaultPrevented: false,
@@ -149,14 +171,21 @@ export class ScrollFollow extends Behaviour {
149
171
  // if not prevented apply scroll
150
172
  if (!defaultPrevented) {
151
173
 
152
- const value = this.invert ? 1 - this.current_value : this.current_value;
174
+ const value = this.invert ? 1 - this._current_value : this._current_value;
175
+
176
+ const height = this._rangeEndValue - this._rangeStartValue;
177
+ const pixelValue = this._rangeStartValue + value * height;
153
178
 
154
179
  // apply scroll to target(s)
155
180
  if (Array.isArray(this.target)) {
156
- this.target.forEach(t => t && ScrollFollow.applyScroll(t, value));
181
+ this.target.forEach(t => t && this.applyScroll(t, value));
157
182
  }
158
183
  else if (this.target) {
159
- ScrollFollow.applyScroll(this.target, value);
184
+ this.applyScroll(this.target, value);
185
+ }
186
+
187
+ if (debug && this.context.time.frame % 30 === 0) {
188
+ console.debug(`[ScrollFollow] ${this._current_value.toFixed(5)} — ${(this._target_value * 100).toFixed(0)}%`);
160
189
  }
161
190
  }
162
191
  }
@@ -164,11 +193,16 @@ export class ScrollFollow extends Behaviour {
164
193
 
165
194
  private _lastSelectorValue: string | null = null;
166
195
  private _lastSelectorElement: Element | null = null;
196
+ /** Top y */
197
+ private _rangeStartValue: number = 0;
198
+ /** Bottom y */
199
+ private _rangeEndValue: number = 0;
167
200
 
168
201
  private updateCurrentScrollValue = () => {
169
202
 
170
203
  switch (this.mode) {
171
204
  case "window":
205
+
172
206
  if (this.htmlSelector?.length) {
173
207
  if (this.htmlSelector !== this._lastSelectorValue) {
174
208
  this._lastSelectorElement = document.querySelector(this.htmlSelector);
@@ -176,28 +210,41 @@ export class ScrollFollow extends Behaviour {
176
210
  }
177
211
  if (this._lastSelectorElement) {
178
212
  const rect = this._lastSelectorElement.getBoundingClientRect();
179
- this.target_value = -rect.top / (rect.height - window.innerHeight);
180
- if (isNaN(this.target_value) || !isFinite(this.target_value)) this.target_value = 0;
213
+
214
+ this._scrollStart = rect.top + window.scrollY;
215
+ this._scrollEnd = rect.height - window.innerHeight;
216
+ this._scrollValue = -rect.top;
217
+ this._target_value = -rect.top / (rect.height - window.innerHeight);
218
+ this._rangeStartValue = rect.top + window.scrollY;
219
+ this._rangeEndValue = this._rangeStartValue + rect.height - window.innerHeight;
220
+ this._scrollContainerHeight = rect.height;
181
221
  break;
182
222
  }
183
223
  }
184
224
  else {
185
- this.target_value = window.scrollY / (document.body.scrollHeight - window.innerHeight);
225
+ this._scrollStart = 0;
226
+ this._scrollEnd = window.document.body.scrollHeight - window.innerHeight;
227
+ this._scrollValue = window.scrollY;
228
+ this._target_value = this._scrollValue / (this._scrollEnd || 1);
229
+ this._rangeStartValue = 0;
230
+ this._rangeEndValue = document.body.scrollHeight;
231
+ this._scrollContainerHeight = window.innerHeight;
186
232
  }
187
- if (isNaN(this.target_value) || !isFinite(this.target_value)) this.target_value = 0;
188
233
  break;
189
234
  }
190
235
 
236
+ if (isNaN(this._target_value) || !isFinite(this._target_value)) this._target_value = -1;
191
237
  }
192
238
 
193
239
 
194
- private static applyScroll(target: object, value: number) {
240
+ private applyScroll(target: object, value: number) {
195
241
 
196
242
  if (!target) return;
197
243
 
198
244
  if (target instanceof PlayableDirector) {
199
- target.time = value * target.duration;
200
- if (!target.isPlaying) target.evaluate();
245
+ this.handleTimelineTarget(target, value);
246
+ if (target.isPlaying) target.pause();
247
+ target.evaluate();
201
248
  }
202
249
  else if (target instanceof Animator) {
203
250
  target.setFloat("scroll", value);
@@ -236,4 +283,248 @@ export class ScrollFollow extends Behaviour {
236
283
  }
237
284
  }
238
285
 
286
+
287
+
288
+ private handleTimelineTarget(director: PlayableDirector, value: number) {
289
+
290
+ const duration = director.duration;
291
+
292
+
293
+ let scrollRegionStart = Infinity;
294
+ let scrollRegionEnd = 0;
295
+ markersArray.length = 0;
296
+
297
+ // querySelectorResults.length = 0;
298
+ let markerIndex = 0;
299
+
300
+ // https://scroll-driven-animations.style/tools/view-timeline/ranges
301
+ for (const marker of director.foreachMarker<ScrollMarkerModel & { element?: HTMLElement | null, needsUpdate?: boolean, timeline?: ViewTimeline }>("ScrollMarker")) {
302
+
303
+ const index = markerIndex++;
304
+
305
+ // Get marker elements from DOM
306
+ if ((marker.element === undefined || marker.needsUpdate === true || /** element is not in DOM anymore? */ (!marker.element?.parentNode))) {
307
+ marker.needsUpdate = false;
308
+ try {
309
+ 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
+ // }
322
+ }
323
+ catch (error) {
324
+ marker.element = null;
325
+ console.error("ScrollMarker selector is not valid: " + marker.name + "\n", error);
326
+ }
327
+ }
328
+
329
+ // skip markers without element (e.g. if the selector didn't return any element)
330
+ if (!marker.element) continue;
331
+
332
+ markersArray.push(marker);
333
+
334
+ const top = marker.element.offsetTop;
335
+ const height = marker.element.offsetHeight;
336
+ const bottom = top + height;
337
+ if (top < scrollRegionStart) {
338
+ scrollRegionStart = top;
339
+ }
340
+ if (bottom > scrollRegionEnd) {
341
+ scrollRegionEnd = bottom;
342
+ }
343
+ }
344
+
345
+
346
+
347
+ const currentTop = this._scrollValue;
348
+ const currentBottom = currentTop + this._scrollContainerHeight;
349
+
350
+ weightsArray.length = 0;
351
+ let sum = 0;
352
+
353
+ // 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
+ let markerCount = 0;
355
+ for (const marker of markersArray) {
356
+
357
+ if (!marker.element) continue;
358
+
359
+ markerCount += 1;
360
+
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;
373
+ // if(this.context.time.frame % 10 === 0) console.log(marker.element?.className, timeline, calculateTimelinePositionNormalized(timeline!));
374
+
375
+ const top = marker.element.offsetTop;
376
+ const height = marker.element.offsetHeight;
377
+ const bottom = top + height;
378
+ let overlap = 0;
379
+
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
381
+
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
+ }
396
+
397
+ markerCount += 1;
398
+
399
+ if (overlap > 0) {
400
+ weightsArray.push({ time: marker.time, weight: overlap });
401
+ sum += overlap;
402
+ }
403
+ }
404
+
405
+ if (weightsArray.length <= 0 && markerCount <= 0) {
406
+ director.time = value * duration;
407
+ }
408
+ else if (weightsArray.length > 0) {
409
+ // 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;
417
+ }
418
+ // console.log(time.toFixed(2), [...weightsArray])
419
+ if (this.damping <= 0)
420
+ director.time = time;
421
+ else
422
+ director.time = Mathf.lerp(director.time, time, this.context.time.deltaTime / this.damping);
423
+ }
424
+ }
425
+
426
+ }
427
+
428
+
429
+
430
+ const weightsArray: OverlapInfo[] = [];
431
+ const markersArray: (ScrollMarkerModel & { element?: HTMLElement | null, timeline?: ViewTimeline })[] = [];
432
+
433
+ type OverlapInfo = {
434
+ /** Marker time */
435
+ time: number,
436
+ /** Overlap in pixels */
437
+ weight: number,
438
+ }
439
+
440
+
441
+ // type SelectorCache = {
442
+ // /** The selector used to query the *elements */
443
+ // selector: string,
444
+ // elements: Element[] | null,
445
+ // usedElementCount: number,
446
+ // }
447
+ // const querySelectorResults: Array<SelectorCache> = [];
448
+
449
+ const needleScrollMarkerCacheKey = "data-timeline-marker";
450
+ const needleScrollMarkerIndexCache = new Map<number, Element | null>();
451
+ const needleScrollMarkerNameCache = new Map<string, Element | null>();
452
+ let needsScrollMarkerRefresh = true;
453
+
454
+ function tryGetElementsForSelector(index: number, name: string): Element | null {
455
+
456
+ if (!needsScrollMarkerRefresh) {
457
+ let element = name?.length ? needleScrollMarkerNameCache.get(name) : null;
458
+ if (element) return element;
459
+ element = needleScrollMarkerIndexCache.get(index) || null;
460
+ return element;
461
+ }
462
+ needsScrollMarkerRefresh = false;
463
+ needleScrollMarkerIndexCache.clear();
464
+ const markers = document.querySelectorAll(`[data-timeline-marker]`);
465
+ markers.forEach((m, i) => {
466
+ needleScrollMarkerIndexCache.set(i, m);
467
+ const name = m.getAttribute("data-timeline-marker");
468
+ if (name?.length) needleScrollMarkerNameCache.set(name, m);
469
+ });
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;
490
+ }
491
+
492
+
493
+ // #region ScrollTimeline
494
+
495
+ function calculateTimelinePositionNormalized(timeline: ViewTimeline) {
496
+ if (!timeline.source) return 0;
497
+ const currentTime = timeline.currentTime;
498
+ const duration = timeline.duration;
499
+ let durationValue = 1;
500
+ if (duration.unit === "seconds") {
501
+ durationValue = duration.value;
502
+ }
503
+ else if (duration.unit === "percent") {
504
+ durationValue = duration.value;
505
+ }
506
+ const t01 = currentTime.unit === "seconds" ? (currentTime.value / durationValue) : (currentTime.value / 100);
507
+ return t01;
508
+ }
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
+
520
+
521
+ declare global {
522
+ interface ViewTimeline {
523
+ axis: 'block' | 'inline';
524
+ currentTime: { unit: 'seconds' | 'percent', value: number };
525
+ duration: { unit: 'seconds' | 'percent', value: number };
526
+ source: Element | null;
527
+ startOffset: { unit: 'px', value: number };
528
+ endOffset: { unit: 'px', value: number };
529
+ }
239
530
  }