Build an Interactive Atwood Machine in Matter.js

This article explains how to build an interactive Atwood machine simulation using the Matter.js 2D physics engine to demonstrate classical mechanics principles such as acceleration and tension. You will learn how to initialize the simulation environment, model two hanging masses connected over a pulley, handle physical constraints, and display real-time readouts for tension and system acceleration.

Understanding the Physics

An ideal Atwood machine consists of two masses, \(m_1\) and \(m_2\), connected by an inextensible, massless string over a frictionless pulley. When released, gravitational force acts upon both masses, creating net acceleration (\(a\)) and tension (\(T\)) throughout the string according to Newton’s second law:

In a dynamic simulation, you can either let the physics engine resolve these forces using interconnected rigid bodies and constraints or apply these equations explicitly within the engine loop to manipulate body velocities directly.

Initializing the Matter.js Environment

Begin by setting up standard Matter.js modules: Engine, Render, Runner, Bodies, Composite, and Constraint.

const { Engine, Render, Runner, Bodies, Composite, Constraint } = 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);

Creating the Pulley and Masses

To build the machine, create a fixed pivot point representing the pulley, along with two movable rectangular bodies representing \(m_1\) and \(m_2\).

const pulleyCenter = { x: 400, y: 150 };
const pulleyRadius = 40;

// Visual pulley body (static)
const pulley = Bodies.circle(pulleyCenter.x, pulleyCenter.y, pulleyRadius, {
  isStatic: true,
  render: { fillStyle: '#4B5563' }
});

// Mass 1 (Left)
const mass1 = Bodies.rectangle(pulleyCenter.x - pulleyRadius, 350, 50, 50, {
  mass: 10,
  inverseMass: 1 / 10,
  frictionAir: 0.001,
  render: { fillStyle: '#3B82F6' }
});

// Mass 2 (Right)
const mass2 = Bodies.rectangle(pulleyCenter.x + pulleyRadius, 350, 60, 60, {
  mass: 15,
  inverseMass: 1 / 15,
  frictionAir: 0.001,
  render: { fillStyle: '#EF4444' }
});

Composite.add(world, [pulley, mass1, mass2]);

Simulating String Mechanics and Constrained Motion

Matter.js constraints represent fixed-length or spring-like rods rather than continuous ropes moving over circular surfaces. To simulate the Atwood string accurately, constrain both masses to vertical motion and link their positions so that the total string length remains constant:

\[y_1 + y_2 = \text{Constant}\]

You can enforce this relationship by hooking into Matter.js's beforeUpdate event:

const totalLength = 500; // Combined vertical string drop

Matter.Events.on(engine, 'beforeUpdate', () => {
  // Lock horizontal drift to simulate vertical guide tracks
  Matter.Body.setPosition(mass1, { x: pulleyCenter.x - pulleyRadius, y: mass1.position.y });
  Matter.Body.setPosition(mass2, { x: pulleyCenter.x + pulleyRadius, y: mass2.position.y });

  // Calculate Atwood acceleration based on gravity
  const g = engine.gravity.y * engine.gravity.scale * 1000; // Scaled acceleration
  const netAcceleration = ((mass2.mass - mass1.mass) / (mass1.mass + mass2.mass)) * g;

  // Apply acceleration directly to vertical velocities
  Matter.Body.setVelocity(mass1, { x: 0, y: mass1.velocity.y - netAcceleration * 0.016 });
  Matter.Body.setVelocity(mass2, { x: 0, y: mass2.velocity.y + netAcceleration * 0.016 });

  // Prevent string overextension
  if (mass1.position.y < pulleyCenter.y + 40) {
    Matter.Body.setPosition(mass1, { x: mass1.position.x, y: pulleyCenter.y + 40 });
    Matter.Body.setVelocity(mass1, { x: 0, y: 0 });
    Matter.Body.setVelocity(mass2, { x: 0, y: 0 });
  } else if (mass2.position.y < pulleyCenter.y + 40) {
    Matter.Body.setPosition(mass2, { x: mass2.position.x, y: pulleyCenter.y + 40 });
    Matter.Body.setVelocity(mass1, { x: 0, y: 0 });
    Matter.Body.setVelocity(mass2, { x: 0, y: 0 });
  }
});

Visualizing Strings and Dynamic Values

To make the demonstration interactive and analytical, render custom string lines and dynamic text for calculated values (acceleration \(a\) and tension \(T\)) during the afterRender event:

Matter.Events.on(render, 'afterRender', () => {
  const ctx = render.context;

  // Draw rope over pulley
  ctx.beginPath();
  ctx.moveTo(mass1.position.x, mass1.position.y);
  ctx.lineTo(pulleyCenter.x - pulleyRadius, pulleyCenter.y);
  ctx.arc(pulleyCenter.x, pulleyCenter.y, pulleyRadius, Math.PI, 0, false);
  ctx.lineTo(mass2.position.x, mass2.position.y);
  ctx.strokeStyle = '#1F2937';
  ctx.lineWidth = 4;
  ctx.stroke();

  // Theoretical physics calculations
  const g = 9.81;
  const m1 = mass1.mass;
  const m2 = mass2.mass;
  const theoreticalAcc = Math.abs((m1 - m2) / (m1 + m2) * g).toFixed(2);
  const theoreticalTension = ((2 * m1 * m2) / (m1 + m2) * g).toFixed(2);

  // HUD Readout
  ctx.fillStyle = '#111827';
  ctx.font = '16px monospace';
  ctx.fillText(`Mass 1: ${m1.toFixed(1)} kg`, 20, 30);
  ctx.fillText(`Mass 2: ${m2.toFixed(1)} kg`, 20, 55);
  ctx.fillText(`System Acceleration: ${theoreticalAcc} m/s²`, 20, 80);
  ctx.fillText(`Rope Tension: ${theoreticalTension} N`, 20, 105);
});

Adding Interactivity

Allow users to alter parameters dynamically by linking standard HTML input ranges directly to mass1.mass and mass2.mass. Updating these properties immediately alters the force distribution and modifies the visual motion and calculation readouts in the real-time loop.