mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix: hide descendants of inactive render clips (#1662)
This commit is contained in:
@@ -116,7 +116,7 @@ describe("initSandboxRuntimeModular", () => {
|
||||
window.cancelAnimationFrame = originalCancelAnimationFrame;
|
||||
});
|
||||
|
||||
it("uses the shorter live child timeline when the authored window is longer", () => {
|
||||
it("keeps authored composition hosts visible when the live child timeline is shorter", () => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
root.setAttribute("data-root", "true");
|
||||
@@ -143,6 +143,37 @@ describe("initSandboxRuntimeModular", () => {
|
||||
|
||||
player?.renderSeek(9);
|
||||
|
||||
expect(child.style.visibility).toBe("visible");
|
||||
});
|
||||
|
||||
it("uses live child timeline duration when a composition host has no authored duration", () => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
root.setAttribute("data-root", "true");
|
||||
root.setAttribute("data-start", "0");
|
||||
root.setAttribute("data-width", "1920");
|
||||
root.setAttribute("data-height", "1080");
|
||||
document.body.appendChild(root);
|
||||
|
||||
const child = document.createElement("div");
|
||||
child.setAttribute("data-composition-id", "slide-1");
|
||||
child.setAttribute("data-start", "0");
|
||||
root.appendChild(child);
|
||||
|
||||
window.__timelines = {
|
||||
main: createMockTimeline(20),
|
||||
"slide-1": createMockTimeline(8),
|
||||
};
|
||||
|
||||
initSandboxRuntimeModular();
|
||||
|
||||
const player = window.__player;
|
||||
expect(player).toBeDefined();
|
||||
|
||||
player?.renderSeek(7);
|
||||
expect(child.style.visibility).toBe("visible");
|
||||
|
||||
player?.renderSeek(9);
|
||||
expect(child.style.visibility).toBe("hidden");
|
||||
});
|
||||
|
||||
@@ -491,7 +522,7 @@ describe("initSandboxRuntimeModular", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("does not suppress descendant visibility in render mode (top-level page)", () => {
|
||||
it("hides timed descendants inside a hidden timed clip in render mode", () => {
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
root.setAttribute("data-root", "true");
|
||||
@@ -507,12 +538,13 @@ describe("initSandboxRuntimeModular", () => {
|
||||
panel.setAttribute("data-duration", "2");
|
||||
root.appendChild(panel);
|
||||
|
||||
const headline = document.createElement("h1");
|
||||
headline.className = "headline";
|
||||
// Authored child window outlives the parent clip — render keeps legacy behavior.
|
||||
headline.setAttribute("data-start", "0");
|
||||
headline.setAttribute("data-duration", "8");
|
||||
panel.appendChild(headline);
|
||||
const bottomBand = document.createElement("div");
|
||||
bottomBand.className = "bottom-band";
|
||||
// Regression shape: a child strip outlives its parent scene. Without
|
||||
// ancestor suppression it can paint through after the parent has ended.
|
||||
bottomBand.setAttribute("data-start", "0");
|
||||
bottomBand.setAttribute("data-duration", "8");
|
||||
panel.appendChild(bottomBand);
|
||||
|
||||
window.__timelines = {
|
||||
main: createMockTimeline(8),
|
||||
@@ -526,7 +558,7 @@ describe("initSandboxRuntimeModular", () => {
|
||||
player?.seek(3);
|
||||
|
||||
expect(panel.style.visibility).toBe("hidden");
|
||||
expect(headline.style.visibility).toBe("visible");
|
||||
expect(bottomBand.style.visibility).toBe("hidden");
|
||||
});
|
||||
|
||||
it("does not stamp Studio timing on GSAP targets inside authored timed clips", () => {
|
||||
|
||||
@@ -432,18 +432,13 @@ export function initSandboxRuntimeModular(): void {
|
||||
}
|
||||
}
|
||||
|
||||
const usesExternalCompositionSlot =
|
||||
rawNode.hasAttribute("data-composition-src") ||
|
||||
rawNode.hasAttribute("data-composition-file");
|
||||
const hasAuthoredTiming =
|
||||
rawNode.hasAttribute("data-duration") ||
|
||||
rawNode.hasAttribute("data-end") ||
|
||||
rawNode.hasAttribute(AUTHORED_DURATION_ATTR) ||
|
||||
rawNode.hasAttribute(AUTHORED_END_ATTR);
|
||||
|
||||
if (
|
||||
duration != null &&
|
||||
duration > 0 &&
|
||||
liveDuration != null &&
|
||||
!usesExternalCompositionSlot
|
||||
) {
|
||||
duration = Math.min(duration, liveDuration);
|
||||
} else if ((duration == null || duration <= 0) && liveDuration != null) {
|
||||
if (!hasAuthoredTiming && (duration == null || duration <= 0) && liveDuration != null) {
|
||||
duration = liveDuration;
|
||||
}
|
||||
}
|
||||
@@ -1481,10 +1476,9 @@ export function initSandboxRuntimeModular(): void {
|
||||
const resolveMediaCompositionContext = (element: HTMLVideoElement | HTMLAudioElement) => {
|
||||
const compositionRoot = element.closest("[data-composition-id]");
|
||||
const inheritedStart = compositionRoot ? resolveStartForElement(compositionRoot, 0) : null;
|
||||
// Media sync intentionally uses the authored host window here instead of
|
||||
// the live child timeline duration. Visibility prefers live truth so a
|
||||
// shrinking child composition hides early, but nested media needs a
|
||||
// stable authored window so seeks clamp against the host clip timing.
|
||||
// Media sync follows the authored host window, matching visibility for
|
||||
// authored composition hosts. Live child timeline duration only fills in
|
||||
// when no authored timing exists, so seeks clamp against host clip timing.
|
||||
const inheritedDuration = compositionRoot
|
||||
? resolveDurationForElement(compositionRoot, { includeAuthoredTimingAttrs: true })
|
||||
: null;
|
||||
@@ -1566,11 +1560,10 @@ export function initSandboxRuntimeModular(): void {
|
||||
if (!(rawNode instanceof HTMLElement)) continue;
|
||||
|
||||
let isVisibleNow = isTimedElementVisibleAt(rawNode, state.currentTime);
|
||||
// Studio-only defense-in-depth: pseudo-clips stamped on tween targets can
|
||||
// get visibility:visible for the full composition. Render mode never stamps
|
||||
// those targets, so keep the prior per-element visibility semantics there.
|
||||
if (isVisibleNow && window.parent !== window) {
|
||||
// Descendants must not override a hidden ancestor clip.
|
||||
// Descendants must not override a hidden ancestor clip. CSS visibility can
|
||||
// otherwise leak child pixels through inactive scenes because a descendant
|
||||
// with visibility:visible escapes an ancestor's visibility:hidden.
|
||||
if (isVisibleNow) {
|
||||
let ancestor = rawNode.parentElement;
|
||||
while (ancestor) {
|
||||
if (ancestor === rootComp) break;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Timed descendant visibility",
|
||||
"description": "Regression for hidden timed clips leaking visible descendants during production render. A bottom band inside the first scene intentionally outlives its parent; render-time visibility sync must hide it once the parent scene becomes inactive.",
|
||||
"tags": ["visibility", "regression", "runtime"],
|
||||
"minPsnr": 45,
|
||||
"maxFrameFailures": 0,
|
||||
"minAudioCorrelation": 0,
|
||||
"maxAudioLagWindows": 1,
|
||||
"renderConfig": {
|
||||
"fps": 30,
|
||||
"workers": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><style data-hyperframes-text-rendering="true">html,body,*{text-rendering:geometricPrecision}</style>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=640, height=360">
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 640px;
|
||||
height: 360px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #f5fafd;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: relative;
|
||||
width: 640px;
|
||||
height: 360px;
|
||||
overflow: hidden;
|
||||
background: #f5fafd;
|
||||
}
|
||||
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#scene-a {
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
#scene-b {
|
||||
background: #f5fafd;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: absolute;
|
||||
inset: 22px 42px 48px;
|
||||
border: 3px solid #333b44;
|
||||
border-radius: 8px;
|
||||
background: #fffdfa;
|
||||
}
|
||||
|
||||
.panel::before {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 34px;
|
||||
width: 170px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
background: #333b44;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.panel::after {
|
||||
position: absolute;
|
||||
top: 84px;
|
||||
left: 34px;
|
||||
width: 310px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: #9aa7b5;
|
||||
box-shadow:
|
||||
0 24px 0 #c3ccd7,
|
||||
0 48px 0 #d6dde7;
|
||||
content: "";
|
||||
}
|
||||
|
||||
#leaky-band {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
height: 31px;
|
||||
background: #daebff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" data-composition-id="main" data-root="true" data-width="640" data-height="360" data-start="0" data-duration="9">
|
||||
<section id="scene-a" class="scene clip" data-start="0" data-duration="6">
|
||||
<div class="panel"></div>
|
||||
<div id="leaky-band" class="clip" data-start="0" data-duration="9"></div>
|
||||
</section>
|
||||
<section id="scene-b" class="scene clip" data-start="6" data-duration="3">
|
||||
<div class="panel"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
||||
<script>window.__timelines = window.__timelines || {};
|
||||
window.__timelines.main = {
|
||||
duration: function () {
|
||||
return 9;
|
||||
},
|
||||
totalDuration: function () {
|
||||
return 9;
|
||||
},
|
||||
pause: function () {},
|
||||
seek: function () {},
|
||||
};</script></body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c5aac54303c4422aed7b0264c007dcfb314fca3dc74a16905eafff3914fb95e7
|
||||
size 21971
|
||||
@@ -0,0 +1,119 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=640, height=360" />
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 640px;
|
||||
height: 360px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #f5fafd;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: relative;
|
||||
width: 640px;
|
||||
height: 360px;
|
||||
overflow: hidden;
|
||||
background: #f5fafd;
|
||||
}
|
||||
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#scene-a {
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
#scene-b {
|
||||
background: #f5fafd;
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: absolute;
|
||||
inset: 22px 42px 48px;
|
||||
border: 3px solid #333b44;
|
||||
border-radius: 8px;
|
||||
background: #fffdfa;
|
||||
}
|
||||
|
||||
.panel::before {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 34px;
|
||||
width: 170px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
background: #333b44;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.panel::after {
|
||||
position: absolute;
|
||||
top: 84px;
|
||||
left: 34px;
|
||||
width: 310px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: #9aa7b5;
|
||||
box-shadow:
|
||||
0 24px 0 #c3ccd7,
|
||||
0 48px 0 #d6dde7;
|
||||
content: "";
|
||||
}
|
||||
|
||||
#leaky-band {
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
height: 31px;
|
||||
background: #daebff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-root="true"
|
||||
data-width="640"
|
||||
data-height="360"
|
||||
data-start="0"
|
||||
data-duration="9"
|
||||
>
|
||||
<section id="scene-a" class="scene clip" data-start="0" data-duration="6">
|
||||
<div class="panel"></div>
|
||||
<div id="leaky-band" class="clip" data-start="0" data-duration="9"></div>
|
||||
</section>
|
||||
<section id="scene-b" class="scene clip" data-start="6" data-duration="3">
|
||||
<div class="panel"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines.main = {
|
||||
duration: function () {
|
||||
return 9;
|
||||
},
|
||||
totalDuration: function () {
|
||||
return 9;
|
||||
},
|
||||
pause: function () {},
|
||||
seek: function () {},
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user