Reorient Velocity Through Angled Portals in Matter.js

This guide explains how to calculate the relative angle between two portals and transform a dynamic body's velocity vector when passing between them in Matter.js. By determining the difference between the entry and exit portal rotations, you can apply a standard 2D rotation matrix to the body's current velocity vector and assign the new vector via Matter.Body.setVelocity, ensuring seamless momentum transfer across angled surfaces.

1. Understanding the Rotation Mathematics

Portals transfer an object entering the front side of Portal A out of the front side of Portal B. Because the body enters facing into Portal A and must exit facing out of Portal B, a \(180^\circ\) (\(\pi\) radians) inversion is added to the rotation difference:

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

To rotate any 2D vector \((v_x, v_y)\) by this angle change (\(\Delta\theta\)), use the standard 2D rotation formulas:

\[v_{x,\text{new}} = v_x \cdot \cos(\Delta\theta) - v_y \cdot \sin(\Delta\theta)\] \[v_{y,\text{new}} = v_x \cdot \sin(\Delta\theta) + v_y \cdot \cos(\Delta\theta)\]

2. Implementation in Matter.js

In Matter.js, a body's velocity is stored in body.velocity. After transforming this vector, update the physics body using Matter.Body.setVelocity(). To keep visual orientation natural, the body's physical rotation should also be adjusted using Matter.Body.setAngle().

function teleportBody(body, entryPortal, exitPortal) {
    // 1. Calculate the change in angle (including the 180-degree flip)
    const deltaAngle = exitPortal.angle - entryPortal.angle + Math.PI;

    // 2. Rotate the body's current velocity vector
    const currentVelocity = body.velocity;
    const cos = Math.cos(deltaAngle);
    const sin = Math.sin(deltaAngle);

    const newVelocity = {
        x: currentVelocity.x * cos - currentVelocity.y * sin,
        y: currentVelocity.x * sin + currentVelocity.y * cos
    };

    // 3. Compute exit position with a forward offset to avoid re-triggering collision
    const exitNormal = {
        x: Math.cos(exitPortal.angle),
        y: Math.sin(exitPortal.angle)
    };
    
    const forwardOffset = 10; // Clearance distance away from exit portal surface
    const newPosition = {
        x: exitPortal.position.x + exitNormal.x * forwardOffset,
        y: exitPortal.position.y + exitNormal.y * forwardOffset
    };

    // 4. Apply changes to the Matter.js body
    Matter.Body.setPosition(body, newPosition);
    Matter.Body.setAngle(body, body.angle + deltaAngle);
    Matter.Body.setVelocity(body, newVelocity);
}

3. Handling Position Clearance and Infinite Loops

When an object exits an angled portal, spawning it directly at the portal's center can cause the exit portal's collision sensor to trigger instantly, creating an infinite teleportation loop.

To prevent this, calculate the exit portal's directional normal vector based on its angle and displace the body's new position slightly outward along this normal before setting the velocity. Alternatively, attach a temporary cooldown flag or timestamp to the body (e.g., body.lastTeleport = Date.now()) and ignore teleportation checks for that body for a brief window (such as 50–100 milliseconds).