From 861fcc4dd0fecc3f0b7c23f27385a97aa926cc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20=C3=81ngel?= Date: Thu, 24 Sep 2026 14:18:53 -0400 Subject: [PATCH] fix(core): a preview that is never seeked shows only the clips of its current time (#4422) The visibility pass ran only on seek, pause, play and during playback, and at init for video and img alone, so a paused page published __renderReady with every caption and composition host visible until someone seeked. Readiness now runs that pass for non-media timed elements; media keeps its init pass and seeks, which own color grading and audio scheduling. --- packages/core/src/runtime/init.test.ts | 57 +++++++++++++++++++++++++- packages/core/src/runtime/init.ts | 6 +++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/core/src/runtime/init.test.ts b/packages/core/src/runtime/init.test.ts index bc50d005ba..6dc6b150ea 100644 --- a/packages/core/src/runtime/init.test.ts +++ b/packages/core/src/runtime/init.test.ts @@ -2559,8 +2559,8 @@ describe("initSandboxRuntimeModular", () => { expect(getContextSpy).toHaveBeenCalledTimes(1); expect(document.getElementById("first")?.style.visibility).toBe("visible"); expect(document.getElementById("second")?.style.visibility).toBe("hidden"); - expect(futureComposition.style.visibility).toBe(""); - expect(futureComposition.style.display).toBe(""); + expect(futureComposition.style.visibility).toBe("hidden"); + expect(futureComposition.style.display).toBe("none"); window.__player?.seek(3); @@ -2805,6 +2805,59 @@ describe("initSandboxRuntimeModular", () => { expect(window.__player).toBeDefined(); }); + function mountRootWithClip(start: string): 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); + return appendClip(root, start); + } + + function appendClip(parent: Element, start: string): HTMLElement { + const clip = document.createElement("div"); + clip.className = "clip"; + clip.setAttribute("data-start", start); + clip.setAttribute("data-duration", "2"); + clip.setAttribute("data-track-index", "1"); + parent.appendChild(clip); + return clip; + } + + it("publishes render readiness with out-of-window clips already hidden, no seek needed", () => { + const caption = mountRootWithClip("5"); + window.__timelines = { main: createMockTimeline(10) }; + + initSandboxRuntimeModular(); + + expect(window.__renderReady).toBe(true); + expect(caption.style.visibility).toBe("hidden"); + }); + + it("paints clips mounted before readiness at the time sought before readiness", () => { + mountRootWithClip("0"); + const root = document.querySelector("[data-composition-id='main']")!; + window.__timelines = { main: createMockTimeline(10) }; + window.__hfTimelinesBuilding = true; + + initSandboxRuntimeModular(); + expect(window.__renderReady).toBe(false); + window.__player?.seek(6); + const inWindow = appendClip(root, "5"); + const outOfWindow = appendClip(root, "0"); + window.__hfTimelinesBuilding = false; + window.dispatchEvent(new CustomEvent("hf-timelines-built")); + + expect(window.__renderReady).toBe(true); + expect(window.__player?.getTime()).toBe(6); + expect([inWindow.style.visibility, outOfWindow.style.visibility]).toEqual([ + "visible", + "hidden", + ]); + }); + it("waits for GSAP batching to finish before publishing render readiness", () => { const root = document.createElement("div"); root.setAttribute("data-composition-id", "main"); diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index ffb6ba685c..076554b167 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -3341,6 +3341,12 @@ export function initSandboxRuntimeModular(): void { ); } } + // Nothing else hides out-of-window clips on a paused page until someone seeks, so readiness does. + // Media is left to init's media pass and to seeks: they own color grading and audio scheduling. + syncTimedElementVisibility( + state.currentTime, + Array.from(document.querySelectorAll("[data-start]:not(video, audio, img)")), + ); // __renderReady = timeline binding attempted, safe for deterministic seeking. // Set after any GSAP batching has completed. renderSeek works with or // without a GSAP timeline (CSS/WAAPI/Lottie compositions use adapters only).