@needle-tools/engine 5.1.0-alpha.8 → 5.1.0-alpha.9

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 (111) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/components.needle.json +1 -1
  3. package/dist/{materialx-CE2sUv2B.umd.cjs → materialx-B47Bz-xs.umd.cjs} +1 -1
  4. package/dist/{materialx-u1EqYrhu.min.js → materialx-B85WjP7E.min.js} +1 -1
  5. package/dist/{materialx-vyB2Zbt4.js → materialx-jkHmVPez.js} +1 -1
  6. package/dist/{needle-engine.bundle-BuI2pjv1.min.js → needle-engine.bundle-BotdnwVF.min.js} +172 -172
  7. package/dist/{needle-engine.bundle-DQy9vQdJ.umd.cjs → needle-engine.bundle-DagaNrZK.umd.cjs} +145 -145
  8. package/dist/{needle-engine.bundle-3LOy9tj8.js → needle-engine.bundle-mtqHZ45d.js} +6580 -6392
  9. package/dist/needle-engine.d.ts +295 -23
  10. package/dist/needle-engine.js +592 -582
  11. package/dist/needle-engine.min.js +1 -1
  12. package/dist/needle-engine.umd.cjs +1 -1
  13. package/dist/{vendor-ButPLzor.umd.cjs → vendor-1UvpPPSB.umd.cjs} +2 -2
  14. package/dist/{vendor-DqZC4Is7.min.js → vendor-BQ2Vuntm.min.js} +4 -4
  15. package/dist/{vendor-DkWSNjMV.js → vendor-BslSKZPo.js} +113 -106
  16. package/lib/engine/api.d.ts +1 -1
  17. package/lib/engine/api.js +1 -1
  18. package/lib/engine/api.js.map +1 -1
  19. package/lib/engine/engine_audio.d.ts +30 -1
  20. package/lib/engine/engine_audio.js +57 -13
  21. package/lib/engine/engine_audio.js.map +1 -1
  22. package/lib/engine/engine_init.js +2 -2
  23. package/lib/engine/engine_init.js.map +1 -1
  24. package/lib/engine/engine_license.d.ts +7 -7
  25. package/lib/engine/engine_license.js +72 -72
  26. package/lib/engine/engine_license.js.map +1 -1
  27. package/lib/engine/engine_math.d.ts +1 -1
  28. package/lib/engine/engine_math.js +10 -4
  29. package/lib/engine/engine_math.js.map +1 -1
  30. package/lib/engine/engine_networking_blob.js +3 -3
  31. package/lib/engine/engine_networking_blob.js.map +1 -1
  32. package/lib/engine/engine_scenedata.js +7 -0
  33. package/lib/engine/engine_scenedata.js.map +1 -1
  34. package/lib/engine/engine_serialization_core.js +7 -2
  35. package/lib/engine/engine_serialization_core.js.map +1 -1
  36. package/lib/engine/engine_time_utils.d.ts +10 -6
  37. package/lib/engine/engine_time_utils.js +10 -6
  38. package/lib/engine/engine_time_utils.js.map +1 -1
  39. package/lib/engine/engine_utils_qrcode.js +2 -2
  40. package/lib/engine/engine_utils_qrcode.js.map +1 -1
  41. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
  42. package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
  43. package/lib/engine/webcomponents/needle menu/needle-menu.js +5 -5
  44. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  45. package/lib/engine/webcomponents/needle-engine.js +2 -2
  46. package/lib/engine/webcomponents/needle-engine.js.map +1 -1
  47. package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
  48. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  49. package/lib/engine/xr/TempXRContext.js +2 -2
  50. package/lib/engine/xr/TempXRContext.js.map +1 -1
  51. package/lib/engine-components/AudioSource.d.ts +46 -2
  52. package/lib/engine-components/AudioSource.js +268 -60
  53. package/lib/engine-components/AudioSource.js.map +1 -1
  54. package/lib/engine-components/DragControls.js +2 -2
  55. package/lib/engine-components/DragControls.js.map +1 -1
  56. package/lib/engine-components/DragControlsConstraints.d.ts +0 -15
  57. package/lib/engine-components/DragControlsConstraints.js +14 -8
  58. package/lib/engine-components/DragControlsConstraints.js.map +1 -1
  59. package/lib/engine-components/EventList.js +1 -1
  60. package/lib/engine-components/EventList.js.map +1 -1
  61. package/lib/engine-components/SceneSwitcher.js +17 -2
  62. package/lib/engine-components/SceneSwitcher.js.map +1 -1
  63. package/lib/engine-components/codegen/components.d.ts +10 -0
  64. package/lib/engine-components/codegen/components.js +10 -0
  65. package/lib/engine-components/codegen/components.js.map +1 -1
  66. package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
  67. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  68. package/lib/engine-components/timeline/PlayableDirector.js +1 -1
  69. package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
  70. package/lib/engine-components/web/ScrollFollow.d.ts +33 -0
  71. package/lib/engine-components/web/ScrollFollow.js +164 -22
  72. package/lib/engine-components/web/ScrollFollow.js.map +1 -1
  73. package/lib/needle-app.d.ts +18 -0
  74. package/lib/needle-app.js +19 -0
  75. package/lib/needle-app.js.map +1 -0
  76. package/package.json +3 -2
  77. package/plugins/common/license.js +25 -4
  78. package/plugins/common/needle-engine.js +5 -2
  79. package/plugins/types/userconfig.d.ts +22 -0
  80. package/plugins/vite/dts-generator.d.ts +24 -0
  81. package/plugins/vite/dts-generator.js +40 -7
  82. package/plugins/vite/index.d.ts +2 -0
  83. package/plugins/vite/index.js +17 -0
  84. package/plugins/vite/license.js +4 -4
  85. package/plugins/vite/needle-app.js +78 -5
  86. package/plugins/vite/remote.d.ts +28 -0
  87. package/plugins/vite/remote.js +124 -0
  88. package/src/engine/api.ts +1 -1
  89. package/src/engine/engine_audio.ts +68 -15
  90. package/src/engine/engine_init.ts +2 -2
  91. package/src/engine/engine_license.ts +69 -69
  92. package/src/engine/engine_math.ts +11 -4
  93. package/src/engine/engine_networking_blob.ts +3 -3
  94. package/src/engine/engine_scenedata.ts +7 -2
  95. package/src/engine/engine_serialization_core.ts +7 -2
  96. package/src/engine/engine_time_utils.ts +10 -6
  97. package/src/engine/engine_utils_qrcode.ts +2 -2
  98. package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
  99. package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
  100. package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
  101. package/src/engine/webcomponents/needle-engine.ts +2 -2
  102. package/src/engine/xr/TempXRContext.ts +2 -2
  103. package/src/engine-components/AudioSource.ts +256 -55
  104. package/src/engine-components/DragControls.ts +3 -2
  105. package/src/engine-components/DragControlsConstraints.ts +16 -8
  106. package/src/engine-components/EventList.ts +1 -1
  107. package/src/engine-components/SceneSwitcher.ts +14 -2
  108. package/src/engine-components/codegen/components.ts +10 -0
  109. package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
  110. package/src/engine-components/timeline/PlayableDirector.ts +1 -1
  111. package/src/engine-components/web/ScrollFollow.ts +168 -22
