fix(core): scenes stop resetting during playback when the root has no animation of its own (#5693)

This commit is contained in:
Miguel Ángel
2026-10-16 17:32:02 +00:00
committed by GitHub
parent 1714f7c7b4
commit 19a8d44b48
2 changed files with 41 additions and 3 deletions
@@ -199,6 +199,29 @@ describe("runtime sub-composition placement", () => {
expect(window.__player?.getDuration()).toBe(9);
});
it("keeps the scene in one root after a tween first renders, when the root's own timeline is empty", async () => {
const runFrames = stubFrames();
document.body.innerHTML =
`<div data-composition-id="main" data-root="true" data-duration="10">` +
`<div id="host" class="clip" data-composition-id="scene" data-start="1" data-duration="9">` +
`<div id="s"></div></div></div>`;
const scene = gsap
.timeline({ paused: true })
.fromTo("#s", { x: -100 }, { x: 0, duration: 0.5, ease: "none" }, 0.5)
.to({}, { duration: 9 }, 0);
const root = gsap.timeline({ paused: true });
window.__timelines = { main: root, scene } as unknown as Record<string, RuntimeTimelineLike>;
initSandboxRuntimeModular();
window.__player?.seek(1.2);
const bound = scene.parent;
window.__player?.seek(3);
await runFrames(6);
expect(scene.parent).toBe(bound);
expect(shownX()).toBe(0);
});
it("binds a root timeline that registers after the runtime started on its children", async () => {
const runFrames = stubFrames();
loadWithoutRootTimeline(`<div id="r"></div>`);
+18 -3
View File
@@ -1,13 +1,28 @@
import { swallow } from "./diagnostics";
import type { RuntimeTimelineLike } from "./types";
/** A registered timeline's finite duration(); a partial, throwing or non-numeric entry reads as null. */
type ChildTimeline = {
paused?: () => boolean;
startTime?: () => number;
totalDuration?: () => number;
timeScale?: () => number;
};
const pausedChildEnd = (child: ChildTimeline): number =>
child.paused?.() === true
? (child.startTime?.() ?? 0) +
(child.totalDuration?.() ?? 0) / Math.abs(child.timeScale?.() || 1)
: 0;
/** A timeline's finite length, counting paused child timelines; a partial or throwing entry reads as null. */
export function readTimelineDurationSeconds(
timeline: Pick<RuntimeTimelineLike, "duration"> | null | undefined,
timeline: Pick<RuntimeTimelineLike, "duration" | "getChildren"> | null | undefined,
): number | null {
try {
if (typeof timeline?.duration !== "function") return null;
const seconds = Number(timeline.duration());
const held: unknown = timeline.getChildren?.(false, false, true);
const children = Array.isArray(held) ? (held as ChildTimeline[]) : [];
const seconds = Math.max(Number(timeline.duration()), ...children.map(pausedChildEnd));
return Number.isFinite(seconds) ? seconds : null;
} catch (err) {
swallow("runtime.timelineDuration", err);