mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(shader-transitions): the render leaves scene visibility to the runtime (#4509)
* fix(shader-transitions): a render no longer shows the last shader scene past its end The page-side render un-hid the scene settled after the last shader transition on every later frame, so a scene that followed it rendered underneath the old one. It now un-hides that scene only while its own data-start/data-duration window is open. * fix(shader-transitions): the render leaves scene visibility to the runtime The page-side compositor un-hid the destination of the last finished shader transition on every non-transition frame, a workaround for the runtime hiding a film's final scene before the end. The runtime has kept the last frame since #4158, so the workaround only showed a shader scene outside its own window: before it starts and after it ends. Remove it, with the narrower check this branch added first.
This commit is contained in:
@@ -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<string, string>) {}
|
||||
getAttribute(name: string) {
|
||||
return this.attrs[name] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
// A browser with drawElementImage and a WebGL context whose every call succeeds.
|
||||
function installWithHiddenScenes(
|
||||
timing: Record<string, readonly [start: string, duration: string]>,
|
||||
) {
|
||||
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__");
|
||||
|
||||
@@ -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 <img> replacements for <video> elements.
|
||||
// Awaits decode on cloned data-URI images so drawElementImage reads
|
||||
// the current frame, not a stale paint cache entry.
|
||||
// fallow-ignore-next-line complexity
|
||||
async function prepareComposite(): Promise<boolean> {
|
||||
const active = currentActive;
|
||||
if (!active) {
|
||||
@@ -319,6 +311,7 @@ export function installPageSideCompositor(options: PageCompositorInstallOptions)
|
||||
|
||||
// Phase 2b: drawElementImage from painted clones + shader composite.
|
||||
// Called after micro-screenshot forces the browser to paint the clones.
|
||||
// fallow-ignore-next-line complexity
|
||||
function resolveComposite(): boolean {
|
||||
const active = currentActive;
|
||||
if (!active) {
|
||||
@@ -401,14 +394,6 @@ export function installPageSideCompositor(options: PageCompositorInstallOptions)
|
||||
pWin.__hf_page_composite_pending = false;
|
||||
while (fromStaging.firstChild) fromStaging.removeChild(fromStaging.firstChild);
|
||||
while (toStaging.firstChild) toStaging.removeChild(toStaging.firstChild);
|
||||
// Live-page screenshot parity with the layered path's forceVisible: the
|
||||
// core clip runtime hides the final scene a beat before the comp ends, so
|
||||
// un-hide the settled scene (others stay at opacity 0).
|
||||
const settledId = settledSceneIdAt(time);
|
||||
const settled = settledId ? document.getElementById(settledId) : null;
|
||||
if (settled instanceof HTMLElement && settled.style.visibility === "hidden") {
|
||||
settled.style.visibility = "visible";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
currentActive = active;
|
||||
|
||||
Reference in New Issue
Block a user