mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(shader-transitions): shader transitions in renders keep scene layout, styles and animations (#4615)
* fix(shader-transitions): page-side shader renders keep scene layout and fonts and leave no ghost * refactor(shader-transitions): one loop stages both scene copies * fix(shader-transitions): stage scene copies inside copies of their ancestors * refactor(shader-transitions): pin the staged root in one assignment * fix(shader-transitions): keep ancestor copies out of scene layout in renders * fix(shader-transitions): scene copies keep wrapper effects and live animation times * fix(shader-transitions): hold copied animations with one lookup per scene and drop the no-op shift * test(shader-transitions): pin the element and pseudo-element parts of the animation match * docs(shader-transitions): keep the staging comments to two lines
This commit is contained in:
@@ -60,7 +60,7 @@ function waveWarpChain(params: Record<string, number>): string {
|
||||
* `.hf-vfx-in` carries an explicit pixel box. Inside a `layoutsubtree` canvas
|
||||
* there is no containing block to resolve `inset: 0` against, so the wrapper
|
||||
* collapses to 0×0 and `drawElementImage` silently draws nothing — the same
|
||||
* collapse `engineModePageComposite.clonePinStyleFor` exists to undo.
|
||||
* collapse the page-side compositor avoids by staging scene copies in a sized frame.
|
||||
*/
|
||||
function fixture(chain: string, innerStyle = "", hostStyle = ""): string {
|
||||
return `<!doctype html>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clonePinStyleFor,
|
||||
installPageSideCompositor,
|
||||
isPageSideCompositingSupported,
|
||||
PAGE_COMPOSITOR_BUILD_CANARY,
|
||||
@@ -71,25 +70,19 @@ describe("isPageSideCompositingSupported", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("clonePinStyleFor", () => {
|
||||
it("fixes a 0x0 inset:0 scene root to its live-measured box (the collapse this exists to prevent)", () => {
|
||||
// A scene root sized only by `position:absolute; inset:0` measures as
|
||||
// the full composition frame in the live document (its containing block
|
||||
// there is the real ancestor chain) — collapses to 0x0 only once cloned
|
||||
// into the staging canvas's own layout subtree.
|
||||
const pin = clonePinStyleFor({ left: 0, top: 0, width: 1080, height: 1920 });
|
||||
expect(pin).toEqual({ left: "0px", top: "0px", width: "1080px", height: "1920px" });
|
||||
});
|
||||
|
||||
it("preserves an authored explicit width/height and offset instead of overriding it", () => {
|
||||
// A scene root with its own explicit size/position (e.g. a picture-in-
|
||||
// picture panel) measures as that exact box in the live document —
|
||||
// clonePinStyleFor must reproduce it verbatim, not the full composition
|
||||
// frame, or the clone would silently grow to fill the canvas.
|
||||
const pin = clonePinStyleFor({ left: 120, top: 240, width: 400, height: 300 });
|
||||
expect(pin).toEqual({ left: "120px", top: "240px", width: "400px", height: "300px" });
|
||||
});
|
||||
});
|
||||
// A WebGL context whose every call succeeds.
|
||||
function fakeWebGl(): object {
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_target, key) => {
|
||||
if (key === "getShaderParameter" || key === "getProgramParameter") return () => true;
|
||||
if (key === "getExtension") return () => ({ loseContext: () => undefined });
|
||||
return () => ({});
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
describe("page-side compositor seek", () => {
|
||||
afterEach(() => {
|
||||
@@ -108,16 +101,7 @@ describe("page-side compositor seek", () => {
|
||||
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 gl = fakeWebGl();
|
||||
const canvas = () => ({
|
||||
style: {},
|
||||
width: 0,
|
||||
@@ -180,6 +164,255 @@ describe("page-side compositor seek", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// The transparent inset case: 640x360 #main holding scenes inset 90px 160px, as in the
|
||||
// page-side-shader-compositor-render-compat fixture with a transparent page.
|
||||
describe("page-side compositor scene copies", () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
// CSS animations the stylesheet gives an element ("::before glow" runs on its pseudo-element),
|
||||
// and the live times of those still running.
|
||||
const CSS_ANIMATIONS: Record<string, string[]> = {
|
||||
stage: ["pulse", "intro", "::before glow", "::before pulse"],
|
||||
scene: ["pulse"],
|
||||
};
|
||||
const LIVE_TIMES: Record<string, Record<string, number>> = {
|
||||
stage: { pulse: 400, "::before glow": 250, "::before pulse": 150 },
|
||||
scene: { pulse: 900 },
|
||||
};
|
||||
|
||||
class FakeAnimation {
|
||||
currentTime: number | null = 0;
|
||||
state = "running";
|
||||
readonly animationName: string;
|
||||
readonly effect: { target: FakeEl; pseudoElement: string | null };
|
||||
constructor(spec: string, target: FakeEl) {
|
||||
const [pseudo, name] = spec.includes(" ") ? spec.split(" ") : [null, spec];
|
||||
this.animationName = name!;
|
||||
this.effect = { target, pseudoElement: pseudo ?? null };
|
||||
}
|
||||
pause() {
|
||||
this.state = "paused";
|
||||
}
|
||||
cancel() {
|
||||
this.state = "cancelled";
|
||||
}
|
||||
}
|
||||
|
||||
class FakeEl {
|
||||
style: Record<string, string> = {};
|
||||
children: FakeEl[] = [];
|
||||
parentElement: FakeEl | null = null;
|
||||
animations: FakeAnimation[] = [];
|
||||
constructor(
|
||||
readonly id: string,
|
||||
readonly copyOf: FakeEl | null = null,
|
||||
) {}
|
||||
getAttribute() {
|
||||
return null;
|
||||
}
|
||||
get key(): string {
|
||||
return (this.copyOf ?? this).id.replace(/^scene-.*/, "scene");
|
||||
}
|
||||
ownAnimations(): FakeAnimation[] {
|
||||
if (this.copyOf) return this.animations;
|
||||
return Object.entries(LIVE_TIMES[this.key] ?? {}).map(([spec, time]) => {
|
||||
const live = new FakeAnimation(spec, this);
|
||||
live.currentTime = time;
|
||||
return live;
|
||||
});
|
||||
}
|
||||
getAnimations(options?: { subtree?: boolean }): FakeAnimation[] {
|
||||
if (!options?.subtree) return this.ownAnimations();
|
||||
return [...this.ownAnimations(), ...this.children.flatMap((c) => c.getAnimations(options))];
|
||||
}
|
||||
cloneNode(deep: boolean) {
|
||||
const copy = new FakeEl(`${this.id}-copy`, this);
|
||||
copy.animations = (CSS_ANIMATIONS[this.key] ?? []).map(
|
||||
(spec) => new FakeAnimation(spec, copy),
|
||||
);
|
||||
if (deep) copy.children = this.children.map((c) => c.cloneNode(true));
|
||||
return copy;
|
||||
}
|
||||
appendChild(child: FakeEl) {
|
||||
this.children.push(child);
|
||||
child.parentElement = this;
|
||||
return child;
|
||||
}
|
||||
querySelectorAll() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function installTransparentInsetFilm(opts: { failDraw?: boolean; underBody?: boolean } = {}) {
|
||||
const calls: Array<{ canvas: number; op: string; args: unknown[] }> = [];
|
||||
const gl = new Proxy(fakeWebGl(), {
|
||||
get: (target, key) => {
|
||||
if (key === "texImage2D") {
|
||||
return (...args: unknown[]) => calls.push({ canvas: -1, op: "texImage2D", args });
|
||||
}
|
||||
return Reflect.get(target, key);
|
||||
},
|
||||
});
|
||||
const canvases: Array<{ id?: string; style: Record<string, string> }> = [];
|
||||
const createCanvas = () => {
|
||||
const index = canvases.length;
|
||||
const children: FakeEl[] = [];
|
||||
const record =
|
||||
(op: string) =>
|
||||
(...args: unknown[]) => {
|
||||
calls.push({ canvas: index, op, args });
|
||||
if (op === "drawElementImage" && opts.failDraw) throw new Error("No cached paint record");
|
||||
};
|
||||
const ctx = {
|
||||
fillStyle: "",
|
||||
fillRect: record("fillRect"),
|
||||
clearRect: record("clearRect"),
|
||||
drawElementImage: record("drawElementImage"),
|
||||
};
|
||||
const canvas: { id?: string; style: Record<string, string>; [key: string]: unknown } = {
|
||||
style: {},
|
||||
width: 0,
|
||||
height: 0,
|
||||
layoutSubtree: true,
|
||||
setAttribute: () => undefined,
|
||||
remove: () => undefined,
|
||||
get firstChild() {
|
||||
return children[0] ?? null;
|
||||
},
|
||||
get firstElementChild() {
|
||||
return children[0] ?? null;
|
||||
},
|
||||
appendChild: (child: FakeEl) => children.push(child),
|
||||
removeChild: () => children.shift(),
|
||||
querySelectorAll: () => [],
|
||||
getContext: (type: string) => (type === "2d" ? ctx : gl),
|
||||
};
|
||||
canvases.push(canvas);
|
||||
return canvas;
|
||||
};
|
||||
const body = new FakeEl("body");
|
||||
// An unsized wrapper between the composition root and the scenes, as in authored films.
|
||||
const parent = opts.underBody
|
||||
? body
|
||||
: body.appendChild(new FakeEl("main")).appendChild(new FakeEl("stage"));
|
||||
const scenes = new Map([
|
||||
["scene-a", parent.appendChild(new FakeEl("scene-a"))],
|
||||
["scene-b", parent.appendChild(new FakeEl("scene-b"))],
|
||||
]);
|
||||
let startPolling: (() => void) | undefined;
|
||||
const hf = { seek: vi.fn() };
|
||||
const win: Record<string, unknown> = {
|
||||
__hf: hf,
|
||||
setInterval: (poll: () => void) => {
|
||||
startPolling = poll;
|
||||
return 1;
|
||||
},
|
||||
clearInterval: () => undefined,
|
||||
};
|
||||
vi.stubGlobal("window", win);
|
||||
vi.stubGlobal("HTMLElement", FakeEl);
|
||||
vi.stubGlobal("document", {
|
||||
createElement: (tag: string) => (tag === "canvas" ? createCanvas() : new FakeEl(tag)),
|
||||
getElementById: (id: string) => scenes.get(id) ?? null,
|
||||
body,
|
||||
documentElement: null,
|
||||
});
|
||||
installPageSideCompositor({
|
||||
scenes: ["scene-a", "scene-b"],
|
||||
transitions: [{ time: 0.75, duration: 0.85, shader: "glitch" }],
|
||||
bgColor: "transparent",
|
||||
accentColors: { accent: [1, 1, 1], dark: [0, 0, 0], bright: [1, 1, 1] },
|
||||
width: 640,
|
||||
height: 360,
|
||||
defaultDuration: 0.85,
|
||||
});
|
||||
startPolling?.();
|
||||
const composite = async (time: number) => {
|
||||
hf.seek(time);
|
||||
await (win.__hf_page_composite_prepare as () => Promise<boolean>)();
|
||||
return (win.__hf_page_composite_resolve as () => boolean)();
|
||||
};
|
||||
const overlay = () => canvases.find((c) => c.id === PAGE_COMPOSITOR_CANVAS_ID)!;
|
||||
return { calls, composite, overlay };
|
||||
}
|
||||
|
||||
it("stages each scene copy inside unaltered ancestor copies in a full-frame box", async () => {
|
||||
const { calls, composite } = installTransparentInsetFilm();
|
||||
expect(await composite(1.2)).toBe(true);
|
||||
const draws = calls.filter((c) => c.op === "drawElementImage");
|
||||
expect(draws).toHaveLength(2);
|
||||
for (const [index, sceneId] of [
|
||||
[0, "scene-a"],
|
||||
[1, "scene-b"],
|
||||
] as const) {
|
||||
const frame = draws[index]?.args[0] as FakeEl;
|
||||
expect(draws[index]?.args.slice(1)).toEqual([0, 0, 640, 360]);
|
||||
expect(frame.style.cssText).toContain("width:640px;height:360px");
|
||||
const main = frame.children[0]!;
|
||||
const stage = main.children[0]!;
|
||||
const scene = stage.children[0]!;
|
||||
expect([main, stage, scene].map((el) => el.copyOf?.id)).toEqual(["main", "stage", sceneId]);
|
||||
expect(main.style).toEqual({});
|
||||
expect(stage.style).toEqual({});
|
||||
expect(scene.style).toEqual({ opacity: "1", visibility: "visible" });
|
||||
}
|
||||
});
|
||||
|
||||
it("holds copied CSS animations at the live time and cancels ones finished live", async () => {
|
||||
const { calls, composite } = installTransparentInsetFilm();
|
||||
await composite(1.2);
|
||||
const frame = calls.find((c) => c.op === "drawElementImage")?.args[0] as FakeEl;
|
||||
const stage = frame.children[0]!.children[0]!;
|
||||
const [pulse, intro, glow, pseudoPulse] = stage.animations;
|
||||
expect(pulse).toMatchObject({ animationName: "pulse", state: "paused", currentTime: 400 });
|
||||
expect(intro).toMatchObject({ animationName: "intro", state: "cancelled" });
|
||||
expect(glow).toMatchObject({ animationName: "glow", state: "paused", currentTime: 250 });
|
||||
// The same name on the stage's ::before and on the scene keeps each one's own time.
|
||||
expect(pseudoPulse).toMatchObject({ state: "paused", currentTime: 150 });
|
||||
expect(stage.children[0]!.animations[0]).toMatchObject({ state: "paused", currentTime: 900 });
|
||||
});
|
||||
|
||||
it("stages a scene directly under body in the full-frame box", async () => {
|
||||
const { calls, composite } = installTransparentInsetFilm({ underBody: true });
|
||||
expect(await composite(1.2)).toBe(true);
|
||||
const draws = calls.filter((c) => c.op === "drawElementImage");
|
||||
for (const draw of draws) {
|
||||
const frame = draw.args[0] as FakeEl;
|
||||
expect(frame.children.map((c) => c.copyOf?.id.slice(0, 5))).toEqual(["scene"]);
|
||||
expect(draw.args.slice(1)).toEqual([0, 0, 640, 360]);
|
||||
}
|
||||
});
|
||||
|
||||
it("clears both staging bitmaps after the textures are uploaded", async () => {
|
||||
const { calls, composite } = installTransparentInsetFilm();
|
||||
await composite(1.2);
|
||||
const lastUpload = calls.map((c) => c.op).lastIndexOf("texImage2D");
|
||||
const staging = new Set(calls.filter((c) => c.op === "drawElementImage").map((c) => c.canvas));
|
||||
expect(staging.size).toBe(2);
|
||||
for (const canvas of staging) {
|
||||
const lastClear = Math.max(
|
||||
...calls.flatMap((c, n) => (c.canvas === canvas && c.op === "clearRect" ? [n] : [])),
|
||||
);
|
||||
expect(lastClear).toBeGreaterThan(lastUpload);
|
||||
const drawn = calls.findIndex((c) => c.canvas === canvas && c.op === "drawElementImage");
|
||||
const clearedBeforeUpload = calls.some(
|
||||
(c, n) => c.canvas === canvas && c.op === "clearRect" && n > drawn && n < lastUpload,
|
||||
);
|
||||
expect(clearedBeforeUpload).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("clears the staging bitmaps and hides the overlay when a scene capture fails", async () => {
|
||||
const { calls, composite, overlay } = installTransparentInsetFilm({ failDraw: true });
|
||||
expect(await composite(1.2)).toBe(false);
|
||||
const drawn = calls.find((c) => c.op === "drawElementImage")?.canvas;
|
||||
expect(calls.some((c) => c.canvas === drawn && c.op === "clearRect")).toBe(true);
|
||||
expect(overlay().style.display).toBe("none");
|
||||
});
|
||||
});
|
||||
|
||||
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__");
|
||||
|
||||
@@ -76,34 +76,61 @@ interface ResolvedTransition {
|
||||
export const PAGE_COMPOSITOR_CANVAS_ID = "__hf-page-side-compositor";
|
||||
export const PAGE_COMPOSITOR_BUILD_CANARY = "__hf_page_compositor_v1__";
|
||||
|
||||
export interface ClonePinStyle {
|
||||
left: string;
|
||||
top: string;
|
||||
width: string;
|
||||
height: string;
|
||||
/** Wraps a scene clone in childless copies of its ancestors below `<body>`, so inherited styles,
|
||||
* scoped selectors and ancestor effects apply as they do live. */
|
||||
function stageWithAncestors(
|
||||
scene: HTMLElement,
|
||||
clone: HTMLElement,
|
||||
): { root: HTMLElement; liveRoot: HTMLElement; pairs: Array<[Element, Element]> } {
|
||||
const liveDescendants = scene.querySelectorAll("*");
|
||||
const cloneDescendants = clone.querySelectorAll("*");
|
||||
const pairs: Array<[Element, Element]> = [[scene, clone]];
|
||||
liveDescendants.forEach((el, n) => pairs.push([el, cloneDescendants[n]!]));
|
||||
let root = clone;
|
||||
let liveRoot = scene;
|
||||
for (
|
||||
let el = scene.parentElement;
|
||||
el && el !== document.body && el !== document.documentElement;
|
||||
el = el.parentElement
|
||||
) {
|
||||
const shell = el.cloneNode(false) as HTMLElement;
|
||||
shell.appendChild(root);
|
||||
pairs.push([el, shell]);
|
||||
root = shell;
|
||||
liveRoot = el;
|
||||
}
|
||||
return { root, liveRoot, pairs };
|
||||
}
|
||||
|
||||
/**
|
||||
* Style values to pin a cloned scene root to the box its source measured
|
||||
* WHILE STILL LIVE in the document — never the composition's full pixel size.
|
||||
* A live-document `getBoundingClientRect()` already resolves `inset:0` (and
|
||||
* any authored explicit width/height) correctly against the real ancestor
|
||||
* chain; reapplying that exact box to the clone fixes the 0x0 collapse a
|
||||
* detached `inset:0` clone would otherwise have inside the staging canvas's
|
||||
* layout subtree, without ever overriding an author's own sizing.
|
||||
*/
|
||||
export function clonePinStyleFor(rect: {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}): ClonePinStyle {
|
||||
return {
|
||||
left: `${rect.left}px`,
|
||||
top: `${rect.top}px`,
|
||||
width: `${rect.width}px`,
|
||||
height: `${rect.height}px`,
|
||||
/** Pauses each copied CSS animation at its live counterpart's time instead of restarting it;
|
||||
* one with no live counterpart has finished live, so it is cancelled. */
|
||||
function holdAnimations(liveRoot: Element, copyRoot: Element, pairs: Array<[Element, Element]>) {
|
||||
const slot = new Map<Element, number>();
|
||||
pairs.forEach(([live, copy], n) => {
|
||||
slot.set(live, n);
|
||||
slot.set(copy, n);
|
||||
});
|
||||
const keyOf = (a: Animation): string | null => {
|
||||
if (!("animationName" in a)) return null;
|
||||
const effect = a.effect as KeyframeEffect;
|
||||
return `${slot.get(effect.target!)}|${effect.pseudoElement ?? ""}|${(a as CSSAnimation).animationName}`;
|
||||
};
|
||||
const liveTimes = new Map<string, CSSNumberish | null>();
|
||||
for (const a of liveRoot.getAnimations({ subtree: true })) {
|
||||
const key = keyOf(a);
|
||||
if (key) liveTimes.set(key, a.currentTime);
|
||||
}
|
||||
for (const a of copyRoot.getAnimations({ subtree: true })) {
|
||||
const key = keyOf(a);
|
||||
if (!key) continue;
|
||||
const time = liveTimes.get(key);
|
||||
if (time === undefined) {
|
||||
a.cancel();
|
||||
} else {
|
||||
a.pause();
|
||||
a.currentTime = time;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isPageSideCompositingSupported(): boolean {
|
||||
@@ -254,44 +281,32 @@ export function installPageSideCompositor(options: PageCompositorInstallOptions)
|
||||
pWin.__hf_page_composite_pending = false;
|
||||
return false;
|
||||
}
|
||||
// Measure each scene's rendered box WHILE STILL LIVE — a scene root sized
|
||||
// only by `position:absolute; inset:0` resolves to 0x0 once cloned into
|
||||
// the staging canvas's layout subtree (no containing-block dimensions
|
||||
// there), and the transition textures blank out (wild report: explicit
|
||||
// 1080x1920 anchors fixed both transitions). The live document already
|
||||
// resolves inset:0 (and any authored explicit width/height) correctly
|
||||
// against the real ancestor chain, so pinning the clone to THIS measured
|
||||
// box fixes the collapse without ever overriding an author's own sizing.
|
||||
const fromPin = clonePinStyleFor(fromEl.getBoundingClientRect());
|
||||
const toPin = clonePinStyleFor(toEl.getBoundingClientRect());
|
||||
|
||||
while (fromStaging.firstChild) fromStaging.removeChild(fromStaging.firstChild);
|
||||
while (toStaging.firstChild) toStaging.removeChild(toStaging.firstChild);
|
||||
const fromClone = fromEl.cloneNode(true) as HTMLElement;
|
||||
const toClone = toEl.cloneNode(true) as HTMLElement;
|
||||
fromStaging.appendChild(fromClone);
|
||||
toStaging.appendChild(toClone);
|
||||
|
||||
// cloneNode copies the GSAP opacity-fade (opacity:0 / hidden data-start), and
|
||||
// Chrome won't paint hidden elements — drawElementImage then throws "No cached
|
||||
// paint record" and the shader degrades to a hard cut. The shader blends from
|
||||
// full-opacity textures via u_progress, so force the clones visible. Cf.
|
||||
// forceSceneVisibleInClone (html2canvas path).
|
||||
for (const [clone, pin] of [
|
||||
[fromClone, fromPin],
|
||||
[toClone, toPin],
|
||||
for (const [live, staging] of [
|
||||
[fromEl, fromStaging],
|
||||
[toEl, toStaging],
|
||||
] as const) {
|
||||
const clone = live.cloneNode(true) as HTMLElement;
|
||||
clone.style.opacity = "1";
|
||||
clone.style.visibility = "visible";
|
||||
clone.style.position = "absolute";
|
||||
clone.style.left = pin.left;
|
||||
clone.style.top = pin.top;
|
||||
clone.style.width = pin.width;
|
||||
clone.style.height = pin.height;
|
||||
clone.querySelectorAll<HTMLElement>("[data-start]").forEach((el) => {
|
||||
el.style.opacity = "1";
|
||||
el.style.visibility = "visible";
|
||||
});
|
||||
const { root, liveRoot, pairs } = stageWithAncestors(live, clone);
|
||||
// A full-frame box gives the copies a sized containing block to lay out against.
|
||||
const frame = document.createElement("div");
|
||||
frame.style.cssText = `position:absolute;left:0;top:0;width:${width}px;height:${height}px;margin:0;`;
|
||||
frame.appendChild(root);
|
||||
staging.appendChild(frame);
|
||||
holdAnimations(liveRoot, root, pairs);
|
||||
}
|
||||
|
||||
// Decode any data-URI images in clones so the browser has current
|
||||
@@ -333,24 +348,30 @@ export function installPageSideCompositor(options: PageCompositorInstallOptions)
|
||||
return false;
|
||||
}
|
||||
|
||||
const staged = [
|
||||
[fromCtx, fromChild],
|
||||
[toCtx, toChild],
|
||||
] as const;
|
||||
try {
|
||||
fromCtx.fillStyle = options.bgColor;
|
||||
fromCtx.fillRect(0, 0, width, height);
|
||||
fromCtx.drawElementImage(fromChild, 0, 0, width, height);
|
||||
|
||||
toCtx.fillStyle = options.bgColor;
|
||||
toCtx.fillRect(0, 0, width, height);
|
||||
toCtx.drawElementImage(toChild, 0, 0, width, height);
|
||||
for (const [ctx, child] of staged) {
|
||||
ctx.fillStyle = options.bgColor;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
ctx.drawElementImage(child, 0, 0, width, height);
|
||||
}
|
||||
uploadTextureSource(gl as WebGLRenderingContext, fromTex, fromStaging);
|
||||
uploadTextureSource(gl as WebGLRenderingContext, toTex, toStaging);
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("[HyperShader] page-side compositor: drawElementImage failed:", err);
|
||||
console.warn("[HyperShader] page-side compositor: scene capture failed:", err);
|
||||
glCanvas.style.display = "none";
|
||||
pWin.__hf_page_composite_pending = false;
|
||||
return false;
|
||||
} finally {
|
||||
// The staging canvases sit behind the page, so a bitmap left on them would show
|
||||
// through any transparent area of the composition for the rest of the film.
|
||||
for (const [ctx] of staged) ctx.clearRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
uploadTextureSource(gl as WebGLRenderingContext, fromTex, fromStaging);
|
||||
uploadTextureSource(gl as WebGLRenderingContext, toTex, toStaging);
|
||||
|
||||
try {
|
||||
renderShader(
|
||||
gl as WebGLRenderingContext,
|
||||
|
||||
Reference in New Issue
Block a user