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:
- \(r\) is the pitch radius of the pinion.
- \(\Delta \theta\) is the change in the pinion's rotation angle (in radians).
- \(\Delta x\) is the linear displacement of the rack.
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.