How to Simulate Damped Springs in Matter.js
This guide explains how to model quantifiable Hooke's law spring
oscillations with viscous damping inside the Matter.js 2D physics
engine. While Matter.js includes a built-in constraint solver with
stiffness and damping properties, these values
are unitless convergence parameters rather than analytical physical
units. To achieve true, quantifiable Hooke's law behavior (\(F = -k \Delta x - c v\)), you can either
map physical parameters to Matter.js constraints or apply custom forces
on each engine update.
Understanding the Physical Model
A damped harmonic oscillator governed by Hooke's law follows the differential equation:
\[F = -k(l - l_0)\hat{r} - c(v_{\text{rel}} \cdot \hat{r})\hat{r}\]
Where:
- \(k\) is the spring constant (stiffness in \(\text{N/m}\)).
- \(l\) is the current distance between the two anchor points.
- \(l_0\) is the rest length of the spring.
- \(\hat{r}\) is the unit vector pointing along the spring axis from anchor A to B.
- \(c\) is the viscous damping coefficient (\(\text{N}\cdot\text{s/m}\)).
- \(v_{\text{rel}} = v_B - v_A\) is the relative velocity vector.
The damping ratio \(\zeta\) dictates the nature of the oscillation:
- Underdamped (\(\zeta < 1\)): \(\zeta = \frac{c}{2\sqrt{m k}}\) (the body oscillates with decaying amplitude).
- Critically damped (\(\zeta = 1\)): Returns to equilibrium as fast as possible without oscillation.
- Overdamped (\(\zeta > 1\)): Returns to equilibrium slowly without oscillating.
Method 1: Using Built-in Constraints (Heuristic Damping)
Matter.js constraints provide native spring behavior via position projection rather than direct force calculation.
const spring = Matter.Constraint.create({
bodyA: anchorBody,
bodyB: oscillatingBody,
length: 150, // Rest length (l_0) in pixels
stiffness: 0.05, // Elastic resistance (range: 0 to 1)
damping: 0.01 // Velocity dampener along constraint axis (range: 0 to 1)
});
Matter.Composite.add(engine.world, spring);Limitations of Built-in Constraints
stiffnessis a multiplier (0 to 1) defining how aggressively distance error is corrected per solver iteration.dampingsimply scales down relative velocity per iteration.- These values depend heavily on
engine.positionIterations,engine.velocityIterations, and the simulation delta time (\(dt\)), making SI-unit calculations inaccurate.
Method 2: Custom Force Integration (Exact Physical Damping)
To achieve mathematically exact spring-damper dynamics with explicit
values for \(k\) and \(c\), use an engine
beforeUpdate hook to apply forces directly to the rigid
bodies.
Implementation
const { Engine, Render, Runner, Bodies, Composite, Body, Vector, Events } = Matter;
// 1. Create Engine and World
const engine = Engine.create();
engine.gravity.y = 0; // Disable gravity for pure spring isolation
// 2. Define Physical Constants
const k = 0.5; // Spring constant (N/px)
const c = 0.08; // Damping coefficient (N·s/px)
const restLength = 150; // Rest length (px)
// 3. Create Bodies
const anchor = Bodies.circle(400, 200, 10, { isStatic: true });
const bob = Bodies.circle(400, 350, 20, {
mass: 2, // Mass m = 2 kg
frictionAir: 0 // Disable default drag to isolate spring damping
});
Composite.add(engine.world, [anchor, bob]);
// 4. Apply Hooke's Law with Damping on Each Step
Events.on(engine, 'beforeUpdate', () => {
const posA = anchor.position;
const posB = bob.position;
// Vector from A to B
const delta = Vector.sub(posB, posA);
const currentLength = Vector.magnitude(delta);
if (currentLength === 0) return;
// Unit vector along the axis
const normal = Vector.div(delta, currentLength);
// Extension: Δx = l - l_0
const displacement = currentLength - restLength;
// Relative velocity: v_rel = v_B - v_A
const relVelocity = Vector.sub(bob.velocity, anchor.velocity);
// Velocity projection along spring axis
const normalVelocity = Vector.dot(relVelocity, normal);
// Total Force magnitude: F = -k * Δx - c * v_normal
const springForceMagnitude = -k * displacement;
const dampingForceMagnitude = -c * normalVelocity;
const totalForceMagnitude = springForceMagnitude + dampingForceMagnitude;
// Force vector applied to bob (Anchor receives the equal and opposite reaction)
const force = Vector.mult(normal, totalForceMagnitude);
Body.applyForce(bob, bob.position, force);
if (!anchor.isStatic) {
Body.applyForce(anchor, anchor.position, Vector.negate(force));
}
});Tuning the Custom System
By using Method 2, standard mechanical engineering formulas apply directly to the simulation:
Calculate the Natural Frequency: \[\omega_0 = \sqrt{\frac{k}{m}} = \sqrt{\frac{0.5}{2}} = 0.5\text{ rad/tick}\]
Select Target Damping Ratio (\(\zeta\)):
- For an underdamped, swinging effect with 10% damping: set \(\zeta = 0.1\).
- Solve for \(c\): \[c = 2\zeta\sqrt{m k} = 2(0.1)\sqrt{2 \times 0.5} = 0.2\]
Ensure Numerical Stability: Explicit Euler approximations become unstable if the force creates a velocity change larger than the displacement error in a single frame. Ensure the simulation timestep is sufficiently small: \[dt < 2\sqrt{\frac{m}{k}}\] If oscillations explode or diverge, increase
runner.deltafrequency or lower \(k\).