mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(core): a clip that starts later keeps the place and size its CSS gives it (#4495)
* fix(core): a clip that starts later keeps the place and size its CSS gives it A timed root clip that had not started when the runtime laid clips out was display:none, so it measured as auto: it was pinned to the top-left corner, and an empty full-frame clip got no size. Measure a hidden clip as it will show and never pin, so it lays out like a clip showing at load. * perf(core): lay out hidden clips in one pass and pin the !important restore Lift every hidden clip's display:none at once so a layout pass forces one layout instead of one per hidden clip; it runs every 20 frames during playback. Add a test that an author's display:none !important comes back with its priority, and fix comments that described the removed pin. * test(core): describe what the applyClipLayout test block covers * refactor(core): name the display lift instead of commenting it
This commit is contained in:
@@ -3902,13 +3902,10 @@ describe("initSandboxRuntimeModular", () => {
|
||||
expect(window.__player?.getDuration()).toBe(5);
|
||||
});
|
||||
|
||||
// applyClipLayout force-absolutizes authored root-level timed clips so they
|
||||
// stack as overlays. But in Studio/preview the runtime also stamps `data-start`
|
||||
// onto ID'd / GSAP-targeted *flow* children (a <header>/<footer> in a column)
|
||||
// so the design panel can discover them — those must NOT be force-absolutized,
|
||||
// or the layout collapses (footer shrink-wraps, `space-between` clusters). The
|
||||
// marker `data-hf-autostamped` distinguishes them; these tests pin both halves.
|
||||
describe("applyClipLayout: runtime-stamped clips stay in document flow", () => {
|
||||
// applyClipLayout force-absolutizes authored root-level timed clips, leaves their
|
||||
// position to CSS, and measures hidden clips as shown. Runtime-stamped flow
|
||||
// children (`data-hf-autostamped`) stay in flow, or a flex column collapses.
|
||||
describe("applyClipLayout", () => {
|
||||
const makeRoot = () => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
@@ -3921,8 +3918,8 @@ describe("initSandboxRuntimeModular", () => {
|
||||
};
|
||||
|
||||
// jsdom does no layout, so a static clip can report computed top "auto" or
|
||||
// "" inconsistently. Pin the values the anchor gate keys on so the assertion
|
||||
// reflects the real-browser path deterministically.
|
||||
// "" inconsistently. Pin the values the layout pass reads so the assertion
|
||||
// is deterministic.
|
||||
const overrideComputed = (
|
||||
target: HTMLElement,
|
||||
overrides: Partial<Record<"position" | "top" | "left" | "bottom" | "right", string>>,
|
||||
@@ -3946,7 +3943,7 @@ describe("initSandboxRuntimeModular", () => {
|
||||
}) as typeof window.getComputedStyle);
|
||||
};
|
||||
|
||||
it("force-absolutizes an authored data-start clip (baseline behavior preserved)", () => {
|
||||
it("force-absolutizes an authored data-start clip and leaves its position to CSS", () => {
|
||||
const root = makeRoot();
|
||||
const clip = document.createElement("div");
|
||||
clip.setAttribute("data-start", "0"); // authored clip, no autostamp marker
|
||||
@@ -3963,8 +3960,88 @@ describe("initSandboxRuntimeModular", () => {
|
||||
initSandboxRuntimeModular();
|
||||
|
||||
expect(clip.style.position).toBe("absolute");
|
||||
expect(clip.style.top).toBe("0px");
|
||||
expect(clip.style.left).toBe("0px");
|
||||
expect(clip.style.top).toBe("");
|
||||
expect(clip.style.left).toBe("");
|
||||
});
|
||||
|
||||
// A browser reports "auto" for every box value of a display:none element and pixels once
|
||||
// it shows. jsdom does no layout, so model that for the clip under test.
|
||||
const modelBrowserLayout = (target: HTMLElement, shown: { width: string; height: string }) => {
|
||||
const real = window.getComputedStyle.bind(window);
|
||||
vi.spyOn(window, "getComputedStyle").mockImplementation(((
|
||||
el: Element,
|
||||
pseudo?: string | null,
|
||||
) => {
|
||||
const style = real(el as Element, pseudo ?? undefined);
|
||||
if (el !== target) return style;
|
||||
const box = (): Record<string, string> => {
|
||||
if (target.style.display === "none") {
|
||||
return { width: "auto", height: "auto", top: "auto", left: "auto" };
|
||||
}
|
||||
return { ...shown, top: "499px", left: "784px" };
|
||||
};
|
||||
return new Proxy(style, {
|
||||
get(t, prop) {
|
||||
if (prop === "position") return target.style.position || "static";
|
||||
if (prop === "bottom" || prop === "right") return "auto";
|
||||
if (typeof prop === "string" && prop in box()) return box()[prop];
|
||||
const value = Reflect.get(t, prop);
|
||||
return typeof value === "function" ? value.bind(t) : value;
|
||||
},
|
||||
}) as CSSStyleDeclaration;
|
||||
}) as typeof window.getComputedStyle);
|
||||
};
|
||||
|
||||
it("keeps a clip that starts later where its CSS puts it (flex-centred title)", () => {
|
||||
const root = makeRoot();
|
||||
root.style.cssText = "display:flex;align-items:center;justify-content:center";
|
||||
const title = document.createElement("h1");
|
||||
title.setAttribute("data-start", "0.88");
|
||||
title.setAttribute("data-duration", "5");
|
||||
title.textContent = "Agent one";
|
||||
root.appendChild(title);
|
||||
modelBrowserLayout(title, { width: "352px", height: "82px" });
|
||||
|
||||
window.__timelines = { main: createMockTimeline(10) };
|
||||
initSandboxRuntimeModular();
|
||||
|
||||
expect(title.style.display).toBe("none");
|
||||
expect(title.style.position).toBe("absolute");
|
||||
expect(title.style.top).toBe("");
|
||||
expect(title.style.left).toBe("");
|
||||
});
|
||||
|
||||
it("sizes an empty clip that starts later like one showing at load", () => {
|
||||
const root = makeRoot();
|
||||
const card = document.createElement("div");
|
||||
card.setAttribute("data-start", "1");
|
||||
card.setAttribute("data-duration", "5");
|
||||
root.appendChild(card);
|
||||
modelBrowserLayout(card, { width: "0px", height: "0px" });
|
||||
|
||||
window.__timelines = { main: createMockTimeline(10) };
|
||||
initSandboxRuntimeModular();
|
||||
|
||||
expect(card.style.display).toBe("none");
|
||||
expect(card.style.width).toBe("100%");
|
||||
expect(card.style.height).toBe("100%");
|
||||
});
|
||||
|
||||
it("puts back an author's display:none !important after measuring the clip", () => {
|
||||
const root = makeRoot();
|
||||
const card = document.createElement("div");
|
||||
card.setAttribute("data-start", "0");
|
||||
card.setAttribute("data-duration", "5");
|
||||
card.style.setProperty("display", "none", "important");
|
||||
root.appendChild(card);
|
||||
modelBrowserLayout(card, { width: "0px", height: "0px" });
|
||||
|
||||
window.__timelines = { main: createMockTimeline(10) };
|
||||
initSandboxRuntimeModular();
|
||||
|
||||
expect(card.style.width).toBe("100%");
|
||||
expect(card.style.getPropertyValue("display")).toBe("none");
|
||||
expect(card.style.getPropertyPriority("display")).toBe("important");
|
||||
});
|
||||
|
||||
it("leaves a runtime-stamped flow child untouched so the layout is preserved", () => {
|
||||
|
||||
@@ -629,11 +629,10 @@ export function initSandboxRuntimeModular(): void {
|
||||
const rootHeight = parseDimensionPx(rootEl.getAttribute("data-height"));
|
||||
if (rootWidth) rootEl.style.width = rootWidth;
|
||||
if (rootHeight) rootEl.style.height = rootHeight;
|
||||
const children = Array.from(rootEl.children) as HTMLElement[];
|
||||
for (const el of children) {
|
||||
const clips = (Array.from(rootEl.children) as HTMLElement[]).filter((el) => {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (tag === "script" || tag === "style" || tag === "link" || tag === "meta") continue;
|
||||
if (!el.hasAttribute("data-start")) continue;
|
||||
if (tag === "script" || tag === "style" || tag === "link" || tag === "meta") return false;
|
||||
if (!el.hasAttribute("data-start")) return false;
|
||||
// Runtime-stamped clips are NOT authored overlay clips. In Studio/preview
|
||||
// the runtime stamps `data-start` onto ID'd or GSAP-targeted flow children
|
||||
// (a <header>/<footer> in a flex column) so the design panel can discover
|
||||
@@ -642,7 +641,14 @@ export function initSandboxRuntimeModular(): void {
|
||||
// `justify-content: space-between` clusters in the top-left. Leave them in
|
||||
// flow so the preview matches the rendered video, which never stamps
|
||||
// (production renders run as the top-level page, not in an iframe).
|
||||
if (el.hasAttribute("data-hf-autostamped")) continue;
|
||||
return !el.hasAttribute("data-hf-autostamped");
|
||||
});
|
||||
const displayNoneLiftedToMeasureShown = clips
|
||||
.filter((el) => el.style.getPropertyValue("display") === "none")
|
||||
.map((el) => ({ el, priority: el.style.getPropertyPriority("display") }));
|
||||
for (const { el } of displayNoneLiftedToMeasureShown) el.style.removeProperty("display");
|
||||
for (const el of clips) {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
const hasLegacyAnchoredDefaults =
|
||||
(el.style.top === "0px" || el.style.top === "0") &&
|
||||
(el.style.left === "0px" || el.style.left === "0") &&
|
||||
@@ -687,22 +693,6 @@ export function initSandboxRuntimeModular(): void {
|
||||
if (shouldForceAbsolute) {
|
||||
el.style.position = "absolute";
|
||||
}
|
||||
const hasExplicitVerticalAnchor =
|
||||
Boolean(el.style.top) ||
|
||||
Boolean(el.style.bottom) ||
|
||||
computed.top !== "auto" ||
|
||||
computed.bottom !== "auto";
|
||||
if (!hasExplicitVerticalAnchor) {
|
||||
el.style.top = "0";
|
||||
}
|
||||
const hasExplicitHorizontalAnchor =
|
||||
Boolean(el.style.left) ||
|
||||
Boolean(el.style.right) ||
|
||||
computed.left !== "auto" ||
|
||||
computed.right !== "auto";
|
||||
if (!hasExplicitHorizontalAnchor) {
|
||||
el.style.left = "0";
|
||||
}
|
||||
if (tag !== "audio") {
|
||||
const forcedWidth = parseDimensionPx(el.getAttribute("data-width"));
|
||||
const forcedHeight = parseDimensionPx(el.getAttribute("data-height"));
|
||||
@@ -724,6 +714,9 @@ export function initSandboxRuntimeModular(): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const { el, priority } of displayNoneLiftedToMeasureShown) {
|
||||
el.style.setProperty("display", "none", priority);
|
||||
}
|
||||
};
|
||||
|
||||
const createTimingResolver = (includeAuthoredTimingAttrs: boolean) =>
|
||||
|
||||
Reference in New Issue
Block a user