diff --git a/packages/runtime-tests/unit/animation-scheduler.sequential.test.ts b/packages/runtime-tests/unit/animation-scheduler.sequential.test.ts index 184c2e0..f6e915b 100644 --- a/packages/runtime-tests/unit/animation-scheduler.sequential.test.ts +++ b/packages/runtime-tests/unit/animation-scheduler.sequential.test.ts @@ -20,6 +20,15 @@ describe.sequential("normalizeInterval", () => { expect(normalizeInterval(Number.POSITIVE_INFINITY)).toBe(100); expect(normalizeInterval(Number.MAX_SAFE_INTEGER)).toBe(2_147_483_647); }); + + test("preserves fractional intervals, matching Ink (no rounding)", () => { + // Ink's normalizeAnimationInterval does NOT round (use-animation.ts:147-151), + // so a 60fps interval (16.67ms) or 8.4ms stays fractional; rounding would drift + // frame=floor(elapsed/interval) and the scheduler's nextDueTime over time. + expect(normalizeInterval(16.67)).toBe(16.67); + expect(normalizeInterval(8.4)).toBe(8.4); + expect(normalizeInterval(0.5)).toBe(1); // still clamped to >= 1 + }); }); describe.sequential("createAnimationScheduler", () => { diff --git a/packages/runtime/src/animation-scheduler.ts b/packages/runtime/src/animation-scheduler.ts index b5dc3c7..b0a1396 100644 --- a/packages/runtime/src/animation-scheduler.ts +++ b/packages/runtime/src/animation-scheduler.ts @@ -3,7 +3,10 @@ const MAX_TIMER_INTERVAL = 2_147_483_647; export function normalizeInterval(interval: number | undefined): number { if (interval === undefined || !Number.isFinite(interval)) return DEFAULT_INTERVAL; - return Math.min(Math.max(1, Math.round(interval)), MAX_TIMER_INTERVAL); + // No rounding — Ink's normalizeAnimationInterval (use-animation.ts:147-151) preserves + // fractional intervals (e.g. 16.67ms for 60fps); rounding would drift frame counts and + // the scheduler's nextDueTime over time. The scheduler already ceil()s the setTimeout delay. + return Math.min(Math.max(1, interval), MAX_TIMER_INTERVAL); } type AnimationSubscriber = {