How to Create a Geneva Drive in Matter.js

This guide explains how to design, assemble, and simulate a functioning Geneva drive mechanism using Matter.js. You will learn the principles of constructing the driving wheel and the slotted driven wheel using compound bodies, anchoring them with pivot constraints, and tuning collision parameters to achieve reliable intermittent rotary motion in a 2D physics environment.


Understanding the Mechanics

A Geneva drive (or Maltese cross) translates continuous rotational motion into intermittent rotary motion. It consists of two primary interacting components:

  1. The Drive Wheel: Rotates continuously and features a driving pin offset from its center, along with a raised circular blocking disc.
  2. The Driven Wheel: Features symmetrical slots (typically four to six) radiating from its center, alternating with concave locking cutouts that interface with the blocking disc to prevent rotation between index steps.

In Matter.js, rigid-body physics replaces ideal mathematical linkages. The mechanism relies purely on contact forces: the pin enters a slot, pushes the wheel forward by a fraction of a turn, and exits, while friction and constraints regulate the movement.


1. Environment Setup

Matter.js requires standard module initialization. Import the engine, runner, bodies, composite, and constraint classes:

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

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

// Geneva drives require higher solver fidelity to avoid pin tunneling
engine.positionIterations = 12;
engine.velocityIterations = 8;

2. Modeling the Driven Wheel (Geneva Cross)

Because standard Geneva wheels have concave geometry (slots), Matter.js cannot treat them as simple single-polygon shapes without concave decomposition or compound assemblies. The most stable approach is building a compound body using overlapping rectangles to form the slots.

For a classic 4-slot Geneva wheel:

function createGenevaWheel(x, y, radius, slotWidth) {
    const parts = [];
    const numSlots = 4;
    const spokeWidth = radius * 0.4;
    const spokeLength = radius;

    // Create central core
    const core = Bodies.circle(x, y, radius * 0.3, { isSensor: true });
    parts.push(core);

    // Build arm walls to define slots
    for (let i = 0; i < numSlots; i++) {
        const angle = (i * Math.PI) / 2;
        
        // Offset rectangular plates defining the slot walls
        const wallOffset = slotWidth / 2 + spokeWidth / 2;
        const armLeft = Bodies.rectangle(
            x + Math.cos(angle - 0.2) * (radius * 0.6),
            y + Math.sin(angle - 0.2) * (radius * 0.6),
            spokeLength,
            spokeWidth,
            { angle: angle }
        );
        parts.push(armLeft);
    }

    const drivenWheel = Body.create({
        parts: parts,
        friction: 0.05,
        restitution: 0,
        density: 0.005
    });

    return drivenWheel;
}

3. Modeling the Drive Wheel and Pin

The drive wheel consists of a central rotating hub and an offset drive pin that enters the slots of the driven wheel.

function createDriveWheel(x, y, radius, pinDistance, pinRadius) {
    // Base hub
    const hub = Bodies.circle(x, y, radius, {
        collisionFilter: { mask: 0 } // Prevent base body from interfering with slots
    });

    // Pin positioned on the perimeter
    const pin = Bodies.circle(x + pinDistance, y, pinRadius, {
        friction: 0.02,
        restitution: 0,
        density: 0.01
    });

    return Body.create({
        parts: [hub, pin],
        frictionAir: 0
    });
}

4. Anchoring and Constraining the Mechanism

Both wheels must rotate about fixed centers. Use Constraint.create with zero length to create static revolute joints.

const driveCenter = { x: 300, y: 400 };
const centerDistance = 141.4; // Calculated based on wheel radii for a 90-degree drive
const drivenCenter = { x: driveCenter.x + centerDistance, y: 400 };

const driveWheel = createDriveWheel(driveCenter.x, driveCenter.y, 80, 100, 8);
const drivenWheel = createGenevaWheel(drivenCenter.x, drivenCenter.y, 100, 18);

// Pivot constraints anchored to fixed world points
const drivePivot = Constraint.create({
    pointA: driveCenter,
    bodyB: driveWheel,
    pointB: { x: 0, y: 0 },
    stiffness: 1,
    length: 0
});

const drivenPivot = Constraint.create({
    pointA: drivenCenter,
    bodyB: drivenWheel,
    pointB: { x: 0, y: 0 },
    stiffness: 1,
    length: 0
});

Composite.add(world, [driveWheel, drivenWheel, drivePivot, drivenPivot]);

5. Driving the Motion and Physics Tuning

To operate the Geneva mechanism, apply constant angular velocity to the driving wheel inside the engine update loop, or maintain continuous rotational velocity directly:

Events.on(engine, 'beforeUpdate', () => {
    // Maintain a constant drive speed
    Body.setAngularVelocity(driveWheel, 0.03);
});

Critical Parameters for Stability