diff --git a/packages/core/src/runtime/adapters/lottie.test.ts b/packages/core/src/runtime/adapters/lottie.test.ts index a8e5eb4a5..b3f540214 100644 --- a/packages/core/src/runtime/adapters/lottie.test.ts +++ b/packages/core/src/runtime/adapters/lottie.test.ts @@ -121,6 +121,27 @@ describe("lottie adapter", () => { adapter.seek({ time: -5 }); expect(anim.goToAndStop).toHaveBeenCalledWith(0, false); }); + + it("seeks a v1 dotlottie player by percentage when duration is known", () => { + // v1 player: has seek() but no setCurrentRawFrameValue. + const seek = vi.fn(); + const player = { play: vi.fn(), pause: vi.fn(), duration: 4, seek }; + lottieWindow.__hfLottie = [player]; + const adapter = createLottieAdapter(); + adapter.seek({ time: 1 }); // 1 / 4 * 100 = 25 + expect(seek).toHaveBeenCalledWith(25); + }); + + it("does not seek a v1 dotlottie player whose duration is 0 (not yet loaded)", () => { + // duration 0 before load: an unguarded (time / 0) * 100 = NaN used to + // reach seek() and blank the first frame at composition time 0. + const seek = vi.fn(); + const player = { play: vi.fn(), pause: vi.fn(), duration: 0, seek }; + lottieWindow.__hfLottie = [player]; + const adapter = createLottieAdapter(); + adapter.seek({ time: 0 }); + expect(seek).not.toHaveBeenCalled(); + }); }); describe("pause", () => { diff --git a/packages/core/src/runtime/adapters/lottie.ts b/packages/core/src/runtime/adapters/lottie.ts index e304f61f7..5a303dc74 100644 --- a/packages/core/src/runtime/adapters/lottie.ts +++ b/packages/core/src/runtime/adapters/lottie.ts @@ -102,10 +102,16 @@ export function createLottieAdapter(): RuntimeDeterministicAdapter { anim.setCurrentRawFrameValue(Math.min(frame, totalFrames - 1)); } } else if (typeof anim.seek === "function") { - // dotlottie-web v1: seek(percentage 0-100) - const duration = anim.duration ?? 1; - const percentage = Math.min(100, (time / duration) * 100); - anim.seek(percentage); + // dotlottie-web v1: seek(percentage 0-100). Mirror the v2 guard + // above: a not-yet-loaded player reports duration 0, and `0 ?? 1` + // stays 0 (nullish coalescing does not replace 0), so an unguarded + // (time / 0) * 100 = NaN reached seek() and blanked the first frame + // at composition time 0. + const duration = anim.duration ?? 0; + if (duration > 0) { + const percentage = Math.max(0, Math.min(100, (time / duration) * 100)); + anim.seek(percentage); + } } } } catch (err) {