Spring Viz
A spring-mass-damper system, simulated and plotted. A bob hanging off a wall-anchored spring of stiffness k and damping c obeys m·ẍ + c·ẋ + k·x = 0. Release the mass from a displacement and it oscillates, decays, or creeps depending on the damping ratio ζ = c / (2·sqrt(k·m)) — the same physics that gives gradient descent with momentum its overshoot, settling, and critical-damping regimes.
Spring-mass-damper visualisation.Spring-mass-damper system: stiffness 100.0 newtons per metre, damping 6.0 newton-seconds per metre, mass 1.00 kilograms. Regime underdamped (damping ratio ζ 0.30). At time 0.90 seconds the mass is at displacement -0.032 with velocity -0.487.
100
6.0
0.90s
x -0.032v -0.487ζ 0.30m 1.00regime underdamped
Customize
Stiffness
100
Damping
6.0
Mass
1.00
Time
0.90s
Playback
Installation
npx shadcn@latest add https://craftbits.dev/r/spring-viz.jsonUsage
import { SpringViz } from "@craft-bits/core";
<SpringViz />Drive stiffness and damping from outside:
const [k, setK] = useState(100);
const [c, setC] = useState(10);
<SpringViz
stiffness={k}
onStiffnessChange={setK}
damping={c}
onDampingChange={setC}
/>Scrub the time cursor:
const [t, setT] = useState(0);
<SpringViz
currentTime={t}
onCurrentTimeChange={setT}
duration={6}
/>Autoplay in real time:
<SpringViz playing playSpeed={1} />Understanding the component
- Two scalar state variables, integrated with semi-implicit Euler. Every simulation step blends the new acceleration
a = -(k·x + c·v) / minto the velocity, then advances the position byv · dt. Semi-implicit Euler stays stable at the highk/mthat would blow up forward Euler — the rendered waveform matches the analytic closed formx(t) = A · e^(-ζω₀t) · cos(ω_d t + φ)to within a pixel. - The displacement waveform is precomputed and scrubbed. The component integrates 240 samples per second of
durationup-front wheneverstiffness,damping,mass,initialX,initialV, ordurationchanges.currentTimeis a cursor into that array; autoplay and the slider both shift the cursor without rerunning the math, so identical inputs always yield identical waveforms — safe across SSR and hydration. - Bob + waveform read the same value. The top half draws the spring + bob at the current
x(t); the bottom half plots the full waveform with a cursor at the samet. As you scrub, both updates stay locked — the bob is just a vertical slice of the waveform. - Damping regimes named in the readout. The component computes
c_crit = 2·sqrt(k·m)and the damping ratioζ = c / c_crit, then labels the regime asundamped,underdamped,critically damped, oroverdamped. Drag the damping slider and watch the regime label flip — the same boundary that separates "overshoot then settle" from "creep to rest". - Real-time autoplay via
requestAnimationFrame. Unlike step-driven vizes, this one ticks in seconds. The RAF loop reads the wall-clock delta, scales byplaySpeed, advancescurrentTime, and stops atduration. - Reduced-motion fallback. With
prefers-reduced-motion: reducethe bob and cursor snap directly to their new positions and autoplay is suppressed; the waveform path itself never animates.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
stiffness | number | — | Controlled spring constant k. Pair with onStiffnessChange. |
defaultStiffness | number | 100 | Uncontrolled initial stiffness. |
onStiffnessChange | (stiffness) => void | — | Fires when the stiffness slider changes. |
damping | number | — | Controlled damping coefficient c. Pair with onDampingChange. |
defaultDamping | number | 10 | Uncontrolled initial damping. |
onDampingChange | (damping) => void | — | Fires when the damping slider changes. |
mass | number | 1 | Mass of the bob m (kg). |
currentTime | number | — | Controlled time cursor (seconds). Pair with onCurrentTimeChange. |
defaultCurrentTime | number | 0 | Uncontrolled initial time. |
onCurrentTimeChange | (time) => void | — | Fires on autoplay tick and manual scrub. |
playing | boolean | false | When true, advances currentTime in real time. |
playSpeed | number | 1 | Playback rate multiplier. 1 = real time. |
initialX | number | 1 | Initial displacement at t = 0. |
initialV | number | 0 | Initial velocity at t = 0. |
duration | number | 4 | Total simulated duration (seconds). |
transition | Transition | SPRINGS.smooth | Spring used for the bob + cursor visual smoothing. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The figure is
role="figure"with a labelled title and anaria-live="polite"summary that names the stiffness, damping, mass, damping ratio, regime, and the current(t, x, v)— every value the SVG shows is also exposed textually. - The three sliders are native
<input type="range">instances wrapped inLabeledSlider, so keyboard arrows nudge the value and screen readers narrate the value viaaria-valuetext. - Color is never the only signal — the bob, cursor, and waveform share the same accent hue but the textual readouts below the canvas repeat every quantity.
prefers-reduced-motion: reducesnaps the bob and cursor directly to their target positions and suppresses autoplay; the waveform polyline itself does not animate.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/viz/SpringViz.tsx). Generalised from a fixed(prediction, truth)squared-error spring scene into a configurable spring-mass-damper primitive: stiffness, damping, and mass as inputs; precomputed trajectory and time cursor as the playback model; controlled / uncontrolled pairs forstiffness,damping, andcurrentTime; real-time autoplay viarequestAnimationFramewith aplaySpeedmultiplier.