Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/shader-transitions/src/engineModePageComposite.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
clonePinStyleFor,
isPageSideCompositingSupported,
PAGE_COMPOSITOR_BUILD_CANARY,
PAGE_COMPOSITOR_CANVAS_ID,
Expand Down Expand Up @@ -69,6 +70,26 @@ 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" });
});
});

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__");
Expand Down
51 changes: 50 additions & 1 deletion packages/shader-transitions/src/engineModePageComposite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,36 @@ 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;
}

/**
* 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`,
};
}

export function isPageSideCompositingSupported(): boolean {
if (typeof window === "undefined" || typeof document === "undefined") return false;
if (!isHtmlInCanvasCaptureSupported()) return false;
Expand Down Expand Up @@ -232,6 +262,17 @@ 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;
Expand All @@ -244,9 +285,17 @@ export function installPageSideCompositor(options: PageCompositorInstallOptions)
// 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 of [fromClone, toClone]) {
for (const [clone, pin] of [
[fromClone, fromPin],
[toClone, toPin],
] as const) {
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";
Expand Down
Loading