How to Draw Matter.js Trajectory Prediction Dots
Predicting the trajectory of a launched body in Matter.js requires simulating its discrete numerical integration rather than relying on standard continuous kinematic formulas. This guide explains how Matter.js processes motion—specifically accounting for gravity and air friction—and provides a lightweight, step-by-step JavaScript implementation to calculate and render trajectory prediction dots on an HTML5 canvas.
Understanding Matter.js Physics Integration
Matter.js updates body positions using a discrete semi-implicit Euler integration method applied at fixed delta time steps (usually 60 Hz or approximately 16.66ms). Standard projectile motion equations (\(x = v_x t\) and \(y = v_y t + \frac{1}{2} g t^2\)) drift out of sync with Matter.js simulations because they ignore the engine's step-based air friction damping.
Each frame, Matter.js updates linear velocity and position roughly as follows:
- Gravity Application: The engine adds global gravity to the body’s velocity.
- Air Resistance Damping: The velocity is scaled down
by
(1 - body.frictionAir). - Position Integration: The new velocity is added to the current position.
To draw perfectly matching prediction dots, your code must iterate through these identical calculations step-by-step.
The Trajectory Prediction Algorithm
Instead of instantiating real Matter.js bodies for simulation, calculate the virtual points using a lightweight projection loop:
function getTrajectoryPoints(startX, startY, initialVelocity, engine, options = {}) {
const points = [];
const steps = options.steps || 60; // Total dots to draw
const stepInterval = options.stepInterval || 2; // Steps between dots for spacing
// Engine parameters
const gravity = engine.gravity;
const gravityScale = gravity.scale; // Default: 0.001
const gx = gravity.x * gravityScale;
const gy = gravity.y * gravityScale;
// Body parameters (defaults from standard Matter.js body)
const frictionAir = options.frictionAir !== undefined ? options.frictionAir : 0.01;
const damping = 1 - frictionAir;
// Virtual state
let posX = startX;
let posY = startY;
let vx = initialVelocity.x;
let vy = initialVelocity.y;
for (let i = 0; i < steps * stepInterval; i++) {
// 1. Apply gravity
vx += gx;
vy += gy;
// 2. Apply air friction
vx *= damping;
vy *= damping;
// 3. Update position
posX += vx;
posY += vy;
// Collect point based on the spacing interval
if (i % stepInterval === 0) {
points.push({ x: posX, y: posY });
}
}
return points;
}Rendering the Dots on Canvas
Once the coordinates are calculated, render them onto your canvas context before or after rendering the Matter.js world.
function drawTrajectory(context, points) {
context.save();
for (let i = 0; i < points.length; i++) {
const point = points[i];
// Optionally decrease size or opacity along the path
const alpha = 1 - (i / points.length);
const radius = Math.max(1, 4 * alpha);
context.beginPath();
context.arc(point.x, point.y, radius, 0, Math.PI * 2);
context.fillStyle = `rgba(255, 255, 255, ${alpha})`;
context.fill();
}
context.restore();
}Implementing in the Animation Loop
Integrate the trajectory generator into your aiming logic, typically triggered during drag or aim events:
// Example usage inside an event or render loop
const initialVelocity = {
x: (dragStartX - currentMouseX) * 0.1,
y: (dragStartY - currentMouseY) * 0.1
};
const trajectoryPoints = getTrajectoryPoints(
launchOrigin.x,
launchOrigin.y,
initialVelocity,
engine,
{
frictionAir: 0.01,
steps: 30,
stepInterval: 3
}
);
drawTrajectory(canvasContext, trajectoryPoints);Matching Non-Default Settings
If your simulation modifies engine.timing.timeScale or
uses variable delta times, adjust damping and gravity
multipliers accordingly:
- Multiply
gxandgybyMath.pow(engine.timing.timeScale, 2). - Adjust air damping using
Math.pow(1 - frictionAir, engine.timing.timeScale).
This ensures the trajectory points directly overlap the path the physical body traverses upon launch.