mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
feat(hdr): z-ordered multi-layer compositing with PQ support (#289)
* 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>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a21a62b574
commit
0cc79a35b0
@@ -225,3 +225,95 @@ export async function queryVideoElementBounds(
|
||||
});
|
||||
}, videoIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stacking info for a single timed element, used by the z-ordered layer compositor.
|
||||
*/
|
||||
export interface ElementStackingInfo {
|
||||
id: string;
|
||||
zIndex: number;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
opacity: number;
|
||||
visible: boolean;
|
||||
isHdr: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query Chrome for ALL timed elements' stacking context.
|
||||
* Returns z-index, bounds, opacity, and whether each element is a native HDR video.
|
||||
*
|
||||
* Queries every element with `data-start` (not just videos) so the layer compositor
|
||||
* can determine z-ordering between DOM content and HDR video elements.
|
||||
*/
|
||||
export async function queryElementStacking(
|
||||
page: Page,
|
||||
nativeHdrVideoIds: Set<string>,
|
||||
): Promise<ElementStackingInfo[]> {
|
||||
const hdrIds = Array.from(nativeHdrVideoIds);
|
||||
return page.evaluate((hdrIdList: string[]): ElementStackingInfo[] => {
|
||||
const hdrSet = new Set(hdrIdList);
|
||||
const elements = document.querySelectorAll("[data-start]");
|
||||
const results: ElementStackingInfo[] = [];
|
||||
|
||||
// Walk up the DOM to find the effective z-index from the nearest
|
||||
// positioned ancestor with a z-index. CSS z-index only applies to
|
||||
// positioned elements; video elements inside positioned wrappers
|
||||
// inherit the wrapper's stacking context.
|
||||
//
|
||||
// ## Supported subset
|
||||
//
|
||||
// This implementation looks for explicit `z-index` on positioned
|
||||
// (non-static) ancestors. It does NOT detect the CSS stacking contexts
|
||||
// created implicitly by other properties — including `opacity < 1`,
|
||||
// `transform`, `filter`, `will-change`, `isolation: isolate`, and
|
||||
// `mix-blend-mode`. GSAP routinely sets `transform` on wrappers, which
|
||||
// creates an implicit stacking context with auto z-index; an HDR video
|
||||
// inside such a wrapper with no explicit z-index will return the
|
||||
// wrapper-of-the-wrapper's z-index here, potentially reordering layers
|
||||
// incorrectly relative to sibling stacking contexts.
|
||||
//
|
||||
// The workaround is to set explicit `z-index` on the positioned wrapper
|
||||
// when you want it treated as a compositing layer root. This matches
|
||||
// what compositions need to do anyway for deterministic z-ordering.
|
||||
function getEffectiveZIndex(node: Element): number {
|
||||
let current: Element | null = node;
|
||||
while (current) {
|
||||
const cs = window.getComputedStyle(current);
|
||||
const pos = cs.position;
|
||||
const z = parseInt(cs.zIndex);
|
||||
if (!Number.isNaN(z) && pos !== "static") return z;
|
||||
current = current.parentElement;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const el of elements) {
|
||||
const id = el.id;
|
||||
if (!id) continue;
|
||||
const rect = el.getBoundingClientRect();
|
||||
const style = window.getComputedStyle(el);
|
||||
const zIndex = getEffectiveZIndex(el);
|
||||
const opacity = parseFloat(style.opacity) || 1;
|
||||
const visible =
|
||||
style.visibility !== "hidden" &&
|
||||
style.display !== "none" &&
|
||||
rect.width > 0 &&
|
||||
rect.height > 0;
|
||||
results.push({
|
||||
id,
|
||||
zIndex,
|
||||
x: Math.round(rect.x),
|
||||
y: Math.round(rect.y),
|
||||
width: Math.round(rect.width),
|
||||
height: Math.round(rect.height),
|
||||
opacity,
|
||||
visible,
|
||||
isHdr: hdrSet.has(id),
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}, hdrIds);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user