mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
fix: activate nested child timelines during renderSeek
The renderSeek override in init.ts called seekTimelineAndAdapters() which only did rootTimeline.totalTime(t) without activating child timelines. GSAP does not propagate totalTime() to internally paused children. Also simplifies pollSubCompositionTimelines to always call rebind when timelines are ready, removing the before/after count comparison that could skip the rebind on fast page loads.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #07110d;
|
||||
}
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #07110d;
|
||||
}
|
||||
.scene {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
#scene-hook {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="6"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<!-- First sub-comp at near-zero start: the bug trigger -->
|
||||
<div
|
||||
id="scene-hook"
|
||||
class="scene"
|
||||
data-layout-allow-overflow
|
||||
data-composition-id="hook"
|
||||
data-composition-src="compositions/hook.html"
|
||||
data-start="0.001"
|
||||
data-duration="2.0"
|
||||
data-track-index="0"
|
||||
></div>
|
||||
|
||||
<!-- Second sub-comp at a later start: works correctly -->
|
||||
<div
|
||||
id="scene-later"
|
||||
class="scene"
|
||||
data-layout-allow-overflow
|
||||
data-composition-id="later"
|
||||
data-composition-src="compositions/later.html"
|
||||
data-start="2.5"
|
||||
data-duration="3.5"
|
||||
data-track-index="1"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
|
||||
// Transition: hook zooms out at 2.5s
|
||||
tl.to("#scene-hook", { scale: 2.5, opacity: 0, duration: 0.4, ease: "power3.in" }, 2.5);
|
||||
tl.fromTo(
|
||||
"#scene-later",
|
||||
{ scale: 0.5, opacity: 0 },
|
||||
{ scale: 1, opacity: 1, duration: 0.4, ease: "power3.out" },
|
||||
2.65,
|
||||
);
|
||||
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -444,6 +444,75 @@ describe("initSandboxRuntimeModular", () => {
|
||||
expect(video.currentTime).toBe(0);
|
||||
});
|
||||
|
||||
it("activates sub-composition timelines at data-start near 0 during renderSeek", () => {
|
||||
// Regression: sub-compositions starting at or near t=0 had their GSAP
|
||||
// sub-timelines ignored during render because renderSeek did not
|
||||
// activate (unpause) nested child timelines before seeking the root.
|
||||
// The children were added to the root while paused, and GSAP's
|
||||
// totalTime() does not propagate to paused children.
|
||||
const root = document.createElement("div");
|
||||
root.setAttribute("data-composition-id", "main");
|
||||
root.setAttribute("data-root", "true");
|
||||
root.setAttribute("data-start", "0");
|
||||
root.setAttribute("data-duration", "24");
|
||||
root.setAttribute("data-width", "1920");
|
||||
root.setAttribute("data-height", "1080");
|
||||
document.body.appendChild(root);
|
||||
|
||||
const hookHost = document.createElement("div");
|
||||
hookHost.setAttribute("data-composition-id", "hook");
|
||||
hookHost.setAttribute("data-start", "0.001");
|
||||
hookHost.setAttribute("data-duration", "2");
|
||||
hookHost.setAttribute("data-track-index", "0");
|
||||
hookHost.classList.add("clip");
|
||||
root.appendChild(hookHost);
|
||||
|
||||
const laterHost = document.createElement("div");
|
||||
laterHost.setAttribute("data-composition-id", "tweet");
|
||||
laterHost.setAttribute("data-start", "1.5");
|
||||
laterHost.setAttribute("data-duration", "4.5");
|
||||
laterHost.setAttribute("data-track-index", "1");
|
||||
laterHost.classList.add("clip");
|
||||
root.appendChild(laterHost);
|
||||
|
||||
const hookTimeline = createMockTimeline(2);
|
||||
const tweetTimeline = createMockTimeline(4.5);
|
||||
const rootTimeline = createMockTimeline(24);
|
||||
|
||||
(window as Window & { __timelines?: Record<string, RuntimeTimelineLike> }).__timelines = {
|
||||
main: rootTimeline,
|
||||
hook: hookTimeline,
|
||||
tweet: tweetTimeline,
|
||||
};
|
||||
|
||||
initSandboxRuntimeModular();
|
||||
|
||||
const player = (
|
||||
window as Window & {
|
||||
__player?: { renderSeek: (timeSeconds: number) => void };
|
||||
}
|
||||
).__player;
|
||||
expect(player).toBeDefined();
|
||||
|
||||
// Simulate that the hook timeline was paused (as happens when
|
||||
// children are added to a paused root timeline in GSAP)
|
||||
hookTimeline.paused!(true);
|
||||
tweetTimeline.paused!(true);
|
||||
|
||||
// Seek to 0.5s — well within the hook's window [0.001, 2.001]
|
||||
player?.renderSeek(0.5);
|
||||
|
||||
// renderSeek should activate (unpause) all child timelines before
|
||||
// seeking the root. Without the fix, children stay paused and GSAP's
|
||||
// totalTime() propagation skips them, leaving elements at initial CSS
|
||||
// state (opacity: 0).
|
||||
expect(hookTimeline.paused!()).toBe(false);
|
||||
expect(tweetTimeline.paused!()).toBe(false);
|
||||
|
||||
// The hook host should be visible at t=0.5
|
||||
expect(hookHost.style.visibility).toBe("visible");
|
||||
});
|
||||
|
||||
it("plays scheduled child timelines without a captured root timeline when audio has failed", () => {
|
||||
const raf = createManualRaf();
|
||||
vi.spyOn(performance, "now").mockImplementation(() => raf.now());
|
||||
|
||||
@@ -1724,9 +1724,37 @@ export function initSandboxRuntimeModular(): void {
|
||||
}
|
||||
};
|
||||
|
||||
const seekTimelineAndAdapters = (t: number) => {
|
||||
const activateNestedChildTimelines = (masterTimeline: RuntimeTimelineLike) => {
|
||||
const timelines = (window.__timelines ?? {}) as Record<string, RuntimeTimelineLike | undefined>;
|
||||
for (const tl of Object.values(timelines)) {
|
||||
if (!tl || tl === masterTimeline) continue;
|
||||
try {
|
||||
const tlWithPaused = tl as RuntimeTimelineLike & {
|
||||
paused?: (value?: boolean) => unknown;
|
||||
};
|
||||
if (typeof tlWithPaused.paused === "function") {
|
||||
tlWithPaused.paused(false);
|
||||
}
|
||||
} catch (err) {
|
||||
swallow("runtime.init.activateNested", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const seekTimelineAndAdapters = (t: number, activateChildren = false) => {
|
||||
const tl = state.capturedTimeline;
|
||||
if (tl) {
|
||||
// When rendering frame-by-frame (activateChildren=true), ensure all
|
||||
// nested child timelines are unpaused before seeking the root. GSAP
|
||||
// does not propagate totalTime() to children that are internally
|
||||
// paused, which leaves sub-compositions at their initial CSS state
|
||||
// (typically opacity:0). This mirrors the activateSiblingTimelines
|
||||
// call in player.ts renderSeek and is critical for sub-compositions
|
||||
// whose data-start is at or near 0 — they are added to the root
|
||||
// while it is paused and may never receive an explicit play().
|
||||
if (activateChildren) {
|
||||
activateNestedChildTimelines(tl);
|
||||
}
|
||||
try {
|
||||
if (typeof tl.totalTime === "function") {
|
||||
tl.totalTime(t, false);
|
||||
@@ -2001,7 +2029,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
state.currentTime = clock.now();
|
||||
state.isPlaying = false;
|
||||
state.mediaForceSyncNextTick = true;
|
||||
seekTimelineAndAdapters(state.currentTime);
|
||||
seekTimelineAndAdapters(state.currentTime, true);
|
||||
syncMediaForCurrentState();
|
||||
postState(true);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user