@@ -3,7 +3,7 @@ import { Euler, Material, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from "t
3
3
 
4
4
  import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../../../engine/debug/index.js";
5
5
  import { findObjectOfType } from "../../../engine/engine_components.js";
6
- import { _$QyV } from "../../../engine/engine_license.js";
6
+ import { rkHTARX } from "../../../engine/engine_license.js";
7
7
  import { serializable } from "../../../engine/engine_serialization.js";
8
8
  import { getFormattedDate, Progress } from "../../../engine/engine_time_utils.js";
9
9
  import { DeviceUtilities, getParam } from "../../../engine/engine_utils.js";
@@ -277,7 +277,7 @@ export class USDZExporter extends Behaviour {
277
277
  let name = this.exportFileName ?? this.objectToExport?.name ?? this.name;
278
278
  name += "-" + getFormattedDate(); // seems iOS caches the file in some cases, this ensures we always have a fresh file
279
279
 
280
- if (!_$QyV()) {
280
+ if (!rkHTARX()) {
281
281
  if (name !== "") name += "-";
282
282
  name += "MadeWithNeedle";
283
283
  }
@@ -682,7 +682,7 @@ export class USDZExporter extends Behaviour {
682
682
  if (debug)
683
683
  showBalloonMessage("Quicklook url: " + callToActionURL);
684
684
  if (callToActionURL) {
685
- if (!_$QyV()) {
685
+ if (!rkHTARX()) {
686
686
  console.warn("Quicklook closed: custom redirects require a Needle Engine Pro license: https://needle.tools/pricing", callToActionURL)
687
687
  }
688
688
  else {
@@ -697,7 +697,7 @@ export class USDZExporter extends Behaviour {
697
697
  private buildQuicklookOverlay(): CustomBranding {
698
698
  const obj: CustomBranding = {};
699
699
  if (this.customBranding) Object.assign(obj, this.customBranding);
700
- if (!_$QyV()) {
700
+ if (!rkHTARX()) {
701
701
  console.log("Custom Quicklook banner text requires pro license: https://needle.tools/pricing");
702
702
  obj.callToAction = "Close";
703
703
  obj.checkoutTitle = "🌵 Made with Needle";
@@ -222,7 +222,7 @@ export class PlayableDirector extends Behaviour {
222
222
 
223
223
  /** @internal */
224
224
  onEnable() {
225
- if (debug) console.log("[Timeline] OnEnable", this.name, this.playOnAwake);
225
+ if (debug) console.log("[Timeline] OnEnable", { name: this.name, playOnAwake: this.playOnAwake });
226
226
 
227
227
  for (const tracks of this._allTracks) {
228
228
  for (const track of tracks) {
@@ -1,12 +1,43 @@
1
1
  // For firefox ViewTimeline support — dynamic import to avoid SSR crashes (polyfill accesses window at module level)
2
2
  if (typeof window !== "undefined") import("scroll-timeline-polyfill/dist/scroll-timeline.js");
3
3
 
4
+ // ─────────────────────────────────────────────────────────────────────────────────────────────
5
+ // NOTE / TODO — ScrollMarker tracking vs. the parent window (useTopWindowScroll)
6
+ //
7
+ // The ScrollMarker feature (see handleTimelineTarget) currently relies on `ViewTimeline` to know
8
+ // how far a marker element has travelled through the viewport. This import polyfills `ViewTimeline`
9
+ // on THIS window only — it never patches the parent/top window. So when `useTopWindowScroll` is on
10
+ // and we resolve markers against the host page (top document), tracking only works if the *host*
11
+ // window also has `ViewTimeline`:
12
+ // - Chromium host (native ViewTimeline) ............. works
13
+ // - Firefox/Safari host without the polyfill ........ does NOT work → falls back to linear scroll
14
+ // - cross-origin host ............................... impossible (no access to top.document at all)
15
+ // We cannot reliably install the polyfill onto a (same-origin) parent from inside the iframe — it is
16
+ // designed to patch its own realm at import time, and doing it cross-realm is fragile.
17
+ //
18
+ // POSSIBLE CHANGE: drop `ViewTimeline` entirely and compute the marker's viewport progress manually
19
+ // with getBoundingClientRect, e.g. for the default "cover 0%→100%" range:
20
+ // const rect = el.getBoundingClientRect();
21
+ // const vh = scroller.clientHeight; // top's viewport when useTopWindowScroll
22
+ // const t01 = (vh - rect.top) / (vh + rect.height);
23
+ // This is mathematically equivalent to the current ViewTimeline values (0 = entering from bottom,
24
+ // 0.5 = centered, 1 = exited past top — matches the existing `1 - |t-0.5|*2` weighting).
25
+ // PROS: works with top/parent content in every browser (same-origin); removes the
26
+ // scroll-timeline-polyfill dependency and this SSR-risky top-level import; cheaper
27
+ // (no ViewTimeline objects / WeakMap per marker). `ViewTimeline` is used ONLY in this file.
28
+ // CONS: re-implements view-progress ourselves instead of leaning on the platform/polyfill, so any
29
+ // future spec range features (cover/contain/entry/exit, named ranges) would have to be
30
+ // reproduced by hand; needs careful re-testing against the existing scrollytelling demos.
31
+ // NOTE: cross-origin embeds still cannot work either way — that needs a postMessage scroll bridge.
32
+ // ─────────────────────────────────────────────────────────────────────────────────────────────
33
+
4
34
  import { Box3, Object3D } from "three";
5
35
 
6
36
  import { isDevEnvironment } from "../../engine/debug/debug.js";
7
37
  import { Mathf } from "../../engine/engine_math.js";
8
38
  import { serializable } from "../../engine/engine_serialization.js";
9
39
  import { getBoundingBox, setVisibleInCustomShadowRendering } from "../../engine/engine_three_utils.js";
40
+ import { validate } from "../../engine/engine_util_decorator.js";
10
41
  import { getParam } from "../../engine/engine_utils.js";
11
42
  import { Animation } from "../Animation.js";
12
43
  import { Animator } from "../Animator.js";
@@ -115,6 +146,25 @@ export class ScrollFollow extends Behaviour {
115
146
  @serializable()
116
147
  mode: "window" = "window";
117
148
 
149
+ /**
150
+ * Read the scroll position from the **top-level window** instead of the window Needle Engine
151
+ * runs in. This matters when the canvas is embedded in an iframe:
152
+ * - When the page is **not** embedded this has no effect — the top window *is* the current window.
153
+ * - When embedded in a **same-origin** iframe, the component follows the *host page's* scroll,
154
+ * which is almost always the intent (an embedded iframe usually isn't scrollable itself).
155
+ *
156
+ * Set this to `false` to always follow the engine's own window scroll, e.g. when the iframe
157
+ * itself is scrollable and you want to follow its own scroll rather than the host's.
158
+ *
159
+ * If the top window is **cross-origin** (and therefore inaccessible), the component
160
+ * transparently falls back to the local window and logs a warning in development —
161
+ * cross-origin embeds would require a `postMessage` scroll bridge from the host page.
162
+ * @default true
163
+ */
164
+ @validate()
165
+ @serializable()
166
+ useTopWindowScroll: boolean = true;
167
+
118
168
  /**
119
169
  * Event fired when the scroll position changes
120
170
  */
@@ -134,6 +184,13 @@ export class ScrollFollow extends Behaviour {
134
184
  private _needsUpdate = false;
135
185
  private _firstUpdate = false;
136
186
 
187
+ /** The window whose scroll position currently drives this component (resolved from {@link useTopWindowScroll}). */
188
+ private _scrollWindow: Window | null = null;
189
+ /** The window the `wheel` listener is attached to — kept so we remove it from the same window. */
190
+ private _wheelTarget: Window | null = null;
191
+ private _crossOriginWarned = false;
192
+ private _viewTimelineWarned = false;
193
+
137
194
  awake() {
138
195
  this._firstUpdate = true;
139
196
  }
@@ -141,14 +198,70 @@ export class ScrollFollow extends Behaviour {
141
198
 
142
199
  /** @internal */
143
200
  onEnable() {
144
- window.addEventListener("wheel", this.updateCurrentScrollValue, { passive: true });
201
+ this.rebindScrollListener();
145
202
  this._appliedValue = -1;
146
203
  this._needsUpdate = true;
147
204
  }
148
205
 
149
206
  /** @internal */
150
207
  onDisable() {
151
- window.removeEventListener("wheel", this.updateCurrentScrollValue);
208
+ this._wheelTarget?.removeEventListener("wheel", this.updateCurrentScrollValue);
209
+ this._wheelTarget = null;
210
+ }
211
+
212
+ /** @internal */
213
+ onValidate(property?: string) {
214
+ // React to a runtime change of `useTopWindowScroll` while the component is active.
215
+ if (property === "useTopWindowScroll" && this._wheelTarget) {
216
+ this.rebindScrollListener();
217
+ this._needsUpdate = true;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Resolves which browsing context's scroll position should drive this component.
223
+ * Honors {@link useTopWindowScroll}. When the top window is requested but is cross-origin
224
+ * (and therefore inaccessible) it falls back to the local window and warns once —
225
+ * cross-origin embeds would need a `postMessage` scroll bridge from the host page.
226
+ */
227
+ private resolveScrollWindow(): Window {
228
+ if (!this.useTopWindowScroll) return window;
229
+ const top = window.top;
230
+ // Not embedded, or we already *are* the top window.
231
+ if (!top || top === window.self) return window;
232
+ try {
233
+ // Any property access on a cross-origin top window throws a SecurityError.
234
+ void top.document;
235
+ return top;
236
+ }
237
+ catch (_e) {
238
+ if (!this._crossOriginWarned) {
239
+ this._crossOriginWarned = true;
240
+ if (debug || isDevEnvironment()) {
241
+ console.warn("[ScrollFollow] useTopWindowScroll is enabled but the top-level window is cross-origin and cannot be read. Falling back to the local window. Supporting cross-origin embeds requires a postMessage scroll bridge from the host page.");
242
+ }
243
+ }
244
+ return window;
245
+ }
246
+ }
247
+
248
+ /** (Re)resolves the scroll window and moves the `wheel` listener onto it. */
249
+ private rebindScrollListener() {
250
+ const next = this.resolveScrollWindow();
251
+ this._scrollWindow = next;
252
+ if (next === this._wheelTarget) return;
253
+ this._wheelTarget?.removeEventListener("wheel", this.updateCurrentScrollValue);
254
+ this._wheelTarget = next;
255
+ next.addEventListener("wheel", this.updateCurrentScrollValue, { passive: true });
256
+ // The selector cache may point into the previous document — invalidate it.
257
+ this._lastSelectorValue = null;
258
+ this._lastSelectorElement = null;
259
+ if(debug) {
260
+ console.debug(`[ScrollFollow] Scroll listener bound to ${next === window ? "local window" : "top-level window"}`, {
261
+ scrollWindow: this._scrollWindow,
262
+ isTopWindow: next === window,
263
+ });
264
+ }
152
265
  }
153
266
 
154
267
  /** @internal */
@@ -214,26 +327,34 @@ export class ScrollFollow extends Behaviour {
214
327
 
215
328
  private updateCurrentScrollValue = () => {
216
329
 
330
+ const win = this._scrollWindow ?? window;
331
+ const doc = win.document;
332
+ // The page's scrollable extent lives on the root scrolling element (documentElement in
333
+ // standards mode, body in quirks mode) — document.body.scrollHeight is unreliable and is
334
+ // often just the viewport height even when the page is scrolled. scrollingElement resolves
335
+ // to the correct one for the current document mode.
336
+ const scroller = doc.scrollingElement ?? doc.documentElement;
337
+
217
338
  switch (this.mode) {
218
339
  case "window":
219
340
  if (this.htmlSelector?.length) {
220
341
  if (this.htmlSelector !== this._lastSelectorValue) {
221
- this._lastSelectorElement = document.querySelector(this.htmlSelector);
342
+ this._lastSelectorElement = doc.querySelector(this.htmlSelector);
222
343
  this._lastSelectorValue = this.htmlSelector;
223
344
  }
224
345
  if (this._lastSelectorElement) {
225
346
  const rect = this._lastSelectorElement.getBoundingClientRect();
226
- this._target_value = -rect.top / (rect.height - window.innerHeight);
347
+ this._target_value = -rect.top / (rect.height - win.innerHeight);
227
348
  break;
228
349
  }
229
350
  }
230
351
  else {
231
- if (window.document.body.scrollHeight <= window.innerHeight) {
232
- // If the page is not scrollable we can still increment the scroll value to allow triggering timelines etc.
352
+ const maxScroll = scroller.scrollHeight - scroller.clientHeight;
353
+ if (maxScroll <= 0) {
354
+ // Page is not scrollable — keep the current value (still allows triggering timelines etc.).
233
355
  }
234
356
  else {
235
- const diff = window.document.body.scrollHeight - window.innerHeight;
236
- this._target_value = window.scrollY / (diff || 1);
357
+ this._target_value = scroller.scrollTop / maxScroll;
237
358
  }
238
359
  }
239
360
 
@@ -241,6 +362,10 @@ export class ScrollFollow extends Behaviour {
241
362
  }
242
363
 
243
364
  if (isNaN(this._target_value) || !isFinite(this._target_value)) this._target_value = -1;
365
+
366
+ if (debug) {
367
+ console.debug(`[ScrollFollow] Scroll value updated: ${this._target_value.toFixed(5)} (ScrollTop: ${scroller.scrollTop}, ScrollHeight: ${scroller.scrollHeight}, ClientHeight: ${scroller.clientHeight})`);
368
+ }
244
369
  }
245
370
 
246
371
 
@@ -295,6 +420,10 @@ export class ScrollFollow extends Behaviour {
295
420
 
296
421
  private handleTimelineTarget(director: PlayableDirector, value: number) {
297
422
 
423
+ // Resolve markers against the same browsing context we read scroll from (see useTopWindowScroll).
424
+ const scrollWin = this._scrollWindow ?? window;
425
+ const doc = scrollWin.document;
426
+
298
427
  const duration = director.duration;
299
428
  let markersArray = timelineMarkerArrays.get(director);
300
429
 
@@ -314,7 +443,7 @@ export class ScrollFollow extends Behaviour {
314
443
  marker.needsUpdate = false;
315
444
  try {
316
445
  // 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
317
- marker.element = tryGetElementsForSelector(index) as HTMLElement | null;
446
+ marker.element = tryGetElementsForSelector(index, doc) as HTMLElement | null;
318
447
  if (debug) console.debug(`ScrollMarker #${index} (${marker.time.toFixed(2)}) found`, marker.element);
319
448
  if (!marker.element) {
320
449
  if (debug || isDevEnvironment()) console.warn(`No HTML element found for ScrollMarker: ${marker.name} (index ${index})`);
@@ -335,7 +464,7 @@ export class ScrollFollow extends Behaviour {
335
464
 
336
465
  // If the timeline has no markers defined we can use timeline-marker elements in the DOM. These must define times then
337
466
  if (markersArray.length <= 0) {
338
- const markers = document.querySelectorAll(`[data-timeline-marker]`);
467
+ const markers = doc.querySelectorAll(`[data-timeline-marker]`);
339
468
  markers.forEach((element) => {
340
469
  const value = element.getAttribute("data-timeline-marker");
341
470
  const time = parseFloat(value || ("NaN"));
@@ -351,15 +480,32 @@ export class ScrollFollow extends Behaviour {
351
480
  });
352
481
  }
353
482
 
354
- // Init ViewTimeline for markers
355
- for (const marker of markersArray) {
356
- if (marker.element) {
357
- // https://scroll-driven-animations.style/tools/view-timeline/ranges
358
- /** @ts-ignore */
359
- marker.timeline = new ViewTimeline({
360
- subject: marker.element,
361
- axis: 'block', // https://drafts.csswg.org/scroll-animations/#scroll-notation
362
- });
483
+ // Init ViewTimeline for markers. Use the constructor from the *owning* window so that
484
+ // markers living in a same-origin host page (useTopWindowScroll) are tracked in the
485
+ // correct realm rather than against the iframe's own (non-scrolling) document.
486
+ const ViewTimelineCtor = (scrollWin as unknown as { ViewTimeline?: new (opts: { subject: Element, axis: string }) => ViewTimeline }).ViewTimeline;
487
+ if (!ViewTimelineCtor) {
488
+ // ViewTimeline isn't available on the target window yet. This is expected transiently
489
+ // on the "self" path while the async scroll-timeline polyfill is still loading, and
490
+ // permanently when useTopWindowScroll targets a host page without ViewTimeline support.
491
+ // Don't persist the half-built marker set — drop the cache so it's rebuilt next frame
492
+ // (self-heals once the polyfill loads), and fall back to linear scrubbing for now.
493
+ if (!this._viewTimelineWarned && (debug || isDevEnvironment())) {
494
+ this._viewTimelineWarned = true;
495
+ console.warn("[ScrollFollow] ViewTimeline is not available on the target window — falling back to linear scroll. If this persists with useTopWindowScroll, the scroll-timeline polyfill must be present on the host page.");
496
+ }
497
+ timelineMarkerArrays.delete(director);
498
+ markersArray.length = 0;
499
+ }
500
+ else {
501
+ for (const marker of markersArray) {
502
+ if (marker.element) {
503
+ // https://scroll-driven-animations.style/tools/view-timeline/ranges
504
+ marker.timeline = new ViewTimelineCtor({
505
+ subject: marker.element,
506
+ axis: 'block', // https://drafts.csswg.org/scroll-animations/#scroll-notation
507
+ });
508
+ }
363
509
  }
364
510
  }
365
511
  }
@@ -473,7 +619,7 @@ const weightsArray: OverlapInfo[] = [];
473
619
  const needleScrollMarkerCache = new Array<Element>();
474
620
  let needsScrollMarkerRefresh = true;
475
621
 
476
- function tryGetElementsForSelector(index: number): Element | null {
622
+ function tryGetElementsForSelector(index: number, doc: Document): Element | null {
477
623
 
478
624
  if (!needsScrollMarkerRefresh) {
479
625
  const element = needleScrollMarkerCache[index] || null;
@@ -481,12 +627,12 @@ function tryGetElementsForSelector(index: number): Element | null {
481
627
  }
482
628
  needsScrollMarkerRefresh = false;
483
629
  needleScrollMarkerCache.length = 0;
484
- const markers = document.querySelectorAll(`[data-timeline-marker]`);
630
+ const markers = doc.querySelectorAll(`[data-timeline-marker]`);
485
631
  markers.forEach((m, i) => {
486
632
  needleScrollMarkerCache[i] = m;
487
633
  });
488
634
  needsScrollMarkerRefresh = false;
489
- return tryGetElementsForSelector(index);
635
+ return tryGetElementsForSelector(index, doc);
490
636
  }
491
637
 
492
638