From f5ecea2b0c263f97e6d3afbb9a20969131676e44 Mon Sep 17 00:00:00 2001 From: Ryan Kiley Date: Sat, 10 Oct 2026 16:11:49 -0700 Subject: [PATCH] Round a spring's derived physics to ten significant figures The duration round trip handed back 300.00000000000006 for a stiffness of 300, and the noise reached changes() and the copied values. Stiffness and damping derived from a duration now snap to ten significant figures, so given values come back exact and derived ones read cleanly. --- src/tweaks/controls/spring.ts | 5 ++++- test/docs-snapshot.json | 8 ++++---- test/motion.test.mjs | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/tweaks/controls/spring.ts b/src/tweaks/controls/spring.ts index d85abd1..a09fd2f 100644 --- a/src/tweaks/controls/spring.ts +++ b/src/tweaks/controls/spring.ts @@ -34,7 +34,10 @@ function createSpring(meta: Meta, onChange: OnChange): Control { const DUR_MIN = 0.1, DUR_MAX = 1; const clampDur = (v: unknown) => clamp(has(v) ? +v : 0.5, DUR_MIN, DUR_MAX); const clampBounce = (v: unknown) => clamp(has(v) ? +v : 0.2, 0, 1); - const timeToPhysics = (t: SpringTime): Phys => { const k = (2 * Math.PI / t.visualDuration) ** 2, z = 1 - t.bounce, m = 1; return { stiffness: k, damping: 2 * z * Math.sqrt(k * m), mass: m }; }; + // Derived values snap to ten significant figures: the trig round trip otherwise hands back + // 300.00000000000006 for a stiffness of 300, and that noise lands in changes() and the copy. + const sig = (x: number) => +x.toPrecision(10); + const timeToPhysics = (t: SpringTime): Phys => { const k = (2 * Math.PI / t.visualDuration) ** 2, z = 1 - t.bounce, m = 1; return { stiffness: sig(k), damping: sig(2 * z * Math.sqrt(k * m)), mass: m }; }; // Initial config — accept top-level props (the inline shorthand // `{ type:"spring", visualDuration:0.3, bounce:0.2 }` or `{ …, stiffness, damping, mass }`) diff --git a/test/docs-snapshot.json b/test/docs-snapshot.json index 6bd3171..ba5dc6a 100644 --- a/test/docs-snapshot.json +++ b/test/docs-snapshot.json @@ -129,8 +129,8 @@ }, "bounce": { "mode": "spring", - "stiffness": 109.6622711232151, - "damping": 10.471975511965978, + "stiffness": 109.6622711, + "damping": 10.47197551, "mass": 1, "visualDuration": 0.6, "bounce": 0.5, @@ -216,8 +216,8 @@ "values": { "move": { "mode": "spring", - "stiffness": 194.9551486634935, - "damping": 19.54768762233649, + "stiffness": 194.9551487, + "damping": 19.54768762, "mass": 1, "visualDuration": 0.45, "bounce": 0.3, diff --git a/test/motion.test.mjs b/test/motion.test.mjs index f74f0a5..909754f 100644 --- a/test/motion.test.mjs +++ b/test/motion.test.mjs @@ -152,3 +152,17 @@ test("review follow-up: `value` as a bare keyword or 4-tuple is the curve on the assert.deepEqual(a.params.t.curve, [0.42, 0, 1, 1]); a.destroy(); }); + +test("a spring's physics round-trips clean: stiffness 300 comes back 300, not 300.00000000000006", async () => { + const p = tweaks("Noise", { m: { type: "motion", stiffness: 300, damping: 20 }, s: { type: "spring", stiffness: 300, damping: 20 } }); + document.body.append(p.el); await p.ready; + for (const k of ["m", "s"]) { + assert.equal(p.params[k].stiffness, 300, `${k}: stiffness`); + assert.equal(p.params[k].damping, 20, `${k}: damping`); + } + p.set("m", { stiffness: 500, damping: 25 }); + const c = p.changes().m; + assert.equal(c.stiffness, 500, "changes() carries the stiffness as set"); + assert.equal(c.damping, 25, "and the damping"); + p.destroy(); +});