diff --git a/packages/core/src/runtime/init.ts b/packages/core/src/runtime/init.ts index 925b72a54b..8a38819f6b 100644 --- a/packages/core/src/runtime/init.ts +++ b/packages/core/src/runtime/init.ts @@ -4792,8 +4792,7 @@ export function initSandboxRuntimeModular(): void { const revision = opts?.revision ?? readCompositionTimingRevision(); if (revision !== tweensRenderedThrough.revision) tweensRenderedThrough = { revision, time: Number.NEGATIVE_INFINITY }; - const jumped = !(Math.abs(t - lastTransportSeekTime) <= 1.5 / state.canonicalFps); - const firstRenders = jumped && t > tweensRenderedThrough.time; + const firstRenders = t > tweensRenderedThrough.time; const hideClipsAgain = firstRenders ? showHiddenClips() : null; if (tl) { // #10: when data-duration exceeds the timeline's intrinsic length the diff --git a/packages/core/src/runtime/timedClipHide.ts b/packages/core/src/runtime/timedClipHide.ts index d0027a0d56..5da3e40320 100644 --- a/packages/core/src/runtime/timedClipHide.ts +++ b/packages/core/src/runtime/timedClipHide.ts @@ -19,7 +19,10 @@ export const CLIP_HIDDEN_ATTR = "data-hf-clip-hidden"; const CONTENTS_ATTR = "data-hf-hidden-clip-contents"; const HIDDEN_CLIP = `[${CLIP_HIDDEN_ATTR}][style*="visibility: hidden"]`; const HIDE_HIDDEN_CLIP_CONTENTS = `${HIDDEN_CLIP} * { visibility: hidden !important; }`; -const HIDE_SVG_GENERATED_FILTER_OUTPUT = `svg${HIDDEN_CLIP}, svg${HIDDEN_CLIP} *, ${HIDDEN_CLIP} svg, ${HIDDEN_CLIP} svg * { filter: none !important; }`; +const FILTER_OUTPUT_LAYERS = [HIDDEN_CLIP, `${HIDDEN_CLIP} *`] + .flatMap((layer) => [layer, `${layer}::before`, `${layer}::after`]) + .join(", "); +const HIDE_FILTER_OUTPUT = `${FILTER_OUTPUT_LAYERS} { filter: none !important; }`; export function showHiddenClips(): () => void { const hidden = Array.from(document.querySelectorAll(`[${CLIP_HIDDEN_ATTR}]`)); @@ -53,11 +56,7 @@ export function hideTimedClipsUntilFirstPass(): void { const preview = document.querySelector(STUDIO_PREVIEW_MARK) !== null; if (preview && !skipsHiddenImages()) appendStyle(parent, SKIP_ATTR, SKIP_HIDDEN_IMAGES); if (!document.querySelector(`style[${CONTENTS_ATTR}]`)) - appendStyle( - parent, - CONTENTS_ATTR, - `${HIDE_HIDDEN_CLIP_CONTENTS} ${HIDE_SVG_GENERATED_FILTER_OUTPUT}`, - ); + appendStyle(parent, CONTENTS_ATTR, `${HIDE_HIDDEN_CLIP_CONTENTS} ${HIDE_FILTER_OUTPUT}`); 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; diff --git a/packages/producer/src/services/coreRuntimeBrowser.test.ts b/packages/producer/src/services/coreRuntimeBrowser.test.ts index 664407c7c8..b0c1f77d47 100644 --- a/packages/producer/src/services/coreRuntimeBrowser.test.ts +++ b/packages/producer/src/services/coreRuntimeBrowser.test.ts @@ -320,52 +320,109 @@ describe("core runtime browser contract", () => { } }, 30_000); - it("paints no SVG filter output from a scene before its start or after its end", async () => { - // feTurbulence draws from nothing, so visibility:hidden alone left grain over earlier scenes. - const grain = (id: string, start: number) => ` + const filterLayers: Record string> = { + "an SVG rect": (id, start) => `
-
`; - const grainPage = await browser.newPage(); - try { - await grainPage.setViewport({ width: 320, height: 180 }); - await grainPage.setContent(` + `, + "an HTML div inside the scene": (id, start) => ` + +
+
+
`, + "the scene itself": (id, start) => ` + +
`, + "a dimmed overlay layer in a late sub-composition": (id, start) => ` + +
+
+
`, + "a pseudo-element of the scene": (id, start) => ` + + +
`, + "a pseudo-element inside the scene": (id, start) => ` + + +
+
+
`, + }; + + it.each(Object.keys(filterLayers))( + "paints no SVG filter output from %s before its scene starts or after it ends", + async (layer) => { + // feTurbulence draws from nothing, so visibility:hidden alone left grain over earlier scenes. + const grain = filterLayers[layer]!; + const grainPage = await browser.newPage(); + try { + await grainPage.setViewport({ width: 320, height: 180 }); + await grainPage.setContent(`
${grain("early", 0)}${grain("late", 2)}
`); - await grainPage.addScriptTag({ content: readFileSync(RUNTIME_PATH, "utf8") }); - await grainPage.waitForFunction(() => window.__playerReady && window.__renderReady); + await grainPage.addScriptTag({ content: readFileSync(RUNTIME_PATH, "utf8") }); + await grainPage.waitForFunction(() => window.__playerReady && window.__renderReady); - const redRangeAt = async (time: number) => { - await grainPage.evaluate((t) => window.__player!.renderSeek!(t), time); - const shot = await grainPage.screenshot({ encoding: "base64" }); - return grainPage.evaluate(async (png) => { - const image = new Image(); - image.src = `data:image/png;base64,${png}`; - await image.decode(); - const context = new OffscreenCanvas(image.width, image.height).getContext("2d")!; - context.drawImage(image, 0, 0); - const { data } = context.getImageData(0, 0, image.width, image.height); - let min = 255; - let max = 0; - for (let i = 0; i < data.length; i += 4) { - min = Math.min(min, data[i]!); - max = Math.max(max, data[i]!); - } - return [min, max]; - }, shot); - }; + const redRangeAt = async (time: number) => { + await grainPage.evaluate((t) => window.__player!.renderSeek!(t), time); + const shot = await grainPage.screenshot({ encoding: "base64" }); + return grainPage.evaluate(async (png) => { + const image = new Image(); + image.src = `data:image/png;base64,${png}`; + await image.decode(); + const context = new OffscreenCanvas(image.width, image.height).getContext("2d")!; + context.drawImage(image, 0, 0); + const { data } = context.getImageData(0, 0, image.width, image.height); + let min = 255; + let max = 0; + for (let i = 0; i < data.length; i += 4) { + min = Math.min(min, data[i]!); + max = Math.max(max, data[i]!); + } + return [min, max]; + }, shot); + }; - expect(await redRangeAt(1.5)).toEqual([128, 128]); - const [min, max] = await redRangeAt(2.5); - expect(max - min).toBeGreaterThan(20); + expect(await redRangeAt(1.5)).toEqual([128, 128]); + const [min, max] = await redRangeAt(2.5); + expect(max - min).toBeGreaterThan(2); + } finally { + await grainPage.close(); + } + }, + 30_000, + ); + + it("ends a later scene's from-opacity fade at its authored opacity when frames step into it", async () => { + const fadePage = await browser.newPage(); + try { + await fadePage.setContent(` +
+
+
`); + await fadePage.addScriptTag({ + path: resolve(import.meta.dirname, "../../../core/node_modules/gsap/dist/gsap.min.js"), + }); + await fadePage.addScriptTag({ + content: `window.__timelines = {root:gsap.timeline({paused:true}).from("#scene",{opacity:0,duration:1,ease:"none",immediateRender:false,lazy:false},2)};`, + }); + await fadePage.addScriptTag({ content: readFileSync(RUNTIME_PATH, "utf8") }); + await fadePage.waitForFunction(() => window.__playerReady && window.__renderReady); + + const opacity = await fadePage.evaluate(() => { + for (let frame = 0; frame <= 90; frame++) window.__player!.renderSeek!(frame / 30); + return getComputedStyle(document.getElementById("scene")!).opacity; + }); + expect(opacity).toBe("1"); } finally { - await grainPage.close(); + await fadePage.close(); } }, 30_000);