How to Create Elastic Rubber Bands in Matter.js

This article explains how to simulate realistic rubber bands in Matter.js that exert pulling forces exclusively when extended beyond their resting length and go slack when compressed. By default, standard constraints in Matter.js resist both extension and compression, acting more like rigid rods or bidirectional springs. Achieving true rubber band behavior requires conditionally modifying the constraint properties or manually applying elastic forces within the engine's update cycle.

The Limitation of Default Constraints

A standard Matter.Constraint maintains a fixed target length. If two connected bodies get closer than this length, the constraint pushes them apart; if they move farther apart, it pulls them together. A real rubber band has no compressive resistance—it should buckle or become slack when the distance between anchor points is less than its resting length.

Method 1: Dynamically Toggling Constraint Stiffness

The cleanest and most stable way to simulate rubber band physics is to listen to the beforeUpdate event of the Matter.js engine. On each tick, measure the Euclidean distance between the two connected points. If the distance is greater than the resting length, set the constraint's stiffness to your desired elasticity; otherwise, set it to 0 so it goes completely slack.

const { Engine, Render, Runner, Bodies, Composite, Constraint, Events, Vector } = Matter;

// Create engine and world
const engine = Engine.create();
const world = engine.world;

// Define bodies
const anchor = Bodies.circle(400, 100, 10, { isStatic: true });
const weight = Bodies.circle(400, 250, 20);

// Define rubber band parameters
const restingLength = 100;
const bandStiffness = 0.05;

// Create constraint with initial length
const rubberBand = Constraint.create({
    bodyA: anchor,
    bodyB: weight,
    length: restingLength,
    stiffness: bandStiffness,
    render: {
        strokeStyle: '#e67e22',
        lineWidth: 3
    }
});

Composite.add(world, [anchor, weight, rubberBand]);

// Adjust stiffness every frame based on distance
Events.on(engine, 'beforeUpdate', () => {
    const pointA = rubberBand.bodyA ? rubberBand.bodyA.position : rubberBand.pointA;
    const pointB = rubberBand.bodyB ? rubberBand.bodyB.position : rubberBand.pointB;
    
    // Calculate current distance between endpoints
    const distance = Vector.magnitude(Vector.sub(pointB, pointA));

    if (distance > restingLength) {
        // Band is stretched: restore tension
        rubberBand.stiffness = bandStiffness;
    } else {
        // Band is slack: disable tension
        rubberBand.stiffness = 0;
    }
});

Method 2: Manual Force Application (Hooke's Law)

If you need finer control over custom elasticity curves, non-linear stretching, or custom damping, omit the Constraint object entirely and apply forces directly via Body.applyForce.

Hooke’s Law calculates the tension force as: F = -k * (x - x0)

Where:

Events.on(engine, 'beforeUpdate', () => {
    const posA = anchor.position;
    const posB = weight.position;

    const delta = Vector.sub(posB, posA);
    const distance = Vector.magnitude(delta);

    if (distance > restingLength) {
        const stretch = distance - restingLength;
        const forceMagnitude = stretch * 0.001; // Adjust spring constant as needed
        const forceDirection = Vector.normalise(delta);
        
        // Force pulling weight toward anchor
        const tensionForce = Vector.mult(forceDirection, -forceMagnitude);

        Matter.Body.applyForce(weight, weight.position, tensionForce);

        if (!anchor.isStatic) {
            // Apply equal and opposite reaction force if anchor can move
            Matter.Body.applyForce(anchor, anchor.position, Vector.neg(tensionForce));
        }
    }
});

Rendering Slack Rubber Bands

When using dynamic stiffness, Matter.js renders the constraint as a straight line regardless of whether it is under tension or slack. To make slack bands visually convincing:

  1. Disable the constraint's default rendering by setting render.visible = false.
  2. Use an afterRender event listener to draw a custom path using the Canvas API.
  3. If distance > restingLength, draw a straight line.
  4. If distance <= restingLength, draw a quadratic or Bézier curve that droops downward under simulated gravity to mimic a drooping cord.