RenderLoop Usage Guide
April 28, 2026 · View on GitHub
RenderLoop implements a fixed-timestep game loop driven by requestAnimationFrame. Physics
and game logic run at a guaranteed step rate regardless of the display's refresh rate, while the
alpha value exposes how far into the next step the renderer currently is — enabling smooth
visual interpolation.
How It Works
Each frame:
- The elapsed wall-clock time since the last frame is added to an accumulator
- The accumulator is consumed in fixed chunks (
frameRatio = 1000 / frameRate) - Your callback is invoked once per consumed chunk, receiving the
RenderLoopinstance - After draining the accumulator,
alpha(accumulator / frameRatio) is ready for interpolation
The accumulator is clamped to 250 ms to prevent the spiral of death when the tab regains focus after being hidden.
Basic Setup
import { RenderLoop } from "craters";
const loop = new RenderLoop((loop) => {
// Called once per fixed tick (default 60 fps)
update(loop.delta);
render(loop.alpha);
});
RenderLoop starts automatically in the constructor. No extra start() call is needed.
Custom Frame Rate
// 30 fps physics tick
const loop = new RenderLoop((loop) => {
update(loop.delta);
}, 30);
Loop Instance Properties
| Property | Type | Description |
|---|---|---|
delta | number | Fixed step duration in ms (always = frameRatio) |
elapsed | number | Total physics time elapsed in ms |
alpha | number | Interpolation factor ∈ [0, 1) — fraction through the next step |
frameRate | number | Target fixed steps per second |
frameRatio | number | Fixed step in ms (1000 / frameRate) |
current | number | requestAnimationFrame timestamp of the last frame |
Render Interpolation with alpha
When the display runs faster than the physics tick (e.g. 144 Hz display, 60 Hz physics),
each rendered frame would otherwise see the same position. Use alpha to linearly interpolate
between the previous and current position to eliminate jitter.
// Store previous position alongside current
let prevX = 0, prevY = 0;
let currX = 0, currY = 0;
const loop = new RenderLoop((loop) => {
prevX = currX;
prevY = currY;
// Update physics
currX += velocityX * loop.delta;
currY += velocityY * loop.delta;
// Render at interpolated position
const renderX = prevX + (currX - prevX) * loop.alpha;
const renderY = prevY + (currY - prevY) * loop.alpha;
drawSprite(renderX, renderY);
});
Tracking Elapsed Time
loop.elapsed accumulates total physics time in ms — useful for animations that are keyed
to physics ticks (e.g. sprite frame selection inside Sprite).
const loop = new RenderLoop((loop) => {
const frameSec = loop.elapsed / 1000;
const spriteFrame = Math.floor(frameSec / 0.1) % totalFrames;
});
Stopping and Restarting
const loop = new RenderLoop(tick);
// Pause (e.g. on game-over screen)
loop.stop();
// Resume — guard inside start() prevents double RAF chains
loop.start();
Integration with ECS
import { EntityComponentSystem as ECS, RenderLoop } from "craters";
const world = new ECS.World();
world.registerSystem(new MovementSystem());
world.registerSystem(new RenderSystem(renderer));
const loop = new RenderLoop((loop) => {
world.execute(loop.delta);
});
API Reference
new RenderLoop(callback, frameRate?)
| Parameter | Default | Description |
|---|---|---|
callback | — | Called once per fixed tick with the RenderLoop instance |
frameRate | 60 | Target physics update rate in Hz |
start()
Starts or resumes the RAF loop. Safe to call when already running (no-op).
stop()
Cancels the RAF loop. Call to pause the game.