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:
- Net Acceleration (\(a\)): \[a = \frac{|m_1 - m_2|}{m_1 + m_2} g\]
- Tension (\(T\)): \[T = \frac{2 m_1 m_2}{m_1 + m_2} g\]
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.