Visualizing Contact Forces in Matter.js
This guide explains how to extract and visually render normal and tangential contact force vectors at active collision points in Matter.js. By tapping into collision events, reading contact manifold data and impulses, and utilizing the 2D canvas context after engine updates, you can draw real-time vector arrows representing normal reaction forces and tangential friction forces directly at collision interfaces.
Accessing Collision Data in Matter.js
Matter.js tracks collisions through the pairs managed by
its internal collision detector. During an active collision, a
pair object contains the collision normal and a list of
active contact points (activeContacts). Each contact point
records position coordinates as well as accumulated impulses:
normalImpulse and tangentImpulse.
To read these values continuously without interfering with the
physics solver, attach a listener to the afterRender event
of the Render module. This grants access to the HTML5
Canvas 2D context right after bodies are drawn.
Calculating Force Vectors
The collision normal vector (\(\vec{n}\)) points outward from body A to body B. The tangential vector (\(\vec{t}\)) is orthogonal to the normal vector.
- Normal Force Vector: \[\vec{F}_{\text{normal}} = \vec{n} \times \text{normalImpulse} \times \text{scale}\]
- Tangential (Friction) Force Vector: \[\vec{t} = (-n_y, n_x)\] \[\vec{F}_{\text{tangent}} = \vec{t} \times \text{tangentImpulse} \times \text{scale}\]
Because impulse values in Matter.js represent momentum change over a single engine update step, multiply them by a visual scale factor to make them clearly visible on the canvas.
Implementation Example
Below is the complete implementation hooking into
afterRender to draw normal forces in red and tangential
forces in blue.
const { Engine, Render, Runner, Bodies, Composite, Events, Vector } = Matter;
// 1. Create engine and renderer
const engine = Engine.create();
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: 800,
height: 600,
wireframes: false
}
});
Render.run(render);
Runner.run(Runner.create(), engine);
// 2. Helper function to draw a line vector
function drawVector(ctx, from, vector, color) {
ctx.save();
ctx.beginPath();
ctx.moveTo(from.x, from.y);
ctx.lineTo(from.x + vector.x, from.y + vector.y);
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
// Draw contact point dot
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(from.x, from.y, 3, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
}
// 3. Hook into render loop to draw forces
Events.on(render, 'afterRender', () => {
const ctx = render.context;
const pairs = engine.pairs.list;
const forceScale = 15; // Scale multiplier for rendering visibility
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i];
if (!pair.isActive) continue;
const normal = pair.collision.normal;
const tangent = Vector.perp(normal);
// Iterate through all active contact points for the pair
for (let j = 0; j < pair.activeContacts.length; j++) {
const contact = pair.activeContacts[j];
const vertex = contact.vertex;
// Retrieve impulses (use fallback values if impulses resolve to 0)
const normalMag = (contact.normalImpulse || 0) * forceScale;
const tangentMag = (contact.tangentImpulse || 0) * forceScale;
// Compute vector components
const normalForce = Vector.mult(normal, normalMag);
const tangentForce = Vector.mult(tangent, tangentMag);
// Render Normal Force (Red)
if (normalMag > 0.01) {
drawVector(ctx, vertex, normalForce, '#ff3333');
}
// Render Tangential/Friction Force (Blue)
if (Math.abs(tangentMag) > 0.01) {
drawVector(ctx, vertex, tangentForce, '#3388ff');
}
}
}
});Interpreting the Visuals
- Normal Force (Red): Points away from the contact surface along the collision normal. Its length corresponds directly to the magnitude of the pressure preventing the bodies from interpenetrating.
- Tangential Force (Blue): Points along the contact plane perpendicular to the normal. It extends along the direction of relative surface motion opposing slip, representing the active friction force.