mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
perf(studio): the layer walk skips clips the runtime hid, so scrubbing a long film stays smooth (#6416)
The off-canvas indicators (always mounted) and the Layers panel rebuild their layer list every 100 ms while a scrub mutates the preview, and each rebuild read the computed style of every element of every scene, on screen or not. The runtime already hides everything inside a clip its visibility pass hid, so those subtrees hold no layer: the walk now skips a clip marked data-hf-clip-hidden with an inline visibility of hidden. The marker constant moves to studio-preview-mark so the runtime and Studio share one owner.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
/** The visibility pass's mark on a timed clip it hides; the runtime's rules hide the clip's contents by it. */
|
||||
export const CLIP_HIDDEN_ATTR = "data-hf-clip-hidden";
|
||||
import { CLIP_HIDDEN_ATTR } from "../studioPreviewMark";
|
||||
|
||||
export { CLIP_HIDDEN_ATTR };
|
||||
|
||||
type ShaderWindow = Window & {
|
||||
HyperShader?: unknown;
|
||||
|
||||
@@ -5,6 +5,7 @@ export const STUDIO_PREVIEW_LAZY_ATTR = "data-hf-preview-lazy";
|
||||
export const STUDIO_PREVIEW_ERRORS = "__hfPreviewErrors";
|
||||
export const PREVIEW_RASTER_ATTR = "data-hf-preview-raster";
|
||||
export const RUNTIME_STAMPED_ATTR = "data-hf-autostamped";
|
||||
export const CLIP_HIDDEN_ATTR = "data-hf-clip-hidden";
|
||||
export const STUDIO_PREVIEW_FAR_SCENE_ATTR = "data-hf-far-scene";
|
||||
export const STUDIO_PREVIEW_PARKED_SCENE_ATTR = "data-hf-parked-scene";
|
||||
export const PREVIEW_ONLY_ATTRS = [
|
||||
@@ -16,6 +17,9 @@ export const PREVIEW_ONLY_ATTRS = [
|
||||
RUNTIME_STAMPED_ATTR,
|
||||
] as const;
|
||||
|
||||
export const isClipHiddenByRuntime = (el: HTMLElement) =>
|
||||
el.hasAttribute(CLIP_HIDDEN_ATTR) && el.style.visibility === "hidden";
|
||||
|
||||
export function applyAuthoredTiming(
|
||||
el: Element,
|
||||
attrs: ReadonlyArray<readonly [name: string, value: string]>,
|
||||
|
||||
@@ -380,3 +380,41 @@ describe("domEditSelectionToFacts hasAudio", () => {
|
||||
expect((await factsFor(`<div id="d1"></div>`)).hasAudio).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectDomEditLayerItems and clips the runtime hid", () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = "";
|
||||
document.head.innerHTML = "";
|
||||
});
|
||||
|
||||
// As the runtime marks and styles a clip outside the playhead, with its rule that hides the clip's contents.
|
||||
function filmWithHiddenScene(hiddenStyle: string): HTMLElement {
|
||||
document.head.innerHTML = `<style>[data-hf-clip-hidden][data-hf-clip-hidden] * { visibility: hidden !important; }</style>`;
|
||||
const cells = (prefix: string, n: number) =>
|
||||
Array.from({ length: n }, (_, i) => `<div id="${prefix}-${i}"></div>`).join("");
|
||||
document.body.innerHTML =
|
||||
`<div data-composition-id="main">` +
|
||||
`<div id="now" data-start="0" data-duration="5">${cells("now", 3)}</div>` +
|
||||
`<div id="later" data-start="5" data-duration="5" data-hf-clip-hidden style="${hiddenStyle}">${cells("later", 50)}</div>` +
|
||||
`</div>`;
|
||||
return document.querySelector<HTMLElement>("[data-composition-id]")!;
|
||||
}
|
||||
|
||||
it("lists what is on screen without reading the style of anything inside a hidden clip", () => {
|
||||
const root = filmWithHiddenScene("visibility: hidden");
|
||||
const read = vi.spyOn(window, "getComputedStyle");
|
||||
const ids = collectDomEditLayerItems(root, opts).map((item) => item.id);
|
||||
const insideHidden = read.mock.calls.filter(([el]) =>
|
||||
document.getElementById("later")!.contains(el),
|
||||
);
|
||||
expect(ids).toEqual(["now", "now-0", "now-1", "now-2"]);
|
||||
expect(insideHidden).toHaveLength(0);
|
||||
});
|
||||
|
||||
// Older shader builds reveal a clip through its inline style alone, leaving the mark on.
|
||||
it("still lists a marked clip whose inline style shows it", () => {
|
||||
const root = filmWithHiddenScene("visibility: visible");
|
||||
const ids = collectDomEditLayerItems(root, opts).map((item) => item.id);
|
||||
expect(ids).toContain("later");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PREVIEW_ONLY_ATTRS } from "@hyperframes/core/studio-preview-mark";
|
||||
import { isClipHiddenByRuntime, PREVIEW_ONLY_ATTRS } from "@hyperframes/core/studio-preview-mark";
|
||||
import { knownSourceAnswer, probeSourceElement, type ProbeTarget } from "./probeSourceElement";
|
||||
import { isAudibleVideoNode } from "../../player/lib/timelineElementHelpers";
|
||||
import type { PatchOperation } from "../../utils/sourcePatcher";
|
||||
@@ -454,7 +454,7 @@ export function collectDomEditLayerItems(
|
||||
const items: DomEditLayerItem[] = [];
|
||||
// fallow-ignore-next-line complexity
|
||||
const visit = (el: HTMLElement, depth: number) => {
|
||||
if (items.length >= maxItems) return;
|
||||
if (items.length >= maxItems || isClipHiddenByRuntime(el)) return;
|
||||
|
||||
const entry = readDomEditLayerWalkEntry(el, options.activeCompositionPath, cache);
|
||||
if (entry) {
|
||||
|
||||
Reference in New Issue
Block a user