Files
bitfielder/src/RefactoredShader.ts
2025-07-15 00:52:00 +02:00

281 lines
6.8 KiB
TypeScript

import { WorkerMessage } from './shader/types';
import { InputManager } from './shader/core/InputManager';
import { WorkerPool } from './shader/core/WorkerPool';
import { RenderController } from './shader/core/RenderController';
/**
* Refactored shader renderer with separated concerns
* Demonstrates the benefits of extracting responsibilities from the God class
*/
export class RefactoredShader {
private canvas: HTMLCanvasElement;
private ctx: CanvasRenderingContext2D;
private code: string;
// Extracted components with single responsibilities
private inputManager: InputManager;
private workerPool: WorkerPool;
private renderController: RenderController;
// Render state
private compiled: boolean = false;
private renderMode: string = 'classic';
private valueMode: string = 'integer';
private hueShift: number = 0;
// Reusable message object for performance
private reusableMessage: WorkerMessage = {
id: '',
type: 'render',
width: 0,
height: 0,
fullWidth: 0,
fullHeight: 0,
time: 0,
renderMode: 'classic',
valueMode: 'integer',
hueShift: 0,
mouseX: 0,
mouseY: 0,
mousePressed: false,
mouseVX: 0,
mouseVY: 0,
mouseClickTime: 0,
touchCount: 0,
touch0X: 0,
touch0Y: 0,
touch1X: 0,
touch1Y: 0,
pinchScale: 1,
pinchRotation: 0,
accelX: 0,
accelY: 0,
accelZ: 0,
gyroX: 0,
gyroY: 0,
gyroZ: 0,
audioLevel: 0,
bassLevel: 0,
midLevel: 0,
trebleLevel: 0,
bpm: 120,
startY: 0,
};
constructor(canvas: HTMLCanvasElement, code: string = 'x^y') {
this.canvas = canvas;
this.ctx = canvas.getContext('2d')!;
this.code = code;
// Initialize separated components
this.inputManager = new InputManager();
this.workerPool = new WorkerPool();
this.renderController = new RenderController();
this.setupEventHandlers();
this.compile();
}
private setupEventHandlers(): void {
// Set up render controller callback
this.renderController.setRenderFrameHandler((time, renderId) => {
this.render(renderId, time);
});
// Set up worker pool callbacks
this.workerPool.setRenderCompleteHandler((imageData) => {
this.ctx.putImageData(imageData, 0, 0);
this.renderController.setRenderingState(false);
});
this.workerPool.setErrorHandler((error) => {
console.error('Rendering error:', error);
this.renderController.setRenderingState(false);
});
}
async compile(): Promise<void> {
try {
await this.workerPool.compile(this.code);
this.compiled = true;
console.log('Shader compiled successfully');
} catch (error) {
console.error('Compilation failed:', error);
this.compiled = false;
throw error;
}
}
private render(id: string, currentTime: number): void {
if (!this.compiled || this.renderController.isCurrentlyRendering()) {
return;
}
this.renderController.setRenderingState(true);
this.renderController.addPendingRender(id);
// Update reusable message to avoid allocations
this.reusableMessage.id = id;
this.reusableMessage.width = this.canvas.width;
this.reusableMessage.height = this.canvas.height;
this.reusableMessage.fullWidth = this.canvas.width;
this.reusableMessage.fullHeight = this.canvas.height;
this.reusableMessage.time = currentTime;
this.reusableMessage.renderMode = this.renderMode;
this.reusableMessage.valueMode = this.valueMode;
this.reusableMessage.hueShift = this.hueShift;
// Populate input data from InputManager
this.inputManager.populateWorkerMessage(this.reusableMessage);
// Choose rendering strategy based on worker count
if (this.workerPool.getWorkerCount() > 1) {
this.workerPool.renderMultiWorker(
this.reusableMessage,
this.canvas.width,
this.canvas.height
);
} else {
this.workerPool.renderSingleWorker(this.reusableMessage);
}
}
// Public API methods
start(): void {
if (!this.compiled) {
console.warn('Cannot start rendering: shader not compiled');
return;
}
this.renderController.start();
}
stop(): void {
this.renderController.stop();
}
setCode(code: string): Promise<void> {
this.code = code;
this.compiled = false;
return this.compile();
}
setRenderMode(mode: string): void {
this.renderMode = mode;
}
setValueMode(mode: string): void {
this.valueMode = mode;
}
setHueShift(shift: number): void {
this.hueShift = shift;
}
setTargetFPS(fps: number): void {
this.renderController.setTargetFPS(fps);
}
setTimeSpeed(speed: number): void {
this.renderController.setTimeSpeed(speed);
}
// Input methods - delegated to InputManager
setMousePosition(x: number, y: number): void {
this.inputManager.setMousePosition(x, y);
}
setMousePressed(pressed: boolean): void {
this.inputManager.setMousePressed(pressed);
}
setMouseVelocity(vx: number, vy: number): void {
this.inputManager.setMouseVelocity(vx, vy);
}
setTouchData(
count: number,
x0: number = 0,
y0: number = 0,
x1: number = 0,
y1: number = 0,
scale: number = 1,
rotation: number = 0
): void {
this.inputManager.setTouchData(count, x0, y0, x1, y1, scale, rotation);
}
setAccelerometer(x: number, y: number, z: number): void {
this.inputManager.setAccelerometer(x, y, z);
}
setGyroscope(x: number, y: number, z: number): void {
this.inputManager.setGyroscope(x, y, z);
}
setAudioLevels(
level: number,
bass: number,
mid: number,
treble: number,
bpm: number
): void {
this.inputManager.setAudioLevels(level, bass, mid, treble, bpm);
}
// Getters
isCompiled(): boolean {
return this.compiled;
}
isAnimating(): boolean {
return this.renderController.isAnimating();
}
getCurrentTime(): number {
return this.renderController.getCurrentTime();
}
getFrameRate(): number {
return this.renderController.getFrameRate();
}
getWorkerCount(): number {
return this.workerPool.getWorkerCount();
}
// Cleanup
destroy(): void {
this.renderController.stop();
this.workerPool.destroy();
}
// Helper methods for generating example shader code
static getExamples(): string[] {
return [
'x^y',
'x|y',
'(x+y+t*10)%256',
'((x>>4)^(y>>4))<<4',
'(x^y^(x*y))%256',
'd * t / 2.0',
'((x&y)|(x^y))%256',
'(x+y)&255',
'x%y',
'((x*t)^y)%256',
'(x&(y|t*8))%256',
'a+d*t',
'n*t*400',
'((x>>2)|(y<<2))%88',
'(x*y*t)%256',
'(x+y*t)%256',
'(x^y^(t*16))%256',
'((x*t)&(y*t))%256',
'(x+(y<<(t%4)))%256',
'((x*t%128)^y)%256',
'(x^(y*t*2))%256',
'((x+t)*(y+t))%256',
'(x&y&(t*8))%256',
'((x|t)^(y|t))%256',
];
}
}