How to Create Pinball Flippers in Matter.js

This guide explains how to construct a fast, responsive pinball flipper in Matter.js that snaps briskly to an exact stop angle. You will learn how to create the flipper body, anchor it to a pivot point using a constraint, and manipulate angular velocity and angle clamping inside the engine's update loop to ensure instantaneous reaction times and rigid angle limits.

1. Create the Flipper Body and Pivot Constraint

To create a realistic flipper, define an elongated polygon or rounded rectangle and secure one of its ends to a fixed point in space using a revolute constraint.

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

// Pivot coordinates
const pivotX = 200;
const pivotY = 500;
const flipperLength = 100;
const flipperWidth = 20;

// Position the body center offset from the pivot point
const flipper = Bodies.rectangle(
  pivotX + flipperLength / 2, 
  pivotY, 
  flipperLength, 
  flipperWidth, 
  {
    chamfer: { radius: 10 },
    density: 0.005,
    restitution: 0.1, // Low restitution prevents unwanted jitter on hard stops
    friction: 0.0
  }
);

// Pin the flipper at the pivot end
const pivotConstraint = Constraint.create({
  pointA: { x: pivotX, y: pivotY },
  bodyB: flipper,
  pointB: { x: -flipperLength / 2, y: 0 },
  stiffness: 1,
  length: 0
});

World.add(engine.world, [flipper, pivotConstraint]);

2. Define Stroke Limits and Control State

Define the resting angle, the active (flipped) angle, and a boolean state flag to handle user input. For a left flipper, angles are typically negative when flipped up and slightly positive or flat at rest.

const DEG_TO_RAD = Math.PI / 180;

const flipperConfig = {
  minAngle: 25 * DEG_TO_RAD,  // Resting angle
  maxAngle: -35 * DEG_TO_RAD, // Flipped up angle
  speed: 0.35,                // Angular speed (radians per frame)
  isFlipping: false
};

// Listen for keyboard controls
window.addEventListener('keydown', (e) => {
  if (e.code === 'KeyZ') flipperConfig.isFlipping = true;
});

window.addEventListener('keyup', (e) => {
  if (e.code === 'KeyZ') flipperConfig.isFlipping = false;
});

3. Implement Brisk Rotation and Precise Clamping

Physics-based torque often leads to spongy deceleration or bouncy overshoot. The most reliable method to achieve an instant, crisp flip is setting the flipper's angular velocity directly during the beforeUpdate engine event, and clamping the angle immediately once it crosses the threshold.

const { Events } = Matter;

Events.on(engine, 'beforeUpdate', () => {
  const currentAngle = flipper.angle;
  const targetAngle = flipperConfig.isFlipping ? flipperConfig.maxAngle : flipperConfig.minAngle;
  const direction = targetAngle < currentAngle ? -1 : 1;
  
  // Check if the flipper has reached or passed the target angle
  const hasReachedTarget = direction === -1 
    ? currentAngle <= targetAngle 
    : currentAngle >= targetAngle;

  if (hasReachedTarget) {
    // Snap to the exact angle and nullify velocity
    Body.setAngle(flipper, targetAngle);
    Body.setAngularVelocity(flipper, 0);
  } else {
    // Drive the flipper briskly toward the target angle
    Body.setAngularVelocity(flipper, direction * flipperConfig.speed);
  }
});

4. Prevent Ball Tunneling During High-Speed Impact

Because the flipper moves across a wide arc in only a few frames, small or high-speed balls may pass directly through the flipper (tunneling). Prevent this by tuning the physics engine runner:

  1. Increase Engine Iterations: Raise positionIterations and velocityIterations on the engine options to a minimum of 8 or 10.
  2. Substep the Physics Runner: If using Matter.Runner, lower the time step or execute multiple Engine.update(engine, delta) cycles per animation frame so the collision solver detects contacts throughout the entire stroke.