diff --git a/packages/shader-transitions/src/engineModePageComposite.test.ts b/packages/shader-transitions/src/engineModePageComposite.test.ts index 4b6a6b1d50..fc491fb2d7 100644 --- a/packages/shader-transitions/src/engineModePageComposite.test.ts +++ b/packages/shader-transitions/src/engineModePageComposite.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { clonePinStyleFor, + installPageSideCompositor, isPageSideCompositingSupported, PAGE_COMPOSITOR_BUILD_CANARY, PAGE_COMPOSITOR_CANVAS_ID, @@ -90,6 +91,95 @@ describe("clonePinStyleFor", () => { }); }); +describe("page-side compositor seek", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + class FakeScene { + style = { visibility: "hidden" }; + constructor(private readonly attrs: Record) {} + getAttribute(name: string) { + return this.attrs[name] ?? null; + } + } + + // A browser with drawElementImage and a WebGL context whose every call succeeds. + function installWithHiddenScenes( + timing: Record, + ) { + const gl = new Proxy( + {}, + { + get: (_target, key) => { + if (key === "getShaderParameter" || key === "getProgramParameter") return () => true; + if (key === "getExtension") return () => ({ loseContext: () => undefined }); + return () => ({}); + }, + }, + ); + const canvas = () => ({ + style: {}, + width: 0, + height: 0, + layoutSubtree: true, + firstChild: null, + setAttribute: () => undefined, + remove: () => undefined, + getContext: (type: string) => (type === "2d" ? { drawElementImage: () => undefined } : gl), + }); + const scenes = new Map( + Object.entries(timing).map(([id, [start, duration]]) => [ + id, + new FakeScene({ "data-start": start, "data-duration": duration }), + ]), + ); + let startPolling: (() => void) | undefined; + const hf = { seek: vi.fn() }; + vi.stubGlobal("window", { + __hf: hf, + setInterval: (poll: () => void) => { + startPolling = poll; + return 1; + }, + clearInterval: () => undefined, + }); + vi.stubGlobal("HTMLElement", FakeScene); + vi.stubGlobal("document", { + createElement: canvas, + getElementById: (id: string) => scenes.get(id) ?? null, + body: { appendChild: () => undefined }, + }); + const installed = installPageSideCompositor({ + scenes: ["s4", "s5"], + transitions: [{ time: 4.4, duration: 0.8, shader: "domain-warp" }], + bgColor: "#000", + accentColors: { accent: [1, 1, 1], dark: [0, 0, 0], bright: [1, 1, 1] }, + width: 1920, + height: 1080, + defaultDuration: 0.8, + }); + startPolling?.(); + return { installed, hf, scenes }; + } + + // s4 runs 2.8 to 4.8 s, s5 4.4 to 8.0 s; a plain scene plays before and after them. + const film = { s4: ["2.8", "2"], s5: ["4.4", "3.6"] } as const; + + it("leaves the runtime's hide on a shader scene before its window", () => { + const { installed, hf, scenes } = installWithHiddenScenes(film); + expect(installed).toBe(true); + hf.seek(2.4); + expect(scenes.get("s4")?.style.visibility).toBe("hidden"); + }); + + it("leaves the runtime's hide on the last shader scene after its window", () => { + const { hf, scenes } = installWithHiddenScenes(film); + hf.seek(8.8); + expect(scenes.get("s5")?.style.visibility).toBe("hidden"); + }); +}); + describe("page-side compositor exported constants", () => { it("exports a stable canary string used by the bundled-CLI smoke", () => { expect(PAGE_COMPOSITOR_BUILD_CANARY).toBe("__hf_page_compositor_v1__"); diff --git a/packages/shader-transitions/src/engineModePageComposite.ts b/packages/shader-transitions/src/engineModePageComposite.ts index 409c5f52f0..e0bcd6a7fb 100644 --- a/packages/shader-transitions/src/engineModePageComposite.ts +++ b/packages/shader-transitions/src/engineModePageComposite.ts @@ -116,6 +116,7 @@ export function isPageSideCompositingSupported(): boolean { return true; } +// fallow-ignore-next-line complexity export function installPageSideCompositor(options: PageCompositorInstallOptions): boolean { if (typeof window === "undefined") return false; (window as unknown as { __HF_PAGE_COMPOSITOR_CANARY__?: string }).__HF_PAGE_COMPOSITOR_CANARY__ = @@ -226,16 +227,6 @@ export function installPageSideCompositor(options: PageCompositorInstallOptions) return null; } - // Scene on screen at a non-transition time: after the last transition whose - // window has passed. Full transitions list so the index matches scene order. - function settledSceneIdAt(time: number): string | undefined { - let idx = 0; - for (const t of transitions) { - if (time >= t.time + (t.duration ?? defaultDuration)) idx += 1; - } - return scenes[Math.min(idx, scenes.length - 1)]; - } - let currentActive: ResolvedTransition | null = null; let currentProgress = 0; @@ -249,6 +240,7 @@ export function installPageSideCompositor(options: PageCompositorInstallOptions) // injection so cloneNode picks up replacements for