mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-04 04:46:59 +00:00
fix(core): re-render a seek from just above too, not with a forced render
A forced render at the seek time redrew every tween GSAP had ever rendered, including ones that start later, at their recorded start values: after a seek past the end, a later tween's start showed early (style-5-prod, hdr-regression). The re-render now steps silently just above the target, then just below, then to it. The step up makes a child that starts exactly at the target cross its start on the way down, so it renders; tweens that have not started are left as the seek put them, and the last move is still forward, so same-time steps keep their authored order.
This commit is contained in:
@@ -4,10 +4,9 @@ import { createRuntimePlayer } from "../src/runtime/player";
|
||||
import type { RuntimeTimelineLike } from "../src/runtime/types";
|
||||
|
||||
type Call = {
|
||||
method: "pause" | "seek" | "totalTime" | "render";
|
||||
method: "pause" | "seek" | "totalTime";
|
||||
time?: number;
|
||||
suppressEvents?: boolean;
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
function createTimeline(withTotalTime: boolean): { calls: Call[]; timeline: RuntimeTimelineLike } {
|
||||
@@ -30,9 +29,6 @@ function createTimeline(withTotalTime: boolean): { calls: Call[]; timeline: Runt
|
||||
timeline.totalTime = (timeSeconds: number, suppressEvents?: boolean) => {
|
||||
calls.push({ method: "totalTime", time: timeSeconds, suppressEvents });
|
||||
};
|
||||
timeline.render = (time: number, suppressEvents?: boolean, force?: boolean) => {
|
||||
calls.push({ method: "render", time, suppressEvents, force });
|
||||
};
|
||||
}
|
||||
return { calls, timeline };
|
||||
}
|
||||
@@ -105,9 +101,12 @@ function testGsapAdapterPreservesTotalTime(): void {
|
||||
[
|
||||
{ method: "pause" },
|
||||
{ method: "totalTime", time: seekTime, suppressEvents: false },
|
||||
{ method: "render", time: seekTime, suppressEvents: true, force: true },
|
||||
// Silent re-render arriving from below, so same-time steps apply in authored order
|
||||
{ method: "totalTime", time: seekTime + 0.001, suppressEvents: true },
|
||||
{ method: "totalTime", time: seekTime - 0.001, suppressEvents: true },
|
||||
{ method: "totalTime", time: seekTime, suppressEvents: true },
|
||||
],
|
||||
"GSAP adapter should seek via totalTime() then force a silent re-render at the same time",
|
||||
"GSAP adapter should seek via totalTime() then re-render from just below",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ describe("gsap adapter on a step", () => {
|
||||
|
||||
describe("gsap adapter at a tween's start", () => {
|
||||
// An edit at 2 s turns the later tween into keyframes; its first keyframe must win over the from() end.
|
||||
it.each([0, 1, 2.5, 3])(
|
||||
it.each([0, 1, 2, 2.5, 3])(
|
||||
"shows the tween that starts at the seek time, seeking from %s s",
|
||||
(from) => {
|
||||
const box = document.body.appendChild(document.createElement("div"));
|
||||
@@ -77,4 +77,16 @@ describe("gsap adapter at a tween's start", () => {
|
||||
expect(gsap.getProperty(box, "x")).toBe(5);
|
||||
},
|
||||
);
|
||||
|
||||
it("leaves a tween that starts later alone after a seek past the end", () => {
|
||||
const box = document.body.appendChild(document.createElement("div"));
|
||||
const timeline = gsap.timeline({ paused: true });
|
||||
timeline.to(box, { x: -400, duration: 0.4 }, 4.8);
|
||||
timeline.to(box, { x: 0, duration: 0.4 }, 16);
|
||||
const adapter = createGsapAdapter({
|
||||
getTimeline: () => timeline as unknown as RuntimeTimelineLike,
|
||||
});
|
||||
for (const time of [20, 0, 4]) adapter.seek({ time });
|
||||
expect(gsap.getProperty(box, "x")).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,14 +5,16 @@ type GsapAdapterDeps = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Re-renders a timeline already at `t`, silently. A forced render at the same time walks the children
|
||||
* forward in authored order and re-renders each one, including a child that starts exactly at `t`.
|
||||
* Re-renders a timeline already at `t`, silently: just above, just below (not below 0), then `t`. The last move
|
||||
* is forward so same-time steps apply in authored order, and a child starting exactly at `t` crosses its start.
|
||||
*/
|
||||
export function rerenderGsapTimelineAt(
|
||||
timeline: Pick<RuntimeTimelineLike, "render">,
|
||||
timeline: { totalTime: (time: number, suppressEvents?: boolean) => unknown },
|
||||
t: number,
|
||||
): void {
|
||||
timeline.render?.(t, true, true);
|
||||
timeline.totalTime(t + 0.001, true);
|
||||
if (t >= 0.001) timeline.totalTime(t - 0.001, true);
|
||||
timeline.totalTime(t, true);
|
||||
}
|
||||
|
||||
export function createGsapAdapter(deps: GsapAdapterDeps): RuntimeDeterministicAdapter {
|
||||
@@ -27,7 +29,7 @@ export function createGsapAdapter(deps: GsapAdapterDeps): RuntimeDeterministicAd
|
||||
const suppressEvents = ctx.suppressEvents === true;
|
||||
if (typeof timeline.totalTime === "function") {
|
||||
timeline.totalTime(safeTime, suppressEvents);
|
||||
rerenderGsapTimelineAt(timeline, safeTime);
|
||||
rerenderGsapTimelineAt({ totalTime: timeline.totalTime.bind(timeline) }, safeTime);
|
||||
} else {
|
||||
timeline.seek(safeTime, suppressEvents);
|
||||
}
|
||||
|
||||
@@ -2835,16 +2835,13 @@ describe("initSandboxRuntimeModular", () => {
|
||||
root.setAttribute("data-height", "1080");
|
||||
document.body.appendChild(root);
|
||||
|
||||
const seekCalls: Array<{ time: number; suppressEvents?: boolean; force?: boolean }> = [];
|
||||
const seekCalls: Array<{ time: number; suppressEvents?: boolean }> = [];
|
||||
const rootTimeline = createMockTimeline(10);
|
||||
const originalTotalTime = rootTimeline.totalTime;
|
||||
rootTimeline.totalTime = (time: number, suppressEvents?: boolean) => {
|
||||
seekCalls.push({ time, suppressEvents });
|
||||
return originalTotalTime?.(time, suppressEvents);
|
||||
};
|
||||
rootTimeline.render = (time: number, suppressEvents?: boolean, force?: boolean) => {
|
||||
seekCalls.push({ time, suppressEvents, force });
|
||||
};
|
||||
|
||||
window.__timelines = { main: rootTimeline };
|
||||
initSandboxRuntimeModular();
|
||||
@@ -2854,7 +2851,9 @@ describe("initSandboxRuntimeModular", () => {
|
||||
|
||||
expect(seekCalls).toEqual([
|
||||
{ time: 2, suppressEvents: false },
|
||||
{ time: 2, suppressEvents: true, force: true },
|
||||
{ time: 2.001, suppressEvents: true },
|
||||
{ time: 1.999, suppressEvents: true },
|
||||
{ time: 2, suppressEvents: true },
|
||||
]);
|
||||
|
||||
seekCalls.length = 0;
|
||||
@@ -2873,16 +2872,13 @@ describe("initSandboxRuntimeModular", () => {
|
||||
root.setAttribute("data-height", "1080");
|
||||
document.body.appendChild(root);
|
||||
|
||||
const seekCalls: Array<{ time: number; suppressEvents?: boolean; force?: boolean }> = [];
|
||||
const seekCalls: Array<{ time: number; suppressEvents?: boolean }> = [];
|
||||
const rootTimeline = createMockTimeline(10);
|
||||
const originalTotalTime = rootTimeline.totalTime;
|
||||
rootTimeline.totalTime = (time: number, suppressEvents?: boolean) => {
|
||||
seekCalls.push({ time, suppressEvents });
|
||||
return originalTotalTime?.(time, suppressEvents);
|
||||
};
|
||||
rootTimeline.render = (time: number, suppressEvents?: boolean, force?: boolean) => {
|
||||
seekCalls.push({ time, suppressEvents, force });
|
||||
};
|
||||
Object.assign(rootTimeline, {
|
||||
getChildren: () => [
|
||||
{
|
||||
|
||||
@@ -4361,7 +4361,7 @@ export function initSandboxRuntimeModular(): void {
|
||||
tl.totalTime(tlSeekTime, suppressEvents);
|
||||
if (!suppressEvents && !hasZeroDurationCallbackTween(tl)) {
|
||||
// The first seek is the only eventful one; the re-render only refreshes styles.
|
||||
rerenderGsapTimelineAt(tl, tlSeekTime);
|
||||
rerenderGsapTimelineAt({ totalTime: tl.totalTime.bind(tl) }, tlSeekTime);
|
||||
}
|
||||
} else {
|
||||
tl.seek(tlSeekTime, suppressEvents);
|
||||
|
||||
@@ -315,7 +315,6 @@ export type RuntimeTimelineLike = {
|
||||
seek: (timeSeconds?: number, suppressEvents?: boolean) => unknown;
|
||||
totalTime?: (timeSeconds?: number, suppressEvents?: boolean) => unknown;
|
||||
progress?: (value?: number, suppressEvents?: boolean) => unknown;
|
||||
render?: (totalTime: number, suppressEvents?: boolean, force?: boolean) => unknown;
|
||||
time: () => number;
|
||||
duration: () => number;
|
||||
add: (timeline: RuntimeTimelineLike, startAtSeconds: number) => void;
|
||||
|
||||
Reference in New Issue
Block a user