Calculate Trajectory with Air Friction in Matter.js

Calculating an accurate trajectory arc in Matter.js requires a discrete numerical simulation rather than standard parabolic kinematic equations. Because Matter.js applies an iterative damping factor via the frictionAir property on every physics step, velocity decays exponentially over time rather than remaining constant. This article explains how Matter.js applies air friction internally and provides an efficient algorithm to precompute future trajectory coordinates accounting for both gravity and drag.

How Matter.js Applies Air Friction

In basic physics, a trajectory is calculated using standard projectile equations where horizontal velocity is constant (\(v_x = v_0\)). However, Matter.js does not use continuous calculus; it uses discrete numerical integration.

On each update cycle (typically 60 times per second), Matter.js modifies the body's velocity using the body's frictionAir property:

\[\text{damping} = 1 - \text{frictionAir}\] \[v_x = v_x \times \text{damping}\] \[v_y = (v_y + g_y) \times \text{damping}\]

By default, every Matter.js body has a frictionAir value of 0.01 (1% reduction per frame), and the global engine applies gravity scaled by engine.gravity.scale (default 0.001) multiplied by engine.gravity.y (default 1). Because friction is applied continuously per frame, you must simulate the arc frame-by-frame to generate matching path points.

The Trajectory Prediction Function

To generate an array of \((x, y)\) points for rendering the trajectory arc, simulate the motion using a forward projection loop that mirrors the engine's update steps:

/**
 * Calculates trajectory points accounting for gravity and air friction.
 * 
 * @param {Matter.Vector} startPosition - Starting coordinates {x, y}.
 * @param {Matter.Vector} initialVelocity - Starting velocity {x, y}.
 * @param {Matter.Engine} engine - The Matter.js engine instance.
 * @param {number} frictionAir - Body frictionAir (default 0.01).
 * @param {number} steps - Number of future frames to simulate.
 * @returns {Array<{x: number, y: number}>} Array of predicted positions.
 */
function calculateTrajectory(startPosition, initialVelocity, engine, frictionAir = 0.01, steps = 60) {
    const points = [];
    
    // Resolve gravity per update tick
    const gravity = engine.gravity;
    const gravityX = gravity.x * gravity.scale * (engine.timing.timeScale ** 2);
    const gravityY = gravity.y * gravity.scale * (engine.timing.timeScale ** 2);
    
    // Calculate the linear drag multiplier
    const damping = 1 - frictionAir;

    let posX = startPosition.x;
    let posY = startPosition.y;
    let velX = initialVelocity.x;
    let velY = initialVelocity.y;

    for (let i = 0; i < steps; i++) {
        // Apply gravitational acceleration
        velX += gravityX;
        velY += gravityY;

        // Apply air resistance damping
        velX *= damping;
        velY *= damping;

        // Advance position
        posX += velX;
        posY += velY;

        points.push({ x: posX, y: posY });
    }

    return points;
}

Key Considerations for Accuracy