Build a Working Matter.js Trebuchet

This guide explains how to construct a functional, physics-driven trebuchet in Matter.js featuring a rotating beam, a freely swinging counterweight basket, and a projectile release mechanism. By properly configuring rigid bodies, revolute constraints, mass distribution, and collision filters, you can accurately simulate the mechanical advantage and rotational torque required to launch projectiles in a 2D browser canvas.

1. Project Setup and Physics Engine Initialization

Begin by setting up a basic Matter.js boilerplate including an Engine, Render, Runner, and World.

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

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

const render = Render.create({
    element: document.body,
    engine: engine,
    options: {
        width: 1200,
        height: 700,
        wireframes: false
    }
});

Render.run(render);
Runner.run(Runner.create(), engine);

2. Collision Filtering

To prevent the counterweight, beam, and support structure from colliding with each other during rotation, define collision groups.

const GROUP_TREBUCHET = Body.nextGroup(true); // Negative group ensures non-collision

3. Creating the Frame, Beam, and Fulcrum

A trebuchet relies on an uneven lever arm ratio (typically between 1:4 and 1:6). The beam is anchored to a fixed fulcrum point using a constraint.

// Pivot position in the world
const fulcrumX = 400;
const fulcrumY = 300;

// Beam dimensions (e.g., 500px long, 1:4 leverage ratio)
const beamLength = 500;
const beamHeight = 20;
const shortArmLength = 100;
const longArmLength = 400;

// Center of mass placement for the beam body
const beam = Bodies.rectangle(fulcrumX + (longArmLength - shortArmLength) / 2, fulcrumY, beamLength, beamHeight, {
    collisionFilter: { group: GROUP_TREBUCHET },
    density: 0.002
});

// Fixed pivot constraint (Fulcrum)
const fulcrum = Constraint.create({
    pointA: { x: fulcrumX, y: fulcrumY },
    bodyB: beam,
    pointB: { x: -(longArmLength - shortArmLength) / 2, y: 0 },
    stiffness: 1,
    length: 0
});

Composite.add(world, [beam, fulcrum]);

4. Attaching the Rotating Counterweight Basket

A rigidly fixed counterweight reduces efficiency. Connecting a dense weight via a revolute constraint allows the basket to hang vertically throughout the rotation, maximizing downward acceleration.

const basketWidth = 60;
const basketHeight = 60;
const basketOffset = shortArmLength;

// Create a high-density counterweight body
const counterweight = Bodies.rectangle(
    fulcrumX - basketOffset,
    fulcrumY + 50,
    basketWidth,
    basketHeight,
    {
        collisionFilter: { group: GROUP_TREBUCHET },
        density: 0.5 // High density creates heavy mass
    }
);

// Hinge connecting the short arm tip to the basket
const basketHinge = Constraint.create({
    bodyA: beam,
    pointA: { x: -shortArmLength - (longArmLength - shortArmLength) / 2, y: 0 },
    bodyB: counterweight,
    pointB: { x: 0, y: -basketHeight / 2 },
    stiffness: 1,
    length: 40
});

Composite.add(world, [counterweight, basketHinge]);

5. Adding the Projectile and Sling

Place the projectile at the long end of the beam. You can tether the projectile via a sling constraint to further amplify release velocity.

const projectileRadius = 12;
const longArmTipX = fulcrumX + longArmLength;

const projectile = Bodies.circle(longArmTipX, fulcrumY, projectileRadius, {
    density: 0.004,
    frictionAir: 0.001
});

// Sling constraint linking beam tip to projectile
const sling = Constraint.create({
    bodyA: beam,
    pointA: { x: longArmLength - (longArmLength - shortArmLength) / 2, y: 0 },
    bodyB: projectile,
    pointB: { x: 0, y: 0 },
    stiffness: 0.9,
    length: 60
});

Composite.add(world, [projectile, sling]);

6. Implementing the Release Mechanism

To fire the projectile, remove the sling constraint when the beam reaches its optimal launch angle (typically around 45 to 60 degrees from the horizontal).

Events.on(engine, 'beforeUpdate', () => {
    if (!sling) return;

    // Detect when the beam swings past vertical release threshold
    const beamAngle = beam.angle % (Math.PI * 2);
    
    // Release condition based on rotation angle and upward velocity
    if (beamAngle < -0.8 && projectile.velocity.y < 0) {
        Composite.remove(world, sling);
    }
});

7. Ground and Rest Supports

Add a static ground platform to support the payload before firing and to catch the counterweight after launch.

const ground = Bodies.rectangle(600, 680, 1200, 40, { isStatic: true });
Composite.add(world, ground);