Variable Frame Timesteps: The Source of Simulation Drift
Modern browser displays vary widely across 60 Hz mobile panels, 120 Hz ProMotion laptop screens, and 144 Hz or 240 Hz desktop gaming monitors. Coupling physics simulations directly to requestAnimationFrame timestamp deltas introduces numerical integration drift, unpredictable collision tunneling, and non-deterministic gameplay across differing client hardware.
When Euler integration runs with variable dt, two players running identical input sequences on different machines end up with divergent rigid-body trajectories within seconds. In competitive arcade engines or verifiable physics models, this divergence renders replays un-reproducible and anti-cheat validation impossible.
Fixed-Timestep Accumulators & State Interpolation
To guarantee bit-identical, reproducible physics simulations regardless of screen refresh rate, we decouple physical integration from graphical interpolation. The simulation advances in deterministic fixed slices (e.g. exactly 16.666ms per step), while visual transforms interpolate smoothly between previous and current spatial coordinates:
const FIXED_TIMESTEP_MS = 1000 / 60; // Exactly 16.666ms
let accumulator = 0;
let lastTimestamp = performance.now();
function gameLoop(now: number) {
const frameDelta = Math.min(now - lastTimestamp, 250); // Bound spiral of death
lastTimestamp = now;
accumulator += frameDelta;
while (accumulator >= FIXED_TIMESTEP_MS) {
integrateRigidBodies(physicsState, FIXED_TIMESTEP_MS);
resolveCollisions(physicsState);
accumulator -= FIXED_TIMESTEP_MS;
}
// Fractional alpha for butter-smooth visual interpolation
const alpha = accumulator / FIXED_TIMESTEP_MS;
renderInterpolated(renderContext, physicsState, alpha);
requestAnimationFrame(gameLoop);
}
Decoupling Physics from Display Refresh Rates
The fractional accumulator value alpha represents how far between the previous and current physics ticks the renderer is currently positioned. By interpolating position and rotation vectors using lerp(prevPos, currPos, alpha), a 144 Hz monitor renders buttery-smooth visuals with zero jitter while the underlying physics simulation executes at a locked 60 Hz rate.
Zero-Allocation Geometry Pipelines
JavaScript garbage collection spikes are the primary culprit behind micro-stutters and dropped frames during complex 2D canvas rendering. Instantiating temporary objects (such as coordinate vectors) inside inner simulation loops forces the browser runtime to execute frequent garbage collection sweeps.
Contiguous TypedArray Pools & Particle Rings
By pre-allocating contiguous Float64Array memory pools and circular ring buffers at engine initialization, runtime memory allocations drop to exactly zero during active gameplay cycles:
class ParticleRingBuffer {
private readonly positions: Float64Array; // [x0, y0, vx0, vy0, ...]
private head = 0;
constructor(public readonly capacity: number) {
this.positions = new Float64Array(capacity * 4);
}
spawn(x: number, y: number, vx: number, vy: number): void {
const idx = (this.head % this.capacity) * 4;
this.positions[idx] = x;
this.positions[idx + 1] = y;
this.positions[idx + 2] = vx;
this.positions[idx + 3] = vy;
this.head++;
}
}
Sub-Pixel Canvas Rendering & High-DPI Clarity
On high-DPI displays (such as Apple Retina panels with devicePixelRatio = 2 or 3), naive canvas sizing produces blurry rasterization. Scaling the canvas backing buffer by the device pixel ratio while clamping CSS dimensions ensures crisp, sub-pixel rasterization with proper geometric anti-aliasing.
To experience these deterministic physics loops and zero-allocation memory pools in action, play the Quasi-Perfect Puzzler or navigate the physics obstacles in Retro Labyrinth. For the mathematical underpinnings, review the Laser Loon Architecture.