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
- Step Frequency: Ensure the simulation loop
corresponds to the engine's update rate. If your game runs at variable
delta times, pass a fixed delta to
Matter.Engine.updateto prevent discrepancies between the predicted arc and the actual physics step. - Order of Operations: In Matter.js, forces and
gravity are accumulated, velocities are damped by
frictionAir, and positions are updated. Reversing this order in your calculation function will cause slight divergence over longer distances. - Collision Stopping: To stop the arc upon impact
with terrain or obstacles, incorporate raycasting (using
Matter.Query.ray) between consecutive points inside the loop to terminate prediction upon intersection with static bodies.