mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
* feat(hdr): add z-ordered multi-layer compositing with PQ support Per-frame z-order analysis groups elements into DOM and HDR layers, composited bottom-to-top. Adjacent DOM elements merge into single screenshots. PQ (HDR10/smpte2084) support via sRGB-to-PQ LUT with 203-nit SDR reference white. queryElementStacking walks DOM for effective z-index, groupIntoLayers splits on HDR/DOM boundaries. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(hdr): address review feedback across stack - Document groupIntoLayers tie-break (V8 stable sort → DOM order). - Expand layerCompositor docstring: merge rationale, visibility inclusion. - Add tests: empty input, negative z-index, stable tie-break at equal z. - Document getEffectiveZIndex CSS stacking-context limitations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
/**
|
|
* Layer Compositor — z-order analysis for multi-layer HDR compositing.
|
|
*
|
|
* Groups timed elements into z-ordered layers (DOM or HDR) for the
|
|
* per-frame compositing loop. Adjacent DOM elements merge into a single
|
|
* layer to minimize Chrome screenshots.
|
|
*/
|
|
|
|
import type { ElementStackingInfo } from "../services/videoFrameInjector.js";
|
|
|
|
export type { ElementStackingInfo };
|
|
|
|
export type CompositeLayer =
|
|
| { type: "dom"; elementIds: string[] }
|
|
| { type: "hdr"; element: ElementStackingInfo };
|
|
|
|
/**
|
|
* Group z-sorted elements into composite layers. Adjacent DOM elements merge
|
|
* into a single layer; each HDR video/image is its own layer.
|
|
*
|
|
* Elements are sorted by \`zIndex\` ascending (back to front). Ties fall
|
|
* through to V8's stable sort, which preserves \`querySelectorAll\` DOM order —
|
|
* this is the same order Chrome uses for equal-z elements in a stacking
|
|
* context, so the blit order matches what the user sees in-browser.
|
|
*
|
|
* The DOM merge doesn't lose information: DOM layers are rendered via a
|
|
* full-page screenshot with non-layer elements hidden, so within-layer
|
|
* z-order is handled by Chrome itself.
|
|
*
|
|
* Invisible elements ARE included (video elements are hidden by the frame
|
|
* injector, but their injected \`<img>\` replacements are visible — they must
|
|
* stay in the correct z-ordered layer so sibling layers' DOM screenshots
|
|
* hide them).
|
|
*/
|
|
export function groupIntoLayers(elements: ElementStackingInfo[]): CompositeLayer[] {
|
|
// Include ALL elements regardless of visibility. Video elements are hidden by
|
|
// the frame injector (HEVC can't decode in headless Chrome) but their injected
|
|
// <img> replacements ARE visible. We need them in the correct z-ordered layer
|
|
// so they get hidden from other layers' DOM screenshots.
|
|
const sorted = [...elements].sort((a, b) => a.zIndex - b.zIndex);
|
|
|
|
const layers: CompositeLayer[] = [];
|
|
|
|
for (const el of sorted) {
|
|
if (el.isHdr) {
|
|
layers.push({ type: "hdr", element: el });
|
|
} else {
|
|
const last = layers[layers.length - 1];
|
|
if (last && last.type === "dom") {
|
|
last.elementIds.push(el.id);
|
|
} else {
|
|
layers.push({ type: "dom", elementIds: [el.id] });
|
|
}
|
|
}
|
|
}
|
|
|
|
return layers;
|
|
}
|