Matter.js Rack and Pinion Constraint Guide

This article explains how to build a rack-and-pinion mechanism in Matter.js to convert rotational movement into linear displacement. Because Matter.js does not feature a native rack-and-pinion constraint out of the box, you must construct one programmatically by linking a rotating circular body (the pinion) to an axially constrained rectangular body (the rack) using custom update loops.

1. Create the Pinion and the Rack

Begin by instantiating the two rigid bodies. The pinion is represented by a circle, and the rack is represented by a long rectangle.

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

const engine = Engine.create();
const world = engine.world;

// Pinion (gear)
const pinionRadius = 40;
const pinion = Bodies.circle(300, 200, pinionRadius, {
    density: 0.05
});

// Pin the gear to a fixed point in the world
const pinionPivot = Constraint.create({
    pointA: { x: 300, y: 200 },
    bodyB: pinion,
    stiffness: 1,
    length: 0
});

// Rack (linear bar)
const rack = Bodies.rectangle(300, 200 + pinionRadius + 10, 300, 20, {
    inertia: Infinity, // Prevents the rack from rotating
    friction: 0.001
});

Composite.add(world, [pinion, pinionPivot, rack]);

Setting inertia: Infinity on the rack prevents unwanted rotation while keeping it free to translate horizontally and vertically.

2. Restrict Rack Movement to a Single Axis

A proper rack-and-pinion setup confines the rack to linear movement along a single axis. You can lock the unwanted axis (such as the vertical Y-axis) during each engine tick by resetting its coordinate and vertical velocity:

const fixedRackY = rack.position.y;

Events.on(engine, 'beforeUpdate', () => {
    // Keep rack aligned along a single axis
    Body.setPosition(rack, { x: rack.position.x, y: fixedRackY });
    Body.setVelocity(rack, { x: rack.velocity.x, y: 0 });
});

3. Couple Rotation to Linear Displacement

The core kinematic formula for a rack and pinion is:

\[\Delta x = r \cdot \Delta \theta\]

Where:

In Matter.js, linking their velocities preserves natural collision interactions better than manually overriding positions. Since linear velocity \(v\) equals the radius \(r\) multiplied by angular velocity \(\omega\):

\[v_x = r \cdot \omega\]

You can synchronize them using the beforeUpdate event:

Events.on(engine, 'beforeUpdate', () => {
    // Determine driving force: if the pinion has angular velocity, drive the rack
    if (Math.abs(pinion.angularVelocity) > 0.0001) {
        const linearVelocity = pinion.angularVelocity * pinionRadius;
        Body.setVelocity(rack, { x: linearVelocity, y: 0 });
    } 
    // If the rack is pushed linearly, back-drive the pinion
    else if (Math.abs(rack.velocity.x) > 0.0001) {
        const angularVelocity = rack.velocity.x / pinionRadius;
        Body.setAngularVelocity(pinion, angularVelocity);
    }
});

4. Position-Based Correction (Preventing Drift)

Over long simulations, relying solely on velocity coupling can lead to slight numerical drift. To enforce absolute synchronization between angle and displacement, track the initial angle and position, then correct positional deviations:

let lastAngle = pinion.angle;

Events.on(engine, 'beforeUpdate', () => {
    const deltaAngle = pinion.angle - lastAngle;
    lastAngle = pinion.angle;

    // Calculate displacement based on angular change
    const deltaX = deltaAngle * pinionRadius;

    // Translate the rack by the exact displacement
    Body.translate(rack, { x: deltaX, y: 0 });
    Body.setVelocity(rack, { x: pinion.angularVelocity * pinionRadius, y: 0 });
});

This hybrid approach ensures that external forces applied to either body translate correctly between rotational and linear frames without slipping or accumulating floating-point drift over time.