// For firefox ViewTimeline support — dynamic import to avoid SSR crashes (polyfill accesses window at module level) if (typeof window !== "undefined") import("scroll-timeline-polyfill/dist/scroll-timeline.js"); // ───────────────────────────────────────────────────────────────────────────────────────────── // NOTE / TODO — ScrollMarker tracking vs. the parent window (useTopWindowScroll) // // The ScrollMarker feature (see handleTimelineTarget) currently relies on `ViewTimeline` to know // how far a marker element has travelled through the viewport. This import polyfills `ViewTimeline` // on THIS window only — it never patches the parent/top window. So when `useTopWindowScroll` is on // and we resolve markers against the host page (top document), tracking only works if the *host* // window also has `ViewTimeline`: // - Chromium host (native ViewTimeline) ............. works // - Firefox/Safari host without the polyfill ........ does NOT work → falls back to linear scroll // - cross-origin host ............................... impossible (no access to top.document at all) // We cannot reliably install the polyfill onto a (same-origin) parent from inside the iframe — it is // designed to patch its own realm at import time, and doing it cross-realm is fragile. // // POSSIBLE CHANGE: drop `ViewTimeline` entirely and compute the marker's viewport progress manually // with getBoundingClientRect, e.g. for the default "cover 0%→100%" range: // const rect = el.getBoundingClientRect(); // const vh = scroller.clientHeight; // top's viewport when useTopWindowScroll // const t01 = (vh - rect.top) / (vh + rect.height); // This is mathematically equivalent to the current ViewTimeline values (0 = entering from bottom, // 0.5 = centered, 1 = exited past top — matches the existing `1 - |t-0.5|*2` weighting). // PROS: works with top/parent content in every browser (same-origin); removes the // scroll-timeline-polyfill dependency and this SSR-risky top-level import; cheaper // (no ViewTimeline objects / WeakMap per marker). `ViewTimeline` is used ONLY in this file. // CONS: re-implements view-progress ourselves instead of leaning on the platform/polyfill, so any // future spec range features (cover/contain/entry/exit, named ranges) would have to be // reproduced by hand; needs careful re-testing against the existing scrollytelling demos. // NOTE: cross-origin embeds still cannot work either way — that needs a postMessage scroll bridge. // ───────────────────────────────────────────────────────────────────────────────────────────── import { Box3, Object3D } from "three"; import { isDevEnvironment } from "../../engine/debug/debug.js"; import { Mathf } from "../../engine/engine_math.js"; import { serializable } from "../../engine/engine_serialization.js"; import { getBoundingBox, setVisibleInCustomShadowRendering } from "../../engine/engine_three_utils.js"; import { validate } from "../../engine/engine_util_decorator.js"; import { getParam } from "../../engine/engine_utils.js"; import { Animation } from "../Animation.js"; import { Animator } from "../Animator.js"; import { AudioSource } from "../AudioSource.js"; import { Behaviour } from "../Component.js"; import { EventList } from "../EventList.js"; import { Light } from "../Light.js"; import { SplineWalker } from "../splines/SplineWalker.js"; import { PlayableDirector } from "../timeline/PlayableDirector.js"; import { ScrollMarkerModel } from "../timeline/TimelineModels.js"; const debug = getParam("debugscroll"); type ScrollFollowEvent = { /** Event type */ type: "change", /** Current scroll value */ value: number, /** ScrollFollow component that raised the event */ component: ScrollFollow, /** Call to prevent invocation of default (e.g. updating targets) */ preventDefault: () => void, defaultPrevented: boolean, } /** * The [ScrollFollow](https://engine.needle.tools/docs/api/ScrollFollow) component allows you to link the scroll position of the page (or a specific element) to one or more target objects. * This can be used to create scroll-based animations, audio playback, or other effects. For example you can link the scroll position to a timeline (PlayableDirector) to create scroll-based storytelling effects or to an Animator component to change the animation state based on scroll. * *  *  * * Assign {@link target} objects to the component to have them updated based on the current scroll position (check the 'target' property for supported types). * * @link Example at https://scrollytelling-2-z23hmxby7c6x-u30ld.needle.run/ * @link Template at https://github.com/needle-engine/scrollytelling-template * @link [Scrollytelling Bike Demo](https://scrollytelling-bike-z23hmxb2gnu5a.needle.run/) * * ## How to use with an Animator * 1. Create an Animator component and set up a float parameter named "scroll". * 2. Create transitions between animation states based on the "scroll" parameter (e.g. from 0 to 1). * 3. Add a ScrollFollow component to the same GameObject or another GameObject in the scene. * 4. Assign the Animator component to the ScrollFollow's target property. * * ## How to use with a PlayableDirector (timeline) * 1. Create a PlayableDirector component and set up a timeline asset. * 2. Add a ScrollFollow component to the same GameObject or another GameObject in the scene. * 3. Assign the PlayableDirector component to the ScrollFollow's target property. * 4. The timeline will now scrub based on the scroll position of the page. * 5. (Optional) Add ScrollMarker markers to your HTML to define specific points in the timeline that correspond to elements on the page. For example: * ```html *