mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(core): renders keep a fromTo's from-only values from the frame it starts on (#5125)
* fix(core): renders keep a fromTo's from-only values from the frame it starts on * refactor(core): prime a tween at its start in one helper * fix(core): a keyframed fromTo also keeps its from-only values at its start * fix(core): each staggered target keeps its from-only values from the frame it starts on
This commit is contained in:
@@ -62,6 +62,96 @@ describe("gsap adapter on a step", () => {
|
||||
});
|
||||
|
||||
describe("gsap adapter at a tween's start", () => {
|
||||
// Issue #5122: a render seeks every frame in order; only the from-vars set transformOrigin.
|
||||
it("keeps a fromTo's from-only values from the frame it starts on, with immediateRender off", () => {
|
||||
const box = document.body.appendChild(document.createElement("div"));
|
||||
const timeline = gsap.timeline({ paused: true });
|
||||
timeline.fromTo(
|
||||
box,
|
||||
{ scale: 1.5, transformOrigin: "0% 0%" },
|
||||
{ scale: 1, duration: 0.45, ease: "none", immediateRender: false },
|
||||
0.1,
|
||||
);
|
||||
timeline.set({}, {}, 1);
|
||||
const adapter = createGsapAdapter({
|
||||
getTimeline: () => timeline as unknown as RuntimeTimelineLike,
|
||||
});
|
||||
const origins = [2, 3, 4, 5].map((frame) => {
|
||||
adapter.seek({ time: frame / 30 });
|
||||
return box.style.transformOrigin;
|
||||
});
|
||||
expect(origins).toEqual(["", "0% 0%", "0% 0%", "0% 0%"]);
|
||||
expect(gsap.getProperty(box, "scaleX")).toBeCloseTo(1.4259, 3);
|
||||
});
|
||||
|
||||
it("keeps a keyframed fromTo's from-only values over an earlier tween's on the same element", () => {
|
||||
const box = document.body.appendChild(document.createElement("div"));
|
||||
const timeline = gsap.timeline({ paused: true });
|
||||
const fromTo = (origin: string, to: gsap.TweenVars, at: number) =>
|
||||
timeline.fromTo(
|
||||
box,
|
||||
{ scale: 1.5, transformOrigin: origin },
|
||||
{ scale: 1, immediateRender: false, ...to },
|
||||
at,
|
||||
);
|
||||
fromTo("0% 0%", { duration: 0.1 }, 0);
|
||||
fromTo("100% 100%", { duration: 0.1 }, 0.1);
|
||||
fromTo("0% 0%", { keyframes: { y: [0, 40] }, duration: 0.5 }, 0.2);
|
||||
timeline.set({}, {}, 1);
|
||||
const adapter = createGsapAdapter({
|
||||
getTimeline: () => timeline as unknown as RuntimeTimelineLike,
|
||||
});
|
||||
const origins = [0, 2 / 30, 0.1, 4 / 30, 0.2, 7 / 30].map((time) => {
|
||||
adapter.seek({ time });
|
||||
return box.style.transformOrigin;
|
||||
});
|
||||
expect(origins.slice(1)).toEqual(["0% 0%", "100% 100%", "100% 100%", "0% 0%", "0% 0%"]);
|
||||
});
|
||||
|
||||
it("keeps each staggered target's from-only values from the frame it starts on", () => {
|
||||
const film = () => {
|
||||
const boxes = [0, 1, 2].map(() => document.body.appendChild(document.createElement("div")));
|
||||
const timeline = gsap.timeline({ paused: true });
|
||||
timeline.fromTo(
|
||||
boxes,
|
||||
{ scale: 1.5, transformOrigin: "0% 0%" },
|
||||
{ scale: 1, duration: 0.3, stagger: 0.1, ease: "none", immediateRender: false },
|
||||
0.1,
|
||||
);
|
||||
timeline.set({}, {}, 1);
|
||||
return { boxes, timeline };
|
||||
};
|
||||
const played = film();
|
||||
const seeked = film();
|
||||
const adapter = createGsapAdapter({
|
||||
getTimeline: () => seeked.timeline as unknown as RuntimeTimelineLike,
|
||||
});
|
||||
for (let frame = 0; frame <= 12; frame++) {
|
||||
played.timeline.totalTime(frame / 30, false);
|
||||
adapter.seek({ time: frame / 30 });
|
||||
const origins = (film: { boxes: HTMLElement[] }) =>
|
||||
film.boxes.map((box) => box.style.transformOrigin);
|
||||
expect(origins(seeked), `frame ${frame}`).toEqual(origins(played));
|
||||
}
|
||||
});
|
||||
|
||||
it("still lets a set authored later on the tween's start win over its from-values", () => {
|
||||
const box = document.body.appendChild(document.createElement("div"));
|
||||
const timeline = gsap.timeline({ paused: true });
|
||||
timeline.fromTo(
|
||||
box,
|
||||
{ opacity: 0, transformOrigin: "0% 0%" },
|
||||
{ opacity: 1, duration: 0.5, immediateRender: false },
|
||||
0.1,
|
||||
);
|
||||
timeline.set(box, { opacity: 0.3, transformOrigin: "100% 100%" }, 0.1);
|
||||
timeline.set({}, {}, 1);
|
||||
createGsapAdapter({ getTimeline: () => timeline as unknown as RuntimeTimelineLike }).seek({
|
||||
time: 0.1,
|
||||
});
|
||||
expect([box.style.opacity, box.style.transformOrigin]).toEqual(["0.3", "100% 100%"]);
|
||||
});
|
||||
|
||||
// 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, 2.5, 3])(
|
||||
"shows the tween that starts at the seek time, seeking from %s s",
|
||||
|
||||
@@ -6,7 +6,7 @@ type GsapAdapterDeps = {
|
||||
|
||||
/**
|
||||
* Re-renders a timeline already at `t`, silently, from just below (above at 0) so same-time steps apply in authored
|
||||
* order. That step skips a keyframed tween already at its start, so each one is first moved across its start alone.
|
||||
* order. That step skips a keyframed tween at its start and undoes a tween's from-values there, so both are redone.
|
||||
*/
|
||||
export function rerenderGsapTimelineAt(
|
||||
timeline: {
|
||||
@@ -33,7 +33,7 @@ export function rerenderGsapTimelineAt(
|
||||
for (const [child] of skipped) child._ts = 0;
|
||||
try {
|
||||
timeline.totalTime(t >= 0.001 ? t - 0.001 : t + 0.001, true);
|
||||
primeKeyframedTweensStartingAt(children, t);
|
||||
primeTweensStartingAt(children, t);
|
||||
timeline.totalTime(t, true);
|
||||
} finally {
|
||||
for (const [child, timeScale] of skipped) child._ts = timeScale;
|
||||
@@ -96,7 +96,11 @@ type GsapAnimation = {
|
||||
paused: () => boolean;
|
||||
render: (totalTime: number, suppressEvents: boolean) => unknown;
|
||||
vars?: { keyframes?: unknown };
|
||||
_startAt?:
|
||||
| 0
|
||||
| { render: (totalTime: number, suppressEvents: boolean, force: boolean) => unknown };
|
||||
getChildren?: (nested: boolean, tweens: boolean, timelines: boolean) => unknown[];
|
||||
timeline?: Pick<GsapAnimation, "getChildren">;
|
||||
};
|
||||
|
||||
const BELOW_GSAP_TIME_RESOLUTION = 2e-8;
|
||||
@@ -113,14 +117,22 @@ const playsForward = (value: unknown): value is GsapAnimation => {
|
||||
);
|
||||
};
|
||||
|
||||
function primeKeyframedTweensStartingAt(children: unknown[], time: number): void {
|
||||
function primedAtItsStart(tween: GsapAnimation): boolean {
|
||||
if (tween.vars?.keyframes) {
|
||||
tween.render(BELOW_GSAP_TIME_RESOLUTION, true);
|
||||
tween.render(-BELOW_GSAP_TIME_RESOLUTION, true);
|
||||
}
|
||||
if (tween._startAt) tween._startAt.render(BELOW_GSAP_TIME_RESOLUTION, true, true);
|
||||
return Boolean(tween.vars?.keyframes || tween._startAt);
|
||||
}
|
||||
|
||||
function primeTweensStartingAt(children: unknown[], time: number): void {
|
||||
for (const child of children.filter(playsForward)) {
|
||||
const local = (time - child.startTime()) * child.timeScale();
|
||||
if (Math.abs(local) < 1e-9 && child.vars?.keyframes) {
|
||||
child.render(BELOW_GSAP_TIME_RESOLUTION, true);
|
||||
child.render(-BELOW_GSAP_TIME_RESOLUTION, true);
|
||||
} else if (child.getChildren && local > 0 && local <= child.totalDuration()) {
|
||||
primeKeyframedTweensStartingAt(child.getChildren(false, true, true), local);
|
||||
if (Math.abs(local) < 1e-9 && primedAtItsStart(child)) continue;
|
||||
const nested = child.getChildren ? child : child.timeline;
|
||||
if (nested?.getChildren && local > 0 && local <= child.totalDuration()) {
|
||||
primeTweensStartingAt(nested.getChildren(false, true, true), local);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user