Matter.js Scissor Lift with Cross-Pinned Constraints

This article explains how to build a functioning scissor lift physics simulation using cross-pinned constraints in Matter.js. By pairing crossed rigid bodies, pinning their centers with zero-length rotational constraints, chaining multiple tiers, and actuating the base horizontally, you can achieve realistic vertical mechanical extension.

Core Mechanics of a 2D Scissor Lift

A scissor lift works through pantograph geometry. Pairs of rigid beams intersect to form an "X" shape. When the bottom points of the "X" are pushed toward each other along a horizontal axis, the vertical distance between the bottom and top points increases rapidly.

In Matter.js, this requires three constraint types:

  1. Central Pivot Constraints: A revolute joint pinning the centers of two intersecting beams together.
  2. Tier-Linking Constraints: Revolute joints connecting the top ends of one tier to the bottom ends of the tier above it.
  3. Base Actuation: A static hinge on one side of the bottom tier and a horizontally sliding constraint or body on the opposite side to drive the motion.

Step 1: Defining the Beams and Center Pin

Each tier consists of two identical rectangular bodies arranged in an "X". To allow them to overlap without repelling each other, their collision categories must be adjusted or collisions disabled between them using collision groups.

const { Bodies, Body, Constraint, Composite } = Matter;

const beamWidth = 200;
const beamHeight = 15;
const collisionGroup = Body.nextGroup(true); // Negative group prevents mutual collision

function createScissorTier(x, y, angleOffset) {
  const beamA = Bodies.rectangle(x, y, beamWidth, beamHeight, {
    collisionFilter: { group: -1 },
    angle: angleOffset
  });

  const beamB = Bodies.rectangle(x, y, beamWidth, beamHeight, {
    collisionFilter: { group: -1 },
    angle: -angleOffset
  });

  // Cross-pin constraint at the center
  const centerPin = Constraint.create({
    bodyA: beamA,
    bodyB: beamB,
    pointA: { x: 0, y: 0 },
    pointB: { x: 0, y: 0 },
    stiffness: 1,
    length: 0
  });

  return { beamA, beamB, centerPin };
}

Step 2: Chaining Multiple Tiers

To build height, multiple tiers are linked together at their outer tips. A Matter.js zero-length constraint joins the upper-left tip of the lower tier to the lower-left tip of the upper tier. The same connection is made on the right side.

function linkTiers(lowerTier, upperTier) {
  const halfWidth = (beamWidth / 2) * 0.9; // Slight inset for joint stability

  // Left side hinge
  const leftHinge = Constraint.create({
    bodyA: lowerTier.beamA,
    bodyB: upperTier.beamB,
    pointA: { x: -halfWidth, y: 0 },
    pointB: { x: -halfWidth, y: 0 },
    stiffness: 1,
    length: 0
  });

  // Right side hinge
  const rightHinge = Constraint.create({
    bodyA: lowerTier.beamB,
    bodyB: upperTier.beamA,
    pointA: { x: halfWidth, y: 0 },
    pointB: { x: halfWidth, y: 0 },
    stiffness: 1,
    length: 0
  });

  return [leftHinge, rightHinge];
}

Step 3: Anchoring and Actuating the Base

A scissor lift needs one fixed base anchor and one movable base anchor to expand vertically:

  1. Fixed Pivot: Pin the bottom-left joint of the lowest tier to a fixed point in the world using a constraint where pointB is an absolute coordinate and bodyB is null.
  2. Sliding Joint: Connect the bottom-right joint to a rigid body constrained to move only along the horizontal axis, or directly push that corner with a constant linear force.
const halfWidth = (beamWidth / 2) * 0.9;
const baseTier = createScissorTier(400, 500, Math.PI / 6);

// Fixed left anchor
const fixedBase = Constraint.create({
  bodyA: baseTier.beamA,
  pointA: { x: -halfWidth, y: 0 },
  pointB: { x: 310, y: 500 },
  stiffness: 1,
  length: 0
});

// Slider block for right anchor
const sliderBlock = Bodies.rectangle(490, 500, 30, 30, {
  collisionFilter: { group: -1 }
});

const sliderHinge = Constraint.create({
  bodyA: baseTier.beamB,
  bodyB: sliderBlock,
  pointA: { x: halfWidth, y: 0 },
  pointB: { x: 0, y: 0 },
  stiffness: 1,
  length: 0
});

// Keep the slider restricted to horizontal motion
const horizontalTrack = Constraint.create({
  bodyB: sliderBlock,
  pointB: { x: 490, y: 500 },
  stiffness: 0.1,
  length: 0
});

To drive the lift upward, apply a horizontal force pushing sliderBlock toward the fixed anchor:

Matter.Events.on(engine, 'beforeUpdate', () => {
  // Push the sliding side inward to raise the lift
  Body.applyForce(sliderBlock, sliderBlock.position, { x: -0.05, y: 0 });
});

Key Tuning Parameters for Stability