How to Model a Centrifugal Governor in Matter.js
This article explains how to simulate a centrifugal governor in Matter.js by combining rigid bodies, pivot constraints, and dynamic force calculations. Because Matter.js operates in a 2D coordinate space, modeling the 3D rotation of governor flyweights requires creating a side-profile linkage system and applying calculated centrifugal forces relative to an angular velocity variable. Below is a step-by-step breakdown of the physics principles, assembly structure, and code needed to achieve realistic outward arm deflection as rotational speed increases.
Mechanical Concept in 2D
A traditional Watt centrifugal governor consists of a central rotating shaft, two hinged arms with heavy flyballs at their ends, and link rods connecting them to a sliding sleeve. In real life, rotating the shaft causes inertia (centrifugal force) to push the weights outward against gravity.
In a 2D physics engine, an out-of-plane rotation cannot generate native centrifugal acceleration. Therefore, the most stable and visually accurate approach is a 2D side-view schematic:
- Model the governor arms and weights using Matter.js dynamic bodies and constraints.
- Maintain a global
angularVelocity(\(\omega\)) parameter. - Compute the horizontal centrifugal force for each weight: \[F_c = m \cdot r \cdot \omega^2\] Where \(m\) is the mass of the weight, and \(r\) is the horizontal distance from the central axis.
- Apply this outward force frame-by-frame inside the
beforeUpdateengine loop.
1. Scene and Anchor Setup
Begin by setting up the Matter.js engine, world, and an anchor point representing the top pivot on the central shaft.
const { Engine, Render, Runner, Bodies, Body, Composite, Constraint, Events, Vector } = Matter;
const engine = Engine.create();
const world = engine.world;
// Set standard downward gravity
world.gravity.y = 1;
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 600,
wireframes: false
}
});
Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);
// Central axis position
const centerX = 400;
const topY = 150;
// Top fixed anchor
const topPivot = Bodies.circle(centerX, topY, 8, {
isStatic: true,
render: { fillStyle: '#333' }
});
Composite.add(world, topPivot);2. Creating Flyweights and Arm Constraints
Construct two identical dynamic circles to act as the flyweights. Attach each flyweight to the top pivot using stiff distance constraints to represent rigid rods.
const armLength = 140;
const ballRadius = 18;
// Left and right flyweights
const leftWeight = Bodies.circle(centerX - 40, topY + 120, ballRadius, {
mass: 2,
frictionAir: 0.02,
render: { fillStyle: '#e74c3c' }
});
const rightWeight = Bodies.circle(centerX + 40, topY + 120, ballRadius, {
mass: 2,
frictionAir: 0.02,
render: { fillStyle: '#e74c3c' }
});
// Link arms from top pivot to weights
const leftArm = Constraint.create({
bodyA: topPivot,
bodyB: leftWeight,
length: armLength,
stiffness: 0.9,
render: { strokeStyle: '#555', lineWidth: 4 }
});
const rightArm = Constraint.create({
bodyA: topPivot,
bodyB: rightWeight,
length: armLength,
stiffness: 0.9,
render: { strokeStyle: '#555', lineWidth: 4 }
});
Composite.add(world, [leftWeight, rightWeight, leftArm, rightArm]);3. Adding the Sliding Collar (Optional)
To complete the mechanism, add a central sliding collar that rises when the weights extend outward. Constrain the collar to move solely along the Y-axis.
const collar = Bodies.rectangle(centerX, topY + 200, 30, 20, {
mass: 1,
frictionAir: 0.05,
render: { fillStyle: '#34495e' }
});
// Lower linkages connecting weights to collar
const lowerArmLength = 100;
const leftLowerArm = Constraint.create({
bodyA: leftWeight,
bodyB: collar,
length: lowerArmLength,
stiffness: 0.9,
render: { strokeStyle: '#777', lineWidth: 3 }
});
const rightLowerArm = Constraint.create({
bodyA: rightWeight,
bodyB: collar,
length: lowerArmLength,
stiffness: 0.9,
render: { strokeStyle: '#777', lineWidth: 3 }
});
Composite.add(world, [collar, leftLowerArm, rightLowerArm]);4. Applying Centrifugal Forces in the Engine Loop
Simulate rotation by applying horizontal forces outward from
centerX. Hook into the beforeUpdate event to
update the force application every physics tick.
let currentRpm = 0;
const targetRpm = 120; // Adjust to expand or contract arms
Events.on(engine, 'beforeUpdate', () => {
// Smoothly ramp RPM toward target
currentRpm += (targetRpm - currentRpm) * 0.02;
// Convert RPM to angular velocity (radians per second)
const omega = (currentRpm * 2 * Math.PI) / 60;
// Force multiplier to scale physical units to Matter.js engine scale
const forceScaling = 0.00002;
// Calculate left weight force (directed leftward: -X)
const rLeft = Math.abs(leftWeight.position.x - centerX);
const leftForceMagnitude = leftWeight.mass * rLeft * Math.pow(omega, 2) * forceScaling;
Body.applyForce(leftWeight, leftWeight.position, {
x: -leftForceMagnitude,
y: 0
});
// Calculate right weight force (directed rightward: +X)
const rRight = Math.abs(rightWeight.position.x - centerX);
const rightForceMagnitude = rightWeight.mass * rRight * Math.pow(omega, 2) * forceScaling;
Body.applyForce(rightWeight, rightWeight.position, {
x: rightForceMagnitude,
y: 0
});
// Restrict collar strictly to the vertical center axis
Body.setPosition(collar, { x: centerX, y: collar.position.y });
Body.setVelocity(collar, { x: 0, y: collar.velocity.y });
});5. Tuning Stability and Performance
- Friction and Damping: Keep
frictionAirbetween0.01and0.05on the weights. Without air resistance, the weights will oscillate continuously due to inertia. - Force Scaling Factor: Matter.js applies forces as
instantaneous momentum impulses per frame. Tune
forceScalingto match the mass and gravity settings of your world. - Constraint Iterations: If the rods stretch under
high rotational speeds, increase
engine.constraintIterations(default is 2, set to 4 or 6) to keep constraints rigid.