Build a Ragdoll Posing Tool with Matter.js Constraints
This guide demonstrates how to build an interactive, browser-based ragdoll posing tool using the Matter.js 2D physics engine. By assembling rigid bodies with revolute constraints and implementing dynamic pin constraints, you can create a character that users can drag, balance, and pin in place. The walkthrough covers building the skeletal hierarchy, configuring joint limits, and creating a toggle-pinning mechanism for interactive posing.
1. Setting Up the Matter.js Engine
Initialize the basic Matter.js environment by creating an engine, renderer, runner, and adding canvas interaction modules.
const { Engine, Render, Runner, Bodies, Composite, Constraint, Mouse, MouseConstraint } = 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);
const runner = Runner.create();
Runner.run(runner, engine);To prevent posed ragdolls from collapsing or falling off-screen when working with zero-gravity posing, you can optionally reduce or disable world gravity:
engine.gravity.y = 0.5; // Lower gravity for easier posing, or 0 for complete suspension2. Constructing the Ragdoll Rigid Bodies
A basic humanoid ragdoll consists of individual rectangular and
circular bodies representing the head, torso, and limbs. Group all parts
using a shared negative collisionFilter.group so they pass
through each other at the joints without erratic collisions.
const group = Matter.Body.nextGroup(true); // Negative group index disables internal collisions
const createPart = (x, y, w, h, isCircle = false) => {
const options = {
collisionFilter: { group: group },
frictionAir: 0.05,
render: { fillStyle: '#4A90E2' }
};
return isCircle ? Bodies.circle(x, y, w / 2, options) : Bodies.rectangle(x, y, w, h, options);
};
// Character parts
const head = createPart(400, 150, 40, 40, true);
const chest = createPart(400, 200, 50, 60);
const lowerTorso = createPart(400, 255, 45, 50);
const leftUpperArm = createPart(360, 185, 16, 40);
const leftLowerArm = createPart(360, 225, 14, 40);
const rightUpperArm = createPart(440, 185, 16, 40);
const rightLowerArm = createPart(440, 225, 14, 40);
const leftUpperLeg = createPart(385, 305, 18, 50);
const leftLowerLeg = createPart(385, 355, 16, 50);
const rightUpperLeg = createPart(415, 305, 18, 50);
const rightLowerLeg = createPart(415, 355, 16, 50);3. Connecting Limbs with Revolute Constraints
Use Matter.Constraint.create to hinge body parts
together. A revolute joint is formed when two anchor points share the
same relative position in world space with a constraint length of
zero.
const join = (bodyA, bodyB, pointA, pointB, stiffness = 0.9) => {
return Constraint.create({
bodyA,
bodyB,
pointA,
pointB,
stiffness,
length: 0,
render: { visible: false }
});
};
const joints = [
// Neck and spine
join(chest, head, { x: 0, y: -30 }, { x: 0, y: 20 }),
join(chest, lowerTorso, { x: 0, y: 30 }, { x: 0, y: -25 }),
// Left Arm
join(chest, leftUpperArm, { x: -25, y: -20 }, { x: 0, y: -18 }),
join(leftUpperArm, leftLowerArm, { x: 0, y: 18 }, { x: 0, y: -18 }),
// Right Arm
join(chest, rightUpperArm, { x: 25, y: -20 }, { x: 0, y: -18 }),
join(rightUpperArm, rightLowerArm, { x: 0, y: 18 }, { x: 0, y: -18 }),
// Left Leg
join(lowerTorso, leftUpperLeg, { x: -15, y: 25 }, { x: 0, y: -23 }),
join(leftUpperLeg, leftLowerLeg, { x: 0, y: 23 }, { x: 0, y: -23 }),
// Right Leg
join(lowerTorso, rightUpperLeg, { x: 15, y: 25 }, { x: 0, y: -23 }),
join(rightUpperLeg, rightLowerLeg, { x: 0, y: 23 }, { x: 0, y: -23 })
];
Composite.add(world, [
head, chest, lowerTorso,
leftUpperArm, leftLowerArm, rightUpperArm, rightLowerArm,
leftUpperLeg, leftLowerLeg, rightUpperLeg, rightLowerLeg,
...joints
]);4. Implementing Interactive Constraint Pinning
Posing requires fixing a body part to a static coordinate in the
world. Pinning is accomplished by attaching a constraint that binds a
body to a fixed world coordinate (pointB) without a
bodyB.
Maintain a tracking structure for active pins so that users can add or remove pins interactively.
const activePins = new Map();
function pinBody(body, worldPosition) {
// If already pinned, remove the old pin
if (activePins.has(body)) {
Composite.remove(world, activePins.get(body));
activePins.delete(body);
return;
}
// Create a static anchor constraint to the world coordinate
const pinConstraint = Constraint.create({
bodyA: body,
pointA: {
x: worldPosition.x - body.position.x,
y: worldPosition.y - body.position.y
},
pointB: {
x: worldPosition.x,
y: worldPosition.y
},
stiffness: 1,
length: 0,
render: {
strokeStyle: '#FF3B30',
lineWidth: 4
}
});
activePins.set(body, pinConstraint);
Composite.add(world, pinConstraint);
}5. Adding Mouse Interaction and Pin Controls
Add a MouseConstraint to enable body dragging, and
attach event listeners to handle pinning actions (such as
double-clicking or holding an alternate key while clicking).
const mouse = Mouse.create(render.canvas);
const mouseConstraint = MouseConstraint.create(engine, {
mouse: mouse,
constraint: {
stiffness: 0.2,
render: { visible: true }
}
});
Composite.add(world, mouseConstraint);
render.mouse = mouse;
// Toggle pin on right-click or double-click
render.canvas.addEventListener('contextmenu', (event) => {
event.preventDefault();
const mousePosition = mouse.position;
const bodies = [
head, chest, lowerTorso,
leftLowerArm, rightLowerArm,
leftLowerLeg, rightLowerLeg
];
// Find if a body was clicked
const clickedBody = Matter.Query.point(bodies, mousePosition)[0];
if (clickedBody) {
pinBody(clickedBody, mousePosition);
}
});Using this setup, you can drag individual limbs with the left mouse
button to adjust rotation and physics, and right-click key extremities
(such as hands, feet, or head) to pin them in space. Clearing the
activePins map releases all constraints, immediately
subjecting the ragdoll back to standard physical dynamics.