281 lines
6.8 KiB
TypeScript
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',
|
|
];
|
|
}
|
|
} |