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 suspension

2. 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.