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:

The damping ratio \(\zeta\) dictates the nature of the oscillation:


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


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:

  1. Calculate the Natural Frequency: \[\omega_0 = \sqrt{\frac{k}{m}} = \sqrt{\frac{0.5}{2}} = 0.5\text{ rad/tick}\]

  2. 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\]
  3. 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.delta frequency or lower \(k\).