mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-04 21:17:30 +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";
|
import type { RuntimeTimelineLike } from "../src/runtime/types";
|
||||||
|
|
||||||
type Call = {
|
type Call = {
|
||||||
method: "pause" | "seek" | "totalTime" | "render";
|
method: "pause" | "seek" | "totalTime";
|
||||||
time?: number;
|
time?: number;
|
||||||
suppressEvents?: boolean;
|
suppressEvents?: boolean;
|
||||||
force?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function createTimeline(withTotalTime: boolean): { calls: Call[]; timeline: RuntimeTimelineLike } {
|
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) => {
|
timeline.totalTime = (timeSeconds: number, suppressEvents?: boolean) => {
|
||||||
calls.push({ method: "totalTime", time: timeSeconds, suppressEvents });
|
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 };
|
return { calls, timeline };
|
||||||
}
|
}
|
||||||
@@ -105,9 +101,12 @@ function testGsapAdapterPreservesTotalTime(): void {
|
|||||||
[
|
[
|
||||||
{ method: "pause" },
|
{ method: "pause" },
|
||||||
{ method: "totalTime", time: seekTime, suppressEvents: false },
|
{ 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", () => {
|
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.
|
// 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",
|
"shows the tween that starts at the seek time, seeking from %s s",
|
||||||
(from) => {
|
(from) => {
|
||||||
const box = document.body.appendChild(document.createElement("div"));
|
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);
|
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
|
* Re-renders a timeline already at `t`, silently: just above, just below (not below 0), then `t`. The last move
|
||||||
* forward in authored order and re-renders each one, including a child that starts exactly at `t`.
|
* is forward so same-time steps apply in authored order, and a child starting exactly at `t` crosses its start.
|
||||||
*/
|
*/
|
||||||
export function rerenderGsapTimelineAt(
|
export function rerenderGsapTimelineAt(
|
||||||
timeline: Pick<RuntimeTimelineLike, "render">,
|
timeline: { totalTime: (time: number, suppressEvents?: boolean) => unknown },
|
||||||
t: number,
|
t: number,
|
||||||
): void {
|
): 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 {
|
export function createGsapAdapter(deps: GsapAdapterDeps): RuntimeDeterministicAdapter {
|
||||||
@@ -27,7 +29,7 @@ export function createGsapAdapter(deps: GsapAdapterDeps): RuntimeDeterministicAd
|
|||||||
const suppressEvents = ctx.suppressEvents === true;
|
const suppressEvents = ctx.suppressEvents === true;
|
||||||
if (typeof timeline.totalTime === "function") {
|
if (typeof timeline.totalTime === "function") {
|
||||||
timeline.totalTime(safeTime, suppressEvents);
|
timeline.totalTime(safeTime, suppressEvents);
|
||||||
rerenderGsapTimelineAt(timeline, safeTime);
|
rerenderGsapTimelineAt({ totalTime: timeline.totalTime.bind(timeline) }, safeTime);
|
||||||
} else {
|
} else {
|
||||||
timeline.seek(safeTime, suppressEvents);
|
timeline.seek(safeTime, suppressEvents);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2835,16 +2835,13 @@ describe("initSandboxRuntimeModular", () => {
|
|||||||
root.setAttribute("data-height", "1080");
|
root.setAttribute("data-height", "1080");
|
||||||
document.body.appendChild(root);
|
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 rootTimeline = createMockTimeline(10);
|
||||||
const originalTotalTime = rootTimeline.totalTime;
|
const originalTotalTime = rootTimeline.totalTime;
|
||||||
rootTimeline.totalTime = (time: number, suppressEvents?: boolean) => {
|
rootTimeline.totalTime = (time: number, suppressEvents?: boolean) => {
|
||||||
seekCalls.push({ time, suppressEvents });
|
seekCalls.push({ time, suppressEvents });
|
||||||
return originalTotalTime?.(time, suppressEvents);
|
return originalTotalTime?.(time, suppressEvents);
|
||||||
};
|
};
|
||||||
rootTimeline.render = (time: number, suppressEvents?: boolean, force?: boolean) => {
|
|
||||||
seekCalls.push({ time, suppressEvents, force });
|
|
||||||
};
|
|
||||||
|
|
||||||
window.__timelines = { main: rootTimeline };
|
window.__timelines = { main: rootTimeline };
|
||||||
initSandboxRuntimeModular();
|
initSandboxRuntimeModular();
|
||||||
@@ -2854,7 +2851,9 @@ describe("initSandboxRuntimeModular", () => {
|
|||||||
|
|
||||||
expect(seekCalls).toEqual([
|
expect(seekCalls).toEqual([
|
||||||
{ time: 2, suppressEvents: false },
|
{ 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;
|
seekCalls.length = 0;
|
||||||
@@ -2873,16 +2872,13 @@ describe("initSandboxRuntimeModular", () => {
|
|||||||
root.setAttribute("data-height", "1080");
|
root.setAttribute("data-height", "1080");
|
||||||
document.body.appendChild(root);
|
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 rootTimeline = createMockTimeline(10);
|
||||||
const originalTotalTime = rootTimeline.totalTime;
|
const originalTotalTime = rootTimeline.totalTime;
|
||||||
rootTimeline.totalTime = (time: number, suppressEvents?: boolean) => {
|
rootTimeline.totalTime = (time: number, suppressEvents?: boolean) => {
|
||||||
seekCalls.push({ time, suppressEvents });
|
seekCalls.push({ time, suppressEvents });
|
||||||
return originalTotalTime?.(time, suppressEvents);
|
return originalTotalTime?.(time, suppressEvents);
|
||||||
};
|
};
|
||||||
rootTimeline.render = (time: number, suppressEvents?: boolean, force?: boolean) => {
|
|
||||||
seekCalls.push({ time, suppressEvents, force });
|
|
||||||
};
|
|
||||||
Object.assign(rootTimeline, {
|
Object.assign(rootTimeline, {
|
||||||
getChildren: () => [
|
getChildren: () => [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4361,7 +4361,7 @@ export function initSandboxRuntimeModular(): void {
|
|||||||
tl.totalTime(tlSeekTime, suppressEvents);
|
tl.totalTime(tlSeekTime, suppressEvents);
|
||||||
if (!suppressEvents && !hasZeroDurationCallbackTween(tl)) {
|
if (!suppressEvents && !hasZeroDurationCallbackTween(tl)) {
|
||||||
// The first seek is the only eventful one; the re-render only refreshes styles.
|
// 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 {
|
} else {
|
||||||
tl.seek(tlSeekTime, suppressEvents);
|
tl.seek(tlSeekTime, suppressEvents);
|
||||||
|
|||||||
@@ -315,7 +315,6 @@ export type RuntimeTimelineLike = {
|
|||||||
seek: (timeSeconds?: number, suppressEvents?: boolean) => unknown;
|
seek: (timeSeconds?: number, suppressEvents?: boolean) => unknown;
|
||||||
totalTime?: (timeSeconds?: number, suppressEvents?: boolean) => unknown;
|
totalTime?: (timeSeconds?: number, suppressEvents?: boolean) => unknown;
|
||||||
progress?: (value?: number, suppressEvents?: boolean) => unknown;
|
progress?: (value?: number, suppressEvents?: boolean) => unknown;
|
||||||
render?: (totalTime: number, suppressEvents?: boolean, force?: boolean) => unknown;
|
|
||||||
time: () => number;
|
time: () => number;
|
||||||
duration: () => number;
|
duration: () => number;
|
||||||
add: (timeline: RuntimeTimelineLike, startAtSeconds: number) => void;
|
add: (timeline: RuntimeTimelineLike, startAtSeconds: number) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user