@needle-tools/engine 5.1.0-alpha.7 → 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.
- package/CHANGELOG.md +17 -0
- package/components.needle.json +1 -1
- package/dist/{materialx-CE2sUv2B.umd.cjs → materialx-B47Bz-xs.umd.cjs} +1 -1
- package/dist/{materialx-u1EqYrhu.min.js → materialx-B85WjP7E.min.js} +1 -1
- package/dist/{materialx-vyB2Zbt4.js → materialx-jkHmVPez.js} +1 -1
- package/dist/{needle-engine.bundle-BNj2FJS7.min.js → needle-engine.bundle-BotdnwVF.min.js} +173 -173
- package/dist/{needle-engine.bundle-tHGdwr9a.umd.cjs → needle-engine.bundle-DagaNrZK.umd.cjs} +147 -147
- package/dist/{needle-engine.bundle-BwS5IsLn.js → needle-engine.bundle-mtqHZ45d.js} +7832 -7624
- package/dist/needle-engine.d.ts +301 -23
- package/dist/needle-engine.js +575 -565
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/dist/{vendor-ButPLzor.umd.cjs → vendor-1UvpPPSB.umd.cjs} +2 -2
- package/dist/{vendor-DqZC4Is7.min.js → vendor-BQ2Vuntm.min.js} +4 -4
- package/dist/{vendor-DkWSNjMV.js → vendor-BslSKZPo.js} +113 -106
- package/lib/engine/api.d.ts +1 -1
- package/lib/engine/api.js +1 -1
- package/lib/engine/api.js.map +1 -1
- package/lib/engine/engine_audio.d.ts +30 -1
- package/lib/engine/engine_audio.js +57 -13
- package/lib/engine/engine_audio.js.map +1 -1
- package/lib/engine/engine_init.js +2 -2
- package/lib/engine/engine_init.js.map +1 -1
- package/lib/engine/engine_instantiate_resolve.js +40 -10
- package/lib/engine/engine_instantiate_resolve.js.map +1 -1
- package/lib/engine/engine_license.d.ts +7 -7
- package/lib/engine/engine_license.js +72 -72
- package/lib/engine/engine_license.js.map +1 -1
- package/lib/engine/engine_math.d.ts +1 -1
- package/lib/engine/engine_math.js +10 -4
- package/lib/engine/engine_math.js.map +1 -1
- package/lib/engine/engine_networking_blob.js +3 -3
- package/lib/engine/engine_networking_blob.js.map +1 -1
- package/lib/engine/engine_scenedata.js +7 -0
- package/lib/engine/engine_scenedata.js.map +1 -1
- package/lib/engine/engine_serialization_core.js +7 -2
- package/lib/engine/engine_serialization_core.js.map +1 -1
- package/lib/engine/engine_time_utils.d.ts +10 -6
- package/lib/engine/engine_time_utils.js +10 -6
- package/lib/engine/engine_time_utils.js.map +1 -1
- package/lib/engine/engine_utils_qrcode.js +2 -2
- package/lib/engine/engine_utils_qrcode.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js +2 -2
- package/lib/engine/webcomponents/needle menu/needle-menu-spatial.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.js +5 -5
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.js +2 -2
- package/lib/engine/webcomponents/needle-engine.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.loading.js +2 -2
- package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
- package/lib/engine/xr/TempXRContext.js +2 -2
- package/lib/engine/xr/TempXRContext.js.map +1 -1
- package/lib/engine-components/AudioSource.d.ts +46 -2
- package/lib/engine-components/AudioSource.js +268 -60
- package/lib/engine-components/AudioSource.js.map +1 -1
- package/lib/engine-components/DragControls.js +2 -2
- package/lib/engine-components/DragControls.js.map +1 -1
- package/lib/engine-components/DragControlsConstraints.d.ts +0 -15
- package/lib/engine-components/DragControlsConstraints.js +14 -8
- package/lib/engine-components/DragControlsConstraints.js.map +1 -1
- package/lib/engine-components/EventList.js +1 -1
- package/lib/engine-components/EventList.js.map +1 -1
- package/lib/engine-components/SceneSwitcher.js +17 -2
- package/lib/engine-components/SceneSwitcher.js.map +1 -1
- package/lib/engine-components/codegen/components.d.ts +10 -0
- package/lib/engine-components/codegen/components.js +10 -0
- package/lib/engine-components/codegen/components.js.map +1 -1
- package/lib/engine-components/export/usdz/USDZExporter.js +4 -4
- package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
- package/lib/engine-components/timeline/PlayableDirector.d.ts +6 -0
- package/lib/engine-components/timeline/PlayableDirector.js +25 -12
- package/lib/engine-components/timeline/PlayableDirector.js.map +1 -1
- package/lib/engine-components/web/ScrollFollow.d.ts +33 -0
- package/lib/engine-components/web/ScrollFollow.js +164 -22
- package/lib/engine-components/web/ScrollFollow.js.map +1 -1
- package/lib/needle-app.d.ts +18 -0
- package/lib/needle-app.js +19 -0
- package/lib/needle-app.js.map +1 -0
- package/package.json +3 -2
- package/plugins/common/license.js +25 -4
- package/plugins/common/needle-engine.js +5 -2
- package/plugins/types/userconfig.d.ts +22 -0
- package/plugins/vite/dts-generator.d.ts +24 -0
- package/plugins/vite/dts-generator.js +40 -7
- package/plugins/vite/index.d.ts +2 -0
- package/plugins/vite/index.js +17 -0
- package/plugins/vite/license.js +4 -4
- package/plugins/vite/needle-app.js +78 -5
- package/plugins/vite/remote.d.ts +28 -0
- package/plugins/vite/remote.js +124 -0
- package/src/engine/api.ts +1 -1
- package/src/engine/engine_audio.ts +68 -15
- package/src/engine/engine_init.ts +2 -2
- package/src/engine/engine_instantiate_resolve.ts +37 -10
- package/src/engine/engine_license.ts +69 -69
- package/src/engine/engine_math.ts +11 -4
- package/src/engine/engine_networking_blob.ts +3 -3
- package/src/engine/engine_scenedata.ts +7 -2
- package/src/engine/engine_serialization_core.ts +7 -2
- package/src/engine/engine_time_utils.ts +10 -6
- package/src/engine/engine_utils_qrcode.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu-spatial.ts +2 -2
- package/src/engine/webcomponents/needle menu/needle-menu.ts +5 -5
- package/src/engine/webcomponents/needle-engine.loading.ts +6 -6
- package/src/engine/webcomponents/needle-engine.ts +2 -2
- package/src/engine/xr/TempXRContext.ts +2 -2
- package/src/engine-components/AudioSource.ts +256 -55
- package/src/engine-components/DragControls.ts +3 -2
- package/src/engine-components/DragControlsConstraints.ts +16 -8
- package/src/engine-components/EventList.ts +1 -1
- package/src/engine-components/SceneSwitcher.ts +14 -2
- package/src/engine-components/codegen/components.ts +10 -0
- package/src/engine-components/export/usdz/USDZExporter.ts +4 -4
- package/src/engine-components/timeline/PlayableDirector.ts +23 -12
- 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 {
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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";
|
|
@@ -120,9 +120,11 @@ export class PlayableDirector extends Behaviour {
|
|
|
120
120
|
if (this._playableAsset !== value) {
|
|
121
121
|
this._playableAsset = value;
|
|
122
122
|
this._needsGraphRebuild = true;
|
|
123
|
+
this._didWarnInvalid = false;
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
private _playableAsset?: Models.TimelineAssetModel;
|
|
127
|
+
private _didWarnInvalid = false;
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
130
|
* When true, the timeline starts playing automatically when the component awakens.
|
|
@@ -195,24 +197,32 @@ export class PlayableDirector extends Behaviour {
|
|
|
195
197
|
awake(): void {
|
|
196
198
|
if (debug) console.log(`[Timeline] Awake '${this.name}'`, this);
|
|
197
199
|
|
|
200
|
+
// Note: we intentionally do NOT warn here if there are no tracks yet. A playableAsset can
|
|
201
|
+
// legitimately be assigned after awake (e.g. via TimelineBuilder.install() or deserialization),
|
|
202
|
+
// see rebuildGraph() in play()/evaluate(). The validity warning lives in play()/evaluate(),
|
|
203
|
+
// which are the points where a still-empty director actually indicates a misconfiguration.
|
|
198
204
|
this.rebuildGraph();
|
|
205
|
+
}
|
|
199
206
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Returns true if the director has a valid playable asset. When invalid, warns once (dev only)
|
|
209
|
+
* that the director has no tracks — called from the user-facing play()/evaluate() entry points.
|
|
210
|
+
*/
|
|
211
|
+
private warnIfInvalid(method: string): boolean {
|
|
212
|
+
if (this.isValid()) return true;
|
|
213
|
+
if (!this._didWarnInvalid && (debug || isDevEnvironment())) {
|
|
214
|
+
this._didWarnInvalid = true;
|
|
215
|
+
if (!this.playableAsset?.tracks?.length)
|
|
216
|
+
console.warn(`PlayableDirector '${this.name}' has no tracks — assign a playableAsset (or build one via TimelineBuilder) before calling ${method}().`, this);
|
|
217
|
+
else
|
|
218
|
+
console.warn(`PlayableDirector '${this.name}' is not valid`, this);
|
|
210
219
|
}
|
|
220
|
+
return false;
|
|
211
221
|
}
|
|
212
222
|
|
|
213
223
|
/** @internal */
|
|
214
224
|
onEnable() {
|
|
215
|
-
if (debug) console.log("[Timeline] OnEnable", this.name, this.playOnAwake);
|
|
225
|
+
if (debug) console.log("[Timeline] OnEnable", { name: this.name, playOnAwake: this.playOnAwake });
|
|
216
226
|
|
|
217
227
|
for (const tracks of this._allTracks) {
|
|
218
228
|
for (const track of tracks) {
|
|
@@ -276,7 +286,7 @@ export class PlayableDirector extends Behaviour {
|
|
|
276
286
|
* If the timeline is already playing this method does nothing.
|
|
277
287
|
*/
|
|
278
288
|
async play() {
|
|
279
|
-
if (!this.
|
|
289
|
+
if (!this.warnIfInvalid("play")) return;
|
|
280
290
|
// Ensure graph is built (handles the case where playableAsset is assigned after awake)
|
|
281
291
|
if (this._needsGraphRebuild) this.rebuildGraph();
|
|
282
292
|
const pauseChanged = this._isPaused == true;
|
|
@@ -354,6 +364,7 @@ export class PlayableDirector extends Behaviour {
|
|
|
354
364
|
* Evaluate the timeline at the current time. This is useful when you want to manually update the timeline e.g. when the timeline is paused and you set `time` to a new value.
|
|
355
365
|
*/
|
|
356
366
|
evaluate() {
|
|
367
|
+
if (!this.warnIfInvalid("evaluate")) return;
|
|
357
368
|
if (this._needsGraphRebuild) this.rebuildGraph();
|
|
358
369
|
this.internalEvaluate(true);
|
|
359
370
|
}
|
|
@@ -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
|
-
|
|
201
|
+
this.rebindScrollListener();
|
|
145
202
|
this._appliedValue = -1;
|
|
146
203
|
this._needsUpdate = true;
|
|
147
204
|
}
|
|
148
205
|
|
|
149
206
|
/** @internal */
|
|
150
207
|
onDisable() {
|
|
151
|
-
|
|
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 =
|
|
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 -
|
|
347
|
+
this._target_value = -rect.top / (rect.height - win.innerHeight);
|
|
227
348
|
break;
|
|
228
349
|
}
|
|
229
350
|
}
|
|
230
351
|
else {
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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 =
|
|
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
|
|