Simulate Momentum-Preserving Portals in Matter.js

This article explains how to build a portal teleportation mechanic in the Matter.js 2D physics engine that accurately preserves a rigid body’s speed and relative trajectory. By leveraging collision sensors, rotation mathematics, and direct velocity manipulation, you will learn how to transfer a body from an entry portal to an exit portal while seamlessly redirecting its linear momentum according to the portals' orientations.

Understanding Portal Mechanics

A portal pair consists of an entry portal and an exit portal, each defined with a position and an angle of orientation. When a dynamic body enters a portal, three operations must take place within a single simulation step:

  1. Repositioning: Translate the body's coordinates to the exit portal, placing it far enough outward along the exit normal to prevent infinite teleportation loops.
  2. Angle Transformation: Rotate the body's orientation to match the rotation delta between the two portals.
  3. Momentum Redirection: Rotate the linear velocity vector of the body by the difference in orientation between the entry and exit portals so that momentum feels continuous.

Setting Up Portal Sensors

Portals must detect collisions without physically deflecting the traveling bodies. In Matter.js, configure portal bodies as sensors using the isSensor: true flag.

const portalA = Matter.Bodies.rectangle(200, 500, 20, 100, {
  isStatic: true,
  isSensor: true,
  angle: 0, // facing right (normal along +X)
  label: 'portal_A'
});

const portalB = Matter.Bodies.rectangle(600, 200, 20, 100, {
  isStatic: true,
  isSensor: true,
  angle: Math.PI / 2, // facing down (normal along +Y)
  label: 'portal_B'
});

// Link portals via references
portalA.targetPortal = portalB;
portalB.targetPortal = portalA;

Matter.Composite.add(engine.world, [portalA, portalB]);

Calculating Velocity and Position Offsets

To preserve momentum, compute the relative angle delta (\(\Delta\theta\)) between the two portals. Because exiting a portal involves traveling outward rather than inward, add \(\pi\) (180 degrees) to invert the entry direction:

\[\Delta\theta = \theta_{\text{exit}} - \theta_{\text{entry}} + \pi\]

Transform the velocity vector using standard 2D vector rotation:

\[v'_x = v_x \cos(\Delta\theta) - v_y \sin(\Delta\theta)\] \[v'_y = v_x \sin(\Delta\theta) + v_y \cos(\Delta\theta)\]

Implementing the Collision Listener

Listen for collisionStart events on the engine. When a dynamic body touches a portal, calculate the new state and apply it immediately using Matter.Body.setPosition, Matter.Body.setAngle, and Matter.Body.setVelocity.

Matter.Events.on(engine, 'collisionStart', (event) => {
  event.pairs.forEach((pair) => {
    let body = null;
    let portal = null;

    if (pair.bodyA.targetPortal && !pair.bodyB.isStatic) {
      portal = pair.bodyA;
      body = pair.bodyB;
    } else if (pair.bodyB.targetPortal && !pair.bodyA.isStatic) {
      portal = pair.bodyB;
      body = pair.bodyA;
    }

    if (body && portal) {
      teleportBody(body, portal, portal.targetPortal);
    }
  });
});

function teleportBody(body, entry, exit) {
  // 1. Calculate rotation delta
  const deltaAngle = exit.angle - entry.angle + Math.PI;

  // 2. Rotate current velocity to preserve momentum
  const currentVelocity = body.velocity;
  const newVelocity = Matter.Vector.rotate(currentVelocity, deltaAngle);

  // 3. Compute exit position with a forward safety offset
  const exitNormal = {
    x: Math.cos(exit.angle),
    y: Math.sin(exit.angle)
  };
  
  const spawnDistance = 25; // Exceeds body radius to prevent re-triggering
  const newPosition = {
    x: exit.position.x + exitNormal.x * spawnDistance,
    y: exit.position.y + exitNormal.y * spawnDistance
  };

  // 4. Update body transform and dynamics
  Matter.Body.setPosition(body, newPosition);
  Matter.Body.setAngle(body, body.angle + deltaAngle);
  Matter.Body.setVelocity(body, newVelocity);
}

Avoiding Infinite Teleportation Loops

When the body arrives at the exit portal, it may trigger an immediate reverse teleportation if the exit position overlaps the exit portal's sensor. Mitigate this by: