Files
bitfielder/src/FakeShader.ts
2025-07-05 21:55:50 +00:00

533 lines
18 KiB
TypeScript

interface WorkerMessage {
id: string;
type: 'compile' | 'render';
code?: string;
width?: number;
height?: number;
time?: number;
renderMode?: string;
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;
}
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 offscreenCanvas: OffscreenCanvas | null = null;
private offscreenCtx: OffscreenCanvasRenderingContext2D | null = null;
private useOffscreen: boolean = false;
// Multi-worker state
private tileResults: Map<number, ImageData> = new Map();
private tilesCompleted: number = 0;
private totalTiles: number = 0;
private currentRenderID: string = '';
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);
this.offscreenCtx = this.offscreenCanvas.getContext('2d');
this.useOffscreen = this.offscreenCtx !== null;
} catch (error) {
console.warn('OffscreenCanvas not supported:', error);
this.useOffscreen = false;
}
}
}
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<WorkerResponse>) => 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;
const currentTime = (Date.now() - this.startTime) / 1000;
// 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,
time: currentTime,
renderMode: this.renderMode,
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
} 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,
time: currentTime,
renderMode: this.renderMode,
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
} 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;
}
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 compositeTiles(): void {
const width = this.canvas.width;
const height = this.canvas.height;
const tileHeight = Math.ceil(height / this.workerCount);
// Clear main canvas
this.ctx.clearRect(0, 0, width, height);
// Composite all tiles directly on main canvas
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);
}
}
// 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);
}
}
}
// 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 = <T>(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)();
}
}
}