From 2cee4084c00b33d31d2174eff67ed6447dc3e115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Tue, 15 Jul 2025 00:52:00 +0200 Subject: [PATCH] optimisations --- src/FakeShader.ts | 23 +- src/RefactoredShader.ts | 281 +++++++ src/Storage.ts | 5 +- src/shader/core/InputManager.ts | 188 +++++ src/shader/core/RenderController.ts | 114 +++ src/shader/core/WorkerPool.ts | 188 +++++ src/shader/rendering/FeedbackSystem.ts | 4 +- src/shader/rendering/PixelRenderer.ts | 640 +-------------- src/shader/rendering/ValueModeProcessor.ts | 872 +++++++++++++++++++++ src/utils/colorModes.ts | 347 +++++--- src/utils/constants.ts | 127 ++- 11 files changed, 2067 insertions(+), 722 deletions(-) create mode 100644 src/RefactoredShader.ts create mode 100644 src/shader/core/InputManager.ts create mode 100644 src/shader/core/RenderController.ts create mode 100644 src/shader/core/WorkerPool.ts create mode 100644 src/shader/rendering/ValueModeProcessor.ts diff --git a/src/FakeShader.ts b/src/FakeShader.ts index d1555c9..5edbafa 100644 --- a/src/FakeShader.ts +++ b/src/FakeShader.ts @@ -1,4 +1,5 @@ import { WorkerMessage, WorkerResponse } from './shader/types'; +import { TIMING, WORKER, DEFAULTS } from './utils/constants'; export class FakeShader { private canvas: HTMLCanvasElement; @@ -15,8 +16,8 @@ export class FakeShader { private renderMode: string = 'classic'; private valueMode: string = 'integer'; private hueShift: number = 0; - private timeSpeed: number = 1.0; - private currentBPM: number = 120; + private timeSpeed: number = DEFAULTS.TIME_SPEED; + private currentBPM: number = TIMING.DEFAULT_BPM; // ID generation optimization private idCounter: number = 0; @@ -56,7 +57,7 @@ export class FakeShader { bassLevel: 0, midLevel: 0, trebleLevel: 0, - bpm: 120, + bpm: TIMING.DEFAULT_BPM, startY: 0, }; @@ -76,7 +77,7 @@ export class FakeShader { private touch0Y: number = 0; private touch1X: number = 0; private touch1Y: number = 0; - private pinchScale: number = 1; + private pinchScale: number = WORKER.DEFAULT_PINCH_SCALE; private pinchRotation: number = 0; private accelX: number = 0; private accelY: number = 0; @@ -90,8 +91,8 @@ export class FakeShader { private trebleLevel: number = 0; // Frame rate limiting - private targetFPS: number = 30; - private frameInterval: number = 1000 / this.targetFPS; + private targetFPS: number = TIMING.DEFAULT_FPS; + private frameInterval: number = TIMING.MILLISECONDS_PER_SECOND / this.targetFPS; private lastFrameTime: number = 0; constructor(canvas: HTMLCanvasElement, code: string = 'x^y') { @@ -103,10 +104,10 @@ export class FakeShader { this.initializeOffscreenCanvas(); // Always use maximum available cores - this.workerCount = navigator.hardwareConcurrency || 4; + this.workerCount = navigator.hardwareConcurrency || WORKER.FALLBACK_CORE_COUNT; // 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); + this.workerCount = Math.min(this.workerCount, WORKER.MAX_WORKERS); console.log( `Auto-detected ${this.workerCount} CPU cores, using all for maximum performance` ); @@ -221,7 +222,7 @@ export class FakeShader { this.isRendering = true; // this._currentRenderID = id; // Removed unused property - const currentTime = (Date.now() - this.startTime) / 1000 * this.timeSpeed; + const currentTime = (Date.now() - this.startTime) / TIMING.MILLISECONDS_PER_SECOND * this.timeSpeed; // Always use multiple workers if available if (this.workerCount > 1) { @@ -366,8 +367,8 @@ export class FakeShader { } setTargetFPS(fps: number): void { - this.targetFPS = Math.max(1, Math.min(120, fps)); // Clamp between 1-120 FPS - this.frameInterval = 1000 / this.targetFPS; + this.targetFPS = Math.max(TIMING.MIN_FPS, Math.min(TIMING.MAX_FPS, fps)); // Clamp between 1-120 FPS + this.frameInterval = TIMING.MILLISECONDS_PER_SECOND / this.targetFPS; } setRenderMode(mode: string): void { diff --git a/src/RefactoredShader.ts b/src/RefactoredShader.ts new file mode 100644 index 0000000..52e09c2 --- /dev/null +++ b/src/RefactoredShader.ts @@ -0,0 +1,281 @@ +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 { + 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 { + 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', + ]; + } +} \ No newline at end of file diff --git a/src/Storage.ts b/src/Storage.ts index 9a300c8..e489a06 100644 --- a/src/Storage.ts +++ b/src/Storage.ts @@ -3,6 +3,7 @@ import { STORAGE_KEYS, PERFORMANCE, DEFAULTS, + FORMAT, ValueMode, } from './utils/constants'; @@ -146,12 +147,12 @@ export class Storage { } private static generateId(): string { - return Date.now().toString(36) + Math.random().toString(36).substr(2); + return Date.now().toString(FORMAT.ID_RADIX) + Math.random().toString(FORMAT.ID_RADIX).substr(FORMAT.ID_SUBSTRING_START); } static exportShaders(): string { const shaders = this.getShaders(); - return JSON.stringify(shaders, null, 2); + return JSON.stringify(shaders, null, FORMAT.JSON_INDENT); } static importShaders(jsonData: string): boolean { diff --git a/src/shader/core/InputManager.ts b/src/shader/core/InputManager.ts new file mode 100644 index 0000000..7c54457 --- /dev/null +++ b/src/shader/core/InputManager.ts @@ -0,0 +1,188 @@ +/** + * Manages all input tracking (mouse, touch, accelerometer, audio) + * Extracted from FakeShader to follow Single Responsibility Principle + */ +export class InputManager { + // Mouse state + private mouseX: number = 0; + private mouseY: number = 0; + private mousePressed: boolean = false; + private mouseVX: number = 0; + private mouseVY: number = 0; + private mouseClickTime: number = 0; + + // Touch state + 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; + + // Accelerometer state + private accelX: number = 0; + private accelY: number = 0; + private accelZ: number = 0; + + // Gyroscope state + private gyroX: number = 0; + private gyroY: number = 0; + private gyroZ: number = 0; + + // Audio state + private audioLevel: number = 0; + private bassLevel: number = 0; + private midLevel: number = 0; + private trebleLevel: number = 0; + private currentBPM: number = 120; + + setMousePosition(x: number, y: number): void { + this.mouseX = x; + this.mouseY = y; + } + + setMousePressed(pressed: boolean): void { + this.mousePressed = pressed; + if (pressed) { + this.mouseClickTime = Date.now(); + } + } + + setMouseVelocity(vx: number, vy: number): void { + this.mouseVX = vx; + this.mouseVY = vy; + } + + setTouchData( + 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; + } + + setAccelerometer(x: number, y: number, z: number): void { + this.accelX = x; + this.accelY = y; + this.accelZ = z; + } + + setGyroscope(x: number, y: number, z: number): void { + this.gyroX = x; + this.gyroY = y; + this.gyroZ = z; + } + + setAudioLevels( + level: number, + bass: number, + mid: number, + treble: number, + bpm: number + ): void { + this.audioLevel = level; + this.bassLevel = bass; + this.midLevel = mid; + this.trebleLevel = treble; + this.currentBPM = bpm; + } + + // Getters for all input values + getMouseData() { + return { + x: this.mouseX, + y: this.mouseY, + pressed: this.mousePressed, + vx: this.mouseVX, + vy: this.mouseVY, + clickTime: this.mouseClickTime, + }; + } + + getTouchData() { + return { + count: this.touchCount, + x0: this.touch0X, + y0: this.touch0Y, + x1: this.touch1X, + y1: this.touch1Y, + scale: this.pinchScale, + rotation: this.pinchRotation, + }; + } + + getAccelerometerData() { + return { + x: this.accelX, + y: this.accelY, + z: this.accelZ, + }; + } + + getGyroscopeData() { + return { + x: this.gyroX, + y: this.gyroY, + z: this.gyroZ, + }; + } + + getAudioData() { + return { + level: this.audioLevel, + bass: this.bassLevel, + mid: this.midLevel, + treble: this.trebleLevel, + bpm: this.currentBPM, + }; + } + + // Helper method to populate worker message with all input data + populateWorkerMessage(message: any): void { + const mouse = this.getMouseData(); + const touch = this.getTouchData(); + const accel = this.getAccelerometerData(); + const gyro = this.getGyroscopeData(); + const audio = this.getAudioData(); + + message.mouseX = mouse.x; + message.mouseY = mouse.y; + message.mousePressed = mouse.pressed; + message.mouseVX = mouse.vx; + message.mouseVY = mouse.vy; + message.mouseClickTime = mouse.clickTime; + + message.touchCount = touch.count; + message.touch0X = touch.x0; + message.touch0Y = touch.y0; + message.touch1X = touch.x1; + message.touch1Y = touch.y1; + message.pinchScale = touch.scale; + message.pinchRotation = touch.rotation; + + message.accelX = accel.x; + message.accelY = accel.y; + message.accelZ = accel.z; + + message.gyroX = gyro.x; + message.gyroY = gyro.y; + message.gyroZ = gyro.z; + + message.audioLevel = audio.level; + message.bassLevel = audio.bass; + message.midLevel = audio.mid; + message.trebleLevel = audio.treble; + message.bpm = audio.bpm; + } +} \ No newline at end of file diff --git a/src/shader/core/RenderController.ts b/src/shader/core/RenderController.ts new file mode 100644 index 0000000..33f5997 --- /dev/null +++ b/src/shader/core/RenderController.ts @@ -0,0 +1,114 @@ +import { TIMING } from '../../utils/constants'; + +/** + * Manages animation timing and frame rate control + * Extracted from FakeShader for better separation of concerns + */ +export class RenderController { + private animationId: number | null = null; + private startTime: number = Date.now(); + private targetFPS: number = TIMING.DEFAULT_FPS; + private frameInterval: number = TIMING.MILLISECONDS_PER_SECOND / this.targetFPS; + private lastFrameTime: number = 0; + private timeSpeed: number = 1.0; + private isRendering: boolean = false; + private pendingRenders: string[] = []; + private idCounter: number = 0; + + private onRenderFrame?: (time: number, renderId: string) => void; + + setRenderFrameHandler(handler: (time: number, renderId: string) => void): void { + this.onRenderFrame = handler; + } + + start(): void { + if (this.animationId !== null) return; + + const animate = (timestamp: number) => { + if (timestamp - this.lastFrameTime >= this.frameInterval) { + const currentTime = (Date.now() - this.startTime) / TIMING.MILLISECONDS_PER_SECOND * this.timeSpeed; + const renderId = this.generateId(); + + this.onRenderFrame?.(currentTime, renderId); + this.lastFrameTime = timestamp; + } + + this.animationId = requestAnimationFrame(animate); + }; + + this.animationId = requestAnimationFrame(animate); + } + + stop(): void { + if (this.animationId !== null) { + cancelAnimationFrame(this.animationId); + this.animationId = null; + } + } + + setTargetFPS(fps: number): void { + this.targetFPS = Math.max(TIMING.MIN_FPS, Math.min(TIMING.MAX_FPS, fps)); + this.frameInterval = TIMING.MILLISECONDS_PER_SECOND / this.targetFPS; + } + + setTimeSpeed(speed: number): void { + this.timeSpeed = speed; + } + + getTimeSpeed(): number { + return this.timeSpeed; + } + + getCurrentTime(): number { + return (Date.now() - this.startTime) / TIMING.MILLISECONDS_PER_SECOND * this.timeSpeed; + } + + isAnimating(): boolean { + return this.animationId !== null; + } + + generateId(): string { + return `render_${this.idCounter++}_${Date.now()}`; + } + + setRenderingState(isRendering: boolean): void { + this.isRendering = isRendering; + } + + isCurrentlyRendering(): boolean { + return this.isRendering; + } + + addPendingRender(renderId: string): void { + this.pendingRenders.push(renderId); + + // Keep only the latest render to avoid backlog + if (this.pendingRenders.length > 3) { + const latestId = this.pendingRenders[this.pendingRenders.length - 1]; + this.pendingRenders = [latestId]; + } + } + + removePendingRender(renderId: string): void { + const index = this.pendingRenders.indexOf(renderId); + if (index !== -1) { + this.pendingRenders.splice(index, 1); + } + } + + getPendingRenders(): string[] { + return [...this.pendingRenders]; + } + + clearPendingRenders(): void { + this.pendingRenders = []; + } + + getFrameRate(): number { + return this.targetFPS; + } + + getFrameInterval(): number { + return this.frameInterval; + } +} \ No newline at end of file diff --git a/src/shader/core/WorkerPool.ts b/src/shader/core/WorkerPool.ts new file mode 100644 index 0000000..0e78ed8 --- /dev/null +++ b/src/shader/core/WorkerPool.ts @@ -0,0 +1,188 @@ +import { WorkerMessage, WorkerResponse } from '../types'; +import { WORKER } from '../../utils/constants'; + +/** + * Manages worker lifecycle and multi-worker rendering + * Extracted from FakeShader for better separation of concerns + */ +export class WorkerPool { + private workers: Worker[] = []; + private workerCount: number; + private tileResults: Map = new Map(); + private tilesCompleted: number = 0; + private totalTiles: number = 0; + private onRenderComplete?: (imageData: ImageData) => void; + private onError?: (error: any) => void; + + constructor() { + this.workerCount = navigator.hardwareConcurrency || WORKER.FALLBACK_CORE_COUNT; + this.workerCount = Math.min(this.workerCount, WORKER.MAX_WORKERS); + console.log(`WorkerPool: Using ${this.workerCount} workers for rendering`); + this.initializeWorkers(); + } + + private initializeWorkers(): void { + for (let i = 0; i < this.workerCount; i++) { + const worker = new Worker(new URL('../worker/ShaderWorker.ts', import.meta.url), { + type: 'module', + }); + + worker.onmessage = (event) => { + this.handleWorkerMessage(event.data, i); + }; + + worker.onerror = (error) => { + console.error(`Worker ${i} error:`, error); + this.onError?.(error); + }; + + this.workers.push(worker); + } + } + + private handleWorkerMessage(response: WorkerResponse, workerIndex: number): void { + switch (response.type) { + case 'compiled': + // Handle compilation response if needed + break; + + case 'rendered': + if (this.workerCount > 1) { + this.handleTileResult(response, workerIndex); + } else { + this.onRenderComplete?.(response.imageData!); + } + break; + + case 'error': + console.error(`Worker ${workerIndex} error:`, response.error); + this.onError?.(response.error); + break; + } + } + + private handleTileResult(response: WorkerResponse, workerIndex: number): void { + if (!response.imageData || response.tileIndex === undefined) return; + + this.tileResults.set(response.tileIndex, response.imageData); + this.tilesCompleted++; + + if (this.tilesCompleted >= this.totalTiles) { + this.assembleTiles(); + } + } + + private assembleTiles(): void { + if (this.tileResults.size === 0) return; + + const firstTile = this.tileResults.get(0); + if (!firstTile) return; + + const tileWidth = firstTile.width; + const tileHeight = firstTile.height; + const tilesPerRow = Math.ceil(Math.sqrt(this.totalTiles)); + const totalWidth = tileWidth; + const totalHeight = tileHeight * this.totalTiles; + + const canvas = new OffscreenCanvas(totalWidth, totalHeight); + const ctx = canvas.getContext('2d')!; + const finalImageData = ctx.createImageData(totalWidth, totalHeight); + + for (let i = 0; i < this.totalTiles; i++) { + const tileData = this.tileResults.get(i); + if (!tileData) continue; + + const startY = i * tileHeight; + const sourceData = tileData.data; + const targetData = finalImageData.data; + + for (let y = 0; y < tileHeight; y++) { + for (let x = 0; x < tileWidth; x++) { + const sourceIndex = (y * tileWidth + x) * 4; + const targetIndex = ((startY + y) * totalWidth + x) * 4; + + targetData[targetIndex] = sourceData[sourceIndex]; + targetData[targetIndex + 1] = sourceData[sourceIndex + 1]; + targetData[targetIndex + 2] = sourceData[sourceIndex + 2]; + targetData[targetIndex + 3] = sourceData[sourceIndex + 3]; + } + } + } + + this.onRenderComplete?.(finalImageData); + this.tileResults.clear(); + this.tilesCompleted = 0; + } + + compile(code: string): Promise { + return new Promise((resolve, reject) => { + const worker = this.workers[0]; // Use first worker for compilation + + const compileMessage = { + type: 'compile', + code, + }; + + const handleResponse = (event: MessageEvent) => { + const response = event.data; + if (response.type === 'compiled') { + worker.removeEventListener('message', handleResponse); + resolve(); + } else if (response.type === 'error') { + worker.removeEventListener('message', handleResponse); + reject(response.error); + } + }; + + worker.addEventListener('message', handleResponse); + worker.postMessage(compileMessage); + }); + } + + renderSingleWorker(message: WorkerMessage): void { + const worker = this.workers[0]; + worker.postMessage(message); + } + + renderMultiWorker(baseMessage: WorkerMessage, width: number, height: number): void { + this.tileResults.clear(); + this.tilesCompleted = 0; + this.totalTiles = this.workerCount; + + const tileHeight = Math.ceil(height / this.totalTiles); + + for (let i = 0; i < this.totalTiles; i++) { + const worker = this.workers[i]; + const startY = i * tileHeight; + const endY = Math.min((i + 1) * tileHeight, height); + const actualTileHeight = endY - startY; + + const tileMessage: WorkerMessage = { + ...baseMessage, + startY, + height: actualTileHeight, + tileIndex: i, + }; + + worker.postMessage(tileMessage); + } + } + + setRenderCompleteHandler(handler: (imageData: ImageData) => void): void { + this.onRenderComplete = handler; + } + + setErrorHandler(handler: (error: any) => void): void { + this.onError = handler; + } + + getWorkerCount(): number { + return this.workerCount; + } + + destroy(): void { + this.workers.forEach(worker => worker.terminate()); + this.workers = []; + this.tileResults.clear(); + } +} \ No newline at end of file diff --git a/src/shader/rendering/FeedbackSystem.ts b/src/shader/rendering/FeedbackSystem.ts index 2351e0f..3882fe8 100644 --- a/src/shader/rendering/FeedbackSystem.ts +++ b/src/shader/rendering/FeedbackSystem.ts @@ -1,3 +1,5 @@ +import { LUMINANCE_WEIGHTS } from '../../utils/constants'; + /** * Manages feedback buffers for shader rendering */ @@ -35,7 +37,7 @@ export class FeedbackSystem { if (!this.feedbackBuffer) return; // Use the actual displayed luminance as feedback (0-255 range) - const luminance = (r * 0.299 + g * 0.587 + b * 0.114); + const luminance = (r * LUMINANCE_WEIGHTS.RED + g * LUMINANCE_WEIGHTS.GREEN + b * LUMINANCE_WEIGHTS.BLUE); // Frame rate independent decay const decayFactor = Math.pow(0.95, deltaTime * 60); // 5% decay at 60fps diff --git a/src/shader/rendering/PixelRenderer.ts b/src/shader/rendering/PixelRenderer.ts index 8f5eeec..8b997ab 100644 --- a/src/shader/rendering/PixelRenderer.ts +++ b/src/shader/rendering/PixelRenderer.ts @@ -1,6 +1,10 @@ import { ShaderFunction, ShaderContext, WorkerMessage } from '../types'; import { FeedbackSystem } from './FeedbackSystem'; import { calculateColorDirect } from '../../utils/colorModes'; +import { + ValueModeProcessorRegistry, + PixelContext, +} from './ValueModeProcessor'; /** * Handles pixel-level rendering operations @@ -8,10 +12,12 @@ import { calculateColorDirect } from '../../utils/colorModes'; export class PixelRenderer { private feedbackSystem: FeedbackSystem; private shaderContext: ShaderContext; + private valueModeRegistry: ValueModeProcessorRegistry; constructor(feedbackSystem: FeedbackSystem, shaderContext: ShaderContext) { this.feedbackSystem = feedbackSystem; this.shaderContext = shaderContext; + this.valueModeRegistry = ValueModeProcessorRegistry.getInstance(); } /** @@ -50,15 +56,15 @@ export class PixelRenderer { // Calculate coordinate variables with optimized math const u = x * invFullWidth; const v = actualY * invFullHeight; - + // Pre-calculate deltas for reuse const dx = x - centerX; const dy = actualY - centerY; - + // Use more efficient radius calculation const radiusSquared = dx * dx + dy * dy; const radius = Math.sqrt(radiusSquared); - + // Optimize angle calculation - avoid atan2 for common cases let angle: number; if (dx === 0) { @@ -68,32 +74,36 @@ export class PixelRenderer { } else { angle = Math.atan2(dy, dx); } - + // Use pre-computed max distance inverse to avoid division const normalizedDistance = radius * invMaxDistance; - + // Optimize Manhattan distance using absolute values of pre-computed deltas const manhattanDistance = Math.abs(dx) + Math.abs(dy); - - // Optimize noise calculation with cached sin/cos values - const noise = (Math.sin(x * 0.1) * Math.cos(actualY * 0.1) + 1) * 0.5; + + // Pre-compute noise factors + const sinX01 = Math.sin(x * 0.1); + const cosY01 = Math.cos(actualY * 0.1); + const noise = (sinX01 * cosY01 + 1) * 0.5; + + // Cache canvas dimensions + const canvasWidth = message.fullWidth || width; + const canvasHeight = message.fullHeight || message.height! + (message.startY || 0); // Get feedback values const feedbackValue = this.feedbackSystem.getFeedback(pixelIndex); - const neighbors = this.feedbackSystem.getNeighborFeedback(pixelIndex, x, y, width, message.fullHeight || message.height! + (message.startY || 0)); + const neighbors = this.feedbackSystem.getNeighborFeedback(pixelIndex, x, y, width, canvasHeight); const momentum = this.feedbackSystem.getMomentum(pixelIndex); - const laplacian = this.feedbackSystem.getLaplacian(pixelIndex, x, y, width, message.fullHeight || message.height! + (message.startY || 0)); - const curvature = this.feedbackSystem.getCurvature(pixelIndex, x, y, width, message.fullHeight || message.height! + (message.startY || 0)); + const laplacian = this.feedbackSystem.getLaplacian(pixelIndex, x, y, width, canvasHeight); + const curvature = this.feedbackSystem.getCurvature(pixelIndex, x, y, width, canvasHeight); const stateValue = this.feedbackSystem.getState(pixelIndex, feedbackValue, deltaTime); const echoValue = this.feedbackSystem.getEcho(pixelIndex, time); // Calculate other variables - const canvasWidth = message.fullWidth || width; - const canvasHeight = message.fullHeight || message.height! + (message.startY || 0); const pseudoZ = Math.sin(radius * 0.01 + time) * 50; const jitter = ((x * 73856093 + actualY * 19349663) % 256) / 255; const oscillation = Math.sin(timeTwoPi + radius * 0.1); - + // Calculate block coordinates const bx = x >> 4; const by = actualY >> 4; @@ -168,9 +178,9 @@ export class PixelRenderer { // Execute shader const value = compiledFunction(ctx); const safeValue = isFinite(value) ? value : 0; - + // Calculate color - const [r, g, b] = this.calculateColor( + const color = this.calculateColor( safeValue, renderMode, valueMode, @@ -182,13 +192,13 @@ export class PixelRenderer { ); // Set pixel data - data[i] = r; - data[i + 1] = g; - data[i + 2] = b; + data[i] = color[0]; + data[i + 1] = color[1]; + data[i + 2] = color[2]; data[i + 3] = 255; // Update feedback system - this.feedbackSystem.updateFeedback(pixelIndex, r, g, b, deltaTime); + this.feedbackSystem.updateFeedback(pixelIndex, color[0], color[1], color[2], deltaTime); this.feedbackSystem.updateState(pixelIndex, stateValue); } catch (error) { @@ -213,584 +223,20 @@ export class PixelRenderer { width: number = 1, height: number = 1 ): [number, number, number] { + // Use optimized strategy pattern for ALL modes + const context: PixelContext = { x, y, width, height, value }; + const processor = this.valueModeRegistry.getProcessor(valueMode); 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 / cellSize); - const cellY = Math.floor(y / cellSize); - 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; - } - - case 'diffusion': { - // Heat diffusion simulation - simple rule creates complex emergent patterns - const diffusionRate = 0.1 + Math.abs(value) * 0.0001; - const kernelSize = 3; - const halfKernel = Math.floor(kernelSize / 2); - - // Create heat sources based on value - const heatSource = Math.abs(value) * 0.01; - let totalHeat = heatSource; - let sampleCount = 1; - - // Sample neighboring pixels and diffuse heat - for (let dy = -halfKernel; dy <= halfKernel; dy++) { - for (let dx = -halfKernel; dx <= halfKernel; dx++) { - if (dx === 0 && dy === 0) continue; - - const neighborX = x + dx; - const neighborY = y + dy; - - // Check bounds - if (neighborX >= 0 && neighborX < width && neighborY >= 0 && neighborY < height) { - // Use position-based pseudo-random for neighbor heat - const neighborSeed = neighborX + neighborY * width; - const neighborHeat = ((neighborSeed * 1103515245 + 12345) % 256) / 256; - - // Distance-based diffusion weight - const distance = Math.sqrt(dx * dx + dy * dy); - const weight = Math.exp(-distance * distance * 0.5); - - totalHeat += neighborHeat * weight * diffusionRate; - sampleCount += weight; - } - } - } - - // Average heat with temporal decay - const averageHeat = totalHeat / sampleCount; - const decay = 0.95 + Math.sin(Math.abs(value) * 0.01) * 0.04; - - // Add convection currents for more complex patterns - const convectionX = Math.sin(x * 0.01 + Math.abs(value) * 0.001) * 0.1; - const convectionY = Math.cos(y * 0.01 + Math.abs(value) * 0.001) * 0.1; - const convection = (convectionX + convectionY) * 0.5 + 0.5; - - // Final heat value with non-linear response - const finalHeat = (averageHeat * decay + convection * 0.3) % 1; - const enhancedHeat = Math.pow(finalHeat, 1.2); // Gamma correction for contrast - - processedValue = Math.floor(enhancedHeat * 255); - break; - } - - case 'cascade': { - // Cascade system - avalanche-like propagation with multiple scales of complexity - const centerX = width / 2; - const centerY = height / 2; - - // Create multiple cascade trigger points based on value - const triggerPoints = [ - { - x: centerX + Math.sin(Math.abs(value) * 0.01) * 150, - y: centerY + Math.cos(Math.abs(value) * 0.01) * 150, - threshold: 100 + Math.abs(value) * 0.05, - strength: 1.0 + Math.abs(value) * 0.001 - }, - { - x: centerX + Math.cos(Math.abs(value) * 0.015) * 200, - y: centerY + Math.sin(Math.abs(value) * 0.018) * 120, - threshold: 80 + Math.abs(value) * 0.08, - strength: 0.8 + Math.sin(Math.abs(value) * 0.02) * 0.4 - }, - { - x: centerX + Math.sin(Math.abs(value) * 0.012) * 180, - y: centerY + Math.cos(Math.abs(value) * 0.008) * 160, - threshold: 120 + Math.abs(value) * 0.03, - strength: 0.6 + Math.cos(Math.abs(value) * 0.025) * 0.3 - } - ]; - - let cascadeValue = 0; - const baseValue = Math.abs(value) % 256; - - // Process each trigger point - for (const trigger of triggerPoints) { - const dx = x - trigger.x; - const dy = y - trigger.y; - const distance = Math.sqrt(dx * dx + dy * dy); - const maxDistance = Math.sqrt(width * width + height * height); - const normalizedDistance = distance / maxDistance; - - // Check if this point would trigger cascade - if (baseValue > trigger.threshold) { - // Create expanding wave from trigger point - const waveFreq = 0.1 + Math.abs(value) * 0.0001; - const wave = Math.sin(distance * waveFreq + Math.abs(value) * 0.02); - - // Distance-based amplitude with non-linear falloff - const amplitude = trigger.strength * Math.exp(-distance * distance * 0.000001); - const cascadeWave = wave * amplitude; - - // Add interference with perpendicular waves - const perpWave = Math.cos(distance * waveFreq * 1.3 + Math.abs(value) * 0.015); - const interference = cascadeWave + perpWave * amplitude * 0.3; - - cascadeValue += interference; - } - } - - // Add multi-scale turbulence for organic complexity - let turbulence = 0; - const turbFreq = 0.02 + Math.abs(value) * 0.00005; - - for (let octave = 0; octave < 4; octave++) { - const freq = turbFreq * Math.pow(2, octave); - const amplitude = 1.0 / Math.pow(2, octave); - - turbulence += Math.sin(x * freq + Math.abs(value) * 0.01) * - Math.cos(y * freq + Math.abs(value) * 0.012) * amplitude; - } - - // Create threshold-based cascading effects - const combinedValue = baseValue + cascadeValue * 50 + turbulence * 30; - let finalValue = combinedValue; - - // Multi-level cascade thresholds - const thresholds = [150, 100, 200, 175]; - for (const threshold of thresholds) { - if (Math.abs(combinedValue) > threshold) { - // Cascade triggered - create amplification - const amplification = (Math.abs(combinedValue) - threshold) * 0.5; - finalValue += amplification; - - // Add local distortion when cascade triggers - const distortionX = Math.sin(y * 0.05 + Math.abs(value) * 0.01) * amplification * 0.1; - const distortionY = Math.cos(x * 0.05 + Math.abs(value) * 0.015) * amplification * 0.1; - - finalValue += distortionX + distortionY; - } - } - - // Add feedback loops for complexity - const feedback = Math.sin(finalValue * 0.02 + Math.abs(value) * 0.005) * 20; - finalValue += feedback; - - // Non-linear response for richer patterns - const nonLinear = Math.tanh(finalValue * 0.01) * 128 + 128; - - // Final enhancement with edge detection - const edgeDetection = Math.abs( - Math.sin(x * 0.1 + Math.abs(value) * 0.001) - - Math.sin(y * 0.1 + Math.abs(value) * 0.001) - ) * 30; - - processedValue = Math.floor(Math.max(0, Math.min(255, nonLinear + edgeDetection))); - break; - } - - default: - // Integer mode: treat value as 0-255 (original behavior) - processedValue = Math.abs(value) % 256; - break; + + if (processor) { + const precomputed = ValueModeProcessorRegistry.precomputeContext(context); + processedValue = processor(context, precomputed); + } else { + // Fallback for unknown modes + processedValue = Math.abs(value) % 256; } - + return calculateColorDirect(processedValue, renderMode, hueShift); } -} \ No newline at end of file + +} diff --git a/src/shader/rendering/ValueModeProcessor.ts b/src/shader/rendering/ValueModeProcessor.ts new file mode 100644 index 0000000..d91d890 --- /dev/null +++ b/src/shader/rendering/ValueModeProcessor.ts @@ -0,0 +1,872 @@ +import { RGB, MATH } from '../../utils/constants'; + +export interface PixelContext { + x: number; + y: number; + width: number; + height: number; + value: number; +} + +export interface PrecomputedContext { + centerX: number; + centerY: number; + dx: number; + dy: number; + distance: number; + angle: number; + normalizedDistance: number; + normalizedAngle: number; +} + +export type ValueModeProcessor = ( + context: PixelContext, + precomputed: PrecomputedContext +) => number; + +export class ValueModeProcessorRegistry { + private static instance: ValueModeProcessorRegistry; + private processors: Map = new Map(); + + private constructor() { + this.initializeProcessors(); + } + + static getInstance(): ValueModeProcessorRegistry { + if (!ValueModeProcessorRegistry.instance) { + ValueModeProcessorRegistry.instance = new ValueModeProcessorRegistry(); + } + return ValueModeProcessorRegistry.instance; + } + + getProcessor(mode: string): ValueModeProcessor | undefined { + return this.processors.get(mode); + } + + private initializeProcessors(): void { + this.processors.set('integer', this.integerMode); + this.processors.set('float', this.floatMode); + this.processors.set('polar', this.polarMode); + this.processors.set('distance', this.distanceMode); + this.processors.set('wave', this.waveMode); + this.processors.set('fractal', this.fractalMode); + this.processors.set('cellular', this.cellularMode); + this.processors.set('noise', this.noiseMode); + this.processors.set('warp', this.warpMode); + this.processors.set('flow', this.flowMode); + this.processors.set('spiral', this.spiralMode); + this.processors.set('turbulence', this.turbulenceMode); + this.processors.set('crystal', this.crystalMode); + this.processors.set('marble', this.marbleMode); + this.processors.set('quantum', this.quantumMode); + this.processors.set('logarithmic', this.logarithmicMode); + this.processors.set('mirror', this.mirrorMode); + this.processors.set('rings', this.ringsMode); + this.processors.set('mesh', this.meshMode); + this.processors.set('glitch', this.glitchMode); + this.processors.set('diffusion', this.diffusionMode); + this.processors.set('cascade', this.cascadeMode); + this.processors.set('echo', this.echoMode); + this.processors.set('mosh', this.moshMode); + this.processors.set('fold', this.foldMode); + } + + static precomputeContext(context: PixelContext): PrecomputedContext { + const centerX = context.width * 0.5; + const centerY = context.height * 0.5; + const dx = context.x - centerX; + const dy = context.y - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + const angle = Math.atan2(dy, dx); + const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY); + const normalizedDistance = distance / maxDistance; + const normalizedAngle = (angle + Math.PI) / MATH.TWO_PI; + + return { + centerX, + centerY, + dx, + dy, + distance, + angle, + normalizedDistance, + normalizedAngle, + }; + } + + private integerMode = (context: PixelContext): number => { + return Math.abs(context.value); + }; + + private floatMode = (context: PixelContext): number => { + let processedValue = Math.max(0, Math.min(1, Math.abs(context.value))); + processedValue = 1 - processedValue; + return Math.floor(processedValue * RGB.MAX_VALUE); + }; + + private polarMode = ( + context: PixelContext, + precomputed: PrecomputedContext + ): number => { + const radiusNorm = precomputed.normalizedDistance; + const spiralEffect = + (precomputed.normalizedAngle + radiusNorm * 0.5 + Math.abs(context.value) * 0.02) % 1; + const polarValue = Math.sin(spiralEffect * Math.PI * 8) * 0.5 + 0.5; + return Math.floor(polarValue * RGB.MAX_VALUE); + }; + + private distanceMode = ( + context: PixelContext, + precomputed: PrecomputedContext + ): number => { + const frequency = 8 + Math.abs(context.value) * 0.1; + const phase = Math.abs(context.value) * 0.05; + const concentricWave = + Math.sin(precomputed.normalizedDistance * Math.PI * frequency + phase) * 0.5 + 0.5; + const falloff = 1 - Math.pow(precomputed.normalizedDistance, 0.8); + const distanceValue = concentricWave * falloff; + return Math.floor(distanceValue * RGB.MAX_VALUE); + }; + + private waveMode = ( + context: PixelContext, + precomputed: PrecomputedContext + ): number => { + const baseFreq = 0.08; + const valueScale = Math.abs(context.value) * 0.001 + 1; + let waveSum = 0; + + const sources = WaveConstants.SOURCES; + const scaledSources = sources.map(source => ({ + x: context.width * source.x, + y: context.height * source.y, + })); + + for (const source of scaledSources) { + const dx = context.x - source.x; + const dy = context.y - source.y; + const dist = Math.sqrt(dx * dx + dy * dy); + const wave = Math.sin(dist * baseFreq * valueScale + Math.abs(context.value) * 0.02); + const amplitude = 1 / (1 + dist * 0.002); + waveSum += wave * amplitude; + } + + const waveValue = Math.tanh(waveSum) * 0.5 + 0.5; + return Math.floor(waveValue * RGB.MAX_VALUE); + }; + + private fractalMode = (context: PixelContext, _precomputed: PrecomputedContext): number => { + const scale = 0.01; + let fractalValue = 0; + let frequency = 1; + let amplitude = 1; + + for (let i = 0; i < 4; i++) { + const nx = context.x * scale * frequency + Math.abs(context.value) * 0.01; + const ny = context.y * scale * frequency; + const noise = this.simplexNoise(nx, ny); + fractalValue += noise * amplitude; + frequency *= 2; + amplitude *= 0.5; + } + + fractalValue = (fractalValue + 1) * 0.5; + return Math.floor(fractalValue * RGB.MAX_VALUE); + }; + + private cellularMode = (context: PixelContext): number => { + const cellSize = 8; + const cellX = Math.floor(context.x / cellSize); + const cellY = Math.floor(context.y / cellSize); + let liveNeighbors = 0; + + for (let dx = -1; dx <= 1; dx++) { + for (let dy = -1; dy <= 1; dy++) { + if (dx === 0 && dy === 0) continue; + const nx = cellX + dx; + const ny = cellY + dy; + const neighborValue = Math.abs( + this.pseudoRandom(nx * 73856093 + ny * 19349663 + Math.floor(Math.abs(context.value))) + ); + if (neighborValue > 0.5) liveNeighbors++; + } + } + + const cellValue = liveNeighbors >= 4 ? 1 : 0; + return Math.floor(cellValue * RGB.MAX_VALUE); + }; + + private simplexNoise(x: number, y: number): number { + const F2 = 0.5 * (Math.sqrt(3) - 1); + const G2 = (3 - Math.sqrt(3)) / 6; + + const s = (x + y) * F2; + const i = Math.floor(x + s); + const j = Math.floor(y + s); + + const t = (i + j) * G2; + const X0 = i - t; + const Y0 = j - t; + const x0 = x - X0; + const y0 = y - Y0; + + const i1 = x0 > y0 ? 1 : 0; + const j1 = x0 > y0 ? 0 : 1; + + const x1 = x0 - i1 + G2; + const y1 = y0 - j1 + G2; + const x2 = x0 - 1 + 2 * G2; + const y2 = y0 - 1 + 2 * G2; + + const ii = i & 255; + const jj = j & 255; + + const gi0 = this.permMod12[ii + this.perm[jj]] % 12; + const gi1 = this.permMod12[ii + i1 + this.perm[jj + j1]] % 12; + const gi2 = this.permMod12[ii + 1 + this.perm[jj + 1]] % 12; + + let n0, n1, n2; + + let t0 = 0.5 - x0 * x0 - y0 * y0; + if (t0 < 0) n0 = 0; + else { + t0 *= t0; + n0 = t0 * t0 * this.dot(this.grad3[gi0], x0, y0); + } + + let t1 = 0.5 - x1 * x1 - y1 * y1; + if (t1 < 0) n1 = 0; + else { + t1 *= t1; + n1 = t1 * t1 * this.dot(this.grad3[gi1], x1, y1); + } + + let t2 = 0.5 - x2 * x2 - y2 * y2; + if (t2 < 0) n2 = 0; + else { + t2 *= t2; + n2 = t2 * t2 * this.dot(this.grad3[gi2], x2, y2); + } + + return 70 * (n0 + n1 + n2); + } + + private pseudoRandom(seed: number): number { + const x = Math.sin(seed) * 10000; + return x - Math.floor(x); + } + + private dot(g: number[], x: number, y: number): number { + return g[0] * x + g[1] * y; + } + + private grad3 = [ + [1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0], + [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1], + [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1] + ]; + + private perm = [ + 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, + 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, + 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, + 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, + 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, + 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, + 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, + 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, + 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, + 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, + 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, + 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, + 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, + 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, + 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, + 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180, + 151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, + 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, + 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, + 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, + 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, + 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, + 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, + 200, 196, 135, 130, 116, 188, 159, 86, 164, 100, 109, 198, 173, 186, 3, 64, + 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, + 207, 206, 59, 227, 47, 16, 58, 17, 182, 189, 28, 42, 223, 183, 170, 213, + 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, + 129, 22, 39, 253, 19, 98, 108, 110, 79, 113, 224, 232, 178, 185, 112, 104, + 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, + 81, 51, 145, 235, 249, 14, 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, + 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, + 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180 + ]; + + private permMod12 = this.perm.map(p => p % 12); + + private noiseMode = (context: PixelContext): number => { + const noiseScale = 0.02; + const nx = context.x * noiseScale + Math.abs(context.value) * 0.001; + const ny = context.y * noiseScale + Math.abs(context.value) * 0.001; + + 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; + return Math.floor((combinedNoise + 1) * 0.5 * RGB.MAX_VALUE); + }; + private warpMode = (context: PixelContext, precomputed: PrecomputedContext): number => { + const warpStrength = Math.abs(context.value) * 0.001; + const warpFreq = 0.02; + + const warpX = context.x + Math.sin(context.y * warpFreq + Math.abs(context.value) * 0.01) * warpStrength * 100; + const warpY = context.y + Math.cos(context.x * warpFreq + Math.abs(context.value) * 0.01) * warpStrength * 100; + + const dx = warpX - precomputed.centerX; + const dy = warpY - precomputed.centerY; + const dist = Math.sqrt(dx * dx + dy * dy); + const maxDist = Math.sqrt(precomputed.centerX * precomputed.centerX + precomputed.centerY * precomputed.centerY); + const normDist = dist / maxDist; + + const deform = 1 + Math.sin(normDist * Math.PI + Math.abs(context.value) * 0.05) * 0.3; + const deformedX = precomputed.centerX + dx * deform; + const deformedY = precomputed.centerY + dy * deform; + + const finalValue = (deformedX + deformedY + Math.abs(context.value)) % 256; + return Math.floor(Math.abs(finalValue)); + }; + private flowMode = (context: PixelContext, precomputed: PrecomputedContext): number => { + const flowSources = [ + { + x: precomputed.centerX + Math.sin(Math.abs(context.value) * 0.01) * 200, + y: precomputed.centerY + Math.cos(Math.abs(context.value) * 0.01) * 200, + strength: 1 + Math.abs(context.value) * 0.01, + }, + { + x: precomputed.centerX + Math.cos(Math.abs(context.value) * 0.015) * 150, + y: precomputed.centerY + Math.sin(Math.abs(context.value) * 0.015) * 150, + strength: -0.8 + Math.sin(Math.abs(context.value) * 0.02) * 0.5, + }, + { + x: precomputed.centerX + Math.sin(Math.abs(context.value) * 0.008) * 300, + y: precomputed.centerY + Math.cos(Math.abs(context.value) * 0.012) * 250, + strength: 0.6 + Math.cos(Math.abs(context.value) * 0.018) * 0.4, + }, + ]; + + let flowX = 0; + let flowY = 0; + + for (const source of flowSources) { + const dx = context.x - source.x; + const dy = context.y - source.y; + const distance = Math.sqrt(dx * dx + dy * dy); + const normalizedDist = Math.max(distance, 1); + + const flowStrength = source.strength / (normalizedDist * 0.01); + flowX += (dx / normalizedDist) * flowStrength; + flowY += (dy / normalizedDist) * flowStrength; + + const curlStrength = source.strength * 0.5; + flowX += ((-dy / normalizedDist) * curlStrength) / normalizedDist; + flowY += ((dx / normalizedDist) * curlStrength) / normalizedDist; + } + + const globalFlowAngle = Math.abs(context.value) * 0.02; + flowX += Math.cos(globalFlowAngle) * (Math.abs(context.value) * 0.1); + flowY += Math.sin(globalFlowAngle) * (Math.abs(context.value) * 0.1); + + const turbScale = 0.05; + const turbulence = Math.sin(context.x * turbScale + Math.abs(context.value) * 0.01) * + Math.cos(context.y * turbScale + Math.abs(context.value) * 0.015) * + (Math.abs(context.value) * 0.02); + + flowX += turbulence; + flowY += turbulence * 0.7; + + let particleX = context.x; + let particleY = context.y; + + for (let step = 0; step < 5; step++) { + 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; + + const curlStrength = source.strength * 0.5; + localFlowX += ((-dy / normalizedDist) * curlStrength) / normalizedDist; + localFlowY += ((dx / normalizedDist) * curlStrength) / normalizedDist; + } + + const stepSize = 0.5; + particleX += localFlowX * stepSize; + particleY += localFlowY * stepSize; + } + + const flowMagnitude = Math.sqrt(flowX * flowX + flowY * flowY); + const particleDistance = Math.sqrt( + (particleX - context.x) * (particleX - context.x) + (particleY - context.y) * (particleY - context.y) + ); + + const flowValue = (flowMagnitude * 10 + particleDistance * 2) % 256; + const enhanced = Math.sin(flowValue * 0.05 + Math.abs(context.value) * 0.01) * 0.5 + 0.5; + + return Math.floor(enhanced * RGB.MAX_VALUE); + }; + private spiralMode = (context: PixelContext, precomputed: PrecomputedContext): number => { + const spiralTightness = 1 + Math.abs(context.value) * 0.01; + const spiralValue = precomputed.angle + Math.log(Math.max(precomputed.distance, 1)) * spiralTightness; + return Math.floor((Math.sin(spiralValue) * 0.5 + 0.5) * RGB.MAX_VALUE); + }; + private turbulenceMode = (context: PixelContext): number => { + let turbulence = 0; + const chaos = Math.abs(context.value) * 0.001; + for (let i = 0; i < 4; i++) { + const freq = Math.pow(2, i) * (0.01 + chaos); + turbulence += Math.abs(Math.sin(context.x * freq) * Math.cos(context.y * freq)) / Math.pow(2, i); + } + return Math.floor(Math.min(turbulence, 1) * RGB.MAX_VALUE); + }; + private crystalMode = (context: PixelContext): number => { + const latticeSize = 32 + Math.abs(context.value) * 0.1; + const gridX = Math.floor(context.x / latticeSize); + const gridY = Math.floor(context.y / latticeSize); + const crystal = Math.sin(gridX + gridY + Math.abs(context.value) * 0.01) * + Math.cos(gridX * gridY + Math.abs(context.value) * 0.005); + return Math.floor((crystal * 0.5 + 0.5) * RGB.MAX_VALUE); + }; + private marbleMode = (context: PixelContext): number => { + const noiseFreq = 0.005 + Math.abs(context.value) * 0.00001; + const turbulence = Math.sin(context.x * noiseFreq) * Math.cos(context.y * noiseFreq) + + Math.sin(context.x * noiseFreq * 2) * Math.cos(context.y * noiseFreq * 2) * 0.5; + const marble = Math.sin((context.x + turbulence * 50) * 0.02 + Math.abs(context.value) * 0.001); + return Math.floor((marble * 0.5 + 0.5) * RGB.MAX_VALUE); + }; + private quantumMode = (context: PixelContext, precomputed: PrecomputedContext): number => { + const uncertainty = Math.abs(context.value) * 0.001; + const distSquared = precomputed.dx * precomputed.dx + precomputed.dy * precomputed.dy; + const sigmaSquared = (100 + uncertainty * 1000); + const probability = Math.exp(-distSquared / (2 * sigmaSquared * sigmaSquared)); + const quantum = probability * (1 + Math.sin(context.x * context.y * uncertainty) * 0.5); + return Math.floor(Math.min(quantum, 1) * RGB.MAX_VALUE); + }; + private logarithmicMode = (context: PixelContext): number => { + const logValue = Math.log(1 + Math.abs(context.value)); + return Math.floor((logValue / Math.log(256)) * RGB.MAX_VALUE); + }; + private mirrorMode = (context: PixelContext, precomputed: PrecomputedContext): number => { + const dx = Math.abs(context.x - precomputed.centerX); + const dy = Math.abs(context.y - precomputed.centerY); + const mirrorX = precomputed.centerX + (dx % precomputed.centerX); + const mirrorY = precomputed.centerY + (dy % precomputed.centerY); + const mirrorDistance = Math.sqrt(mirrorX * mirrorX + mirrorY * mirrorY); + const mirrorValue = (Math.abs(context.value) + mirrorDistance) % 256; + return mirrorValue; + }; + private ringsMode = (context: PixelContext, precomputed: PrecomputedContext): number => { + const ringSpacing = 20 + Math.abs(context.value) * 0.1; + const rings = Math.sin((precomputed.distance / ringSpacing) * Math.PI * 2); + const interference = Math.sin((precomputed.distance + Math.abs(context.value)) * 0.05); + return Math.floor(((rings * interference) * 0.5 + 0.5) * RGB.MAX_VALUE); + }; + private meshMode = (context: PixelContext): number => { + const angle = Math.abs(context.value) * 0.001; + const rotX = context.x * Math.cos(angle) - context.y * Math.sin(angle); + const rotY = context.x * Math.sin(angle) + context.y * Math.cos(angle); + const gridSize = 16 + Math.abs(context.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)); + return Math.floor(mesh * RGB.MAX_VALUE); + }; + private glitchMode = (context: PixelContext): number => { + const seed = Math.floor(context.x + context.y * context.width + Math.abs(context.value)); + const random = ((seed * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff; + const glitchThreshold = 0.95 - Math.abs(context.value) * 0.0001; + let glitchValue = Math.abs(context.value) % 256; + + if (random > glitchThreshold) { + glitchValue = (glitchValue << 1) ^ (glitchValue >> 3) ^ ((context.x + context.y) & 0xFF); + } + + return glitchValue % 256; + }; + private diffusionMode = (context: PixelContext): number => { + const diffusionRate = 0.1 + Math.abs(context.value) * 0.0001; + const kernelSize = 3; + const halfKernel = Math.floor(kernelSize / 2); + + const heatSource = Math.abs(context.value) * 0.01; + let totalHeat = heatSource; + let sampleCount = 1; + + for (let dy = -halfKernel; dy <= halfKernel; dy++) { + for (let dx = -halfKernel; dx <= halfKernel; dx++) { + if (dx === 0 && dy === 0) continue; + + const neighborX = context.x + dx; + const neighborY = context.y + dy; + + if (neighborX >= 0 && neighborX < context.width && neighborY >= 0 && neighborY < context.height) { + const neighborSeed = neighborX + neighborY * context.width; + const neighborHeat = ((neighborSeed * 1103515245 + 12345) % 256) / 256; + + const distance = Math.sqrt(dx * dx + dy * dy); + const weight = Math.exp(-distance * distance * 0.5); + + totalHeat += neighborHeat * weight * diffusionRate; + sampleCount += weight; + } + } + } + + const averageHeat = totalHeat / sampleCount; + const decay = 0.95 + Math.sin(Math.abs(context.value) * 0.01) * 0.04; + + const convectionX = Math.sin(context.x * 0.01 + Math.abs(context.value) * 0.001) * 0.1; + const convectionY = Math.cos(context.y * 0.01 + Math.abs(context.value) * 0.001) * 0.1; + const convection = (convectionX + convectionY) * 0.5 + 0.5; + + const finalHeat = (averageHeat * decay + convection * 0.3) % 1; + const enhancedHeat = Math.pow(finalHeat, 1.2); + + return Math.floor(enhancedHeat * RGB.MAX_VALUE); + }; + private cascadeMode = (context: PixelContext, precomputed: PrecomputedContext): number => { + const triggerPoints = [ + { + x: precomputed.centerX + Math.sin(Math.abs(context.value) * 0.01) * 150, + y: precomputed.centerY + Math.cos(Math.abs(context.value) * 0.01) * 150, + threshold: 100 + Math.abs(context.value) * 0.05, + strength: 1.0 + Math.abs(context.value) * 0.001 + }, + { + x: precomputed.centerX + Math.cos(Math.abs(context.value) * 0.015) * 200, + y: precomputed.centerY + Math.sin(Math.abs(context.value) * 0.018) * 120, + threshold: 80 + Math.abs(context.value) * 0.08, + strength: 0.8 + Math.sin(Math.abs(context.value) * 0.02) * 0.4 + }, + { + x: precomputed.centerX + Math.sin(Math.abs(context.value) * 0.012) * 180, + y: precomputed.centerY + Math.cos(Math.abs(context.value) * 0.008) * 160, + threshold: 120 + Math.abs(context.value) * 0.03, + strength: 0.6 + Math.cos(Math.abs(context.value) * 0.025) * 0.3 + } + ]; + + let cascadeValue = 0; + const baseValue = Math.abs(context.value) % 256; + + for (const trigger of triggerPoints) { + const dx = context.x - trigger.x; + const dy = context.y - trigger.y; + const distance = Math.sqrt(dx * dx + dy * dy); + const maxDistance = Math.sqrt(context.width * context.width + context.height * context.height); + const normalizedDistance = distance / maxDistance; + + if (baseValue > trigger.threshold) { + const waveFreq = 0.1 + Math.abs(context.value) * 0.0001; + const wave = Math.sin(distance * waveFreq + Math.abs(context.value) * 0.02); + + const amplitude = trigger.strength * Math.exp(-distance * distance * 0.000001); + const cascadeWave = wave * amplitude; + + const perpWave = Math.cos(distance * waveFreq * 1.3 + Math.abs(context.value) * 0.015); + const interference = cascadeWave + perpWave * amplitude * 0.3; + + cascadeValue += interference; + } + } + + let turbulence = 0; + const turbFreq = 0.02 + Math.abs(context.value) * 0.00005; + + for (let octave = 0; octave < 4; octave++) { + const freq = turbFreq * Math.pow(2, octave); + const amplitude = 1.0 / Math.pow(2, octave); + + turbulence += Math.sin(context.x * freq + Math.abs(context.value) * 0.01) * + Math.cos(context.y * freq + Math.abs(context.value) * 0.012) * amplitude; + } + + const combinedValue = baseValue + cascadeValue * 50 + turbulence * 30; + let finalValue = combinedValue; + + const thresholds = [150, 100, 200, 175]; + for (const threshold of thresholds) { + if (Math.abs(combinedValue) > threshold) { + const amplification = (Math.abs(combinedValue) - threshold) * 0.5; + finalValue += amplification; + + const distortionX = Math.sin(context.y * 0.05 + Math.abs(context.value) * 0.01) * amplification * 0.1; + const distortionY = Math.cos(context.x * 0.05 + Math.abs(context.value) * 0.015) * amplification * 0.1; + + finalValue += distortionX + distortionY; + } + } + + const feedback = Math.sin(finalValue * 0.02 + Math.abs(context.value) * 0.005) * 20; + finalValue += feedback; + + const nonLinear = Math.tanh(finalValue * 0.01) * 128 + 128; + + const edgeDetection = Math.abs( + Math.sin(context.x * 0.1 + Math.abs(context.value) * 0.001) - + Math.sin(context.y * 0.1 + Math.abs(context.value) * 0.001) + ) * 30; + + return Math.floor(Math.max(0, Math.min(RGB.MAX_VALUE, nonLinear + edgeDetection))); + }; + private echoMode = (context: PixelContext): number => { + const baseValue = Math.abs(context.value) % 256; + + const echoSources = [ + { delay: 0.1, decay: 0.8, spatial: 0.02, twist: 1.0 }, + { delay: 0.25, decay: 0.6, spatial: 0.05, twist: -0.7 }, + { delay: 0.4, decay: 0.45, spatial: 0.03, twist: 1.3 }, + { delay: 0.6, decay: 0.3, spatial: 0.08, twist: -0.9 } + ]; + + let echoSum = baseValue; + let totalWeight = 1.0; + + for (const echo of echoSources) { + const spatialOffsetX = Math.sin(context.x * echo.spatial + echo.twist) * echo.delay * 100; + const spatialOffsetY = Math.cos(context.y * echo.spatial + echo.twist * 0.7) * echo.delay * 80; + + const offsetX = context.x + spatialOffsetX; + const offsetY = context.y + spatialOffsetY; + + const offsetSeed = Math.floor(offsetX) + Math.floor(offsetY) * context.width; + const offsetNoise = ((offsetSeed * 1103515245 + 12345) % 256) / 256; + const delayedValue = (baseValue * (1 - echo.delay) + offsetNoise * 255 * echo.delay) % 256; + + const harmonic = Math.sin(delayedValue * 0.05 + echo.twist) * 30; + const distortedEcho = delayedValue + harmonic; + + const feedback = Math.sin(distortedEcho * 0.02 + context.x * 0.01 + context.y * 0.01) * echo.decay * 40; + const finalEcho = distortedEcho + feedback; + + echoSum += finalEcho * echo.decay; + totalWeight += echo.decay; + } + + const interference = Math.sin(echoSum * 0.03) * Math.cos(baseValue * 0.04) * 25; + echoSum += interference; + + const compressed = Math.tanh(echoSum / totalWeight * 0.02) * RGB.MAX_VALUE; + + return Math.floor(Math.max(0, Math.min(RGB.MAX_VALUE, compressed))); + }; + private moshMode = (context: PixelContext): number => { + const baseValue = Math.abs(context.value) % 256; + + const pseudoTime = (context.x + context.y + baseValue) * 0.1; + const temporalDrift = Math.floor(pseudoTime * 100) % 1024; + const microJitter = Math.sin(pseudoTime * 20) * 0.8; + + const blockSize = 8; + const driftedX = context.x + Math.sin(temporalDrift * 0.01 + context.y * 0.1) * microJitter; + const driftedY = context.y + Math.cos(temporalDrift * 0.008 + context.x * 0.12) * microJitter; + + const blockX = Math.floor(driftedX / blockSize); + const blockY = Math.floor(driftedY / blockSize); + const blockId = blockX + blockY * Math.floor(context.width / blockSize); + + const corruptionLevel = (baseValue / 255.0) * 0.8 + 0.1; + const blockSeed = blockId * 1103515245 + baseValue + temporalDrift; + const blockRandom = ((blockSeed % 65536) / 65536); + + let corruptedValue = baseValue; + + if (blockRandom < corruptionLevel * 0.3) { + const motionX = ((blockSeed >> 8) & 7) - 4 + Math.sin(temporalDrift * 0.02) * 0.5; + const motionY = ((blockSeed >> 11) & 7) - 4 + Math.cos(temporalDrift * 0.018) * 0.5; + + const sourceX = context.x + motionX * 2; + const sourceY = context.y + motionY * 2; + + if (sourceX >= 0 && sourceX < context.width && sourceY >= 0 && sourceY < context.height) { + const sourceSeed = Math.floor(sourceX) + Math.floor(sourceY) * context.width; + const sourceNoise = ((sourceSeed * 1664525 + 1013904223) % 256) / 256; + corruptedValue = Math.floor(sourceNoise * 255); + } + } + else if (blockRandom < corruptionLevel * 0.6) { + const localX = context.x % blockSize; + const localY = context.y % blockSize; + + const dctFreqX = Math.floor(localX / 2); + const dctFreqY = Math.floor(localY / 2); + const dctCoeff = Math.sin((dctFreqX + dctFreqY) * Math.PI / 4); + + const corruption = dctCoeff * (blockRandom - 0.5) * 100; + corruptedValue = Math.floor(baseValue + corruption); + } + else if (blockRandom < corruptionLevel * 0.8) { + const bleedIntensity = (blockRandom - 0.6) * 5; + const temporalPhase = temporalDrift * 0.03; + const bleedX = Math.sin(context.x * 0.1 + baseValue * 0.05 + temporalPhase) * bleedIntensity; + const bleedY = Math.cos(context.y * 0.1 + baseValue * 0.03 + temporalPhase * 0.8) * bleedIntensity; + + corruptedValue = Math.floor(baseValue + bleedX + bleedY); + } + else if (blockRandom < corruptionLevel) { + const quantLevels = 8 + Math.floor((1 - corruptionLevel) * 16); + const quantStep = 256 / quantLevels; + corruptedValue = Math.floor(baseValue / quantStep) * quantStep; + + const quantNoise = ((blockSeed >> 16) & 15) - 8; + corruptedValue += quantNoise; + } + + if (blockRandom > 0.95) { + const tempSeed = context.x * 73856093 + context.y * 19349663 + baseValue; + const tempNoise = ((tempSeed % 256) / 256); + const mixRatio = corruptionLevel * 0.7; + + corruptedValue = Math.floor(corruptedValue * (1 - mixRatio) + tempNoise * 255 * mixRatio); + } + + const ringFreq = 0.3 + corruptionLevel * 0.5; + const temporalRingPhase = temporalDrift * 0.01; + const ringing = Math.sin(context.x * ringFreq + temporalRingPhase) * Math.cos(context.y * ringFreq + temporalRingPhase * 0.7) * corruptionLevel * 15; + corruptedValue += ringing; + + const edgeDetect = Math.abs(Math.sin(context.x * 0.2 + temporalDrift * 0.005)) + Math.abs(Math.sin(context.y * 0.2 + temporalDrift * 0.007)); + if (edgeDetect > 1.5) { + const mosquitoNoise = ((blockSeed >> 20) & 31) - 16 + Math.sin(temporalDrift * 0.1) * 3; + corruptedValue += mosquitoNoise * corruptionLevel; + } + + return Math.floor(Math.max(0, Math.min(RGB.MAX_VALUE, corruptedValue))); + }; + private foldMode = (context: PixelContext, precomputed: PrecomputedContext): number => { + const baseValue = Math.abs(context.value) % 256; + const normalizedValue = baseValue / 255.0; + + const foldLines = [ + { + position: 0.2 + Math.sin(context.x * 0.01 + normalizedValue * 4) * 0.15, + angle: Math.PI * 0.25 + normalizedValue * Math.PI * 0.5, + strength: 1.0, + type: 'valley' + }, + { + position: 0.5 + Math.cos(context.y * 0.008 + normalizedValue * 3) * 0.2, + angle: Math.PI * 0.75 + Math.sin(normalizedValue * 6) * Math.PI * 0.3, + strength: 0.8, + type: 'mountain' + }, + { + position: 0.75 + Math.sin((context.x + context.y) * 0.005 + normalizedValue * 2) * 0.1, + angle: Math.PI * 1.1 + Math.cos(normalizedValue * 8) * Math.PI * 0.4, + strength: 0.6, + type: 'valley' + }, + { + position: 0.35 + Math.cos(context.x * 0.012 - context.y * 0.008 + normalizedValue * 5) * 0.18, + angle: Math.PI * 1.5 + normalizedValue * Math.PI, + strength: 0.9, + type: 'mountain' + } + ]; + + let foldedValue = normalizedValue; + let geometryComplexity = 1.0; + + for (const fold of foldLines) { + const cos_a = Math.cos(fold.angle); + const sin_a = Math.sin(fold.angle); + + const rotX = (context.x - precomputed.centerX) * cos_a + (context.y - precomputed.centerY) * sin_a; + const rotY = -(context.x - precomputed.centerX) * sin_a + (context.y - precomputed.centerY) * cos_a; + + const foldDistance = Math.abs(rotY) / context.height; + const foldPosition = (rotX / context.width + 1) * 0.5; + + const foldSide = Math.sign(rotY); + + if (Math.abs(foldedValue - fold.position) < 0.3) { + const foldInfluence = Math.exp(-foldDistance * 8) * fold.strength; + + if (fold.type === 'valley') { + if (foldedValue > fold.position) { + const excess = foldedValue - fold.position; + const foldedExcess = excess * (1 - foldInfluence) - excess * foldInfluence * 0.5; + foldedValue = fold.position + foldedExcess; + } else { + const deficit = fold.position - foldedValue; + const foldedDeficit = deficit * (1 - foldInfluence) - deficit * foldInfluence * 0.5; + foldedValue = fold.position - foldedDeficit; + } + } else { + if (foldedValue > fold.position) { + const excess = foldedValue - fold.position; + const expandedExcess = excess * (1 + foldInfluence * 0.8); + foldedValue = fold.position + expandedExcess; + } else { + const deficit = fold.position - foldedValue; + const expandedDeficit = deficit * (1 + foldInfluence * 0.8); + foldedValue = fold.position - expandedDeficit; + } + } + + const creaseSharpness = Math.exp(-Math.abs(foldedValue - fold.position) * 20) * fold.strength; + const creaseEffect = Math.sin(foldPosition * Math.PI * 8 + fold.angle) * creaseSharpness * 0.1; + foldedValue += creaseEffect; + + geometryComplexity *= (1 + foldInfluence * 0.3); + } + } + + const recursiveFolds = 3; + for (let r = 0; r < recursiveFolds; r++) { + const recursiveScale = Math.pow(0.6, r); + const recursiveFreq = Math.pow(2, r + 2); + + const recursiveFoldPos = 0.5 + Math.sin(foldedValue * Math.PI * recursiveFreq + r) * 0.2 * recursiveScale; + + if (Math.abs(foldedValue - recursiveFoldPos) < 0.1 * recursiveScale) { + const recursiveInfluence = Math.exp(-Math.abs(foldedValue - recursiveFoldPos) * 30 / recursiveScale) * recursiveScale; + + const microFold = (foldedValue - recursiveFoldPos) * (1 - recursiveInfluence * 0.7); + foldedValue = recursiveFoldPos + microFold; + + const microCrease = Math.sin(foldedValue * Math.PI * recursiveFreq * 4) * recursiveInfluence * 0.05; + foldedValue += microCrease; + } + } + + const distortion = Math.sin(foldedValue * Math.PI * geometryComplexity) * Math.cos(geometryComplexity * 2) * 0.15; + foldedValue += distortion; + + const edgeDetection = Math.abs(Math.sin(foldedValue * Math.PI * 16)) * 0.2; + const edgeEnhancement = Math.pow(edgeDetection, 2) * geometryComplexity * 0.1; + foldedValue += edgeEnhancement; + + const materialResponse = Math.tanh(foldedValue * 3) * 0.85 + 0.15; + const paperTexture = Math.sin(materialResponse * Math.PI * 32 + geometryComplexity) * 0.05; + + const finalValue = (materialResponse + paperTexture) * RGB.MAX_VALUE; + + return Math.floor(Math.max(0, Math.min(RGB.MAX_VALUE, finalValue))); + }; +} + +class WaveConstants { + static readonly SOURCES = [ + { x: 0.3, y: 0.3 }, + { x: 0.7, y: 0.3 }, + { x: 0.5, y: 0.7 }, + { x: 0.2, y: 0.8 }, + ]; +} \ No newline at end of file diff --git a/src/utils/colorModes.ts b/src/utils/colorModes.ts index e42abd6..a87a3a9 100644 --- a/src/utils/colorModes.ts +++ b/src/utils/colorModes.ts @@ -1,7 +1,14 @@ +import { + RGB, + HSV, + COLOR_TRANSITIONS, + COLOR_MODE_CONSTANTS, +} from './constants'; + export function rgbToHsv(r: number, g: number, b: number): [number, number, number] { - r /= 255; - g /= 255; - b /= 255; + r /= RGB.MAX_VALUE; + g /= RGB.MAX_VALUE; + b /= RGB.MAX_VALUE; const max = Math.max(r, g, b); const min = Math.min(r, g, b); @@ -13,13 +20,13 @@ export function rgbToHsv(r: number, g: number, b: number): [number, number, numb if (delta !== 0) { if (max === r) { - h = ((g - b) / delta) % 6; + h = ((g - b) / delta) % HSV.HUE_SECTORS; } else if (max === g) { - h = (b - r) / delta + 2; + h = (b - r) / delta + HSV.SECTOR_OFFSETS.GREEN; } else { - h = (r - g) / delta + 4; + h = (r - g) / delta + HSV.SECTOR_OFFSETS.BLUE; } - h /= 6; + h /= HSV.HUE_SECTORS; } if (h < 0) h += 1; @@ -33,30 +40,30 @@ export function hsvToRgb( v: number ): [number, number, number] { const c = v * s; - const x = c * (1 - Math.abs(((h * 6) % 2) - 1)); + const x = c * (1 - Math.abs(((h * HSV.HUE_SECTORS) % 2) - 1)); const m = v - c; let r = 0, g = 0, b = 0; - if (h < 1 / 6) { + if (h < HSV.SECTOR_BOUNDARIES.SIXTH) { r = c; g = x; b = 0; - } else if (h < 2 / 6) { + } else if (h < HSV.SECTOR_BOUNDARIES.THIRD) { r = x; g = c; b = 0; - } else if (h < 3 / 6) { + } else if (h < HSV.SECTOR_BOUNDARIES.HALF) { r = 0; g = c; b = x; - } else if (h < 4 / 6) { + } else if (h < HSV.SECTOR_BOUNDARIES.TWO_THIRDS) { r = 0; g = x; b = c; - } else if (h < 5 / 6) { + } else if (h < HSV.SECTOR_BOUNDARIES.FIVE_SIXTHS) { r = x; g = 0; b = c; @@ -67,9 +74,9 @@ export function hsvToRgb( } return [ - Math.round((r + m) * 255), - Math.round((g + m) * 255), - Math.round((b + m) * 255), + Math.round((r + m) * RGB.MAX_VALUE), + Math.round((g + m) * RGB.MAX_VALUE), + Math.round((b + m) * RGB.MAX_VALUE), ]; } @@ -87,7 +94,7 @@ export function applyHueShift(rgb: [number, number, number], hueShiftDegrees: nu } export function rainbowColor(value: number): [number, number, number] { - const phase = (value / 255.0) * 6; + const phase = (value / RGB.MAX_VALUE) * COLOR_MODE_CONSTANTS.RAINBOW_PHASE_MULTIPLIER; const segment = Math.floor(phase); const remainder = phase - segment; const t = remainder; @@ -95,112 +102,142 @@ export function rainbowColor(value: number): [number, number, number] { switch (segment % 6) { case 0: - return [255, Math.round(t * 255), 0]; + return [RGB.MAX_VALUE, Math.round(t * RGB.MAX_VALUE), 0]; case 1: - return [Math.round(q * 255), 255, 0]; + return [Math.round(q * RGB.MAX_VALUE), RGB.MAX_VALUE, 0]; case 2: - return [0, 255, Math.round(t * 255)]; + return [0, RGB.MAX_VALUE, Math.round(t * RGB.MAX_VALUE)]; case 3: - return [0, Math.round(q * 255), 255]; + return [0, Math.round(q * RGB.MAX_VALUE), RGB.MAX_VALUE]; case 4: - return [Math.round(t * 255), 0, 255]; + return [Math.round(t * RGB.MAX_VALUE), 0, RGB.MAX_VALUE]; case 5: - return [255, 0, Math.round(q * 255)]; + return [RGB.MAX_VALUE, 0, Math.round(q * RGB.MAX_VALUE)]; default: - return [255, 255, 255]; + return [RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE]; } } export function thermalColor(value: number): [number, number, number] { - const t = value / 255.0; + const t = value / RGB.MAX_VALUE; if (t < 0.25) { - return [0, 0, Math.round(t * 4 * 255)]; + return [0, 0, Math.round(t * 4 * RGB.MAX_VALUE)]; } else if (t < 0.5) { - return [0, Math.round((t - 0.25) * 4 * 255), 255]; + return [0, Math.round((t - 0.25) * 4 * RGB.MAX_VALUE), RGB.MAX_VALUE]; } else if (t < 0.75) { return [ - Math.round((t - 0.5) * 4 * 255), - 255, - Math.round((0.75 - t) * 4 * 255), + Math.round((t - 0.5) * 4 * RGB.MAX_VALUE), + RGB.MAX_VALUE, + Math.round((0.75 - t) * 4 * RGB.MAX_VALUE), ]; } else { - return [255, 255, Math.round((t - 0.75) * 4 * 255)]; + return [RGB.MAX_VALUE, RGB.MAX_VALUE, Math.round((t - 0.75) * 4 * RGB.MAX_VALUE)]; } } export function neonColor(value: number): [number, number, number] { - const t = value / 255.0; + const t = value / RGB.MAX_VALUE; const intensity = Math.pow(Math.sin(t * Math.PI), 2); const glow = Math.pow(intensity, 0.5); return [ - Math.round(glow * 255), - Math.round(intensity * 255), - Math.round(Math.pow(intensity, 2) * 255), + Math.round(glow * RGB.MAX_VALUE), + Math.round(intensity * RGB.MAX_VALUE), + Math.round(Math.pow(intensity, 2) * RGB.MAX_VALUE), ]; } +export function cyberpunkColor(value: number): [number, number, number] { + const t = value / RGB.MAX_VALUE; + const pulse = Math.sin(t * Math.PI * COLOR_TRANSITIONS.CYBERPUNK.PULSE_FREQUENCY) * COLOR_TRANSITIONS.CYBERPUNK.PULSE_AMPLITUDE + COLOR_TRANSITIONS.CYBERPUNK.PULSE_OFFSET; + + if (t < COLOR_TRANSITIONS.CYBERPUNK.LOW) { + return [Math.round(t * 5 * 50), 0, Math.round(t * 5 * 100)]; + } else if (t < COLOR_TRANSITIONS.CYBERPUNK.MID) { + const p = (t - COLOR_TRANSITIONS.CYBERPUNK.LOW) / COLOR_TRANSITIONS.CYBERPUNK.LOW; + return [ + Math.round(50 + p * 205 * pulse), + Math.round(p * 50), + Math.round(100 + p * 155) + ]; + } else if (t < COLOR_TRANSITIONS.CYBERPUNK.HIGH) { + const p = (t - COLOR_TRANSITIONS.CYBERPUNK.MID) / (COLOR_TRANSITIONS.CYBERPUNK.HIGH - COLOR_TRANSITIONS.CYBERPUNK.MID); + return [ + Math.round(RGB.MAX_VALUE * pulse), + Math.round(50 + p * 205 * pulse), + Math.round(RGB.MAX_VALUE - p * 100) + ]; + } else { + const p = (t - 0.7) / 0.3; + return [ + Math.round((RGB.MAX_VALUE - p * 155) * pulse), + Math.round(RGB.MAX_VALUE * pulse), + Math.round(155 + p * 100 * pulse) + ]; + } +} + export function sunsetColor(value: number): [number, number, number] { - const t = value / 255.0; + const t = value / RGB.MAX_VALUE; if (t < 0.3) { - return [Math.round(t * 3.33 * 255), 0, Math.round(t * 1.67 * 255)]; + return [Math.round(t * 3.33 * RGB.MAX_VALUE), 0, Math.round(t * 1.67 * RGB.MAX_VALUE)]; } else if (t < 0.6) { const p = (t - 0.3) / 0.3; - return [255, Math.round(p * 100), Math.round(50 * (1 - p))]; + return [RGB.MAX_VALUE, Math.round(p * 100), Math.round(50 * (1 - p))]; } else { const p = (t - 0.6) / 0.4; - return [255, Math.round(100 + p * 155), Math.round(p * 100)]; + return [RGB.MAX_VALUE, Math.round(100 + p * 155), Math.round(p * 100)]; } } export function oceanColor(value: number): [number, number, number] { - const t = value / 255.0; + const t = value / RGB.MAX_VALUE; if (t < 0.25) { - return [0, Math.round(t * 2 * 255), Math.round(100 + t * 4 * 155)]; + return [0, Math.round(t * 2 * RGB.MAX_VALUE), Math.round(100 + t * 4 * 155)]; } else if (t < 0.5) { const p = (t - 0.25) / 0.25; - return [0, Math.round(128 + p * 127), 255]; + return [0, Math.round(128 + p * 127), RGB.MAX_VALUE]; } else if (t < 0.75) { const p = (t - 0.5) / 0.25; - return [Math.round(p * 100), 255, Math.round(255 - p * 100)]; + return [Math.round(p * 100), RGB.MAX_VALUE, Math.round(RGB.MAX_VALUE - p * 100)]; } else { const p = (t - 0.75) / 0.25; - return [Math.round(100 + p * 155), 255, Math.round(155 + p * 100)]; + return [Math.round(100 + p * 155), RGB.MAX_VALUE, Math.round(155 + p * 100)]; } } export function forestColor(value: number): [number, number, number] { - const t = value / 255.0; + const t = value / RGB.MAX_VALUE; if (t < 0.3) { - return [Math.round(t * 2 * 255), Math.round(50 + t * 3 * 205), 0]; + return [Math.round(t * 2 * RGB.MAX_VALUE), Math.round(50 + t * 3 * 205), 0]; } else if (t < 0.6) { const p = (t - 0.3) / 0.3; - return [Math.round(150 - p * 100), 255, Math.round(p * 100)]; + return [Math.round(150 - p * 100), RGB.MAX_VALUE, Math.round(p * 100)]; } else { const p = (t - 0.6) / 0.4; - return [Math.round(50 + p * 100), Math.round(255 - p * 100), Math.round(100 + p * 55)]; + return [Math.round(50 + p * 100), Math.round(RGB.MAX_VALUE - p * 100), Math.round(100 + p * 55)]; } } export function copperColor(value: number): [number, number, number] { - const t = value / 255.0; + const t = value / RGB.MAX_VALUE; if (t < 0.4) { - return [Math.round(t * 2.5 * 255), Math.round(t * 1.5 * 255), Math.round(t * 0.5 * 255)]; + return [Math.round(t * 2.5 * RGB.MAX_VALUE), Math.round(t * 1.5 * RGB.MAX_VALUE), Math.round(t * 0.5 * RGB.MAX_VALUE)]; } else if (t < 0.7) { const p = (t - 0.4) / 0.3; - return [255, Math.round(153 + p * 102), Math.round(51 + p * 51)]; + return [RGB.MAX_VALUE, Math.round(153 + p * 102), Math.round(51 + p * 51)]; } else { const p = (t - 0.7) / 0.3; - return [255, 255, Math.round(102 + p * 153)]; + return [RGB.MAX_VALUE, RGB.MAX_VALUE, Math.round(102 + p * 153)]; } } export function ditheredColor(value: number): [number, number, number] { - const levels = 4; - const step = 255 / (levels - 1); + const levels = COLOR_MODE_CONSTANTS.DITHER_LEVELS; + const step = RGB.MAX_VALUE / (levels - 1); const quantized = Math.round(value / step) * step; const error = value - quantized; - const dither = (Math.random() - 0.5) * 32; - const final = Math.max(0, Math.min(255, quantized + error + dither)); + const dither = (Math.random() - 0.5) * COLOR_MODE_CONSTANTS.DITHER_NOISE_AMPLITUDE; + const final = Math.max(RGB.MIN_VALUE, Math.min(RGB.MAX_VALUE, quantized + error + dither)); return [final, final, final]; } @@ -210,12 +247,12 @@ export function paletteColor(value: number): [number, number, number] { [87, 29, 149], [191, 82, 177], [249, 162, 162], - [255, 241, 165], + [RGB.MAX_VALUE, 241, 165], [134, 227, 206], [29, 161, 242], - [255, 255, 255], + [RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE], ]; - const index = Math.floor((value / 255.0) * (palette.length - 1)); + const index = Math.floor((value / RGB.MAX_VALUE) * (palette.length - 1)); return palette[index] as [number, number, number]; } @@ -230,12 +267,12 @@ export function vintageColor(value: number): [number, number, number] { [166, 124, 82], [245, 222, 179], ]; - const index = Math.floor((value / 255.0) * (palette.length - 1)); + const index = Math.floor((value / RGB.MAX_VALUE) * (palette.length - 1)); return palette[index] as [number, number, number]; } export function plasmaColor(value: number): [number, number, number] { - const t = value / 255.0; + const t = value / RGB.MAX_VALUE; const freq = 2.4; const phase1 = 0.0; const phase2 = 2.094; @@ -246,50 +283,50 @@ export function plasmaColor(value: number): [number, number, number] { const b = Math.sin(freq * t + phase3) * 0.5 + 0.5; return [ - Math.round(r * 255), - Math.round(g * 255), - Math.round(b * 255) + Math.round(r * RGB.MAX_VALUE), + Math.round(g * RGB.MAX_VALUE), + Math.round(b * RGB.MAX_VALUE) ]; } export function fireColor(value: number): [number, number, number] { - const t = value / 255.0; + const t = value / RGB.MAX_VALUE; if (t < 0.2) { - return [Math.round(t * 5 * 255), 0, 0]; + return [Math.round(t * 5 * RGB.MAX_VALUE), 0, 0]; } else if (t < 0.5) { const p = (t - 0.2) / 0.3; - return [255, Math.round(p * 165), 0]; + return [RGB.MAX_VALUE, Math.round(p * 165), 0]; } else if (t < 0.8) { const p = (t - 0.5) / 0.3; - return [255, Math.round(165 + p * 90), Math.round(p * 100)]; + return [RGB.MAX_VALUE, Math.round(165 + p * 90), Math.round(p * 100)]; } else { const p = (t - 0.8) / 0.2; - return [255, 255, Math.round(100 + p * 155)]; + return [RGB.MAX_VALUE, RGB.MAX_VALUE, Math.round(100 + p * 155)]; } } export function iceColor(value: number): [number, number, number] { - const t = value / 255.0; + const t = value / RGB.MAX_VALUE; if (t < 0.25) { - return [Math.round(t * 2 * 255), Math.round(t * 3 * 255), 255]; + return [Math.round(t * 2 * RGB.MAX_VALUE), Math.round(t * 3 * RGB.MAX_VALUE), RGB.MAX_VALUE]; } else if (t < 0.5) { const p = (t - 0.25) / 0.25; - return [Math.round(128 + p * 127), Math.round(192 + p * 63), 255]; + return [Math.round(128 + p * 127), Math.round(192 + p * 63), RGB.MAX_VALUE]; } else if (t < 0.75) { const p = (t - 0.5) / 0.25; - return [255, 255, Math.round(255 - p * 100)]; + return [RGB.MAX_VALUE, RGB.MAX_VALUE, Math.round(RGB.MAX_VALUE - p * 100)]; } else { const p = (t - 0.75) / 0.25; - return [255, 255, Math.round(155 + p * 100)]; + return [RGB.MAX_VALUE, RGB.MAX_VALUE, Math.round(155 + p * 100)]; } } export function infraredColor(value: number): [number, number, number] { - const t = value / 255.0; + const t = value / RGB.MAX_VALUE; const intensity = Math.pow(t, 0.6); const heat = Math.sin(t * Math.PI * 1.5) * 0.5 + 0.5; - const r = Math.round(255 * intensity); + const r = Math.round(RGB.MAX_VALUE * intensity); const g = Math.round(128 * heat * intensity); const b = Math.round(64 * (1 - intensity) * heat); @@ -297,12 +334,12 @@ export function infraredColor(value: number): [number, number, number] { } export function xrayColor(value: number): [number, number, number] { - const t = value / 255.0; + const t = value / RGB.MAX_VALUE; const inverted = 1.0 - t; const contrast = Math.pow(inverted, 1.8); const glow = Math.sin(t * Math.PI) * 0.3; - const intensity = Math.round(contrast * 255); + const intensity = Math.round(contrast * RGB.MAX_VALUE); const cyan = Math.round((contrast + glow) * 180); const blue = Math.round((contrast + glow * 0.5) * 120); @@ -310,7 +347,7 @@ export function xrayColor(value: number): [number, number, number] { } export function spectrumColor(value: number): [number, number, number] { - const t = value / 255.0; + const t = value / RGB.MAX_VALUE; const hue = t * 360; const saturation = 0.7; const lightness = 0.6 + (Math.sin(t * Math.PI * 4) * 0.2); @@ -336,14 +373,14 @@ export function spectrumColor(value: number): [number, number, number] { } return [ - Math.round((r + m) * 255), - Math.round((g + m) * 255), - Math.round((b + m) * 255) + Math.round((r + m) * RGB.MAX_VALUE), + Math.round((g + m) * RGB.MAX_VALUE), + Math.round((b + m) * RGB.MAX_VALUE) ]; } export function acidColor(value: number): [number, number, number] { - const t = value / 255.0; + const t = value / RGB.MAX_VALUE; const phase = t * Math.PI * 2; const r = Math.sin(phase) * 0.5 + 0.5; @@ -354,28 +391,115 @@ export function acidColor(value: number): [number, number, number] { const glow = Math.sin(t * Math.PI * 6) * 0.2 + 0.8; return [ - Math.round(r * intensity * glow * 255), - Math.round(g * intensity * glow * 255), - Math.round(b * intensity * glow * 255) + Math.round(r * intensity * glow * RGB.MAX_VALUE), + Math.round(g * intensity * glow * RGB.MAX_VALUE), + Math.round(b * intensity * glow * RGB.MAX_VALUE) ]; } +export function palette16Color(value: number): [number, number, number] { + const palette = [ + [0, 0, 0], // Black + [RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE], // White + [RGB.MAX_VALUE, 0, 0], // Red + [0, RGB.MAX_VALUE, 0], // Green + [0, 0, RGB.MAX_VALUE], // Blue + [RGB.MAX_VALUE, RGB.MAX_VALUE, 0], // Yellow + [RGB.MAX_VALUE, 0, RGB.MAX_VALUE], // Magenta + [0, RGB.MAX_VALUE, RGB.MAX_VALUE], // Cyan + [RGB.MAX_VALUE, 128, 0], // Orange + [128, 0, RGB.MAX_VALUE], // Purple + [0, RGB.MAX_VALUE, 128], // Spring Green + [RGB.MAX_VALUE, 0, 128], // Pink + [128, RGB.MAX_VALUE, 0], // Lime + [0, 128, RGB.MAX_VALUE], // Sky Blue + [128, 128, 128], // Gray + [192, 192, 192] // Light Gray + ]; + + const index = Math.floor((value / RGB.MAX_VALUE) * (palette.length - 1)); + return palette[index] as [number, number, number]; +} + +export function quantumColor(value: number): [number, number, number] { + const palette = [ + [0, 0, 0], // Void Black + [128, 0, RGB.MAX_VALUE], // Quantum Purple + [0, RGB.MAX_VALUE, 128], // Energy Green + [RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE] // Pure White + ]; + + const index = Math.floor((value / RGB.MAX_VALUE) * (palette.length - 1)); + return palette[index] as [number, number, number]; +} + +export function neonStrikeColor(value: number): [number, number, number] { + const palette = [ + [10, 0, 20], // Deep Dark + [RGB.MAX_VALUE, 20, 147], // Hot Pink + [0, RGB.MAX_VALUE, RGB.MAX_VALUE], // Electric Cyan + [RGB.MAX_VALUE, RGB.MAX_VALUE, 0], // Neon Yellow + [RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE] // Blinding White + ]; + + const index = Math.floor((value / RGB.MAX_VALUE) * (palette.length - 1)); + return palette[index] as [number, number, number]; +} + +export function eightBitColor(value: number): [number, number, number] { + const levels = 4; + const r = Math.floor((value / RGB.MAX_VALUE) * levels) * (RGB.MAX_VALUE / (levels - 1)); + const g = Math.floor(((value * 2) % 256 / RGB.MAX_VALUE) * levels) * (RGB.MAX_VALUE / (levels - 1)); + const b = Math.floor(((value * 3) % 256 / RGB.MAX_VALUE) * levels) * (RGB.MAX_VALUE / (levels - 1)); + + return [ + Math.min(RGB.MAX_VALUE, Math.max(0, r)), + Math.min(RGB.MAX_VALUE, Math.max(0, g)), + Math.min(RGB.MAX_VALUE, Math.max(0, b)) + ]; +} + +export function silkColor(value: number): [number, number, number] { + const t = value / RGB.MAX_VALUE; + const smoothT = t * t * (3.0 - 2.0 * t); + + const r = Math.sin(smoothT * Math.PI * 2.0) * 0.5 + 0.5; + const g = Math.sin(smoothT * Math.PI * 2.0 + Math.PI * 0.66) * 0.5 + 0.5; + const b = Math.sin(smoothT * Math.PI * 2.0 + Math.PI * 1.33) * 0.5 + 0.5; + + const fade = Math.pow(smoothT, 0.3); + + return [ + Math.round(r * fade * RGB.MAX_VALUE), + Math.round(g * fade * RGB.MAX_VALUE), + Math.round(b * fade * RGB.MAX_VALUE) + ]; +} + +export function binaryColor(value: number): [number, number, number] { + const threshold = COLOR_MODE_CONSTANTS.BINARY_THRESHOLD; + return value < threshold + ? [RGB.MIN_VALUE, RGB.MIN_VALUE, RGB.MIN_VALUE] // Pure Black + : [RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE]; // Pure White +} + + export function palette32Color(value: number): [number, number, number] { const palette = [ [0, 0, 0], // Black - [255, 255, 255], // White - [255, 0, 0], // Red - [0, 255, 0], // Green - [0, 0, 255], // Blue - [255, 255, 0], // Yellow - [255, 0, 255], // Magenta - [0, 255, 255], // Cyan - [255, 128, 0], // Orange - [128, 0, 255], // Purple - [0, 255, 128], // Spring Green - [255, 0, 128], // Pink - [128, 255, 0], // Lime - [0, 128, 255], // Sky Blue + [RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE], // White + [RGB.MAX_VALUE, 0, 0], // Red + [0, RGB.MAX_VALUE, 0], // Green + [0, 0, RGB.MAX_VALUE], // Blue + [RGB.MAX_VALUE, RGB.MAX_VALUE, 0], // Yellow + [RGB.MAX_VALUE, 0, RGB.MAX_VALUE], // Magenta + [0, RGB.MAX_VALUE, RGB.MAX_VALUE], // Cyan + [RGB.MAX_VALUE, 128, 0], // Orange + [128, 0, RGB.MAX_VALUE], // Purple + [0, RGB.MAX_VALUE, 128], // Spring Green + [RGB.MAX_VALUE, 0, 128], // Pink + [128, RGB.MAX_VALUE, 0], // Lime + [0, 128, RGB.MAX_VALUE], // Sky Blue [128, 128, 128], // Gray [192, 192, 192], // Light Gray [64, 64, 64], // Dark Gray @@ -385,18 +509,18 @@ export function palette32Color(value: number): [number, number, number] { [128, 0, 64], // Maroon [64, 0, 128], // Indigo [0, 128, 64], // Teal - [255, 192, 128], // Peach - [128, 255, 192], // Mint - [192, 128, 255], // Lavender - [255, 128, 192], // Rose - [128, 192, 255], // Light Blue - [192, 255, 128], // Light Green + [RGB.MAX_VALUE, 192, 128], // Peach + [128, RGB.MAX_VALUE, 192], // Mint + [192, 128, RGB.MAX_VALUE], // Lavender + [RGB.MAX_VALUE, 128, 192], // Rose + [128, 192, RGB.MAX_VALUE], // Light Blue + [192, RGB.MAX_VALUE, 128], // Light Green [64, 32, 16], // Dark Brown [16, 64, 32], // Forest [32, 16, 64] // Deep Purple ]; - const index = Math.floor((value / 255.0) * (palette.length - 1)); + const index = Math.floor((value / RGB.MAX_VALUE) * (palette.length - 1)); return palette[index] as [number, number, number]; } @@ -408,13 +532,12 @@ const COLOR_PALETTE_REGISTRY: Record [number, number, green: (value) => [0, value, 0], blue: (value) => [0, 0, value], rgb: (value) => [value, (value * 2) % 256, (value * 3) % 256], // Same as classic for now - hsv: (value) => rainbowColor(value), // Use rainbow for HSV forest: forestColor, copper: copperColor, rainbow: rainbowColor, thermal: thermalColor, neon: neonColor, - cyberpunk: neonColor, // Use neon for cyberpunk theme + cyberpunk: cyberpunkColor, vaporwave: plasmaColor, // Use plasma for vaporwave theme sunset: sunsetColor, ocean: oceanColor, @@ -428,6 +551,12 @@ const COLOR_PALETTE_REGISTRY: Record [number, number, xray: xrayColor, spectrum: spectrumColor, acid: acidColor, + quantum: quantumColor, + neonstrike: neonStrikeColor, + eightbit: eightBitColor, + silk: silkColor, + binary: binaryColor, + palette16: palette16Color, palette32: palette32Color, }; @@ -437,7 +566,7 @@ export function calculateColorDirect( hueShift: number = 0 ): [number, number, number] { const colorFunction = COLOR_PALETTE_REGISTRY[renderMode]; - const color = colorFunction ? colorFunction(absValue) : [absValue, absValue, absValue]; + const color = colorFunction ? colorFunction(absValue) : [absValue, absValue, absValue] as [number, number, number]; return applyHueShift(color, hueShift); } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index fddbcef..2f85d97 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -19,15 +19,98 @@ export const PERFORMANCE = { // Color Constants export const COLOR_TABLE_SIZE = 256; +// Color Calculation Constants +export const RGB = { + MAX_VALUE: 255, + MIN_VALUE: 0, +} as const; + +// Luminance calculation constants (ITU-R BT.709) +export const LUMINANCE_WEIGHTS = { + RED: 0.299, + GREEN: 0.587, + BLUE: 0.114, +} as const; + +// HSV Color Constants +export const HSV = { + HUE_SECTORS: 6, + HUE_MAX_DEGREES: 360, + SECTOR_OFFSETS: { + GREEN: 2, + BLUE: 4, + }, + SECTOR_BOUNDARIES: { + SIXTH: 1/6, + THIRD: 2/6, + HALF: 3/6, + TWO_THIRDS: 4/6, + FIVE_SIXTHS: 5/6, + }, +} as const; + +// Color Transition Thresholds +export const COLOR_TRANSITIONS = { + THERMAL: { + LOW: 0.25, + MID: 0.5, + HIGH: 0.75, + }, + CYBERPUNK: { + LOW: 0.2, + MID: 0.4, + HIGH: 0.7, + PULSE_FREQUENCY: 8, + PULSE_AMPLITUDE: 0.3, + PULSE_OFFSET: 0.7, + }, + SUNSET: { + LOW: 0.3, + HIGH: 0.6, + }, + FIRE: { + LOW: 0.2, + MID: 0.5, + HIGH: 0.8, + }, +} as const; + +// Color Mode Specific Constants +export const COLOR_MODE_CONSTANTS = { + DITHER_LEVELS: 4, + DITHER_NOISE_AMPLITUDE: 32, + BINARY_THRESHOLD: 128, + RAINBOW_PHASE_MULTIPLIER: 6, + PLASMA: { + FREQUENCY_X: 2.4, + FREQUENCY_Y: 2.094, + FREQUENCY_Z: 4.188, + PHASE_OFFSET: 0.0, + }, + INFRARED: { + INTENSITY_POWER: 0.6, + HEAT_FREQUENCY: 1.5, + }, + XRAY: { + CONTRAST_POWER: 1.8, + }, + SPECTRUM: { + HUE_DEGREES: 360, + SATURATION: 0.7, + LIGHTNESS_BASE: 0.6, + LIGHTNESS_FREQUENCY: 4, + LIGHTNESS_AMPLITUDE: 0.2, + }, +} as const; + // Render Mode Constants - Keep in sync with color modes export const RENDER_MODES = [ 'classic', - 'grayscale', + 'grayscale', 'red', 'green', 'blue', 'rgb', - 'hsv', 'rainbow', 'thermal', 'neon', @@ -47,6 +130,12 @@ export const RENDER_MODES = [ 'xray', 'spectrum', 'acid', + 'quantum', + 'neonstrike', + 'eightbit', + 'silk', + 'binary', + 'palette16', 'palette32', ] as const; @@ -91,10 +180,44 @@ export const VALUE_MODES = [ 'glitch', 'diffusion', 'cascade', + 'echo', + 'mosh', + 'fold', ] as const; export type ValueMode = (typeof VALUE_MODES)[number]; +// Frame Rate and Timing Constants +export const TIMING = { + DEFAULT_FPS: 30, + MIN_FPS: 1, + MAX_FPS: 120, + MILLISECONDS_PER_SECOND: 1000, + DEFAULT_TIME_SPEED: 1.0, + DEFAULT_BPM: 120, +} as const; + +// Worker and Threading Constants +export const WORKER = { + FALLBACK_CORE_COUNT: 4, + MAX_WORKERS: 32, + DEFAULT_PINCH_SCALE: 1, +} as const; + +// Mathematical Constants +export const MATH = { + DEGREES_IN_CIRCLE: 360, + RADIANS_TO_DEGREES: 180 / Math.PI, + TWO_PI: 2 * Math.PI, +} as const; + +// JSON and String Constants +export const FORMAT = { + JSON_INDENT: 2, + ID_RADIX: 36, + ID_SUBSTRING_START: 2, +} as const; + // Default Values export const DEFAULTS = { RESOLUTION: 8,