Physics · Mechanics · core
Spring-Mass Oscillator
pull back in proportion, bleed energy, push in rhythm
Hooke's law, damping and a periodic push: the forced oscillator behind springy cameras, wobbly UI and suspension, and why driving at ω₀ explodes.
The orange block rides a cyan spring around the dashed rest line, and the orange trace above records its displacement over time. The default drive sits near resonance (ω ≈ ω₀); move the drive frequency away, or raise damping, and the steady amplitude in the readout drops.
What it is
A mass on a spring feels a restoring force −k·x that always points back to the rest position. Left alone it trades kinetic energy for elastic energy forever, oscillating at the natural frequency ω₀ = √(k/m). A damper adds −c·v, which turns the swing into a decaying one.
Push it with a periodic force F₀ sin(ωt) and, after the start-up transient dies out, the mass settles into motion at the drive frequency. Its amplitude depends on how close ω is to ω₀: far away the spring barely notices, near ω₀ the response peaks, and with no damping it grows without bound. That peak is resonance.
Games use this model constantly: spring-follow cameras, juicy UI that overshoots and settles, car suspension, ragdoll joints and squash and stretch. The damping ratio ζ is the one number designers actually tune: below 1 it bounces, at 1 it snaps in without overshoot, above 1 it crawls.
Key formulas
- Equation of motion
- Natural frequency
- Damping ratio
ζ < 1 bounces, ζ = 1 is critical (fastest settle, no overshoot), ζ > 1 is sluggish.
- Steady-state amplitude
- Stored elastic energy
Rules of thumb
- Tune springs by frequency and damping ratio, not by raw k and c: pick
ω₀(2–8 Hz feels snappy for UI) andζ(0.4–0.7 for a playful bounce, 1.0 for a camera), then derivek = mω₀²andc = 2ζ√(km). - Use semi-implicit (symplectic) Euler: update velocity first, then position with the new velocity. Explicit Euler gains energy and blows up.
- Step at a fixed rate (120 Hz is plenty) and keep
ω₀·dtwell under 1; stiff springs need smaller steps or an analytic/implicit solver. - Resonance is sharp when damping is small: at ζ = 0.05 the peak is about 10× the static deflection
F₀/k.
Implementation
export class Spring {
x = 0
v = 0
constructor(public freqHz = 4, public zeta = 0.5) {}
/** Advance toward target with a fixed step; call from a fixed-timestep loop. */
step(target: number, dt: number) {
const w = 2 * Math.PI * this.freqHz
const k = w * w // m = 1
const c = 2 * this.zeta * w
const a = -k * (this.x - target) - c * this.v
this.v += a * dt // semi-implicit: velocity first
this.x += this.v * dt
return this.x
}
}
// const camY = new Spring(3, 1) // critically damped follow
// camera.position.y = camY.step(player.position.y, 1 / 120)Common mistakes
- Writing position before velocity (explicit Euler). The oscillation slowly gains energy and the spring eventually explodes.
- Scaling stiffness with frame time instead of using a fixed step, so the bounce looks different at 30 and 144 FPS.
- Tuning k and c by hand: doubling the mass silently changes both the frequency and the damping ratio.
- Driving an undamped spring at its own frequency (e.g. a periodic footstep shake) and watching the amplitude grow without limit.
Prompt
Write a TypeScript Spring class for a Three.js game that smooths a number (or THREE.Vector3) toward a target. Constructor takes frequency in Hz and damping ratio ζ; derive k = (2πf)² and c = 2ζ(2πf) with unit mass. Provide step(target, dt) using semi-implicit Euler and a reset(value). It must be called from a fixed 120 Hz loop. Edge cases: ζ = 0 (pure oscillation), ζ ≥ 1 (no overshoot), dt larger than 1/30 should be subdivided. Add unit tests: with ζ = 1 a step response never overshoots the target, and with ζ = 0 total energy ½kx² + ½v² stays within 1% over 10 s.
Related
Further reading
Updated 2026-11-01