Matter.js Rack and Pinion Steering with Travel Stops
This article explains how to build a robust rack-and-pinion steering mechanism with mechanical travel stops using the Matter.js 2D physics engine. You will learn how to create the structural components, constrain the motion of both the pinion and the rack, synchronize rotational and linear motion reliably, and install static physical barriers to limit maximum steering lock.
Core Concepts of the Assembly
A physical rack-and-pinion translates rotational input from a circular gear (the pinion) into linear motion along a toothed bar (the rack). In real-time physics engines like Matter.js, simulating microscopic gear teeth with rigid bodies often results in jitter, high computational cost, and tooth clipping under torque.
A production-ready implementation combines:
- A pinned pinion that rotates freely around an anchor point.
- A sliding rack constrained to a single axis of linear translation.
- Kinematic coupling to transfer angular displacement directly to linear displacement.
- Mechanical stop blocks positioned to physically arrest the rack at the ends of its travel path.
1. Setting Up the Environment
Begin by importing the required Matter.js modules and initializing the engine, world, and renderer.
const { Engine, Render, Runner, Bodies, Body, Constraint, Composite, Events, Vector } = Matter;
const engine = Engine.create();
const world = engine.world;
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 600,
wireframes: false
}
});
Render.run(render);
Runner.run(Runner.create(), engine);2. Creating the Pinion and Pivot Constraint
The pinion is a circular body anchored to a fixed coordinate via a revolute constraint. Setting a fixed pivot allows user input or a motor script to rotate the wheel while keeping its center of mass stationary.
const pinionX = 400;
const pinionY = 250;
const pinionRadius = 40;
const pinion = Bodies.circle(pinionX, pinionY, pinionRadius, {
density: 0.05,
friction: 0.8,
render: { fillStyle: '#2ecc71' }
});
// Anchor the pinion to its initial position
const pinionPivot = Constraint.create({
pointA: { x: pinionX, y: pinionY },
bodyB: pinion,
pointB: { x: 0, y: 0 },
stiffness: 1,
length: 0
});
Composite.add(world, [pinion, pinionPivot]);3. Building the Linear Rack and Guide Track
The rack is a horizontal rectangular body. To ensure it translates
only along the X-axis without tipping over, set its inertia to infinity
(inertia: Infinity) or use guide rails. Setting infinite
inertia prevents unwanted rotation while allowing linear forces to act
normally.
const rackWidth = 300;
const rackHeight = 20;
const rackY = pinionY + pinionRadius + (rackHeight / 2);
const rack = Bodies.rectangle(pinionX, rackY, rackWidth, rackHeight, {
inertia: Infinity, // Prevents rotation
friction: 0.1,
restitution: 0,
render: { fillStyle: '#3498db' }
});
// Guide rails to prevent vertical drift
const upperRail = Bodies.rectangle(400, rackY - 15, 600, 10, { isStatic: true, isSensor: true });
const lowerRail = Bodies.rectangle(400, rackY + 15, 600, 10, { isStatic: true, friction: 0 });
Composite.add(world, [rack, lowerRail]);4. Adding Mechanical Travel Stops
Mechanical stops are rigid static bodies placed on either side of the rack's designated path. When the rack translates too far in either direction, it collides with these barriers, physically halting both the rack and any coupled movement.
const maxTravel = 80; // Maximum allowed distance left or right from center
const stopWidth = 20;
const stopHeight = 40;
const leftStop = Bodies.rectangle(
pinionX - (rackWidth / 2) - maxTravel - (stopWidth / 2),
rackY,
stopWidth,
stopHeight,
{ isStatic: true, render: { fillStyle: '#e74c3c' } }
);
const rightStop = Bodies.rectangle(
pinionX + (rackWidth / 2) + maxTravel + (stopWidth / 2),
rackY,
stopWidth,
stopHeight,
{ isStatic: true, render: { fillStyle: '#e74c3c' } }
);
Composite.add(world, [leftStop, rightStop]);5. Kinematic Coupling and Collision-Aware Synchronization
To couple the rack and pinion without slipping, use the engine's
beforeUpdate event. The displacement formula equates linear
movement (\(\Delta x\)) to angular
movement (\(\Delta \theta \times r\)).
If the rack encounters a mechanical stop, its horizontal velocity drops
to zero, and that resistance is transferred back to the pinion to stop
its rotation.
let lastPinionAngle = pinion.angle;
Events.on(engine, 'beforeUpdate', () => {
const deltaAngle = pinion.angle - lastPinionAngle;
lastPinionAngle = pinion.angle;
// Expected linear displacement: dx = r * dTheta
const targetVelocityX = deltaAngle * pinionRadius * 60; // Scaled to engine frame rate
// Apply linear velocity to rack if not impeded
Body.setVelocity(rack, {
x: targetVelocityX,
y: 0 // Constrain vertical movement entirely
});
// Check if rack has struck travel stops
const rackLeftEdge = rack.position.x - rackWidth / 2;
const rackRightEdge = rack.position.x + rackWidth / 2;
const atLeftLimit = rackLeftEdge <= leftStop.position.x + stopWidth / 2;
const atRightLimit = rackRightEdge >= rightStop.position.x - stopWidth / 2;
// Zero out velocity when hitting limits to prevent jitter
if ((atLeftLimit && targetVelocityX < 0) || (atRightLimit && targetVelocityX > 0)) {
Body.setVelocity(rack, { x: 0, y: 0 });
Body.setAngularVelocity(pinion, 0);
}
});6. Controlling the Steering Assembly
To operate the assembly, apply an external torque or set the angular velocity of the pinion directly. Because of the programmatic coupling and static stop blocks, the entire assembly responds predictably:
// Example input handlers for steering control
window.addEventListener('keydown', (event) => {
const steerSpeed = 0.05;
if (event.key === 'ArrowLeft') {
Body.setAngularVelocity(pinion, -steerSpeed);
} else if (event.key === 'ArrowRight') {
Body.setAngularVelocity(pinion, steerSpeed);
}
});This hybrid approach ensures stable mechanics, eliminates teeth slipping, and enforces precise physical limits on your steering linkage.