UNPKG

typed-loop

Version:

A loop class to enable flexible intervals for visual experiments and games. It provides delta time in various formats and uses `requestAnimationFrame` for the timeouts. It's possible to use `setTimeout` with given `targetDeltaTime`.

111 lines (94 loc) 3.1 kB
enum TimeoutFunction { REQUEST_ANIMATION_FRAME = 'requestAnimationFrame', SET_TIMEOUT = 'setTimeout', } export enum DeltaTimeFormat { RELATIVE = 'relative', MILLISECONDS = 'milliseconds', SECONDS = 'seconds', } export interface LoopParameters { onTick: (deltaTime: number) => any; deltaTimeFormat?: DeltaTimeFormat; deltaTimeLimit?: number; startWithoutDelay?: boolean; targetTimeout?: number; } export class Loop { onTick: (deltaTime: number) => any; private lastTickTime: number; private active: boolean; private deltaTimeFormat: DeltaTimeFormat; private deltaTimeLimit: number | undefined; private startWithoutDelay: boolean; private timeoutFunction: TimeoutFunction; private targetTimeout: number | undefined; private timeoutId: number; static estimatedDeltaTimeMs = 16; constructor({ onTick, deltaTimeLimit, targetTimeout, deltaTimeFormat = DeltaTimeFormat.MILLISECONDS, startWithoutDelay = false, }: LoopParameters) { this.onTick = onTick; this.deltaTimeLimit = deltaTimeLimit; this.targetTimeout = targetTimeout; this.timeoutFunction = targetTimeout !== undefined ? TimeoutFunction.SET_TIMEOUT : TimeoutFunction.REQUEST_ANIMATION_FRAME; this.startWithoutDelay = startWithoutDelay; this.deltaTimeFormat = deltaTimeFormat; this.lastTickTime = 0; this.timeoutId = 0; this.active = false; this.tick = this.tick.bind(this); // to enable method reference passing directly to timeout functions } start() { if (this.active) return this; this.active = true; if (this.startWithoutDelay) { this.onTick(Loop.estimatedDeltaTimeMs); this.scheduleNextTick(Date.now() - Loop.estimatedDeltaTimeMs); } else { this.scheduleNextTick(); } return this; } stop() { this.active = false; if(this.timeoutFunction === TimeoutFunction.REQUEST_ANIMATION_FRAME) { cancelAnimationFrame(this.timeoutId); } else { clearTimeout(this.timeoutId); } } private tick() { if (!this.active) return; const now = Date.now(); const actualDeltaTimeMs = now - this.lastTickTime; const limitedDeltaTimeInMs = this.deltaTimeLimit !== undefined ? Math.min(actualDeltaTimeMs, this.deltaTimeLimit) : actualDeltaTimeMs; const formattedDeltaTime = this.getFormattedDeltaTime(limitedDeltaTimeInMs); this.onTick(formattedDeltaTime); this.scheduleNextTick(now); } private getFormattedDeltaTime(deltaTimeInMs: number) { switch (this.deltaTimeFormat) { case DeltaTimeFormat.RELATIVE: return deltaTimeInMs / (this.targetTimeout ?? Loop.estimatedDeltaTimeMs); case DeltaTimeFormat.SECONDS: return deltaTimeInMs * 0.001; default: return deltaTimeInMs; } } private scheduleNextTick(currentTime = Date.now()) { this.lastTickTime = currentTime; if (this.timeoutFunction === TimeoutFunction.SET_TIMEOUT) { this.timeoutId = setTimeout(this.tick, this.targetTimeout); } else { this.timeoutId = requestAnimationFrame(this.tick); } } }