interface WorkerMessage { id: string; type: 'compile' | 'render'; code?: string; width?: number; height?: number; time?: number; renderMode?: string; valueMode?: string; hueShift?: number; startY?: number; // Y offset for tile rendering mouseX?: number; mouseY?: number; mousePressed?: boolean; mouseVX?: number; mouseVY?: number; mouseClickTime?: number; touchCount?: number; touch0X?: number; touch0Y?: number; touch1X?: number; touch1Y?: number; pinchScale?: number; pinchRotation?: number; accelX?: number; accelY?: number; accelZ?: number; gyroX?: number; gyroY?: number; gyroZ?: number; audioLevel?: number; bassLevel?: number; midLevel?: number; trebleLevel?: number; bpm?: number; } interface WorkerResponse { id: string; type: 'compiled' | 'rendered' | 'error'; success: boolean; imageData?: ImageData; error?: string; } export class FakeShader { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private code: string; private worker: Worker; // Single worker for backwards compatibility private workers: Worker[] = []; private workerCount: number; private animationId: number | null = null; private startTime: number = Date.now(); private isCompiled: boolean = false; private isRendering: boolean = false; private pendingRenders: string[] = []; private renderMode: string = 'classic'; private valueMode: string = 'integer'; private hueShift: number = 0; private timeSpeed: number = 1.0; private currentBPM: number = 120; // Multi-worker state private tileResults: Map = new Map(); private tilesCompleted: number = 0; private totalTiles: number = 0; private mouseX: number = 0; private mouseY: number = 0; private mousePressed: boolean = false; private mouseVX: number = 0; private mouseVY: number = 0; private mouseClickTime: number = 0; private touchCount: number = 0; private touch0X: number = 0; private touch0Y: number = 0; private touch1X: number = 0; private touch1Y: number = 0; private pinchScale: number = 1; private pinchRotation: number = 0; private accelX: number = 0; private accelY: number = 0; private accelZ: number = 0; private gyroX: number = 0; private gyroY: number = 0; private gyroZ: number = 0; private audioLevel: number = 0; private bassLevel: number = 0; private midLevel: number = 0; private trebleLevel: number = 0; // Frame rate limiting private targetFPS: number = 30; private frameInterval: number = 1000 / this.targetFPS; private lastFrameTime: number = 0; constructor(canvas: HTMLCanvasElement, code: string = 'x^y') { this.canvas = canvas; this.ctx = canvas.getContext('2d')!; this.code = code; // Initialize offscreen canvas if supported this.initializeOffscreenCanvas(); // Always use maximum available cores this.workerCount = navigator.hardwareConcurrency || 4; // Some browsers report logical processors (hyperthreading), which is good // But cap at a reasonable maximum to avoid overhead this.workerCount = Math.min(this.workerCount, 32); console.log( `Auto-detected ${this.workerCount} CPU cores, using all for maximum performance` ); // Initialize workers this.initializeWorkers(); // Keep single worker reference for backwards compatibility this.worker = this.workers[0]; this.compile(); } private initializeOffscreenCanvas(): void { if (typeof OffscreenCanvas !== 'undefined') { try { // this.offscreenCanvas = new OffscreenCanvas(this.canvas.width, this.canvas.height); // Removed unused // this._offscreenCtx = this.offscreenCanvas.getContext('2d'); // Removed unused // this._useOffscreen = this._offscreenCtx !== null; // Removed unused property } catch (error) { console.warn('OffscreenCanvas not supported:', error); // this._useOffscreen = false; // Removed unused property } } } private initializeWorkers(): void { // Create worker pool for (let i = 0; i < this.workerCount; i++) { const worker = new Worker(new URL('./ShaderWorker.ts', import.meta.url), { type: 'module', }); worker.onmessage = (e: MessageEvent) => this.handleWorkerMessage(e.data, i); worker.onerror = (error) => console.error(`Worker ${i} error:`, error); this.workers.push(worker); } } private handleWorkerMessage( response: WorkerResponse, workerIndex: number = 0 ): void { switch (response.type) { case 'compiled': this.isCompiled = response.success; if (!response.success) { console.error('Compilation failed:', response.error); this.fillBlack(); } break; case 'rendered': if (this.workerCount > 1) { this.handleTileResult(response, workerIndex); } else { // Single worker mode this.isRendering = false; if (response.success && response.imageData) { // Put ImageData directly on main canvas this.ctx.putImageData(response.imageData, 0, 0); } else { console.error('Render failed:', response.error); this.fillBlack(); } } // Process pending renders if (this.pendingRenders.length > 0) { this.pendingRenders.shift(); // Remove completed render if (this.pendingRenders.length > 0) { // Skip to latest render request const latestId = this.pendingRenders[this.pendingRenders.length - 1]; this.pendingRenders = [latestId]; this.executeRender(latestId); } } break; case 'error': this.isRendering = false; console.error('Worker error:', response.error); this.fillBlack(); break; } } private fillBlack(): void { this.ctx.fillStyle = '#000'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); } private compile(): void { this.isCompiled = false; const id = `compile_${Date.now()}`; // Send compile message to all workers this.workers.forEach((worker) => { worker.postMessage({ id, type: 'compile', code: this.code, } as WorkerMessage); }); } private executeRender(id: string): void { if (!this.isCompiled || this.isRendering) { return; } this.isRendering = true; // this._currentRenderID = id; // Removed unused property const currentTime = (Date.now() - this.startTime) / 1000 * this.timeSpeed; // Always use multiple workers if available if (this.workerCount > 1) { this.renderWithMultipleWorkers(id, currentTime); } else { this.renderWithSingleWorker(id, currentTime); } } private renderWithSingleWorker(id: string, currentTime: number): void { this.worker.postMessage({ id, type: 'render', width: this.canvas.width, height: this.canvas.height, fullWidth: this.canvas.width, fullHeight: this.canvas.height, time: currentTime, renderMode: this.renderMode, valueMode: this.valueMode, hueShift: this.hueShift, mouseX: this.mouseX, mouseY: this.mouseY, mousePressed: this.mousePressed, mouseVX: this.mouseVX, mouseVY: this.mouseVY, mouseClickTime: this.mouseClickTime, touchCount: this.touchCount, touch0X: this.touch0X, touch0Y: this.touch0Y, touch1X: this.touch1X, touch1Y: this.touch1Y, pinchScale: this.pinchScale, pinchRotation: this.pinchRotation, accelX: this.accelX, accelY: this.accelY, accelZ: this.accelZ, gyroX: this.gyroX, gyroY: this.gyroY, gyroZ: this.gyroZ, audioLevel: this.audioLevel, bassLevel: this.bassLevel, midLevel: this.midLevel, trebleLevel: this.trebleLevel, bpm: this.currentBPM, } as WorkerMessage); } private renderWithMultipleWorkers(id: string, currentTime: number): void { // Reset tile tracking this.tileResults.clear(); this.tilesCompleted = 0; this.totalTiles = this.workerCount; const width = this.canvas.width; const height = this.canvas.height; const tileHeight = Math.ceil(height / this.workerCount); // Distribute tiles to workers this.workers.forEach((worker, index) => { const startY = index * tileHeight; const endY = Math.min((index + 1) * tileHeight, height); if (startY >= height) return; // Skip if tile is outside canvas worker.postMessage({ id: `${id}_tile_${index}`, type: 'render', width: width, height: endY - startY, // Pass the Y offset for correct coordinate calculation startY: startY, // Pass full canvas dimensions for center calculations fullWidth: width, fullHeight: height, time: currentTime, renderMode: this.renderMode, valueMode: this.valueMode, hueShift: this.hueShift, mouseX: this.mouseX, mouseY: this.mouseY, mousePressed: this.mousePressed, mouseVX: this.mouseVX, mouseVY: this.mouseVY, mouseClickTime: this.mouseClickTime, touchCount: this.touchCount, touch0X: this.touch0X, touch0Y: this.touch0Y, touch1X: this.touch1X, touch1Y: this.touch1Y, pinchScale: this.pinchScale, pinchRotation: this.pinchRotation, accelX: this.accelX, accelY: this.accelY, accelZ: this.accelZ, gyroX: this.gyroX, gyroY: this.gyroY, gyroZ: this.gyroZ, audioLevel: this.audioLevel, bassLevel: this.bassLevel, midLevel: this.midLevel, trebleLevel: this.trebleLevel, bpm: this.currentBPM, } as WorkerMessage); }); } setCode(code: string): void { this.code = code; this.compile(); } render(animate: boolean = false): void { const currentTime = performance.now(); // Frame rate limiting if (animate && currentTime - this.lastFrameTime < this.frameInterval) { if (animate) { this.animationId = requestAnimationFrame(() => this.render(true)); } return; } this.lastFrameTime = currentTime; if (!this.isCompiled) { this.fillBlack(); if (animate) { this.animationId = requestAnimationFrame(() => this.render(true)); } return; } const renderId = `render_${Date.now()}_${Math.random()}`; // Add to pending renders queue this.pendingRenders.push(renderId); // If not currently rendering, start immediately if (!this.isRendering) { this.executeRender(renderId); } // Continue animation if (animate) { this.animationId = requestAnimationFrame(() => this.render(true)); } } startAnimation(): void { this.stopAnimation(); this.startTime = Date.now(); this.lastFrameTime = 0; // Reset frame timing this.render(true); } stopAnimation(): void { if (this.animationId !== null) { cancelAnimationFrame(this.animationId); this.animationId = null; } // Clear pending renders this.pendingRenders = []; } setTargetFPS(fps: number): void { this.targetFPS = Math.max(1, Math.min(120, fps)); // Clamp between 1-120 FPS this.frameInterval = 1000 / this.targetFPS; } setRenderMode(mode: string): void { this.renderMode = mode; } setValueMode(mode: string): void { this.valueMode = mode; } setHueShift(shift: number): void { this.hueShift = shift; } setTimeSpeed(speed: number): void { this.timeSpeed = speed; } setBPM(bpm: number): void { this.currentBPM = bpm; } setMousePosition( x: number, y: number, pressed: boolean = false, vx: number = 0, vy: number = 0, clickTime: number = 0 ): void { this.mouseX = x; this.mouseY = y; this.mousePressed = pressed; this.mouseVX = vx; this.mouseVY = vy; this.mouseClickTime = clickTime; } setTouchPosition( count: number, x0: number = 0, y0: number = 0, x1: number = 0, y1: number = 0, scale: number = 1, rotation: number = 0 ): void { this.touchCount = count; this.touch0X = x0; this.touch0Y = y0; this.touch1X = x1; this.touch1Y = y1; this.pinchScale = scale; this.pinchRotation = rotation; } setDeviceMotion( ax: number, ay: number, az: number, gx: number, gy: number, gz: number ): void { this.accelX = ax; this.accelY = ay; this.accelZ = az; this.gyroX = gx; this.gyroY = gy; this.gyroZ = gz; } setAudioData(level: number, bass: number, mid: number, treble: number): void { this.audioLevel = level; this.bassLevel = bass; this.midLevel = mid; this.trebleLevel = treble; } destroy(): void { this.stopAnimation(); this.workers.forEach((worker) => worker.terminate()); } private handleTileResult( response: WorkerResponse, workerIndex: number ): void { if (!response.success || !response.imageData) { console.error( `Tile render failed for worker ${workerIndex}:`, response.error ); return; } // Store tile result this.tileResults.set(workerIndex, response.imageData); this.tilesCompleted++; // Check if all tiles are complete if (this.tilesCompleted === this.totalTiles) { this.compositeTiles(); } } private async compositeTiles(): Promise { const height = this.canvas.height; const tileHeight = Math.ceil(height / this.workerCount); // Use ImageBitmap for faster compositing if available if (typeof createImageBitmap !== 'undefined') { try { const bitmapPromises: Promise[] = []; const positions: number[] = []; for (let i = 0; i < this.workerCount; i++) { const tileData = this.tileResults.get(i); if (tileData) { bitmapPromises.push(createImageBitmap(tileData)); positions.push(i * tileHeight); } } const bitmaps = await Promise.all(bitmapPromises); for (let i = 0; i < bitmaps.length; i++) { this.ctx.drawImage(bitmaps[i], 0, positions[i]); bitmaps[i].close(); // Free memory } } catch (error) { // Fallback to putImageData if ImageBitmap fails this.fallbackCompositeTiles(); } } else { this.fallbackCompositeTiles(); } // Clear tile results this.tileResults.clear(); // Mark rendering as complete this.isRendering = false; // Process pending renders if (this.pendingRenders.length > 0) { this.pendingRenders.shift(); if (this.pendingRenders.length > 0) { const latestId = this.pendingRenders[this.pendingRenders.length - 1]; this.pendingRenders = [latestId]; this.executeRender(latestId); } } } private fallbackCompositeTiles(): void { const tileHeight = Math.ceil(this.canvas.height / this.workerCount); for (let i = 0; i < this.workerCount; i++) { const tileData = this.tileResults.get(i); if (tileData) { const startY = i * tileHeight; this.ctx.putImageData(tileData, 0, startY); } } } // Simplified method - kept for backward compatibility but always uses all cores setMultiWorkerMode(_enabled: boolean, _workerCount?: number): void { // Always use all available cores, ignore the enabled parameter console.log( `Multi-worker mode is always enabled, using ${this.workerCount} cores for maximum performance` ); } getWorkerCount(): number { return this.workerCount; } static generateRandomCode(): string { const presets = [ 'x^y', 'x&y', 'x|y', '(x*y)%256', '(x+y+t*10)%256', '((x>>4)^(y>>4))<<4', '(x^y^(x*y))%256', '((x&y)|(x^y))%256', '(x+y)&255', 'x%y', '(x^(y<<2))%256', '((x*t)^y)%256', '(x&(y|t*8))%256', '((x>>2)|(y<<2))%256', '(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', ]; const vars = ['x', 'y', 't', 'i']; const ops = ['^', '&', '|', '+', '-', '*', '%']; const shifts = ['<<', '>>']; const numbers = ['2', '4', '8', '16', '32', '64', '128', '256']; const randomChoice = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]; const dynamicExpressions = [ () => `${randomChoice(vars)}${randomChoice(ops)}${randomChoice(vars)}`, () => `(${randomChoice(vars)}${randomChoice(ops)}${randomChoice(vars)})%${randomChoice(numbers)}`, () => `${randomChoice(vars)}${randomChoice(shifts)}${Math.floor(Math.random() * 8)}`, () => `(${randomChoice(vars)}*${randomChoice(vars)})%${randomChoice(numbers)}`, () => `${randomChoice(vars)}^${randomChoice(vars)}^${randomChoice(vars)}`, ]; // 70% chance to pick from presets, 30% chance to generate dynamic if (Math.random() < 0.7) { return randomChoice(presets); } else { return randomChoice(dynamicExpressions)(); } } }