fix(core): a Lottie animation in a mounted composition plays from that composition's start (#4471)

This commit is contained in:
Miguel Ángel
2026-09-25 04:43:49 +00:00
committed by GitHub
parent 53e2e20a15
commit ad87313ee2
3 changed files with 52 additions and 3 deletions
@@ -1,5 +1,6 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createLottieAdapter } from "./lottie";
import { createRuntimeStartTimeResolver } from "../startResolver";
const lottieWindow = window as Window & {
lottie?: {
@@ -84,6 +85,37 @@ describe("lottie adapter", () => {
});
describe("seek", () => {
function mountedAt(start: string) {
document.body.innerHTML = `<div data-composition-id="root"><div data-composition-id="host" data-start="${start}"><div data-composition-id="walk"><div id="player"></div></div></div></div>`;
const { resolveStartForElement } = createRuntimeStartTimeResolver({});
const adapter = createLottieAdapter({
resolveStartSeconds: (el) => resolveStartForElement(el, 0),
});
return { adapter, player: document.getElementById("player")! };
}
it("seeks a player in a mounted composition to the time since that composition started", () => {
const { adapter, player } = mountedAt("3");
const anim = { ...createLottieWebAnim(), wrapper: player };
const dot = { ...createDotLottiePlayer({ totalFrames: 60, frameRate: 30 }), canvas: player };
lottieWindow.__hfLottie = [anim, dot];
adapter.seek({ time: 4 });
expect(anim.goToAndStop).toHaveBeenCalledWith(1000, false);
expect(dot.setCurrentRawFrameValue).toHaveBeenCalledWith(30);
adapter.seek({ time: 1 });
expect(anim.goToAndStop).toHaveBeenLastCalledWith(0, false);
document.body.innerHTML = "";
});
it("keeps page time for a player in a composition that starts at 0", () => {
const { adapter, player } = mountedAt("0");
const anim = { ...createLottieWebAnim(), wrapper: player };
lottieWindow.__hfLottie = [anim];
adapter.seek({ time: 2 });
expect(anim.goToAndStop).toHaveBeenCalledWith(2000, false);
document.body.innerHTML = "";
});
it("seeks lottie-web with goToAndStop in ms", () => {
const anim = createLottieWebAnim();
lottieWindow.__hfLottie = [anim];
+17 -2
View File
@@ -49,7 +49,19 @@ import { swallow } from "../diagnostics";
* via the global `lottie` object, so compositions that call
* `lottie.loadAnimation(...)` without manually registering still work.
*/
export function createLottieAdapter(): RuntimeDeterministicAdapter {
export function createLottieAdapter(params?: {
resolveStartSeconds?: (element: Element) => number;
}): RuntimeDeterministicAdapter {
const secondsIntoComposition = (anim: unknown, pageTime: number): number => {
const el = isLottieWebAnimation(anim) ? anim.wrapper : (anim as DotLottiePlayer).canvas;
const composition =
el && typeof (el as Element).closest === "function"
? (el as Element).closest("[data-composition-id]")
: null;
if (!composition || !params?.resolveStartSeconds) return pageTime;
return Math.max(0, pageTime - params.resolveStartSeconds(composition));
};
return {
name: "lottie",
@@ -78,12 +90,13 @@ export function createLottieAdapter(): RuntimeDeterministicAdapter {
},
seek: (ctx) => {
const time = Math.max(0, Number(ctx.time) || 0);
const pageTime = Math.max(0, Number(ctx.time) || 0);
const instances = (window as LottieWindow).__hfLottie;
if (!instances || instances.length === 0) return;
for (const anim of instances) {
try {
const time = secondsIntoComposition(anim, pageTime);
const loops = anim.loop === true;
if (isLottieWebAnimation(anim)) {
// lottie-web: AnimationItem
@@ -245,6 +258,7 @@ interface LottieWebAnimation {
totalFrames: number;
frameRate: number;
loop?: boolean | number;
wrapper?: unknown;
}
interface LottieWebGlobal {
@@ -261,6 +275,7 @@ interface DotLottiePlayer {
frameRate?: number;
duration?: number;
loop?: boolean;
canvas?: unknown;
}
interface LottieWindow extends Window {
+3 -1
View File
@@ -3269,7 +3269,9 @@ export function initSandboxRuntimeModular(): void {
resolveStartSeconds: (element) => resolveStartForElement(element, 0),
}),
createAnimeJsAdapter(),
createLottieAdapter(),
createLottieAdapter({
resolveStartSeconds: (element) => resolveStartForElement(element, 0),
}),
createThreeAdapter(),
createMapboxAdapter(),
createLeafletAdapter(),