Skip to content

Commit f9185a7

Browse files
committed
fix(core): move keyframed tweens across their start before the forward step
Re-crossing a keyframed tween after the timeline's forward render made it win over a later same-time sibling (a set after it at the same time showed the keyframe, not the set). The keyframed tweens that start at the target are now moved across their start during the step from below, and the timeline's own forward step renders them with every sibling, in authored order.
1 parent ca6697a commit f9185a7

2 files changed

Lines changed: 31 additions & 5 deletions

File tree

‎packages/core/src/runtime/adapters/gsap.steps.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,33 @@ describe("gsap adapter at a tween's start", () => {
9090
expect(gsap.getProperty(box, "x")).toBe(0);
9191
});
9292

93+
it.each([
94+
{ order: "keyframes, then a set", x: 42 },
95+
{ order: "a set, then keyframes", x: 5 },
96+
])("keeps the authored order of $order at the seek time", ({ order, x }) => {
97+
const box = document.body.appendChild(document.createElement("div"));
98+
const timeline = gsap.timeline({ paused: true });
99+
timeline.from(box, { x: -60, duration: 2, ease: "none" }, 0);
100+
const keyframes = () =>
101+
timeline.to(box, { keyframes: { "0%": { x: 5 }, "100%": { x: 60 } }, duration: 1 }, 2);
102+
const set = () => timeline.set(box, { x: 42 }, 2);
103+
if (order.startsWith("keyframes")) {
104+
keyframes();
105+
set();
106+
} else {
107+
set();
108+
keyframes();
109+
}
110+
const adapter = createGsapAdapter({
111+
getTimeline: () => timeline as unknown as RuntimeTimelineLike,
112+
});
113+
for (const from of [0, 1, 2, 2.5, 3]) {
114+
timeline.totalTime(from, true);
115+
adapter.seek({ time: 2 });
116+
expect(gsap.getProperty(box, "x")).toBe(x);
117+
}
118+
});
119+
93120
it("shows a keyframed tween's start inside a nested timeline, seeking onto it twice", () => {
94121
const box = document.body.appendChild(document.createElement("div"));
95122
const timeline = gsap.timeline({ paused: true });

‎packages/core/src/runtime/adapters/gsap.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ type GsapAdapterDeps = {
66

77
/**
88
* Re-renders a timeline already at `t`, silently, from just below (above at 0) so same-time steps apply in authored
9-
* order. That step skips a keyframed tween already at its start, so each one re-crosses its own start alone.
9+
* order. That step skips a keyframed tween already at its start, so each one is first moved across its start alone.
1010
*/
1111
export function rerenderGsapTimelineAt(
1212
timeline: {
@@ -16,8 +16,8 @@ export function rerenderGsapTimelineAt(
1616
t: number,
1717
): void {
1818
timeline.totalTime(t >= 0.001 ? t - 0.001 : t + 0.001, true);
19+
primeKeyframedTweensStartingAt(timeline.getChildren?.(false, true, true) ?? [], t);
1920
timeline.totalTime(t, true);
20-
recrossTweensStartingAt(timeline.getChildren?.(false, true, true) ?? [], t);
2121
}
2222

2323
type GsapAnimation = {
@@ -44,15 +44,14 @@ const playsForward = (value: unknown): value is GsapAnimation => {
4444
);
4545
};
4646

47-
function recrossTweensStartingAt(children: unknown[], time: number): void {
47+
function primeKeyframedTweensStartingAt(children: unknown[], time: number): void {
4848
for (const child of children.filter(playsForward)) {
4949
const local = (time - child.startTime()) * child.timeScale();
5050
if (Math.abs(local) < 1e-9 && child.vars?.keyframes) {
5151
child.render(BELOW_GSAP_TIME_RESOLUTION, true);
5252
child.render(-BELOW_GSAP_TIME_RESOLUTION, true);
53-
child.render(0, true);
5453
} else if (child.getChildren && local > 0 && local <= child.totalDuration()) {
55-
recrossTweensStartingAt(child.getChildren(false, true, true), local);
54+
primeKeyframedTweensStartingAt(child.getChildren(false, true, true), local);
5655
}
5756
}
5857
}

0 commit comments

Comments
 (0)