Creating physical simulations in the browser—whether an interactive particle field, ragdoll mechanics, or gravitational orbital bodies—is one of the most rewarding challenges in computer graphics. When executed properly, physics creates an immediate sense of tactile realism and playful interactivity.
However, many frontend developers struggle when their simulations encounter real-world browser conditions: games running at double speed on 120Hz/144Hz gaming monitors, objects tunneling through collision boundaries during frame drops, or visible micro-stutter (jank) caused by garbage collection pauses. In this guide, we explore the mathematics of numerical integration, explain why Euler integration fails, and dissect the architecture behind our Gravity Pulse canvas simulation.
1. The Problem with Explicit Euler Integration
The simplest way to update a moving object's position in physics code is standard Explicit Euler integration:
// NAIVE EXPLICIT EULER: Highly unstable with springs or orbital mechanics
velocity.x += acceleration.x * dt;
velocity.y += acceleration.y * dt;
position.x += velocity.x * dt;
position.y += velocity.y * dt;
While intuitive, Euler integration introduces systematic mathematical error on every single step. In physical reality, acceleration curves continuously; Euler integration approximates this curve using a sequence of straight tangent lines. When simulating harmonic oscillators (like springs) or planetary orbits, Euler integration continuously injects artificial kinetic energy into the system. Over time, orbits expand uncontrollably, springs explode into infinity, and particles shatter through solid walls.
2. The Elegance of Verlet Integration
In 1967, French physicist Loup Verlet developed a numerical integration method widely adopted in molecular dynamics, astrophysics, and video game physics (including the original Hitman game's ragdoll engine).
Instead of storing explicit velocity vectors, Verlet Integration determines motion by comparing an object's current position with its position in the previous frame:
// Verlet Position Update Formula
x_(t + dt) = 2 * x_t - x_(t - dt) + a_t * (dt)^2
Notice that velocity is implicitly encoded as (x_t - x_(t - dt))! This delivers several profound mathematical benefits:
- Energy Conservation: Verlet integration is symplectic, meaning it preserves energy over long simulation runs without artificial orbital decay or explosive drift.
- Effortless Constraint Resolution: To enforce distance constraints (such as a rigid rope or cloth mesh), you simply move the particle positions directly. The velocity adjusts itself automatically in the subsequent frame!
- Collision Stability: When an object collides with a boundary, clamping its position naturally cancels its velocity without complex momentum reflection math.
Verlet in JavaScript
function updateVerlet(particle, dt, gravity) {
const vx = (particle.x - particle.oldX) * particle.friction;
const vy = (particle.y - particle.oldY) * particle.friction;
particle.oldX = particle.x;
particle.oldY = particle.y;
particle.x += vx + particle.ax * dt * dt;
particle.y += vy + (particle.ay + gravity) * dt * dt;
}
3. Taming the Display Refresh Rate: Fix Your Timestep!
Modern displays operate at varying refresh rates: 60Hz (16.6ms), 90Hz (11.1ms), 120Hz (8.3ms), or 240Hz (4.1ms). If an engine ties physical updates directly to the requestAnimationFrame callback without tracking delta time, the simulation runs twice as fast on a 120Hz iPad Pro as it does on a standard 60Hz laptop.
Conversely, passing variable delta time directly into physics equations (dt = currentTime - lastTime) causes instability whenever a background task causes a sudden frame spike (a 100ms stutter can propel a particle clear across the screen, tunneling through walls).
The industry gold standard—popularized by game developer Glenn Fiedler in his seminal article "Fix Your Timestep!"—is Delta Time Accumulation with a Fixed Physics Timestep:
let lastTime = performance.now();
let accumulator = 0;
const FIXED_STEP = 1000 / 60; // Fixed 60Hz physics (16.666ms)
function gameLoop(currentTime) {
let frameTime = currentTime - lastTime;
lastTime = currentTime;
// Prevent spiral of death on tab refocus
if (frameTime > 250) frameTime = 250;
accumulator += frameTime;
// Run physics in deterministic, fixed chunks
while (accumulator >= FIXED_STEP) {
updatePhysics(FIXED_STEP / 1000);
accumulator -= FIXED_STEP;
}
// Render as fast as monitor can refresh (interpolated if needed)
renderCanvas();
requestAnimationFrame(gameLoop);
}
4. HTML5 Canvas Rendering Optimizations
Executing physics math is rarely the bottleneck in JavaScript; rendering thousands of pixels to the canvas context is what causes frame drops. High-performance canvas engines employ three optimization rules:
- Minimize Context State Changes: Calling
ctx.fillStyle,ctx.strokeStyle, orctx.shadowBlurforces the GPU driver to flush command buffers. Batch all particles sharing the same color into a singlebeginPath()andfill()call. - Clear Rect vs. Fill Rect:
ctx.clearRect(0, 0, width, height)is generally faster thanctx.fillRect()because modern GPU drivers recognizeclearRectas a buffer discard command. - Offscreen Canvas Prerendering: For complex particle shapes or glows, render the particle once to an offscreen canvas and use
ctx.drawImage(offscreenCanvas, x, y)rather than drawing paths procedurally on every frame.
Conclusion
High-frame-rate web graphics are achieved through the marriage of numerical mathematics and browser lifecycle discipline. By replacing volatile Euler equations with Verlet integration, decoupling physical simulation from monitor refresh rates, and batching canvas draw calls, developers can engineer interactive simulations that maintain an unwavering 60 or 120 FPS across any device.
To experience real-time gravitational particle physics, orbital mechanics, and interactive tactile force fields directly in your browser, explore our free Gravity Pulse simulation.