import { TIMING } from '../../utils/constants'; /** * Manages animation timing and frame rate control * Extracted from FakeShader for better separation of concerns */ export class RenderController { private animationId: number | null = null; private startTime: number = Date.now(); private targetFPS: number = TIMING.DEFAULT_FPS; private frameInterval: number = TIMING.MILLISECONDS_PER_SECOND / this.targetFPS; private lastFrameTime: number = 0; private timeSpeed: number = 1.0; private isRendering: boolean = false; private pendingRenders: string[] = []; private idCounter: number = 0; private onRenderFrame?: (time: number, renderId: string) => void; setRenderFrameHandler(handler: (time: number, renderId: string) => void): void { this.onRenderFrame = handler; } start(): void { if (this.animationId !== null) return; const animate = (timestamp: number) => { if (timestamp - this.lastFrameTime >= this.frameInterval) { const currentTime = (Date.now() - this.startTime) / TIMING.MILLISECONDS_PER_SECOND * this.timeSpeed; const renderId = this.generateId(); this.onRenderFrame?.(currentTime, renderId); this.lastFrameTime = timestamp; } this.animationId = requestAnimationFrame(animate); }; this.animationId = requestAnimationFrame(animate); } stop(): void { if (this.animationId !== null) { cancelAnimationFrame(this.animationId); this.animationId = null; } } setTargetFPS(fps: number): void { this.targetFPS = Math.max(TIMING.MIN_FPS, Math.min(TIMING.MAX_FPS, fps)); this.frameInterval = TIMING.MILLISECONDS_PER_SECOND / this.targetFPS; } setTimeSpeed(speed: number): void { this.timeSpeed = speed; } getTimeSpeed(): number { return this.timeSpeed; } getCurrentTime(): number { return (Date.now() - this.startTime) / TIMING.MILLISECONDS_PER_SECOND * this.timeSpeed; } isAnimating(): boolean { return this.animationId !== null; } generateId(): string { return `render_${this.idCounter++}_${Date.now()}`; } setRenderingState(isRendering: boolean): void { this.isRendering = isRendering; } isCurrentlyRendering(): boolean { return this.isRendering; } addPendingRender(renderId: string): void { this.pendingRenders.push(renderId); // Keep only the latest render to avoid backlog if (this.pendingRenders.length > 3) { const latestId = this.pendingRenders[this.pendingRenders.length - 1]; this.pendingRenders = [latestId]; } } removePendingRender(renderId: string): void { const index = this.pendingRenders.indexOf(renderId); if (index !== -1) { this.pendingRenders.splice(index, 1); } } getPendingRenders(): string[] { return [...this.pendingRenders]; } clearPendingRenders(): void { this.pendingRenders = []; } getFrameRate(): number { return this.targetFPS; } getFrameInterval(): number { return this.frameInterval; } }