Real-Time Momentum Vectors in Matter.js

This guide explains how to build an interactive educational physics sandbox using Matter.js that calculates and renders real-time momentum vector arrows on dynamic bodies. By hooking into the Matter.js render lifecycle, you will learn how to extract velocity and mass, compute the linear momentum vector (\(\vec{p} = m\vec{v}\)), and draw dynamic arrows over the physics canvas to visually demonstrate the laws of motion and momentum conservation.

Core Architecture Overview

Matter.js provides built-in rendering via HTML5 Canvas, but it does not natively draw vector overlays like momentum or force. To display real-time vectors:

  1. Initialize the Matter.js physics engine and render loop.
  2. Spawn rigid bodies with defined mass and velocity properties.
  3. Attach an event listener to the afterRender event of the Render module.
  4. Calculate momentum (\(\vec{p} = m \cdot \vec{v}\)) for each target body on every frame.
  5. Draw scaled vector arrows on top of the rendered simulation using standard 2D canvas drawing methods.

Step 1: Setting Up the Physics Environment

Create the baseline simulation using Matter.Engine, Matter.Render, Matter.Runner, and Matter.Bodies.

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

// Create engine and world
const engine = Engine.create();
const world = engine.world;

// Create renderer
const render = Render.create({
    element: document.body,
    engine: engine,
    options: {
        width: 800,
        height: 600,
        wireframes: false,
        background: '#1e1e24'
    }
});

Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);

Step 2: Adding Bodies with Variable Masses

To clearly demonstrate how momentum depends on both velocity and mass, create bodies with contrasting properties:

// Boundaries
const ground = Bodies.rectangle(400, 590, 810, 30, { isStatic: true });
const leftWall = Bodies.rectangle(10, 300, 30, 600, { isStatic: true });
const rightWall = Bodies.rectangle(790, 300, 30, 600, { isStatic: true });

// Light, fast body (mass is automatically derived from density and area)
const lightBall = Bodies.circle(200, 300, 20, {
    restitution: 0.9,
    render: { fillStyle: '#4ea8de' }
});

// Heavy, slow body
const heavyBall = Bodies.circle(600, 300, 40, {
    density: 0.005, // Higher density results in higher mass
    restitution: 0.9,
    render: { fillStyle: '#e63946' }
});

Composite.add(world, [ground, leftWall, rightWall, lightBall, heavyBall]);

// Impart initial velocities toward each other
Matter.Body.setVelocity(lightBall, { x: 8, y: -2 });
Matter.Body.setVelocity(heavyBall, { x: -2, y: 0 });

Step 3: Drawing Momentum Vector Arrows

The momentum vector is computed by multiplying the body's scalar mass by its 2D velocity vector:

\[\vec{p}_x = m \cdot v_x\] \[\vec{p}_y = m \cdot v_y\]

Because velocity and mass units can yield large or small pixel representations, a scaling factor is required to keep arrows readable within the canvas boundaries.

Use the afterRender event to draw directly onto the canvas context:

const VECTOR_SCALE = 0.5; // Adjust based on visual preference

Events.on(render, 'afterRender', () => {
    const context = render.context;
    const dynamicBodies = Composite.allBodies(world).filter(body => !body.isStatic);

    dynamicBodies.forEach(body => {
        // Calculate linear momentum
        const px = body.mass * body.velocity.x * VECTOR_SCALE;
        const py = body.mass * body.velocity.y * VECTOR_SCALE;

        const startX = body.position.x;
        const startY = body.position.y;
        const endX = startX + px;
        const endY = startY + py;

        // Only draw if the body is moving significantly
        const magnitude = Math.hypot(px, py);
        if (magnitude > 1) {
            drawArrow(context, startX, startY, endX, endY, '#ffd166');
        }
    });
});

Step 4: The Vector Arrow Rendering Function

Implement a reusable Canvas 2D helper function to draw the vector line and the arrowhead:

function drawArrow(ctx, fromX, fromY, toX, toY, color) {
    const headLength = 10;
    const dx = toX - fromX;
    const dy = toY - fromY;
    const angle = Math.atan2(dy, dx);

    ctx.save();
    ctx.strokeStyle = color;
    ctx.fillStyle = color;
    ctx.lineWidth = 3;

    // Draw main vector line
    ctx.beginPath();
    ctx.moveTo(fromX, fromY);
    ctx.lineTo(toX, toY);
    ctx.stroke();

    // Draw arrowhead
    ctx.beginPath();
    ctx.moveTo(toX, toY);
    ctx.lineTo(
        toX - headLength * Math.cos(angle - Math.PI / 6),
        toY - headLength * Math.sin(angle - Math.PI / 6)
    );
    ctx.lineTo(
        toX - headLength * Math.cos(angle + Math.PI / 6),
        toY - headLength * Math.sin(angle + Math.PI / 6)
    );
    ctx.closePath();
    ctx.fill();

    ctx.restore();
}

Educational Enhancements

To make the sandbox more instructional: