@zakkster/lite-ticker
v1.0.1
Published
Zero-dependency RAF scheduler with game-pausable timers, fixed-timestep physics, and tab-switch guard.
Maintainers
Readme
@zakkster/lite-ticker
A zero-dependency RAF scheduler with game-pausable timers and optional fixed-timestep physics.
One requestAnimationFrame loop to rule them all — per-frame tasks, timeouts, and intervals that pause when the game pauses and don't explode after a tab switch.
Features
- Variable or fixed-timestep loops — choose per game
- Game-pausable setTimeout / setInterval — tied to RAF, not wall clock
- Tab-switch guard — caps dt to prevent physics explosions after background
- Disposer pattern — every add/setTimeout/setInterval returns a cleanup function
- Snapshot-safe timer iteration — callbacks can safely add/remove timers mid-frame
- Zero dependencies, < 1 KB
Installation
npm install @zakkster/lite-tickerQuick Start
import { Ticker } from '@zakkster/lite-ticker';
const ticker = new Ticker();
// Add a per-frame task (receives dt in ms)
const removeUpdate = ticker.add((dt) => {
player.x += player.vx * (dt / 1000);
});
// Game-pausable timeout (freezes when ticker is paused)
const cancelCountdown = ticker.setTimeout(() => {
console.log('3 seconds of GAME TIME have passed');
}, 3000);
ticker.start();
// Later: pause (all tasks and timers freeze)
ticker.pause();
// Resume (no delta spike — timestamp resets)
ticker.start();
// Clean up
removeUpdate();
cancelCountdown();
ticker.destroy();Options
const ticker = new Ticker({
fixedStep: 16.66, // Fixed timestep in ms (0 = variable dt, default)
maxDt: 100, // Tab-switch dt cap in ms (default: 100)
});API
Lifecycle
| Method | Description |
|--------|-------------|
| .start() | Start the loop. Resets timestamp. Idempotent. |
| .pause() | Pause. Tasks and timers freeze. |
| .destroy() | Stop and clear everything. Idempotent. |
Per-Frame Tasks
const remove = ticker.add((dt) => {
// dt is in ms — either variable or fixedStep
physics.update(dt);
renderer.draw();
});
remove(); // stop receiving framesTimers
// One-shot (like setTimeout but game-pausable)
const cancel = ticker.setTimeout(() => {
showModal('Game Over');
}, 5000);
// Repeating (like setInterval but game-pausable)
const stop = ticker.setInterval(() => {
spawnEnemy();
}, 2000);
cancel(); // cancel before it fires
stop(); // stop the intervalFixed Timestep
For deterministic physics, use fixedStep:
const ticker = new Ticker({ fixedStep: 16.66 }); // 60 updates/sec
ticker.add((dt) => {
// dt is always 16.66, regardless of frame rate.
// If a frame takes 33ms, this runs twice.
// If a frame takes 8ms, this doesn't run (accumulator < step).
body.velocity.y += GRAVITY * dt;
body.position.y += body.velocity.y * dt;
});The accumulator pattern prevents the "spiral of death" — if the game can't keep up, the dt cap kicks in and frames are skipped rather than endlessly catching up.
Tab-Switch Guard
When a user switches tabs and comes back 5 seconds later, the raw dt would be 5000ms. Without a guard, physics objects teleport across the screen and timers fire all at once.
lite-ticker caps dt at maxDt (default: 100ms). After the cap, dt falls back to fixedStep (or 16.66ms if variable), producing one normal-looking frame instead of a physics explosion.
Timer Safety
Timer callbacks can safely add or remove other timers during execution — the timer set is snapshotted before iteration. Intervals carry over their remainder for drift-free scheduling (elapsed %= delay).
TypeScript
import { Ticker, type TickerOptions, type Disposer } from '@zakkster/lite-ticker';
const ticker = new Ticker({ fixedStep: 16.66 });
const remove: Disposer = ticker.add((dt: number) => {
// fully typed
});License
MIT
