From 431966d498e636af949a893f3ba328bcc832ea24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Mon, 14 Jul 2025 21:08:21 +0200 Subject: [PATCH] essais --- public/sw.js | 5 + src/FakeShader.ts | 227 +++-- src/ShaderWorker.ts | 1162 ------------------------ src/components/TopBar.tsx | 258 +++--- src/hooks/useWebcam.ts | 80 ++ src/shader/core/ShaderCache.ts | 53 ++ src/shader/core/ShaderCompiler.ts | 145 +++ src/shader/index.ts | 15 + src/shader/rendering/FeedbackSystem.ts | 198 ++++ src/shader/rendering/PixelRenderer.ts | 639 +++++++++++++ src/shader/types/ShaderContext.ts | 112 +++ src/shader/types/WorkerMessage.ts | 56 ++ src/shader/types/index.ts | 7 + src/shader/worker/ShaderWorker.ts | 317 +++++++ src/stores/input.ts | 2 + src/styles/main.css | 20 +- src/utils/constants.ts | 2 +- 17 files changed, 1856 insertions(+), 1442 deletions(-) delete mode 100644 src/ShaderWorker.ts create mode 100644 src/hooks/useWebcam.ts create mode 100644 src/shader/core/ShaderCache.ts create mode 100644 src/shader/core/ShaderCompiler.ts create mode 100644 src/shader/index.ts create mode 100644 src/shader/rendering/FeedbackSystem.ts create mode 100644 src/shader/rendering/PixelRenderer.ts create mode 100644 src/shader/types/ShaderContext.ts create mode 100644 src/shader/types/WorkerMessage.ts create mode 100644 src/shader/types/index.ts create mode 100644 src/shader/worker/ShaderWorker.ts diff --git a/public/sw.js b/public/sw.js index efccd1c..5e3150f 100644 --- a/public/sw.js +++ b/public/sw.js @@ -15,6 +15,7 @@ const CORE_ASSETS = [ const DYNAMIC_ASSETS_PATTERNS = [ /\/src\/.+\.(ts|tsx|js|jsx)$/, /\/src\/.+\.css$/, + /\/assets\/.+\.(js|css)$/, /fonts\.googleapis\.com/, /fonts\.gstatic\.com/ ]; @@ -30,6 +31,10 @@ self.addEventListener('install', event => { }), caches.open(DYNAMIC_CACHE).then(cache => { console.log('Dynamic cache initialized'); + // Pre-cache critical assets if they exist + return cache.addAll([]).catch(() => { + console.log('No additional assets to pre-cache'); + }); }) ]).then(() => { console.log('Service Worker installed successfully'); diff --git a/src/FakeShader.ts b/src/FakeShader.ts index fb1d47b..d1555c9 100644 --- a/src/FakeShader.ts +++ b/src/FakeShader.ts @@ -1,47 +1,4 @@ -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; -} +import { WorkerMessage, WorkerResponse } from './shader/types'; export class FakeShader { private canvas: HTMLCanvasElement; @@ -61,6 +18,48 @@ export class FakeShader { private timeSpeed: number = 1.0; private currentBPM: number = 120; + // ID generation optimization + private idCounter: number = 0; + + // Reusable message object to avoid allocations + 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, + }; + // Multi-worker state private tileResults: Map = new Map(); private tilesCompleted: number = 0; @@ -137,7 +136,7 @@ export class FakeShader { 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), { + const worker = new Worker(new URL('./shader/worker/ShaderWorker.ts', import.meta.url), { type: 'module', }); worker.onmessage = (e: MessageEvent) => @@ -203,7 +202,7 @@ export class FakeShader { private compile(): void { this.isCompiled = false; - const id = `compile_${Date.now()}`; + const id = `compile_${++this.idCounter}`; // Send compile message to all workers this.workers.forEach((worker) => { @@ -232,43 +231,47 @@ export class FakeShader { } } + private updateReusableMessage(id: string, currentTime: number, width: number, height: number, fullWidth: number, fullHeight: number, startY: number = 0): void { + this.reusableMessage.id = id; + this.reusableMessage.type = 'render'; + this.reusableMessage.width = width; + this.reusableMessage.height = height; + this.reusableMessage.fullWidth = fullWidth; + this.reusableMessage.fullHeight = fullHeight; + this.reusableMessage.time = currentTime; + this.reusableMessage.renderMode = this.renderMode; + this.reusableMessage.valueMode = this.valueMode; + this.reusableMessage.hueShift = this.hueShift; + this.reusableMessage.startY = startY; + this.reusableMessage.mouseX = this.mouseX; + this.reusableMessage.mouseY = this.mouseY; + this.reusableMessage.mousePressed = this.mousePressed; + this.reusableMessage.mouseVX = this.mouseVX; + this.reusableMessage.mouseVY = this.mouseVY; + this.reusableMessage.mouseClickTime = this.mouseClickTime; + this.reusableMessage.touchCount = this.touchCount; + this.reusableMessage.touch0X = this.touch0X; + this.reusableMessage.touch0Y = this.touch0Y; + this.reusableMessage.touch1X = this.touch1X; + this.reusableMessage.touch1Y = this.touch1Y; + this.reusableMessage.pinchScale = this.pinchScale; + this.reusableMessage.pinchRotation = this.pinchRotation; + this.reusableMessage.accelX = this.accelX; + this.reusableMessage.accelY = this.accelY; + this.reusableMessage.accelZ = this.accelZ; + this.reusableMessage.gyroX = this.gyroX; + this.reusableMessage.gyroY = this.gyroY; + this.reusableMessage.gyroZ = this.gyroZ; + this.reusableMessage.audioLevel = this.audioLevel; + this.reusableMessage.bassLevel = this.bassLevel; + this.reusableMessage.midLevel = this.midLevel; + this.reusableMessage.trebleLevel = this.trebleLevel; + this.reusableMessage.bpm = this.currentBPM; + } + 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); + this.updateReusableMessage(id, currentTime, this.canvas.width, this.canvas.height, this.canvas.width, this.canvas.height, 0); + this.worker.postMessage(this.reusableMessage); } private renderWithMultipleWorkers(id: string, currentTime: number): void { @@ -288,45 +291,18 @@ export class FakeShader { 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); + // Update reusable message with worker-specific values + this.updateReusableMessage( + `${id}_tile_${index}`, + currentTime, + width, + endY - startY, + width, + height, + startY + ); + + worker.postMessage(this.reusableMessage); }); } @@ -356,7 +332,7 @@ export class FakeShader { return; } - const renderId = `render_${Date.now()}_${Math.random()}`; + const renderId = `render_${++this.idCounter}`; // Add to pending renders queue this.pendingRenders.push(renderId); @@ -607,9 +583,10 @@ export class FakeShader { const ops = ['^', '&', '|', '+', '-', '*', '%', '**', '%']; const shifts = ['<<', '>>']; - const numbers = []; - for (let i = 0; i < Math.random(200); i++) { - numbers.push(Math.floor(Math.random(400))) + const numbers: number[] = []; + const numCount = Math.floor(Math.random() * 20) + 10; // Generate 10-30 numbers + for (let i = 0; i < numCount; i++) { + numbers.push(Math.floor(Math.random() * 400)) } const randomChoice = (arr: T[]): T => arr[Math.floor(Math.random() * arr.length)]; diff --git a/src/ShaderWorker.ts b/src/ShaderWorker.ts deleted file mode 100644 index 80c4c9e..0000000 --- a/src/ShaderWorker.ts +++ /dev/null @@ -1,1162 +0,0 @@ -// WebWorker for safe shader compilation and execution -interface WorkerMessage { - id: string; - type: 'compile' | 'render'; - code?: string; - width?: number; - height?: number; - time?: number; - renderMode?: string; - valueMode?: string; // 'integer' or 'float' - hueShift?: number; // Hue shift in degrees (0-360) - startY?: number; // Y offset for tile rendering - fullWidth?: number; // Full canvas width for center calculations - fullHeight?: number; // Full canvas height for center calculations - 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; -} - -import { LRUCache } from './utils/LRUCache'; -import { calculateColorDirect } from './utils/colorModes'; -import { PERFORMANCE } from './utils/constants'; - -type ShaderFunction = (...args: number[]) => number; - -class ShaderWorker { - private compiledFunction: ShaderFunction | null = null; - private lastCode: string = ''; - private imageDataCache: LRUCache = new LRUCache( - PERFORMANCE.IMAGE_DATA_CACHE_SIZE - ); - private compilationCache: LRUCache = new LRUCache( - PERFORMANCE.COMPILATION_CACHE_SIZE - ); - private feedbackBuffer: Float32Array | null = null; - private previousFeedbackBuffer: Float32Array | null = null; - private stateBuffer: Float32Array | null = null; - private echoBuffers: Float32Array[] = []; - private echoFrameCounter: number = 0; - private echoInterval: number = 30; // Store echo every 30 frames (~0.5s at 60fps) - private lastFrameTime: number = 0; - - constructor() { - self.onmessage = (e: MessageEvent) => { - this.handleMessage(e.data); - }; - - } - - private handleMessage(message: WorkerMessage): void { - try { - switch (message.type) { - case 'compile': - this.compileShader(message.id, message.code!); - break; - case 'render': - this.renderShader( - message.id, - message.width!, - message.height!, - message.time!, - message.renderMode || 'classic', - message.valueMode || 'integer', - message, - message.startY || 0 - ); - break; - } - } catch (error) { - this.postError( - message.id, - error instanceof Error ? error.message : 'Unknown error' - ); - } - } - - private compileShader(id: string, code: string): void { - const codeHash = this.hashCode(code); - - if (code === this.lastCode && this.compiledFunction) { - this.postMessage({ id, type: 'compiled', success: true }); - return; - } - - // Check compilation cache - const cachedFunction = this.compilationCache.get(codeHash); - if (cachedFunction) { - this.compiledFunction = cachedFunction; - this.lastCode = code; - this.postMessage({ id, type: 'compiled', success: true }); - return; - } - - try { - const safeCode = this.sanitizeCode(code); - - // Check if expression is static (contains no variables) - const isStatic = this.isStaticExpression(safeCode); - - if (isStatic) { - // Pre-compute static value - const staticValue = this.evaluateStaticExpression(safeCode); - this.compiledFunction = () => staticValue; - } else { - this.compiledFunction = new Function( - 'x', - 'y', - 't', - 'i', - 'r', - 'a', - 'u', - 'v', - 'c', - 'f', - 'd', - 'n', - 'b', - 'bn', - 'bs', - 'be', - 'bw', - 'w', - 'h', - 'p', - 'z', - 'j', - 'o', - 'g', - 'm', - 'l', - 'k', - 's', - 'e', - 'mouseX', - 'mouseY', - 'mousePressed', - 'mouseVX', - 'mouseVY', - 'mouseClickTime', - 'touchCount', - 'touch0X', - 'touch0Y', - 'touch1X', - 'touch1Y', - 'pinchScale', - 'pinchRotation', - 'accelX', - 'accelY', - 'accelZ', - 'gyroX', - 'gyroY', - 'gyroZ', - 'audioLevel', - 'bassLevel', - 'midLevel', - 'trebleLevel', - 'bpm', - '_t', - 'bx', - 'by', - 'sx', - 'sy', - 'qx', - 'qy', - ` - // Shader-specific helper functions - const clamp = (value, min, max) => Math.min(Math.max(value, min), max); - const lerp = (a, b, t) => a + (b - a) * t; - const smooth = (edge, x) => { const t = Math.min(Math.max((x - edge) / (1 - edge), 0), 1); return t * t * (3 - 2 * t); }; - const step = (edge, x) => x < edge ? 0 : 1; - const fract = (x) => x - Math.floor(x); - const mix = (a, b, t) => a + (b - a) * t; - - // Timeout protection - const startTime = performance.now(); - let iterations = 0; - - function checkTimeout() { - iterations++; - if (iterations % ${PERFORMANCE.TIMEOUT_CHECK_INTERVAL} === 0 && performance.now() - startTime > ${PERFORMANCE.MAX_SHADER_TIMEOUT_MS}) { - throw new Error('Shader timeout'); - } - } - - return (function() { - checkTimeout(); - return ${safeCode}; - })(); - ` - ) as ShaderFunction; - } - - // Cache the compiled function - if (this.compiledFunction) { - this.compilationCache.set(codeHash, this.compiledFunction); - } - - this.lastCode = code; - this.postMessage({ id, type: 'compiled', success: true }); - } catch (error) { - this.compiledFunction = null; - this.postError( - id, - error instanceof Error ? error.message : 'Compilation failed' - ); - } - } - - private isStaticExpression(code: string): boolean { - // Check if code contains any variables using regex for better accuracy - const variablePattern = /\b(x|y|t|i|r|a|u|v|c|f|d|n|b|bn|bs|be|bw|m|l|k|s|e|w|h|p|z|j|o|g|bpm|bx|by|sx|sy|qx|qy|mouse[XY]|mousePressed|mouseV[XY]|mouseClickTime|touchCount|touch[01][XY]|pinchScale|pinchRotation|accel[XYZ]|gyro[XYZ]|audioLevel|bassLevel|midLevel|trebleLevel)\b/; - - return !variablePattern.test(code); - } - - private evaluateStaticExpression(code: string): number { - try { - // Safely evaluate numeric expression - const result = new Function(`return ${code}`)(); - return isFinite(result) ? result : 0; - } catch (error) { - return 0; - } - } - - private hashCode(str: string): string { - let hash = 0; - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i); - hash = (hash << 5) - hash + char; - hash = hash & hash; // Convert to 32-bit integer - } - return hash.toString(36); - } - - private renderShader( - id: string, - width: number, - height: number, - time: number, - renderMode: string, - valueMode: string, - message: WorkerMessage, - startY: number = 0 - ): void { - if (!this.compiledFunction) { - this.postError(id, 'No compiled shader'); - return; - } - - const imageData = this.getOrCreateImageData(width, height); - const data = imageData.data; - const startTime = performance.now(); - const maxRenderTime = PERFORMANCE.MAX_RENDER_TIME_MS; - - // Initialize feedback buffers if needed - if (!this.feedbackBuffer || this.feedbackBuffer.length !== width * height) { - this.feedbackBuffer = new Float32Array(width * height); - this.previousFeedbackBuffer = new Float32Array(width * height); - this.stateBuffer = new Float32Array(width * height); - - // Initialize echo buffers (4 buffers for different time delays) - this.echoBuffers = []; - for (let i = 0; i < 4; i++) { - this.echoBuffers.push(new Float32Array(width * height)); - } - } - - // Update frame timing for frame rate independence - const deltaTime = time - this.lastFrameTime; - this.lastFrameTime = time; - - try { - // Use tiled rendering for better timeout handling - this.renderTiled( - data, - width, - height, - time, - renderMode, - valueMode, - message, - startTime, - maxRenderTime, - startY, - deltaTime - ); - - // Copy current feedback to previous for next frame momentum calculations - if (this.feedbackBuffer && this.previousFeedbackBuffer) { - this.previousFeedbackBuffer.set(this.feedbackBuffer); - } - - // Update echo buffers at regular intervals - this.echoFrameCounter++; - if (this.echoFrameCounter >= this.echoInterval && this.echoBuffers.length > 0) { - this.echoFrameCounter = 0; - - // Rotate echo buffers: shift all buffers forward and store current in first buffer - for (let i = this.echoBuffers.length - 1; i > 0; i--) { - if (this.echoBuffers[i] && this.echoBuffers[i - 1]) { - this.echoBuffers[i].set(this.echoBuffers[i - 1]); - } - } - - // Store current feedback in first echo buffer - if (this.feedbackBuffer && this.echoBuffers[0]) { - this.echoBuffers[0].set(this.feedbackBuffer); - } - } - - this.postMessage({ id, type: 'rendered', success: true, imageData }); - } catch (error) { - this.postError( - id, - error instanceof Error ? error.message : 'Render failed' - ); - } - } - - private renderTiled( - data: Uint8ClampedArray, - width: number, - height: number, - time: number, - renderMode: string, - valueMode: string, - message: WorkerMessage, - startTime: number, - maxRenderTime: number, - yOffset: number = 0, - deltaTime: number = 0.016 - ): void { - const tileSize = PERFORMANCE.DEFAULT_TILE_SIZE; - const tilesX = Math.ceil(width / tileSize); - const tilesY = Math.ceil(height / tileSize); - - for (let tileY = 0; tileY < tilesY; tileY++) { - for (let tileX = 0; tileX < tilesX; tileX++) { - // Check timeout before each tile - if (performance.now() - startTime > maxRenderTime) { - const startX = tileX * tileSize; - const startY = tileY * tileSize; - this.fillRemainingPixels(data, width, height, startY, startX); - return; - } - - const tileStartX = tileX * tileSize; - const tileStartY = tileY * tileSize; - const tileEndX = Math.min(tileStartX + tileSize, width); - const tileEndY = Math.min(tileStartY + tileSize, height); - - this.renderTile( - data, - width, - tileStartX, - tileStartY, - tileEndX, - tileEndY, - time, - renderMode, - valueMode, - message, - yOffset, - deltaTime - ); - } - } - } - - private renderTile( - data: Uint8ClampedArray, - width: number, - startX: number, - startY: number, - endX: number, - endY: number, - time: number, - renderMode: string, - valueMode: string, - message: WorkerMessage, - yOffset: number = 0, - deltaTime: number = 0.016 - ): void { - // Get full canvas dimensions for special modes (use provided full dimensions or fall back) - const fullWidth = message.fullWidth || width; - const fullHeight = message.fullHeight || message.height! + yOffset; - for (let y = startY; y < endY; y++) { - for (let x = startX; x < endX; x++) { - const i = (y * width + x) * 4; - const pixelIndex = y * width + x; - - // Adjust y coordinate to account for tile offset - const actualY = y + yOffset; - - try { - // Calculate additional coordinate variables - const u = x / fullWidth; - const v = actualY / fullHeight; - const centerX = fullWidth / 2; - const centerY = fullHeight / 2; - const radius = Math.sqrt( - (x - centerX) ** 2 + (actualY - centerY) ** 2 - ); - const angle = Math.atan2(actualY - centerY, x - centerX); - const maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2); - const normalizedDistance = radius / maxDistance; - const frameCount = Math.floor(time * 60); - const manhattanDistance = - Math.abs(x - centerX) + Math.abs(actualY - centerY); - const noise = (Math.sin(x * 0.1) * Math.cos(actualY * 0.1) + 1) * 0.5; - // Simple, efficient feedback system - const currentFeedback = this.feedbackBuffer ? this.feedbackBuffer[pixelIndex] || 0 : 0; - const feedbackValue = currentFeedback; - - // Simple neighbor feedback with bounds checking - let fbn = 0, fbs = 0, fbe = 0, fbw = 0; - if (this.feedbackBuffer) { - // North neighbor (bounds safe) - if (y > 0) fbn = this.feedbackBuffer[(y - 1) * width + x] || 0; - // South neighbor (bounds safe) - if (y < fullHeight - 1) fbs = this.feedbackBuffer[(y + 1) * width + x] || 0; - // East neighbor (bounds safe) - if (x < width - 1) fbe = this.feedbackBuffer[y * width + (x + 1)] || 0; - // West neighbor (bounds safe) - if (x > 0) fbw = this.feedbackBuffer[y * width + (x - 1)] || 0; - } - - // Calculate feedback-based operators - // m - Momentum/Velocity (change from previous frame) - const previousValue = this.previousFeedbackBuffer ? this.previousFeedbackBuffer[pixelIndex] || 0 : 0; - const momentum = (feedbackValue - previousValue) * 0.5; // Scale for stability - - // l - Laplacian/Diffusion (spatial derivative) - const laplacian = (fbn + fbs + fbe + fbw - feedbackValue * 4) * 0.25; - - // k - Curvature/Contrast (gradient magnitude) - const gradientX = (fbe - fbw) * 0.5; - const gradientY = (fbs - fbn) * 0.5; - const curvature = Math.sqrt(gradientX * gradientX + gradientY * gradientY); - - // s - State/Memory (persistent accumulator) - let currentState = this.stateBuffer ? this.stateBuffer[pixelIndex] || 0 : 0; - // State accumulates when feedback is high, decays when low - if (feedbackValue > 128) { - currentState = Math.min(255, currentState + deltaTime * 200); // Accumulate - } else { - currentState = Math.max(0, currentState - deltaTime * 100); // Decay - } - const stateValue = currentState; - - // e - Echo/History (temporal snapshots) - let echoValue = 0; - if (this.echoBuffers.length > 0) { - // Cycle through different echo delays based on time - const echoIndex = Math.floor(time * 2) % this.echoBuffers.length; // Change every 0.5 seconds - const echoBuffer = this.echoBuffers[echoIndex]; - echoValue = echoBuffer ? echoBuffer[pixelIndex] || 0 : 0; - } - - // Calculate other variables - const canvasWidth = fullWidth; - const canvasHeight = fullHeight; - const phase = (time * Math.PI * 2) % (Math.PI * 2); // 0 to 2π cycling - const pseudoZ = Math.sin(radius * 0.01 + time) * 50; // depth based on radius and time - const jitter = ((x * 73856093 + actualY * 19349663) % 256) / 255; // deterministic per-pixel random - const oscillation = Math.sin(time * 2 * Math.PI + radius * 0.1); // wave oscillation - const goldenRatio = 1.618033988749; // golden ratio constant - - // Calculate new spatial variables - const bx = x >> 4; // Block x coordinate (16-pixel blocks) - const by = actualY >> 4; // Block y coordinate (16-pixel blocks) - const sx = x - (fullWidth >> 1); // Signed x coordinate (centered at origin) - const sy = actualY - (fullHeight >> 1); // Signed y coordinate (centered at origin) - const qx = x >> 3; // Quarter block x coordinate (8-pixel blocks) - const qy = actualY >> 3; // Quarter block y coordinate (8-pixel blocks) - - const value = this.compiledFunction!( - x, - actualY, - time, - pixelIndex, - radius, - angle, - u, - v, - normalizedDistance, - frameCount, - manhattanDistance, - noise, - feedbackValue, - canvasWidth, - canvasHeight, - phase, - pseudoZ, - jitter, - oscillation, - goldenRatio, - momentum, - laplacian, - curvature, - stateValue, - echoValue, - fbn, - fbs, - fbe, - fbw, - message.mouseX || 0, - message.mouseY || 0, - message.mousePressed ? 1 : 0, - message.mouseVX || 0, - message.mouseVY || 0, - message.mouseClickTime || 0, - message.touchCount || 0, - message.touch0X || 0, - message.touch0Y || 0, - message.touch1X || 0, - message.touch1Y || 0, - message.pinchScale || 1, - message.pinchRotation || 0, - message.accelX || 0, - message.accelY || 0, - message.accelZ || 0, - message.gyroX || 0, - message.gyroY || 0, - message.gyroZ || 0, - message.audioLevel || 0, - message.bassLevel || 0, - message.midLevel || 0, - message.trebleLevel || 0, - message.bpm || 120, - (mod: number) => time % mod, - bx, - by, - sx, - sy, - qx, - qy - ); - const safeValue = isFinite(value) ? value : 0; - const [r, g, b] = this.calculateColor( - safeValue, - renderMode, - valueMode, - message.hueShift || 0, - x, - actualY, - fullWidth, - fullHeight - ); - - data[i] = r; - data[i + 1] = g; - data[i + 2] = b; - data[i + 3] = 255; - - // Store feedback as luminance of displayed color for consistency - if (this.feedbackBuffer) { - // Use the actual displayed luminance as feedback (0-255 range) - const luminance = (r * 0.299 + g * 0.587 + b * 0.114); - - // Frame rate independent decay - const decayFactor = Math.pow(0.95, deltaTime * 60); // 5% decay at 60fps - - // Simple mixing to prevent oscillation - const previousValue = this.feedbackBuffer[pixelIndex] || 0; - const mixRatio = Math.min(deltaTime * 10, 0.3); // Max 30% new value per frame - - let newFeedback = luminance * mixRatio + previousValue * (1 - mixRatio); - newFeedback *= decayFactor; - - // Clamp and store - this.feedbackBuffer[pixelIndex] = Math.max(0, Math.min(255, newFeedback)); - } - - // Update state buffer - if (this.stateBuffer) { - this.stateBuffer[pixelIndex] = stateValue; - } - } catch (error) { - data[i] = 0; - data[i + 1] = 0; - data[i + 2] = 0; - data[i + 3] = 255; - } - } - } - } - - private fillRemainingPixels( - data: Uint8ClampedArray, - width: number, - height: number, - startY: number, - startX: number - ): void { - for (let remainingY = startY; remainingY < height; remainingY++) { - const xStart = remainingY === startY ? startX : 0; - for (let remainingX = xStart; remainingX < width; remainingX++) { - const i = (remainingY * width + remainingX) * 4; - data[i] = 0; - data[i + 1] = 0; - data[i + 2] = 0; - data[i + 3] = 255; - } - } - } - - private getOrCreateImageData(width: number, height: number): ImageData { - const key = `${width}x${height}`; - let imageData = this.imageDataCache.get(key); - - if (!imageData) { - imageData = new ImageData(width, height); - this.imageDataCache.set(key, imageData); - } - - return imageData; - } - - private calculateColor( - value: number, - renderMode: string, - valueMode: string = 'integer', - hueShift: number = 0, - x: number = 0, - y: number = 0, - width: number = 1, - height: number = 1 - ): [number, number, number] { - let processedValue: number; - - switch (valueMode) { - case 'float': - // Float mode: treat value as 0.0-1.0, invert it (like original bitfield shaders) - processedValue = Math.max(0, Math.min(1, Math.abs(value))); // Clamp to 0-1 - processedValue = 1 - processedValue; // Invert (like original) - processedValue = Math.floor(processedValue * 255); // Convert to 0-255 - break; - - case 'polar': { - // Polar mode: angular patterns with value-based rotation and radius influence - const centerX = width / 2; - const centerY = height / 2; - const dx = x - centerX; - const dy = y - centerY; - const radius = Math.sqrt(dx * dx + dy * dy); - const angle = Math.atan2(dy, dx); // -π to π - const normalizedAngle = (angle + Math.PI) / (2 * Math.PI); // 0 to 1 - - // Combine angle with radius and value for complex patterns - const radiusNorm = radius / Math.max(centerX, centerY); - const spiralEffect = - (normalizedAngle + radiusNorm * 0.5 + Math.abs(value) * 0.02) % 1; - const polarValue = Math.sin(spiralEffect * Math.PI * 8) * 0.5 + 0.5; // Create wave pattern - - processedValue = Math.floor(polarValue * 255); - break; - } - - case 'distance': { - // Distance mode: concentric patterns with value-based frequency and phase - const distCenterX = width / 2; - const distCenterY = height / 2; - const distance = Math.sqrt( - (x - distCenterX) ** 2 + (y - distCenterY) ** 2 - ); - const maxDistance = Math.sqrt(distCenterX ** 2 + distCenterY ** 2); - const normalizedDistance = distance / maxDistance; // 0 to 1 - - // Create concentric waves with value-controlled frequency and phase - const frequency = 8 + Math.abs(value) * 0.1; // Variable frequency - const phase = Math.abs(value) * 0.05; // Value affects phase shift - const concentricWave = - Math.sin(normalizedDistance * Math.PI * frequency + phase) * 0.5 + - 0.5; - - // Add some radial falloff for more interesting patterns - const falloff = 1 - Math.pow(normalizedDistance, 0.8); - const distanceValue = concentricWave * falloff; - - processedValue = Math.floor(distanceValue * 255); - break; - } - - case 'wave': { - // Wave mode: interference patterns from multiple wave sources - const baseFreq = 0.08; - const valueScale = Math.abs(value) * 0.001 + 1; // Scale frequency by value - let waveSum = 0; - - // Create wave sources at strategic positions for interesting interference - const sources = [ - { x: width * 0.3, y: height * 0.3 }, - { x: width * 0.7, y: height * 0.3 }, - { x: width * 0.5, y: height * 0.7 }, - { x: width * 0.2, y: height * 0.8 }, - ]; - - for (const source of sources) { - const dist = Math.sqrt((x - source.x) ** 2 + (y - source.y) ** 2); - const wave = Math.sin( - dist * baseFreq * valueScale + Math.abs(value) * 0.02 - ); - const amplitude = 1 / (1 + dist * 0.002); // Distance-based amplitude falloff - waveSum += wave * amplitude; - } - - // Normalize and enhance contrast - const waveValue = Math.tanh(waveSum) * 0.5 + 0.5; // tanh for better contrast - processedValue = Math.floor(waveValue * 255); - break; - } - - case 'fractal': { - // Fractal mode: recursive pattern generation - const scale = 0.01; - let fractalValue = 0; - let amplitude = 1; - const octaves = 4; - - for (let i = 0; i < octaves; i++) { - const frequency = Math.pow(2, i) * scale; - const noise = - Math.sin((x + Math.abs(value) * 0.1) * frequency) * - Math.cos((y + Math.abs(value) * 0.1) * frequency); - fractalValue += noise * amplitude; - amplitude *= 0.5; - } - - processedValue = Math.floor((fractalValue + 1) * 0.5 * 255); - break; - } - - case 'cellular': { - // Cellular automata-inspired patterns - const cellSize = 16; - const cellX = Math.floor(x / centerX); - const cellY = Math.floor(y / centerY); - const cellHash = - (cellX * 73856093) ^ (cellY * 19349663) ^ Math.floor(Math.abs(value)); - - // Generate cellular pattern based on neighbors - let neighbors = 0; - for (let dx = -1; dx <= 1; dx++) { - for (let dy = -1; dy <= 1; dy++) { - if (dx === 0 && dy === 0) continue; - const neighborHash = - ((cellX + dx) * 73856093) ^ - ((cellY + dy) * 19349663) ^ - Math.floor(Math.abs(value)); - if (neighborHash % 256 > 128) neighbors++; - } - } - - const cellState = cellHash % 256 > 128 ? 1 : 0; - const evolution = neighbors >= 3 && neighbors <= 5 ? 1 : cellState; - processedValue = evolution * 255; - break; - } - - case 'noise': { - // Perlin-like noise pattern - const noiseScale = 0.02; - const nx = x * noiseScale + Math.abs(value) * 0.001; - const ny = y * noiseScale + Math.abs(value) * 0.001; - - // Simple noise approximation using sine waves - const noise1 = Math.sin(nx * 6.28) * Math.cos(ny * 6.28); - const noise2 = Math.sin(nx * 12.56) * Math.cos(ny * 12.56) * 0.5; - const noise3 = Math.sin(nx * 25.12) * Math.cos(ny * 25.12) * 0.25; - - const combinedNoise = (noise1 + noise2 + noise3) / 1.75; - processedValue = Math.floor((combinedNoise + 1) * 0.5 * 255); - break; - } - - case 'warp': { - // Warp mode: space deformation based on value - const centerX = width / 2; - const centerY = height / 2; - - // Create warping field based on value - const warpStrength = Math.abs(value) * 0.001; - const warpFreq = 0.02; - - // Calculate warped coordinates - const warpX = - x + - Math.sin(y * warpFreq + Math.abs(value) * 0.01) * warpStrength * 100; - const warpY = - y + - Math.cos(x * warpFreq + Math.abs(value) * 0.01) * warpStrength * 100; - - // Create barrel/lens distortion - const dx = warpX - centerX; - const dy = warpY - centerY; - const dist = Math.sqrt(dx * dx + dy * dy); - const maxDist = Math.sqrt(centerX * centerX + centerY * centerY); - const normDist = dist / maxDist; - - // Apply non-linear space deformation - const deform = - 1 + Math.sin(normDist * Math.PI + Math.abs(value) * 0.05) * 0.3; - const deformedX = centerX + dx * deform; - const deformedY = centerY + dy * deform; - - // Sample from deformed space - const finalValue = (deformedX + deformedY + Math.abs(value)) % 256; - processedValue = Math.floor(Math.abs(finalValue)); - break; - } - - case 'flow': { - // Flow field mode: large-scale fluid dynamics simulation - const centerX = width / 2; - const centerY = height / 2; - - // Create multiple flow sources influenced by value - const flowSources = [ - { - x: centerX + Math.sin(Math.abs(value) * 0.01) * 200, - y: centerY + Math.cos(Math.abs(value) * 0.01) * 200, - strength: 1 + Math.abs(value) * 0.01, - }, - { - x: centerX + Math.cos(Math.abs(value) * 0.015) * 150, - y: centerY + Math.sin(Math.abs(value) * 0.015) * 150, - strength: -0.8 + Math.sin(Math.abs(value) * 0.02) * 0.5, - }, - { - x: centerX + Math.sin(Math.abs(value) * 0.008) * 300, - y: centerY + Math.cos(Math.abs(value) * 0.012) * 250, - strength: 0.6 + Math.cos(Math.abs(value) * 0.018) * 0.4, - }, - ]; - - // Calculate flow field at this point - let flowX = 0; - let flowY = 0; - - for (const source of flowSources) { - const dx = x - source.x; - const dy = y - source.y; - const distance = Math.sqrt(dx * dx + dy * dy); - const normalizedDist = Math.max(distance, 1); // Avoid division by zero - - // Create flow vectors (potential field + curl) - const flowStrength = source.strength / (normalizedDist * 0.01); - - // Radial component (attraction/repulsion) - flowX += (dx / normalizedDist) * flowStrength; - flowY += (dy / normalizedDist) * flowStrength; - - // Curl component (rotation) - creates vortices - const curlStrength = source.strength * 0.5; - flowX += ((-dy / normalizedDist) * curlStrength) / normalizedDist; - flowY += ((dx / normalizedDist) * curlStrength) / normalizedDist; - } - - // Add global flow influenced by value - const globalFlowAngle = Math.abs(value) * 0.02; - flowX += Math.cos(globalFlowAngle) * (Math.abs(value) * 0.1); - flowY += Math.sin(globalFlowAngle) * (Math.abs(value) * 0.1); - - // Add turbulence - const turbScale = 0.05; - const turbulence = - Math.sin(x * turbScale + Math.abs(value) * 0.01) * - Math.cos(y * turbScale + Math.abs(value) * 0.015) * - (Math.abs(value) * 0.02); - - flowX += turbulence; - flowY += turbulence * 0.7; - - // Simulate particle flowing through the field - let particleX = x; - let particleY = y; - - // Multiple flow steps for more interesting trajectories - for (let step = 0; step < 5; step++) { - // Sample flow field at current particle position - let localFlowX = 0; - let localFlowY = 0; - - for (const source of flowSources) { - const dx = particleX - source.x; - const dy = particleY - source.y; - const distance = Math.sqrt(dx * dx + dy * dy); - const normalizedDist = Math.max(distance, 1); - - const flowStrength = source.strength / (normalizedDist * 0.01); - localFlowX += (dx / normalizedDist) * flowStrength; - localFlowY += (dy / normalizedDist) * flowStrength; - - // Curl - const curlStrength = source.strength * 0.5; - localFlowX += - ((-dy / normalizedDist) * curlStrength) / normalizedDist; - localFlowY += - ((dx / normalizedDist) * curlStrength) / normalizedDist; - } - - // Move particle - const stepSize = 0.5; - particleX += localFlowX * stepSize; - particleY += localFlowY * stepSize; - } - - // Calculate final value based on particle's final position and flow magnitude - const flowMagnitude = Math.sqrt(flowX * flowX + flowY * flowY); - const particleDistance = Math.sqrt( - (particleX - x) * (particleX - x) + (particleY - y) * (particleY - y) - ); - - // Combine flow magnitude with particle trajectory - const flowValue = (flowMagnitude * 10 + particleDistance * 2) % 256; - const enhanced = - Math.sin(flowValue * 0.05 + Math.abs(value) * 0.01) * 0.5 + 0.5; - - processedValue = Math.floor(enhanced * 255); - break; - } - - case 'spiral': { - // Creates logarithmic spirals based on the shader value - const centerX = width / 2; - const centerY = height / 2; - const dx = x - centerX; - const dy = y - centerY; - const radius = Math.sqrt(dx * dx + dy * dy); - const spiralTightness = 1 + Math.abs(value) * 0.01; - const spiralValue = Math.atan2(dy, dx) + Math.log(Math.max(radius, 1)) * spiralTightness; - processedValue = Math.floor((Math.sin(spiralValue) * 0.5 + 0.5) * 255); - break; - } - - case 'turbulence': { - // Multi-octave turbulence with value-controlled chaos - let turbulence = 0; - const chaos = Math.abs(value) * 0.001; - for (let i = 0; i < 4; i++) { - const freq = Math.pow(2, i) * (0.01 + chaos); - turbulence += Math.abs(Math.sin(x * freq) * Math.cos(y * freq)) / Math.pow(2, i); - } - processedValue = Math.floor(Math.min(turbulence, 1) * 255); - break; - } - - - case 'crystal': { - // Crystalline lattice patterns - const latticeSize = 32 + Math.abs(value) * 0.1; - const gridX = Math.floor(x / latticeSize); - const gridY = Math.floor(y / latticeSize); - const crystal = Math.sin(gridX + gridY + Math.abs(value) * 0.01) * - Math.cos(gridX * gridY + Math.abs(value) * 0.005); - processedValue = Math.floor((crystal * 0.5 + 0.5) * 255); - break; - } - - case 'marble': { - // Marble-like veining patterns - const noiseFreq = 0.005 + Math.abs(value) * 0.00001; - const turbulence = Math.sin(x * noiseFreq) * Math.cos(y * noiseFreq) + - Math.sin(x * noiseFreq * 2) * Math.cos(y * noiseFreq * 2) * 0.5; - const marble = Math.sin((x + turbulence * 50) * 0.02 + Math.abs(value) * 0.001); - processedValue = Math.floor((marble * 0.5 + 0.5) * 255); - break; - } - - - case 'quantum': { - // Quantum uncertainty visualization - const centerX = width / 2; - const centerY = height / 2; - const uncertainty = Math.abs(value) * 0.001; - const probability = Math.exp(-( - (x - centerX) ** 2 + (y - centerY) ** 2 - ) / (2 * (100 + uncertainty * 1000) ** 2)); - const quantum = probability * (1 + Math.sin(x * y * uncertainty) * 0.5); - processedValue = Math.floor(Math.min(quantum, 1) * 255); - break; - } - - case 'logarithmic': { - // Simple mathematical transform: logarithmic scaling - const logValue = Math.log(1 + Math.abs(value)); - processedValue = Math.floor((logValue / Math.log(256)) * 255); - break; - } - - case 'mirror': { - // Mirror/kaleidoscope effect - creates symmetrical patterns - const centerX = width / 2; - const centerY = height / 2; - const dx = Math.abs(x - centerX); - const dy = Math.abs(y - centerY); - const mirrorX = centerX + (dx % centerX); - const mirrorY = centerY + (dy % centerY); - const mirrorDistance = Math.sqrt(mirrorX * mirrorX + mirrorY * mirrorY); - const mirrorValue = (Math.abs(value) + mirrorDistance) % 256; - processedValue = mirrorValue; - break; - } - - case 'rings': { - // Concentric rings with value-controlled spacing and interference - const centerX = width / 2; - const centerY = height / 2; - const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2); - const ringSpacing = 20 + Math.abs(value) * 0.1; - const rings = Math.sin((distance / ringSpacing) * Math.PI * 2); - const interference = Math.sin((distance + Math.abs(value)) * 0.05); - processedValue = Math.floor(((rings * interference) * 0.5 + 0.5) * 255); - break; - } - - case 'mesh': { - // Grid/mesh patterns with value-controlled density and rotation - const angle = Math.abs(value) * 0.001; - const rotX = x * Math.cos(angle) - y * Math.sin(angle); - const rotY = x * Math.sin(angle) + y * Math.cos(angle); - const gridSize = 16 + Math.abs(value) * 0.05; - const gridX = Math.sin((rotX / gridSize) * Math.PI * 2); - const gridY = Math.sin((rotY / gridSize) * Math.PI * 2); - const mesh = Math.max(Math.abs(gridX), Math.abs(gridY)); - processedValue = Math.floor(mesh * 255); - break; - } - - case 'glitch': { - // Digital glitch/corruption effects - const seed = Math.floor(x + y * width + Math.abs(value)); - const random = ((seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff; - const glitchThreshold = 0.95 - Math.abs(value) * 0.0001; - let glitchValue = Math.abs(value) % 256; - - if (random > glitchThreshold) { - // Digital corruption: bit shifts, XOR, scrambling - glitchValue = (glitchValue << 1) ^ (glitchValue >> 3) ^ ((x + y) & 0xFF); - } - - processedValue = glitchValue % 256; - break; - } - - default: - // Integer mode: treat value as 0-255 (original behavior) - processedValue = Math.abs(value) % 256; - break; - } - - // Use direct calculation to support hue shift - return calculateColorDirect(processedValue, renderMode, hueShift); - } - - private sanitizeCode(code: string): string { - // Auto-prefix Math functions - const mathFunctions = [ - 'abs', - 'acos', - 'asin', - 'atan', - 'atan2', - 'ceil', - 'cos', - 'exp', - 'floor', - 'log', - 'max', - 'min', - 'pow', - 'random', - 'round', - 'sin', - 'sqrt', - 'tan', - 'trunc', - 'sign', - 'cbrt', - 'hypot', - 'imul', - 'fround', - 'clz32', - 'acosh', - 'asinh', - 'atanh', - 'cosh', - 'sinh', - 'tanh', - 'expm1', - 'log1p', - 'log10', - 'log2', - ]; - - let processedCode = code; - - // Replace standalone math functions with Math.function - mathFunctions.forEach((func) => { - const regex = new RegExp(`\\b${func}\\(`, 'g'); - processedCode = processedCode.replace(regex, `Math.${func}(`); - }); - - // Add Math constants - processedCode = processedCode.replace(/\bPI\b/g, 'Math.PI'); - processedCode = processedCode.replace(/\bE\b/g, 'Math.E'); - processedCode = processedCode.replace(/\bLN2\b/g, 'Math.LN2'); - processedCode = processedCode.replace(/\bLN10\b/g, 'Math.LN10'); - processedCode = processedCode.replace(/\bLOG2E\b/g, 'Math.LOG2E'); - processedCode = processedCode.replace(/\bLOG10E\b/g, 'Math.LOG10E'); - processedCode = processedCode.replace(/\bSQRT1_2\b/g, 'Math.SQRT1_2'); - processedCode = processedCode.replace(/\bSQRT2\b/g, 'Math.SQRT2'); - - // Add custom time function t() with modulo wrapping - processedCode = processedCode.replace(/\bt\s*\(/g, '_t('); - - return processedCode; - } - - private postMessage(response: WorkerResponse): void { - self.postMessage(response); - } - - private postError(id: string, error: string): void { - this.postMessage({ id, type: 'error', success: false, error }); - } -} - -// Initialize worker -new ShaderWorker(); diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx index 1e2c903..f967a1f 100644 --- a/src/components/TopBar.tsx +++ b/src/components/TopBar.tsx @@ -116,162 +116,132 @@ export function TopBar() { }; return ( -
+
Bitfielder
- + + + + + + + - + + + + - + {VALUE_MODES.map((mode) => ( + + ))} + - + + + + + + + + + + + + + + + + + + + + + + - + + updateAppSettings({ hueShift: parseInt(e.target.value) }) + } + style={{ width: '80px', verticalAlign: 'middle', marginRight: '10px' }} + /> - + + updateAppSettings({ uiOpacity: parseInt(e.target.value) / 100 }) + } + style={{ width: '80px', verticalAlign: 'middle', marginRight: '10px' }} + />