There is a distinct rite of passage every indie web game developer stumbles through: you want a bouncing ball in your browser game, you refuse to import a 200KB physics library for a 13KB game jam entry, and twenty minutes later your pristine circles are either phasing through the floor like ghosts or launching into the stratosphere at Mach 4.
Building a custom 2D physics pipeline in vanilla HTML5 Canvas sounds intimidating, but stripping away the bloat reveals a surprisingly elegant system. With the recent renaissance of minimalist, ultra-tactile web games on itch.io and endless debates across game dev YouTube regarding whether to ditch heavyweight WASM wrappers for native Canvas 2D loops, understanding impulse resolution from the ground up gives your browser games that immediate, crunchy mechanical feel players love.
Direct Answer: What Is Rigid Body Impulse Resolution?
Impulse-based dynamic resolution is a physics simulation technique that instantly modifies the linear and angular velocities of colliding bodies at the moment of contact, using discrete bursts of momentum (impulses) rather than computing continuous penalty springs.
[Detect Overlap] ➔ [Compute Collision Normal & Penetration] ➔ [Apply Normal Impulse (Bounce)] ➔ [Apply Tangent Impulse (Friction)] ➔ [Positional Correction]
- Restitution ($e$): The bounciness factor, clamped between
0.0(inelastic, lumps of clay) and1.0(completely elastic, superballs). - Impulse ($J$): An instantaneous change in linear momentum, calculated using relative velocity along the collision normal.
- Coulomb Friction: A tangential force that resists sliding motion, split into static friction (resisting the start of motion) and dynamic friction (slowing ongoing sliding).
The Mathematics of the Bonk: Resolving Linear Collision
When two circular or convex bodies collide in a 2D plane, detection gives you two vital pieces of data: the collision normal (the unit vector pointing from body A to body B) and the penetration depth.
To separate them without making them look like soggy sponges, we calculate an impulse magnitude ($j$). The formula derived from Newton's laws and conservation of momentum looks like this:
$j = \frac{-(1 + e) \cdot (\mathbf{v}_{rel} \cdot \mathbf{n})}{\frac{1}{m_A} + \frac{1}{m_B}}$
Where:
- $\mathbf{v}_{rel} = \mathbf{v}_B - \mathbf{v}_A$ (relative velocity)
- $\mathbf{n}$ is the contact normal
- $e$ is the coefficient of restitution (typically $\min(e_A, e_B)$)
- $m_A, m_B$ are body masses (with inverse masses $w = 1/m$ used in code so static objects can have mass $\infty$ and inverse mass $0$)
If objects are moving away from each other ($\mathbf{v}_{rel} \cdot \mathbf{n} > 0$), you abort immediately, otherwise bodies will snag and glue together.
Adding Grip: Implementing Coulomb Friction
A frictionless physics engine feels like an ice-skating simulator where every surface has been slathered in butter. To fix this, you need a tangent vector perpendicular to your collision normal.
Collision Normal (n)
▲
│
│
Contact ┼────────► Tangent (t) [Direction of relative sliding]
Point │
│
1. Calculate the tangent vector: Take the relative velocity vector, subtract its projection along the normal, and normalise it.
2. Find tangential impulse magnitude ($j_t$): Calculate the raw friction impulse needed to halt sliding along that tangent.
3. Clamp by Coulomb’s Law: Real friction cannot push an object backwards; it can only resist motion up to normal force multiplied by the friction coefficient ($\mu$).
- If $|j_t| < j_n \cdot \mu_s$, apply static friction ($j_t$).
- Otherwise, cap the drag using dynamic friction: $j_t = -j_n \cdot \mu_d$.
The Clean Implementation (Vanilla JavaScript)
Here is a self-contained impulse resolution function designed for circular bodies in a standard HTML5 Canvas requestAnimationFrame loop:
function resolveCollisionWithFriction(bodyA, bodyB, normal) {
// 1. Calculate relative velocity
const rvX = bodyB.vx - bodyA.vx;
const rvY = bodyB.vy - bodyA.vy;
// Relative velocity along the normal
const velAlongNormal = rvX * normal.x + rvY * normal.y;
// Do not resolve if velocities are separating
if (velAlongNormal > 0) return;
// Combined restitution and inverse masses
const e = Math.min(bodyA.restitution, bodyB.restitution);
const invMassA = bodyA.isStatic ? 0 : 1 / bodyA.mass;
const invMassB = bodyB.isStatic ? 0 : 1 / bodyB.mass;
const invMassSum = invMassA + invMassB;
if (invMassSum === 0) return; // Both static
// 2. Compute normal impulse scalar
const jNormal = -(1 + e) * velAlongNormal / invMassSum;
const impulseNormX = jNormal * normal.x;
const impulseNormY = jNormal * normal.y;
// Apply normal impulse
bodyA.vx -= invMassA * impulseNormX;
bodyA.vy -= invMassA * impulseNormY;
bodyB.vx += invMassB * impulseNormX;
bodyB.vy += invMassB * impulseNormY;
// 3. Compute tangent vector for friction
let tangentX = rvX - velAlongNormal * normal.x;
let tangentY = rvY - velAlongNormal * normal.y;
const tangentLen = Math.hypot(tangentX, tangentY);
if (tangentLen > 0.0001) {
tangentX /= tangentLen;
tangentY /= tangentLen;
// Tangential velocity magnitude
const velAlongTangent = rvX * tangentX + rvY * tangentY;
let jTangent = -velAlongTangent / invMassSum;
// Coulomb's law: clamp friction by normal impulse
const mu = Math.sqrt(bodyA.friction * bodyB.friction);
const maxFriction = Math.abs(jNormal) * mu;
jTangent = Math.max(-maxFriction, Math.min(jTangent, maxFriction));
// Apply friction impulse
const frictionX = jTangent * tangentX;
const frictionY = jTangent * tangentY;
bodyA.vx -= invMassA * frictionX;
bodyA.vy -= invMassA * frictionY;
bodyB.vx += invMassB * frictionX;
bodyB.vy += invMassB * frictionY;
}
}
Physics Engines: How Impulse Compares
When developing casual web games, choosing how you solve collisions dictates your performance ceiling and feel.
| Method | Best For | Pros | Cons |
|---|---|---|---|
| Impulse-Based (Sequential) | Action arcade games, pinball, billiards | Snappy response, low memory overhead, easy to tune | Can jitter when stacking multiple resting bodies |
| Verlet / Position-Based (PBD) | Ragdolls, cloth, ropes, soft bodies | Immensely stable, eliminates velocity tracking | Less accurate restitution, floaty impacts |
| Linear Complementarity (LCP) | Complex simulations, rigid stacks | Mathematically exact, no sinking | High CPU cost, overkill for browser canvas |
Taming the Sinking Glitch: Positional Correction
If you only change velocities, numerical drift from floating-point updates causes objects to sink into floors over time. To avoid this, implement linear projection (also known as Baumgarte stabilisation):
- Move bodies out of each other slightly every frame based on penetration depth.
- Introduce a small threshold or "slop" (typically
0.01to0.05pixels) to prevent jitter when resting. - Multiply the correction by a percentage factor (e.g., 20% to 40%) so objects gently pop out rather than violently vibrating against each other.
Tuning this balance gives your canvas games tactile, predictable impacts without relying on thousands of lines of external dependencies.