114 lines
3.0 KiB
TypeScript
114 lines
3.0 KiB
TypeScript
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;
|
|
}
|
|
} |