diff --git a/packages/core/src/runtime/init.test.ts b/packages/core/src/runtime/init.test.ts index da1a666be6a..38e0436e01a 100644 --- a/packages/core/src/runtime/init.test.ts +++ b/packages/core/src/runtime/init.test.ts @@ -609,7 +609,7 @@ describe("initSandboxRuntimeModular", () => { } }); - it("styles each hidden clip visible once, far scene included, so its cut does not do it first", () => { + it("styles a hidden clip visible once while playing toward its start, far scene included", () => { document.body.innerHTML = `
` + `

` + @@ -624,13 +624,28 @@ describe("initSandboxRuntimeModular", () => { if (el === later && shown && !later.hasAttribute("data-hf-far-scene")) styledVisible++; return original(el, pseudo); }); + const raf = createManualRaf(); + vi.spyOn(performance, "now").mockImplementation(() => raf.now()); + window.requestAnimationFrame = + raf.requestAnimationFrame as typeof window.requestAnimationFrame; + window.cancelAnimationFrame = raf.cancelAnimationFrame as typeof window.cancelAnimationFrame; try { initSandboxRuntimeModular(); - window.__player?.seek(1); - const once = styledVisible; - window.__player?.seek(2); - window.__player?.seek(3); - expect([once, styledVisible]).toEqual([1, 1]); + const counts: number[] = []; + const playFor = (ms: number) => { + for (let at = 0; at < ms; at += 100) raf.step(100); + counts.push(styledVisible); + }; + for (const t of [6.5, 4]) { + window.__player?.seek(t); + counts.push(styledVisible); + } + window.__player?.play(); + playFor(300); + playFor(1800); + playFor(1000); + window.__player?.pause(); + expect(counts).toEqual([0, 0, 0, 1, 1]); expect([later.style.visibility, later.hasAttribute("data-hf-clip-hidden")]).toEqual([ "hidden", true, diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index 1b034cf9e9c..20b57a36dbf 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -2889,14 +2889,19 @@ export function initSandboxRuntimeModular(): void { resolver: timingResolverFor(true), }), ); - // A clip's first visible style pass is slow; each hidden clip gets it once on a seek, so its cut does not. + // A clip's first visible style pass is slow; playing, each hidden clip gets it once just before its start. const clipsNeverStyledVisible = new Set(); const clipsStyledVisible = new WeakSet(); - const styleHiddenClipsVisibleOnce = () => { - const clips = Array.from(clipsNeverStyledVisible).filter( - (el) => el.isConnected && el.hasAttribute(CLIP_HIDDEN_ATTR), - ); - clipsNeverStyledVisible.clear(); + const styleClipsDueVisibleOnce = (t: number) => { + if (clipsNeverStyledVisible.size === 0 || !clock.isPlaying()) return; + const clips: StylableElement[] = []; + for (const el of clipsNeverStyledVisible) { + const start = resolveStartForElement(el, Number.NaN); + if (el.isConnected && el.hasAttribute(CLIP_HIDDEN_ATTR) && !(start <= t + LOOKAHEAD_SECONDS)) + continue; + clipsNeverStyledVisible.delete(el); + if (el.isConnected && el.hasAttribute(CLIP_HIDDEN_ATTR) && start > t) clips.push(el); + } if (clips.length === 0) return; const farHosts = new Set(clips.flatMap((el) => el.closest(`[${FAR_SCENE_ATTR}]`) ?? [])); for (const host of farHosts) host.removeAttribute(FAR_SCENE_ATTR); @@ -5175,7 +5180,7 @@ export function initSandboxRuntimeModular(): void { const revision = opts?.revision ?? readCompositionTimingRevision(); if (revision !== tweensRenderedThrough.revision) tweensRenderedThrough = { revision, time: Number.NEGATIVE_INFINITY }; - styleHiddenClipsVisibleOnce(); + styleClipsDueVisibleOnce(t); const firstRenders = t > tweensRenderedThrough.time; // Only clips holding a tween that renders for the first time on this seek need their authored style read. const firstRendered =