From 8bd5a9ee891ed7013a2f6d1cc67fee017f60e599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 24 Sep 2026 20:29:27 -0400 Subject: [PATCH 1/5] fix(core): paint no timed clip until the runtime decides its visibility Until its first visibility pass, a paused page showed every timed clip at once: an embed without a loader flashed a later scene before the first, and a player's hidden iframe still decoded every scene's images. The runtime now hides timed non-media clips from the moment its script runs, and the first pass that decides them removes that rule, so each clip is shown only by the pass. --- packages/core/src/runtime/entry.test.ts | 70 ++++++++++++++++++++++ packages/core/src/runtime/entry.ts | 4 ++ packages/core/src/runtime/init.ts | 6 ++ packages/core/src/runtime/timedClipHide.ts | 20 +++++++ 4 files changed, 100 insertions(+) create mode 100644 packages/core/src/runtime/entry.test.ts create mode 100644 packages/core/src/runtime/timedClipHide.ts diff --git a/packages/core/src/runtime/entry.test.ts b/packages/core/src/runtime/entry.test.ts new file mode 100644 index 0000000000..97bb1cd668 --- /dev/null +++ b/packages/core/src/runtime/entry.test.ts @@ -0,0 +1,70 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { RuntimeTimelineLike } from "./types"; + +function pausedTimeline(duration: number): RuntimeTimelineLike { + let time = 0; + return { + play: () => {}, + pause: () => {}, + seek: (t?: number) => (t === undefined ? time : (time = t)), + totalTime: (t?: number) => (t === undefined ? time : (time = t)), + time: () => time, + duration: () => duration, + add: () => {}, + paused: () => true, + timeScale: () => {}, + set: () => {}, + getChildren: () => [], + }; +} + +function clip(parent: Element, start: string): HTMLElement { + const el = document.createElement("div"); + el.className = "clip"; + el.setAttribute("data-start", start); + el.setAttribute("data-duration", "2"); + el.setAttribute("data-track-index", "1"); + parent.appendChild(el); + return el; +} + +describe("runtime entry", () => { + afterEach(() => { + window.__hfRuntimeTeardown?.(); + document.head.innerHTML = ""; + document.body.innerHTML = ""; + window.__timelines = {}; + delete window.__player; + delete window.__playerReady; + delete window.__renderReady; + delete window.__hfTimelinesBuilding; + delete (window as { __hyperframeRuntimeBootstrapped?: boolean }) + .__hyperframeRuntimeBootstrapped; + }); + + it("paints no timed clip until the first visibility pass decides it", async () => { + const root = document.createElement("div"); + root.setAttribute("data-composition-id", "main"); + root.setAttribute("data-root", "true"); + root.setAttribute("data-start", "0"); + root.setAttribute("data-width", "1920"); + root.setAttribute("data-height", "1080"); + document.body.appendChild(root); + const current = clip(root, "0"); + const later = clip(root, "5"); + window.__timelines = { main: pausedTimeline(10) }; + // Readiness, which runs the first pass, waits while GSAP batches timelines. + window.__hfTimelinesBuilding = true; + + vi.resetModules(); + await import("./entry"); + const visibility = () => [current, later].map((el) => getComputedStyle(el).visibility); + expect(window.__renderReady).toBe(false); + expect(visibility()).toEqual(["hidden", "hidden"]); + + window.__hfTimelinesBuilding = false; + window.dispatchEvent(new CustomEvent("hf-timelines-built")); + expect(window.__renderReady).toBe(true); + expect(visibility()).toEqual(["visible", "hidden"]); + }); +}); diff --git a/packages/core/src/runtime/entry.ts b/packages/core/src/runtime/entry.ts index 0d1e95f219..98d8901076 100644 --- a/packages/core/src/runtime/entry.ts +++ b/packages/core/src/runtime/entry.ts @@ -1,5 +1,6 @@ import { initSandboxRuntimeModular } from "./init"; import { installAuthoredOpacityCapture } from "./colorGrading"; +import { hideTimedClipsUntilFirstPass } from "./timedClipHide"; import { fitTextFontSize } from "../text/fitTextFontSize"; import { pretext } from "../text/pretext"; import { getVariables } from "./getVariables"; @@ -26,6 +27,9 @@ type HyperframeWindow = Window & { // at script evaluation time, while the document is still parsing. installAuthoredOpacityCapture(); +// Also at evaluation time: the body is parsed, and may paint, before init runs at DOMContentLoaded. +hideTimedClipsUntilFirstPass(); + // Expose runtime helpers immediately so composition scripts can use them // before DOMContentLoaded (font sizing runs during script evaluation, and // getVariables is read by composition setup before the timeline is built). diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index 076554b167..635d2e7682 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -1,6 +1,7 @@ // fallow-ignore-file code-duplication complexity import { installRuntimeControlBridge, postRuntimeMessage, setRuntimeProtocolFps } from "./bridge"; import { isInClipWindow } from "./clipWindow"; +import { revealTimedClipsAfterFirstPass } from "./timedClipHide"; import { initRuntimeAnalytics, emitAnalyticsEvent } from "./analytics"; import { injectCompositionCssVariables } from "./getVariables"; import { createCssAdapter } from "./adapters/css"; @@ -2452,6 +2453,7 @@ export function initSandboxRuntimeModular(): void { 0, timingRevision, ); + let decidedTimedClip = false; for (const rawNode of visibilityNodes) { if (!isHtmlElement(rawNode)) continue; @@ -2516,6 +2518,9 @@ export function initSandboxRuntimeModular(): void { } } rawNode.style.visibility = isVisibleNow ? "visible" : "hidden"; + if (!isVideoElement(rawNode) && !isImageElement(rawNode) && rawNode.tagName !== "AUDIO") { + decidedTimedClip = true; + } if (isVideoElement(rawNode) || isImageElement(rawNode)) { colorGradingRuntime?.setSourceVisibility(rawNode, isVisibleNow); } @@ -2529,6 +2534,7 @@ export function initSandboxRuntimeModular(): void { timedClipDisplayNoneApplied.add(rawNode); } } + if (decidedTimedClip) revealTimedClipsAfterFirstPass(); // Only when a `data-hidden` mutation actually moved something: the skips // this reschedule exists to re-run are what change the active set, so // firing it otherwise was an audible stop-and-restart across the whole mix diff --git a/packages/core/src/runtime/timedClipHide.ts b/packages/core/src/runtime/timedClipHide.ts new file mode 100644 index 0000000000..dd06824ddf --- /dev/null +++ b/packages/core/src/runtime/timedClipHide.ts @@ -0,0 +1,20 @@ +// Until the first visibility pass decides each timed clip, a paused page would paint every clip at once +// (and decode every scene's images). Media is left out: init's media pass owns it. +const HIDE_UNTIL_FIRST_PASS = + "[data-start]:not(video, audio, img) { visibility: hidden !important; }"; + +let hideStyle: HTMLStyleElement | null = null; + +export function hideTimedClipsUntilFirstPass(): void { + if (hideStyle || typeof document === "undefined") return; + const parent = document.head ?? document.documentElement; + if (!parent) return; + hideStyle = document.createElement("style"); + hideStyle.textContent = HIDE_UNTIL_FIRST_PASS; + parent.appendChild(hideStyle); +} + +export function revealTimedClipsAfterFirstPass(): void { + hideStyle?.remove(); + hideStyle = null; +} From dcc45cbbe288bab1f529e5d1d6640843d2ae9259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 24 Sep 2026 20:31:48 -0400 Subject: [PATCH 2/5] refactor(core): name media with the realm-safe helper in the first-pass check --- packages/core/src/runtime/init.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index 635d2e7682..ce16e6a342 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -2518,9 +2518,7 @@ export function initSandboxRuntimeModular(): void { } } rawNode.style.visibility = isVisibleNow ? "visible" : "hidden"; - if (!isVideoElement(rawNode) && !isImageElement(rawNode) && rawNode.tagName !== "AUDIO") { - decidedTimedClip = true; - } + if (!isMediaElement(rawNode) && !isImageElement(rawNode)) decidedTimedClip = true; if (isVideoElement(rawNode) || isImageElement(rawNode)) { colorGradingRuntime?.setSourceVisibility(rawNode, isVisibleNow); } From bb631ddfa978a86a99deaf4ac71a916d603c5470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 24 Sep 2026 20:56:48 -0400 Subject: [PATCH 3/5] fix(core): share the first-pass hide across runtime copies and shader captures A second evaluation of the runtime (a sub-composition carrying its own runtime tag, or a page that includes it twice) added a hide rule nothing removed, so the film stayed invisible. The rule and its flag now live on the page: a copy that finds either, or finds the runtime already initialised, adds none, and the first pass removes every copy. HyperShader's capture clone forces its scene visible with !important, so a transition captured before the first pass still shows the scene's clips. The runtime test now also pins evaluation-time hiding, inline visibility, media, and two copies. --- packages/core/src/runtime/entry.test.ts | 81 ++++++++++++++----- packages/core/src/runtime/timedClipHide.ts | 26 ++++-- .../src/capture.clone.test.ts | 20 +++++ packages/shader-transitions/src/capture.ts | 7 +- 4 files changed, 104 insertions(+), 30 deletions(-) create mode 100644 packages/shader-transitions/src/capture.clone.test.ts diff --git a/packages/core/src/runtime/entry.test.ts b/packages/core/src/runtime/entry.test.ts index 97bb1cd668..d06bc440b0 100644 --- a/packages/core/src/runtime/entry.test.ts +++ b/packages/core/src/runtime/entry.test.ts @@ -1,3 +1,4 @@ +// fallow-ignore-file code-duplication import { afterEach, describe, expect, it, vi } from "vitest"; import type { RuntimeTimelineLike } from "./types"; @@ -18,8 +19,12 @@ function pausedTimeline(duration: number): RuntimeTimelineLike { }; } -function clip(parent: Element, start: string): HTMLElement { - const el = document.createElement("div"); +function timed( + parent: Element, + tag: K, + start: string, +): HTMLElementTagNameMap[K] { + const el = document.createElement(tag); el.className = "clip"; el.setAttribute("data-start", start); el.setAttribute("data-duration", "2"); @@ -28,6 +33,25 @@ function clip(parent: Element, start: string): HTMLElement { return el; } +function mountRoot(): HTMLElement { + const root = document.createElement("div"); + root.setAttribute("data-composition-id", "main"); + root.setAttribute("data-root", "true"); + root.setAttribute("data-start", "0"); + root.setAttribute("data-width", "1920"); + root.setAttribute("data-height", "1080"); + document.body.appendChild(root); + window.__timelines = { main: pausedTimeline(10) }; + return root; +} + +async function evaluateRuntime(): Promise { + vi.resetModules(); + await import("./entry"); +} + +const visibility = (...els: HTMLElement[]) => els.map((el) => getComputedStyle(el).visibility); + describe("runtime entry", () => { afterEach(() => { window.__hfRuntimeTeardown?.(); @@ -38,33 +62,50 @@ describe("runtime entry", () => { delete window.__playerReady; delete window.__renderReady; delete window.__hfTimelinesBuilding; - delete (window as { __hyperframeRuntimeBootstrapped?: boolean }) - .__hyperframeRuntimeBootstrapped; + const win = window as { + __hyperframeRuntimeBootstrapped?: boolean; + __hfFirstPassHidden?: boolean; + }; + delete win.__hyperframeRuntimeBootstrapped; + delete win.__hfFirstPassHidden; + delete (document as { readyState?: unknown }).readyState; }); - it("paints no timed clip until the first visibility pass decides it", async () => { - const root = document.createElement("div"); - root.setAttribute("data-composition-id", "main"); - root.setAttribute("data-root", "true"); - root.setAttribute("data-start", "0"); - root.setAttribute("data-width", "1920"); - root.setAttribute("data-height", "1080"); - document.body.appendChild(root); - const current = clip(root, "0"); - const later = clip(root, "5"); - window.__timelines = { main: pausedTimeline(10) }; + it("paints no timed clip, from script evaluation until the first visibility pass decides it", async () => { + const root = mountRoot(); + const current = timed(root, "div", "0"); + const later = timed(root, "div", "5"); + const poster = timed(root, "img", "0"); + // A composition script may write visibility inline before the runtime runs. + later.style.visibility = "visible"; // Readiness, which runs the first pass, waits while GSAP batches timelines. window.__hfTimelinesBuilding = true; + Object.defineProperty(document, "readyState", { configurable: true, get: () => "loading" }); - vi.resetModules(); - await import("./entry"); - const visibility = () => [current, later].map((el) => getComputedStyle(el).visibility); + await evaluateRuntime(); + expect(window.__player).toBeUndefined(); + expect(visibility(current, later)).toEqual(["hidden", "hidden"]); + + delete (document as { readyState?: unknown }).readyState; + document.dispatchEvent(new Event("DOMContentLoaded")); expect(window.__renderReady).toBe(false); - expect(visibility()).toEqual(["hidden", "hidden"]); + expect(visibility(current, later, poster)).toEqual(["hidden", "hidden", "visible"]); window.__hfTimelinesBuilding = false; window.dispatchEvent(new CustomEvent("hf-timelines-built")); expect(window.__renderReady).toBe(true); - expect(visibility()).toEqual(["visible", "hidden"]); + expect(visibility(current, later, poster)).toEqual(["visible", "hidden", "visible"]); + }); + + it("leaves nothing hidden when the runtime is evaluated a second time", async () => { + const root = mountRoot(); + const current = timed(root, "div", "0"); + + await evaluateRuntime(); + await evaluateRuntime(); + window.__player?.seek(1); + + expect(visibility(root, current)).toEqual(["visible", "visible"]); + expect(document.querySelectorAll("style[data-hf-first-pass-hide]")).toHaveLength(0); }); }); diff --git a/packages/core/src/runtime/timedClipHide.ts b/packages/core/src/runtime/timedClipHide.ts index dd06824ddf..6be8bea5eb 100644 --- a/packages/core/src/runtime/timedClipHide.ts +++ b/packages/core/src/runtime/timedClipHide.ts @@ -1,20 +1,32 @@ // Until the first visibility pass decides each timed clip, a paused page would paint every clip at once // (and decode every scene's images). Media is left out: init's media pass owns it. +// The rule and its flag live on the page, not in this module: every runtime copy on the page shares them. +const HIDE_ATTR = "data-hf-first-pass-hide"; const HIDE_UNTIL_FIRST_PASS = "[data-start]:not(video, audio, img) { visibility: hidden !important; }"; -let hideStyle: HTMLStyleElement | null = null; +type FirstPassWindow = Window & { + __hfFirstPassHidden?: boolean; + __hyperframeRuntimeBootstrapped?: boolean; +}; export function hideTimedClipsUntilFirstPass(): void { - if (hideStyle || typeof document === "undefined") return; + if (typeof document === "undefined") return; + const win = window as FirstPassWindow; + // A runtime that already initialised may never run another pass to lift a new rule. + if (win.__hfFirstPassHidden || win.__hyperframeRuntimeBootstrapped) return; const parent = document.head ?? document.documentElement; if (!parent) return; - hideStyle = document.createElement("style"); - hideStyle.textContent = HIDE_UNTIL_FIRST_PASS; - parent.appendChild(hideStyle); + const style = document.createElement("style"); + style.setAttribute(HIDE_ATTR, ""); + style.textContent = HIDE_UNTIL_FIRST_PASS; + parent.appendChild(style); + win.__hfFirstPassHidden = true; } export function revealTimedClipsAfterFirstPass(): void { - hideStyle?.remove(); - hideStyle = null; + const win = window as FirstPassWindow; + if (!win.__hfFirstPassHidden) return; + win.__hfFirstPassHidden = false; + for (const style of document.querySelectorAll(`style[${HIDE_ATTR}]`)) style.remove(); } diff --git a/packages/shader-transitions/src/capture.clone.test.ts b/packages/shader-transitions/src/capture.clone.test.ts new file mode 100644 index 0000000000..c5746cc682 --- /dev/null +++ b/packages/shader-transitions/src/capture.clone.test.ts @@ -0,0 +1,20 @@ +// @vitest-environment jsdom +import { describe, expect, it } from "vitest"; +import { forceSceneVisibleInClone } from "./capture.js"; + +describe("forceSceneVisibleInClone", () => { + it("shows the scene and its timed clips while the runtime's first-pass hide rule is up", () => { + document.head.innerHTML = + ""; + document.body.innerHTML = '

Title

'; + const scene = document.getElementById("scene") as HTMLElement; + + forceSceneVisibleInClone(scene, document); + + const title = scene.querySelector("p") as HTMLElement; + expect([scene, title].map((el) => getComputedStyle(el).visibility)).toEqual([ + "visible", + "visible", + ]); + }); +}); diff --git a/packages/shader-transitions/src/capture.ts b/packages/shader-transitions/src/capture.ts index 08aa82caff..740b4ed267 100644 --- a/packages/shader-transitions/src/capture.ts +++ b/packages/shader-transitions/src/capture.ts @@ -49,15 +49,16 @@ export interface CaptureSceneOptions { scale?: number; } -function forceSceneVisibleInClone(source: HTMLElement, cloneDoc: Document): void { +export function forceSceneVisibleInClone(source: HTMLElement, cloneDoc: Document): void { if (!source.id) return; const clone = cloneDoc.getElementById(source.id); if (!(clone instanceof HTMLElement)) return; clone.style.opacity = "1"; - clone.style.visibility = "visible"; + // Important: the runtime hides timed clips with an !important rule until its first visibility pass. + clone.style.setProperty("visibility", "visible", "important"); clone.querySelectorAll("[data-start]").forEach((el) => { - el.style.visibility = "visible"; + el.style.setProperty("visibility", "visible", "important"); }); } From 903d6afb23c116daa2105e5a99c6fc30512d6d32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 24 Sep 2026 21:36:19 -0400 Subject: [PATCH 4/5] fix(core): grade media inside a clip once the first pass shows it Color grading registers only media it can see. Media with no timing of its own inside a timed clip inherited the first-pass hide, so grading skipped it, and lifting the rule is not a body mutation, so nothing registered it again: the image or video stayed ungraded. The pass now refreshes grading when it lifts the rule. The second-copy test now asserts on a paused page, before any seek. --- packages/core/src/runtime/entry.test.ts | 25 ++++++++++++++++++++-- packages/core/src/runtime/init.ts | 3 ++- packages/core/src/runtime/timedClipHide.ts | 6 ++++-- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/core/src/runtime/entry.test.ts b/packages/core/src/runtime/entry.test.ts index d06bc440b0..74cc2e98a0 100644 --- a/packages/core/src/runtime/entry.test.ts +++ b/packages/core/src/runtime/entry.test.ts @@ -1,5 +1,6 @@ // fallow-ignore-file code-duplication import { afterEach, describe, expect, it, vi } from "vitest"; +import { HF_COLOR_GRADING_ATTR, serializeHfColorGrading } from "../colorGrading"; import type { RuntimeTimelineLike } from "./types"; function pausedTimeline(duration: number): RuntimeTimelineLike { @@ -103,9 +104,29 @@ describe("runtime entry", () => { await evaluateRuntime(); await evaluateRuntime(); - window.__player?.seek(1); - + // Paused and never sought: no later pass would lift a rule the second copy added. expect(visibility(root, current)).toEqual(["visible", "visible"]); expect(document.querySelectorAll("style[data-hf-first-pass-hide]")).toHaveLength(0); }); + + it("grades media inside a clip once the first pass shows the clip, with no seek", async () => { + const getContext = vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue(null); + const scene = timed(mountRoot(), "div", "0"); + // Untimed: the video inherits the scene's window, so no pass writes its visibility. + const video = document.createElement("video"); + video.setAttribute( + HF_COLOR_GRADING_ATTR, + serializeHfColorGrading({ adjust: { exposure: 0.5 } }), + ); + Object.defineProperty(video, "readyState", { value: HTMLMediaElement.HAVE_CURRENT_DATA }); + Object.defineProperty(video, "videoWidth", { value: 640 }); + Object.defineProperty(video, "videoHeight", { value: 360 }); + scene.appendChild(video); + + await evaluateRuntime(); + + expect(window.__renderReady).toBe(true); + expect(getContext.mock.calls.some(([type]) => String(type).startsWith("webgl"))).toBe(true); + getContext.mockRestore(); + }); }); diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index ce16e6a342..48db1a07a9 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -2532,7 +2532,8 @@ export function initSandboxRuntimeModular(): void { timedClipDisplayNoneApplied.add(rawNode); } } - if (decidedTimedClip) revealTimedClipsAfterFirstPass(); + // Grading registers only media it can see, and media inside a clip was hidden until now. + if (decidedTimedClip && revealTimedClipsAfterFirstPass()) colorGradingRuntime?.refresh(); // Only when a `data-hidden` mutation actually moved something: the skips // this reschedule exists to re-run are what change the active set, so // firing it otherwise was an audible stop-and-restart across the whole mix diff --git a/packages/core/src/runtime/timedClipHide.ts b/packages/core/src/runtime/timedClipHide.ts index 6be8bea5eb..f9a463fa02 100644 --- a/packages/core/src/runtime/timedClipHide.ts +++ b/packages/core/src/runtime/timedClipHide.ts @@ -24,9 +24,11 @@ export function hideTimedClipsUntilFirstPass(): void { win.__hfFirstPassHidden = true; } -export function revealTimedClipsAfterFirstPass(): void { +/** True when this call lifted the rule. */ +export function revealTimedClipsAfterFirstPass(): boolean { const win = window as FirstPassWindow; - if (!win.__hfFirstPassHidden) return; + if (!win.__hfFirstPassHidden) return false; win.__hfFirstPassHidden = false; for (const style of document.querySelectorAll(`style[${HIDE_ATTR}]`)) style.remove(); + return true; } From 5addb18eaa1a33a2f6d0f994edf60c376e04b102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 24 Sep 2026 22:22:25 -0400 Subject: [PATCH 5/5] chore(core): keep the comment share of the first-pass hide files at or under their base The comment ratchet counts one added comment line each in entry.ts, init.ts and capture.ts, and a new file above its package's share. The three lines are gone, and the one note that has to stay (why the hide runs at script evaluation) now lives in the new module's shorter header. --- packages/core/src/runtime/entry.ts | 1 - packages/core/src/runtime/init.ts | 1 - packages/core/src/runtime/timedClipHide.ts | 8 ++++---- packages/shader-transitions/src/capture.ts | 1 - 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/core/src/runtime/entry.ts b/packages/core/src/runtime/entry.ts index cc79d6f912..58c86985c0 100644 --- a/packages/core/src/runtime/entry.ts +++ b/packages/core/src/runtime/entry.ts @@ -29,7 +29,6 @@ type HyperframeWindow = Window & { // at script evaluation time, while the document is still parsing. installAuthoredOpacityCapture(); -// Also at evaluation time: the body is parsed, and may paint, before init runs at DOMContentLoaded. hideTimedClipsUntilFirstPass(); // Expose runtime helpers immediately so composition scripts can use them diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index 48db1a07a9..2dfef746c7 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -2532,7 +2532,6 @@ export function initSandboxRuntimeModular(): void { timedClipDisplayNoneApplied.add(rawNode); } } - // Grading registers only media it can see, and media inside a clip was hidden until now. if (decidedTimedClip && revealTimedClipsAfterFirstPass()) colorGradingRuntime?.refresh(); // Only when a `data-hidden` mutation actually moved something: the skips // this reschedule exists to re-run are what change the active set, so diff --git a/packages/core/src/runtime/timedClipHide.ts b/packages/core/src/runtime/timedClipHide.ts index f9a463fa02..b590ec57b6 100644 --- a/packages/core/src/runtime/timedClipHide.ts +++ b/packages/core/src/runtime/timedClipHide.ts @@ -1,6 +1,6 @@ -// Until the first visibility pass decides each timed clip, a paused page would paint every clip at once -// (and decode every scene's images). Media is left out: init's media pass owns it. -// The rule and its flag live on the page, not in this module: every runtime copy on the page shares them. +// Hides timed non-media clips from script evaluation until the first visibility pass decides them; +// before it, a paused page painted every clip at once. Media is left to init's media pass. +// The rule and its flag live on the page, so every runtime copy shares them. const HIDE_ATTR = "data-hf-first-pass-hide"; const HIDE_UNTIL_FIRST_PASS = "[data-start]:not(video, audio, img) { visibility: hidden !important; }"; @@ -24,7 +24,7 @@ export function hideTimedClipsUntilFirstPass(): void { win.__hfFirstPassHidden = true; } -/** True when this call lifted the rule. */ +/** True when this call lifted the rule; callers then re-register what skipped hidden elements (grading). */ export function revealTimedClipsAfterFirstPass(): boolean { const win = window as FirstPassWindow; if (!win.__hfFirstPassHidden) return false; diff --git a/packages/shader-transitions/src/capture.ts b/packages/shader-transitions/src/capture.ts index 740b4ed267..31c30f2538 100644 --- a/packages/shader-transitions/src/capture.ts +++ b/packages/shader-transitions/src/capture.ts @@ -55,7 +55,6 @@ export function forceSceneVisibleInClone(source: HTMLElement, cloneDoc: Document if (!(clone instanceof HTMLElement)) return; clone.style.opacity = "1"; - // Important: the runtime hides timed clips with an !important rule until its first visibility pass. clone.style.setProperty("visibility", "visible", "important"); clone.querySelectorAll("[data-start]").forEach((el) => { el.style.setProperty("visibility", "visible", "important");