diff --git a/packages/motion-dom/src/animation/__tests__/JSAnimation.test.ts b/packages/motion-dom/src/animation/__tests__/JSAnimation.test.ts index 7130d0ca6e..ab1b65bc5e 100644 --- a/packages/motion-dom/src/animation/__tests__/JSAnimation.test.ts +++ b/packages/motion-dom/src/animation/__tests__/JSAnimation.test.ts @@ -1422,4 +1422,22 @@ describe("JSAnimation", () => { expect(animation.sample(1000).value).toBe("90%") expect(animation.sample(1999).value).toBe("90%") }) + + // https://github.com/motiondivision/motion/issues/2791 + test("Spring over SVG polygon points never produces NaN", () => { + // An invalid spring physics value (here an explicit `undefined` + // stiffness, as forwarded from an optional prop) previously emitted + // NaN progress, which the complex-value mixer turned into an invalid + // "NaN,NaN NaN,NaN" point list written to the element. + const animation = animateValue({ + keyframes: ["150,5 75,200 225,200", "150,5 50,180 250,180"], + type: "spring", + stiffness: undefined, + autoplay: false, + }) + + for (let t = 0; t <= 2000; t += 50) { + expect(animation.sample(t).value).not.toContain("NaN") + } + }) }) diff --git a/packages/motion-dom/src/animation/generators/__tests__/spring.test.ts b/packages/motion-dom/src/animation/generators/__tests__/spring.test.ts index 309b82e9d6..63558edc85 100644 --- a/packages/motion-dom/src/animation/generators/__tests__/spring.test.ts +++ b/packages/motion-dom/src/animation/generators/__tests__/spring.test.ts @@ -285,3 +285,36 @@ describe("toString", () => { ) }) }) + +// https://github.com/motiondivision/motion/issues/2791 +describe("spring NaN guards", () => { + const sample = (options: ValueAnimationOptions) => { + const generator = spring(options) + return [0, 100, 300, 600, 1000].map((t) => generator.next(t).value) + } + + test("stiffness of 0 does not produce NaN", () => { + const values = sample({ keyframes: [0, 100], stiffness: 0 }) + values.forEach((v) => expect(v).not.toBeNaN()) + }) + + test("mass of 0 does not produce NaN", () => { + const values = sample({ keyframes: [0, 100], mass: 0 }) + values.forEach((v) => expect(v).not.toBeNaN()) + }) + + /** + * An explicit `stiffness: undefined` (e.g. a forwarded prop that resolves + * to undefined) clobbers the default via the options spread in + * getSpringOptions, which previously produced NaN spring values. + */ + test("explicit undefined stiffness does not produce NaN", () => { + const values = sample({ keyframes: [0, 100], stiffness: undefined }) + values.forEach((v) => expect(v).not.toBeNaN()) + }) + + test("explicit undefined mass does not produce NaN", () => { + const values = sample({ keyframes: [0, 100], mass: undefined }) + values.forEach((v) => expect(v).not.toBeNaN()) + }) +}) diff --git a/packages/motion-dom/src/animation/generators/spring.ts b/packages/motion-dom/src/animation/generators/spring.ts index 92647be5fa..491526d667 100644 --- a/packages/motion-dom/src/animation/generators/spring.ts +++ b/packages/motion-dom/src/animation/generators/spring.ts @@ -215,6 +215,20 @@ function getSpringOptions(options: SpringOptions) { } } + /** + * Guard against non-positive or non-finite stiffness/mass. These divide + * and feed Math.sqrt() during resolution, so a 0 (or an explicit + * `undefined` that clobbers the default via the spread above) produces NaN + * spring values — which corrupt any animated value, e.g. an SVG polygon's + * points list. See https://github.com/motiondivision/motion/issues/2791 + */ + if (!(springOptions.stiffness > 0)) { + springOptions.stiffness = springDefaults.stiffness + } + if (!(springOptions.mass > 0)) { + springOptions.mass = springDefaults.mass + } + return springOptions }