Compare commits

...

6 Commits

Author SHA1 Message Date
2cee4084c0 optimisations 2025-07-15 00:52:00 +02:00
3eeafc1277 wip 2025-07-14 21:58:04 +02:00
431966d498 essais 2025-07-14 21:08:21 +02:00
2cf306ee8c i dunno 2025-07-14 12:56:16 +02:00
d64b3839e8 refactor help (more) 2025-07-14 12:30:34 +02:00
80537a4a30 refactor help 2025-07-14 12:19:41 +02:00
25 changed files with 3756 additions and 1543 deletions

View File

@ -15,6 +15,7 @@ const CORE_ASSETS = [
const DYNAMIC_ASSETS_PATTERNS = [
/\/src\/.+\.(ts|tsx|js|jsx)$/,
/\/src\/.+\.css$/,
/\/assets\/.+\.(js|css)$/,
/fonts\.googleapis\.com/,
/fonts\.gstatic\.com/
];
@ -30,6 +31,10 @@ self.addEventListener('install', event => {
}),
caches.open(DYNAMIC_CACHE).then(cache => {
console.log('Dynamic cache initialized');
// Pre-cache critical assets if they exist
return cache.addAll([]).catch(() => {
console.log('No additional assets to pre-cache');
});
})
]).then(() => {
console.log('Service Worker installed successfully');

View File

@ -1,47 +1,5 @@
interface WorkerMessage {
id: string;
type: 'compile' | 'render';
code?: string;
width?: number;
height?: number;
time?: number;
renderMode?: string;
valueMode?: string;
hueShift?: number;
startY?: number; // Y offset for tile rendering
mouseX?: number;
mouseY?: number;
mousePressed?: boolean;
mouseVX?: number;
mouseVY?: number;
mouseClickTime?: number;
touchCount?: number;
touch0X?: number;
touch0Y?: number;
touch1X?: number;
touch1Y?: number;
pinchScale?: number;
pinchRotation?: number;
accelX?: number;
accelY?: number;
accelZ?: number;
gyroX?: number;
gyroY?: number;
gyroZ?: number;
audioLevel?: number;
bassLevel?: number;
midLevel?: number;
trebleLevel?: number;
bpm?: number;
}
interface WorkerResponse {
id: string;
type: 'compiled' | 'rendered' | 'error';
success: boolean;
imageData?: ImageData;
error?: string;
}
import { WorkerMessage, WorkerResponse } from './shader/types';
import { TIMING, WORKER, DEFAULTS } from './utils/constants';
export class FakeShader {
private canvas: HTMLCanvasElement;
@ -58,8 +16,50 @@ 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;
// Reusable message object to avoid allocations
private reusableMessage: WorkerMessage = {
id: '',
type: 'render',
width: 0,
height: 0,
fullWidth: 0,
fullHeight: 0,
time: 0,
renderMode: 'classic',
valueMode: 'integer',
hueShift: 0,
mouseX: 0,
mouseY: 0,
mousePressed: false,
mouseVX: 0,
mouseVY: 0,
mouseClickTime: 0,
touchCount: 0,
touch0X: 0,
touch0Y: 0,
touch1X: 0,
touch1Y: 0,
pinchScale: 1,
pinchRotation: 0,
accelX: 0,
accelY: 0,
accelZ: 0,
gyroX: 0,
gyroY: 0,
gyroZ: 0,
audioLevel: 0,
bassLevel: 0,
midLevel: 0,
trebleLevel: 0,
bpm: TIMING.DEFAULT_BPM,
startY: 0,
};
// Multi-worker state
private tileResults: Map<number, ImageData> = new Map();
@ -77,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;
@ -91,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') {
@ -104,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`
);
@ -137,7 +137,7 @@ export class FakeShader {
private initializeWorkers(): void {
// Create worker pool
for (let i = 0; i < this.workerCount; i++) {
const worker = new Worker(new URL('./ShaderWorker.ts', import.meta.url), {
const worker = new Worker(new URL('./shader/worker/ShaderWorker.ts', import.meta.url), {
type: 'module',
});
worker.onmessage = (e: MessageEvent<WorkerResponse>) =>
@ -203,7 +203,7 @@ export class FakeShader {
private compile(): void {
this.isCompiled = false;
const id = `compile_${Date.now()}`;
const id = `compile_${++this.idCounter}`;
// Send compile message to all workers
this.workers.forEach((worker) => {
@ -222,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) {
@ -232,43 +232,47 @@ export class FakeShader {
}
}
private updateReusableMessage(id: string, currentTime: number, width: number, height: number, fullWidth: number, fullHeight: number, startY: number = 0): void {
this.reusableMessage.id = id;
this.reusableMessage.type = 'render';
this.reusableMessage.width = width;
this.reusableMessage.height = height;
this.reusableMessage.fullWidth = fullWidth;
this.reusableMessage.fullHeight = fullHeight;
this.reusableMessage.time = currentTime;
this.reusableMessage.renderMode = this.renderMode;
this.reusableMessage.valueMode = this.valueMode;
this.reusableMessage.hueShift = this.hueShift;
this.reusableMessage.startY = startY;
this.reusableMessage.mouseX = this.mouseX;
this.reusableMessage.mouseY = this.mouseY;
this.reusableMessage.mousePressed = this.mousePressed;
this.reusableMessage.mouseVX = this.mouseVX;
this.reusableMessage.mouseVY = this.mouseVY;
this.reusableMessage.mouseClickTime = this.mouseClickTime;
this.reusableMessage.touchCount = this.touchCount;
this.reusableMessage.touch0X = this.touch0X;
this.reusableMessage.touch0Y = this.touch0Y;
this.reusableMessage.touch1X = this.touch1X;
this.reusableMessage.touch1Y = this.touch1Y;
this.reusableMessage.pinchScale = this.pinchScale;
this.reusableMessage.pinchRotation = this.pinchRotation;
this.reusableMessage.accelX = this.accelX;
this.reusableMessage.accelY = this.accelY;
this.reusableMessage.accelZ = this.accelZ;
this.reusableMessage.gyroX = this.gyroX;
this.reusableMessage.gyroY = this.gyroY;
this.reusableMessage.gyroZ = this.gyroZ;
this.reusableMessage.audioLevel = this.audioLevel;
this.reusableMessage.bassLevel = this.bassLevel;
this.reusableMessage.midLevel = this.midLevel;
this.reusableMessage.trebleLevel = this.trebleLevel;
this.reusableMessage.bpm = this.currentBPM;
}
private renderWithSingleWorker(id: string, currentTime: number): void {
this.worker.postMessage({
id,
type: 'render',
width: this.canvas.width,
height: this.canvas.height,
fullWidth: this.canvas.width,
fullHeight: this.canvas.height,
time: currentTime,
renderMode: this.renderMode,
valueMode: this.valueMode,
hueShift: this.hueShift,
mouseX: this.mouseX,
mouseY: this.mouseY,
mousePressed: this.mousePressed,
mouseVX: this.mouseVX,
mouseVY: this.mouseVY,
mouseClickTime: this.mouseClickTime,
touchCount: this.touchCount,
touch0X: this.touch0X,
touch0Y: this.touch0Y,
touch1X: this.touch1X,
touch1Y: this.touch1Y,
pinchScale: this.pinchScale,
pinchRotation: this.pinchRotation,
accelX: this.accelX,
accelY: this.accelY,
accelZ: this.accelZ,
gyroX: this.gyroX,
gyroY: this.gyroY,
gyroZ: this.gyroZ,
audioLevel: this.audioLevel,
bassLevel: this.bassLevel,
midLevel: this.midLevel,
trebleLevel: this.trebleLevel,
bpm: this.currentBPM,
} as WorkerMessage);
this.updateReusableMessage(id, currentTime, this.canvas.width, this.canvas.height, this.canvas.width, this.canvas.height, 0);
this.worker.postMessage(this.reusableMessage);
}
private renderWithMultipleWorkers(id: string, currentTime: number): void {
@ -288,45 +292,18 @@ export class FakeShader {
if (startY >= height) return; // Skip if tile is outside canvas
worker.postMessage({
id: `${id}_tile_${index}`,
type: 'render',
width: width,
height: endY - startY,
// Pass the Y offset for correct coordinate calculation
startY: startY,
// Pass full canvas dimensions for center calculations
fullWidth: width,
fullHeight: height,
time: currentTime,
renderMode: this.renderMode,
valueMode: this.valueMode,
hueShift: this.hueShift,
mouseX: this.mouseX,
mouseY: this.mouseY,
mousePressed: this.mousePressed,
mouseVX: this.mouseVX,
mouseVY: this.mouseVY,
mouseClickTime: this.mouseClickTime,
touchCount: this.touchCount,
touch0X: this.touch0X,
touch0Y: this.touch0Y,
touch1X: this.touch1X,
touch1Y: this.touch1Y,
pinchScale: this.pinchScale,
pinchRotation: this.pinchRotation,
accelX: this.accelX,
accelY: this.accelY,
accelZ: this.accelZ,
gyroX: this.gyroX,
gyroY: this.gyroY,
gyroZ: this.gyroZ,
audioLevel: this.audioLevel,
bassLevel: this.bassLevel,
midLevel: this.midLevel,
trebleLevel: this.trebleLevel,
bpm: this.currentBPM,
} as WorkerMessage);
// Update reusable message with worker-specific values
this.updateReusableMessage(
`${id}_tile_${index}`,
currentTime,
width,
endY - startY,
width,
height,
startY
);
worker.postMessage(this.reusableMessage);
});
}
@ -356,7 +333,7 @@ export class FakeShader {
return;
}
const renderId = `render_${Date.now()}_${Math.random()}`;
const renderId = `render_${++this.idCounter}`;
// Add to pending renders queue
this.pendingRenders.push(renderId);
@ -390,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 {
@ -507,7 +484,7 @@ export class FakeShader {
try {
const bitmapPromises: Promise<ImageBitmap>[] = [];
const positions: number[] = [];
for (let i = 0; i < this.workerCount; i++) {
const tileData = this.tileResults.get(i);
if (tileData) {
@ -515,9 +492,9 @@ export class FakeShader {
positions.push(i * tileHeight);
}
}
const bitmaps = await Promise.all(bitmapPromises);
for (let i = 0; i < bitmaps.length; i++) {
this.ctx.drawImage(bitmaps[i], 0, positions[i]);
bitmaps[i].close(); // Free memory
@ -549,7 +526,7 @@ export class FakeShader {
private fallbackCompositeTiles(): void {
const tileHeight = Math.ceil(this.canvas.height / this.workerCount);
for (let i = 0; i < this.workerCount; i++) {
const tileData = this.tileResults.get(i);
if (tileData) {
@ -576,17 +553,21 @@ export class FakeShader {
'x^y',
'x&y',
'x|y',
'(x*y)%256',
'a|d|r',
'x|n*t^b*(t % 1.0)',
'(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',
'a^d * [b, r**t][floor(t%2.0)]',
'x%y',
'(x^(y<<2))%256',
'((x*t)^y)%256',
'(x&(y|t*8))%256',
'((x>>2)|(y<<2))%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',
@ -599,11 +580,15 @@ export class FakeShader {
'((x|t)^(y|t))%256',
];
const vars = ['x', 'y', 't', 'i'];
const ops = ['^', '&', '|', '+', '-', '*', '%'];
const vars = ['x', 'y', 't', 'i', 'a', 'd', 'n', 'r', 'u', 'v', 'd', 'b'];
const ops = ['^', '&', '|', '+', '-', '*', '%', '**', '%'];
const shifts = ['<<', '>>'];
const numbers = ['2', '4', '8', '16', '32', '64', '128', '256'];
const numbers: number[] = [];
const numCount = Math.floor(Math.random() * 20) + 10; // Generate 10-30 numbers
for (let i = 0; i < numCount; i++) {
numbers.push(Math.floor(Math.random() * 400))
}
const randomChoice = <T>(arr: T[]): T =>
arr[Math.floor(Math.random() * arr.length)];
@ -618,8 +603,7 @@ export class FakeShader {
() => `${randomChoice(vars)}^${randomChoice(vars)}^${randomChoice(vars)}`,
];
// 70% chance to pick from presets, 30% chance to generate dynamic
if (Math.random() < 0.7) {
if (Math.random() < 0.5) {
return randomChoice(presets);
} else {
return randomChoice(dynamicExpressions)();

281
src/RefactoredShader.ts Normal file
View File

@ -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<void> {
try {
await this.workerPool.compile(this.code);
this.compiled = true;
console.log('Shader compiled successfully');
} catch (error) {
console.error('Compilation failed:', error);
this.compiled = false;
throw error;
}
}
private render(id: string, currentTime: number): void {
if (!this.compiled || this.renderController.isCurrentlyRendering()) {
return;
}
this.renderController.setRenderingState(true);
this.renderController.addPendingRender(id);
// Update reusable message to avoid allocations
this.reusableMessage.id = id;
this.reusableMessage.width = this.canvas.width;
this.reusableMessage.height = this.canvas.height;
this.reusableMessage.fullWidth = this.canvas.width;
this.reusableMessage.fullHeight = this.canvas.height;
this.reusableMessage.time = currentTime;
this.reusableMessage.renderMode = this.renderMode;
this.reusableMessage.valueMode = this.valueMode;
this.reusableMessage.hueShift = this.hueShift;
// Populate input data from InputManager
this.inputManager.populateWorkerMessage(this.reusableMessage);
// Choose rendering strategy based on worker count
if (this.workerPool.getWorkerCount() > 1) {
this.workerPool.renderMultiWorker(
this.reusableMessage,
this.canvas.width,
this.canvas.height
);
} else {
this.workerPool.renderSingleWorker(this.reusableMessage);
}
}
// Public API methods
start(): void {
if (!this.compiled) {
console.warn('Cannot start rendering: shader not compiled');
return;
}
this.renderController.start();
}
stop(): void {
this.renderController.stop();
}
setCode(code: string): Promise<void> {
this.code = code;
this.compiled = false;
return this.compile();
}
setRenderMode(mode: string): void {
this.renderMode = mode;
}
setValueMode(mode: string): void {
this.valueMode = mode;
}
setHueShift(shift: number): void {
this.hueShift = shift;
}
setTargetFPS(fps: number): void {
this.renderController.setTargetFPS(fps);
}
setTimeSpeed(speed: number): void {
this.renderController.setTimeSpeed(speed);
}
// Input methods - delegated to InputManager
setMousePosition(x: number, y: number): void {
this.inputManager.setMousePosition(x, y);
}
setMousePressed(pressed: boolean): void {
this.inputManager.setMousePressed(pressed);
}
setMouseVelocity(vx: number, vy: number): void {
this.inputManager.setMouseVelocity(vx, vy);
}
setTouchData(
count: number,
x0: number = 0,
y0: number = 0,
x1: number = 0,
y1: number = 0,
scale: number = 1,
rotation: number = 0
): void {
this.inputManager.setTouchData(count, x0, y0, x1, y1, scale, rotation);
}
setAccelerometer(x: number, y: number, z: number): void {
this.inputManager.setAccelerometer(x, y, z);
}
setGyroscope(x: number, y: number, z: number): void {
this.inputManager.setGyroscope(x, y, z);
}
setAudioLevels(
level: number,
bass: number,
mid: number,
treble: number,
bpm: number
): void {
this.inputManager.setAudioLevels(level, bass, mid, treble, bpm);
}
// Getters
isCompiled(): boolean {
return this.compiled;
}
isAnimating(): boolean {
return this.renderController.isAnimating();
}
getCurrentTime(): number {
return this.renderController.getCurrentTime();
}
getFrameRate(): number {
return this.renderController.getFrameRate();
}
getWorkerCount(): number {
return this.workerPool.getWorkerCount();
}
// Cleanup
destroy(): void {
this.renderController.stop();
this.workerPool.destroy();
}
// Helper methods for generating example shader code
static getExamples(): string[] {
return [
'x^y',
'x|y',
'(x+y+t*10)%256',
'((x>>4)^(y>>4))<<4',
'(x^y^(x*y))%256',
'd * t / 2.0',
'((x&y)|(x^y))%256',
'(x+y)&255',
'x%y',
'((x*t)^y)%256',
'(x&(y|t*8))%256',
'a+d*t',
'n*t*400',
'((x>>2)|(y<<2))%88',
'(x*y*t)%256',
'(x+y*t)%256',
'(x^y^(t*16))%256',
'((x*t)&(y*t))%256',
'(x+(y<<(t%4)))%256',
'((x*t%128)^y)%256',
'(x^(y*t*2))%256',
'((x+t)*(y+t))%256',
'(x&y&(t*8))%256',
'((x|t)^(y|t))%256',
];
}
}

View File

@ -1,978 +0,0 @@
// WebWorker for safe shader compilation and execution
interface WorkerMessage {
id: string;
type: 'compile' | 'render';
code?: string;
width?: number;
height?: number;
time?: number;
renderMode?: string;
valueMode?: string; // 'integer' or 'float'
hueShift?: number; // Hue shift in degrees (0-360)
startY?: number; // Y offset for tile rendering
fullWidth?: number; // Full canvas width for center calculations
fullHeight?: number; // Full canvas height for center calculations
mouseX?: number;
mouseY?: number;
mousePressed?: boolean;
mouseVX?: number;
mouseVY?: number;
mouseClickTime?: number;
touchCount?: number;
touch0X?: number;
touch0Y?: number;
touch1X?: number;
touch1Y?: number;
pinchScale?: number;
pinchRotation?: number;
accelX?: number;
accelY?: number;
accelZ?: number;
gyroX?: number;
gyroY?: number;
gyroZ?: number;
audioLevel?: number;
bassLevel?: number;
midLevel?: number;
trebleLevel?: number;
bpm?: number;
}
interface WorkerResponse {
id: string;
type: 'compiled' | 'rendered' | 'error';
success: boolean;
imageData?: ImageData;
error?: string;
}
import { LRUCache } from './utils/LRUCache';
import { calculateColorDirect } from './utils/colorModes';
import { PERFORMANCE } from './utils/constants';
type ShaderFunction = (...args: number[]) => number;
class ShaderWorker {
private compiledFunction: ShaderFunction | null = null;
private lastCode: string = '';
private imageDataCache: LRUCache<string, ImageData> = new LRUCache(
PERFORMANCE.IMAGE_DATA_CACHE_SIZE
);
private compilationCache: LRUCache<string, ShaderFunction> = new LRUCache(
PERFORMANCE.COMPILATION_CACHE_SIZE
);
private feedbackBuffer: Float32Array | null = null;
constructor() {
self.onmessage = (e: MessageEvent<WorkerMessage>) => {
this.handleMessage(e.data);
};
}
private handleMessage(message: WorkerMessage): void {
try {
switch (message.type) {
case 'compile':
this.compileShader(message.id, message.code!);
break;
case 'render':
this.renderShader(
message.id,
message.width!,
message.height!,
message.time!,
message.renderMode || 'classic',
message.valueMode || 'integer',
message,
message.startY || 0
);
break;
}
} catch (error) {
this.postError(
message.id,
error instanceof Error ? error.message : 'Unknown error'
);
}
}
private compileShader(id: string, code: string): void {
const codeHash = this.hashCode(code);
if (code === this.lastCode && this.compiledFunction) {
this.postMessage({ id, type: 'compiled', success: true });
return;
}
// Check compilation cache
const cachedFunction = this.compilationCache.get(codeHash);
if (cachedFunction) {
this.compiledFunction = cachedFunction;
this.lastCode = code;
this.postMessage({ id, type: 'compiled', success: true });
return;
}
try {
const safeCode = this.sanitizeCode(code);
// Check if expression is static (contains no variables)
const isStatic = this.isStaticExpression(safeCode);
if (isStatic) {
// Pre-compute static value
const staticValue = this.evaluateStaticExpression(safeCode);
this.compiledFunction = () => staticValue;
} else {
this.compiledFunction = new Function(
'x',
'y',
't',
'i',
'r',
'a',
'u',
'v',
'c',
'f',
'd',
'n',
'b',
'mouseX',
'mouseY',
'mousePressed',
'mouseVX',
'mouseVY',
'mouseClickTime',
'touchCount',
'touch0X',
'touch0Y',
'touch1X',
'touch1Y',
'pinchScale',
'pinchRotation',
'accelX',
'accelY',
'accelZ',
'gyroX',
'gyroY',
'gyroZ',
'audioLevel',
'bassLevel',
'midLevel',
'trebleLevel',
'bpm',
`
// Timeout protection
const startTime = performance.now();
let iterations = 0;
function checkTimeout() {
iterations++;
if (iterations % ${PERFORMANCE.TIMEOUT_CHECK_INTERVAL} === 0 && performance.now() - startTime > ${PERFORMANCE.MAX_SHADER_TIMEOUT_MS}) {
throw new Error('Shader timeout');
}
}
return (function() {
checkTimeout();
return ${safeCode};
})();
`
) as ShaderFunction;
}
// Cache the compiled function
if (this.compiledFunction) {
this.compilationCache.set(codeHash, this.compiledFunction);
}
this.lastCode = code;
this.postMessage({ id, type: 'compiled', success: true });
} catch (error) {
this.compiledFunction = null;
this.postError(
id,
error instanceof Error ? error.message : 'Compilation failed'
);
}
}
private isStaticExpression(code: string): boolean {
// Check if code contains any variables using regex for better accuracy
const variablePattern = /\b(x|y|t|i|r|a|u|v|c|f|d|n|b|bpm|mouse[XY]|mousePressed|mouseV[XY]|mouseClickTime|touchCount|touch[01][XY]|pinchScale|pinchRotation|accel[XYZ]|gyro[XYZ]|audioLevel|bassLevel|midLevel|trebleLevel)\b/;
return !variablePattern.test(code);
}
private evaluateStaticExpression(code: string): number {
try {
// Safely evaluate numeric expression
const result = new Function(`return ${code}`)();
return isFinite(result) ? result : 0;
} catch (error) {
return 0;
}
}
private hashCode(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString(36);
}
private renderShader(
id: string,
width: number,
height: number,
time: number,
renderMode: string,
valueMode: string,
message: WorkerMessage,
startY: number = 0
): void {
if (!this.compiledFunction) {
this.postError(id, 'No compiled shader');
return;
}
const imageData = this.getOrCreateImageData(width, height);
const data = imageData.data;
const startTime = performance.now();
const maxRenderTime = PERFORMANCE.MAX_RENDER_TIME_MS;
// Initialize feedback buffer if needed
if (!this.feedbackBuffer || this.feedbackBuffer.length !== width * height) {
this.feedbackBuffer = new Float32Array(width * height);
}
try {
// Use tiled rendering for better timeout handling
this.renderTiled(
data,
width,
height,
time,
renderMode,
valueMode,
message,
startTime,
maxRenderTime,
startY
);
this.postMessage({ id, type: 'rendered', success: true, imageData });
} catch (error) {
this.postError(
id,
error instanceof Error ? error.message : 'Render failed'
);
}
}
private renderTiled(
data: Uint8ClampedArray,
width: number,
height: number,
time: number,
renderMode: string,
valueMode: string,
message: WorkerMessage,
startTime: number,
maxRenderTime: number,
yOffset: number = 0
): void {
const tileSize = PERFORMANCE.DEFAULT_TILE_SIZE;
const tilesX = Math.ceil(width / tileSize);
const tilesY = Math.ceil(height / tileSize);
for (let tileY = 0; tileY < tilesY; tileY++) {
for (let tileX = 0; tileX < tilesX; tileX++) {
// Check timeout before each tile
if (performance.now() - startTime > maxRenderTime) {
const startX = tileX * tileSize;
const startY = tileY * tileSize;
this.fillRemainingPixels(data, width, height, startY, startX);
return;
}
const tileStartX = tileX * tileSize;
const tileStartY = tileY * tileSize;
const tileEndX = Math.min(tileStartX + tileSize, width);
const tileEndY = Math.min(tileStartY + tileSize, height);
this.renderTile(
data,
width,
tileStartX,
tileStartY,
tileEndX,
tileEndY,
time,
renderMode,
valueMode,
message,
yOffset
);
}
}
}
private renderTile(
data: Uint8ClampedArray,
width: number,
startX: number,
startY: number,
endX: number,
endY: number,
time: number,
renderMode: string,
valueMode: string,
message: WorkerMessage,
yOffset: number = 0
): void {
// Get full canvas dimensions for special modes (use provided full dimensions or fall back)
const fullWidth = message.fullWidth || width;
const fullHeight = message.fullHeight || message.height! + yOffset;
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
const i = (y * width + x) * 4;
const pixelIndex = y * width + x;
// Adjust y coordinate to account for tile offset
const actualY = y + yOffset;
try {
// Calculate additional coordinate variables
const u = x / fullWidth;
const v = actualY / fullHeight;
const centerX = fullWidth / 2;
const centerY = fullHeight / 2;
const radius = Math.sqrt(
(x - centerX) ** 2 + (actualY - centerY) ** 2
);
const angle = Math.atan2(actualY - centerY, x - centerX);
const maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2);
const normalizedDistance = radius / maxDistance;
const frameCount = Math.floor(time * 60);
const manhattanDistance =
Math.abs(x - centerX) + Math.abs(actualY - centerY);
const noise = (Math.sin(x * 0.1) * Math.cos(actualY * 0.1) + 1) * 0.5;
const feedbackValue = this.feedbackBuffer
? this.feedbackBuffer[pixelIndex] || 0
: 0;
const value = this.compiledFunction!(
x,
actualY,
time,
pixelIndex,
radius,
angle,
u,
v,
normalizedDistance,
frameCount,
manhattanDistance,
noise,
feedbackValue,
message.mouseX || 0,
message.mouseY || 0,
message.mousePressed ? 1 : 0,
message.mouseVX || 0,
message.mouseVY || 0,
message.mouseClickTime || 0,
message.touchCount || 0,
message.touch0X || 0,
message.touch0Y || 0,
message.touch1X || 0,
message.touch1Y || 0,
message.pinchScale || 1,
message.pinchRotation || 0,
message.accelX || 0,
message.accelY || 0,
message.accelZ || 0,
message.gyroX || 0,
message.gyroY || 0,
message.gyroZ || 0,
message.audioLevel || 0,
message.bassLevel || 0,
message.midLevel || 0,
message.trebleLevel || 0,
message.bpm || 120
);
const safeValue = isFinite(value) ? value : 0;
const [r, g, b] = this.calculateColor(
safeValue,
renderMode,
valueMode,
message.hueShift || 0,
x,
actualY,
fullWidth,
fullHeight
);
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
data[i + 3] = 255;
// Update feedback buffer with current processed value
if (this.feedbackBuffer) {
this.feedbackBuffer[pixelIndex] = safeValue;
}
} catch (error) {
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
data[i + 3] = 255;
}
}
}
}
private fillRemainingPixels(
data: Uint8ClampedArray,
width: number,
height: number,
startY: number,
startX: number
): void {
for (let remainingY = startY; remainingY < height; remainingY++) {
const xStart = remainingY === startY ? startX : 0;
for (let remainingX = xStart; remainingX < width; remainingX++) {
const i = (remainingY * width + remainingX) * 4;
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
data[i + 3] = 255;
}
}
}
private getOrCreateImageData(width: number, height: number): ImageData {
const key = `${width}x${height}`;
let imageData = this.imageDataCache.get(key);
if (!imageData) {
imageData = new ImageData(width, height);
this.imageDataCache.set(key, imageData);
}
return imageData;
}
private calculateColor(
value: number,
renderMode: string,
valueMode: string = 'integer',
hueShift: number = 0,
x: number = 0,
y: number = 0,
width: number = 1,
height: number = 1
): [number, number, number] {
let processedValue: number;
switch (valueMode) {
case 'float':
// Float mode: treat value as 0.0-1.0, invert it (like original bitfield shaders)
processedValue = Math.max(0, Math.min(1, Math.abs(value))); // Clamp to 0-1
processedValue = 1 - processedValue; // Invert (like original)
processedValue = Math.floor(processedValue * 255); // Convert to 0-255
break;
case 'polar': {
// Polar mode: angular patterns with value-based rotation and radius influence
const centerX = width / 2;
const centerY = height / 2;
const dx = x - centerX;
const dy = y - centerY;
const radius = Math.sqrt(dx * dx + dy * dy);
const angle = Math.atan2(dy, dx); // -π to π
const normalizedAngle = (angle + Math.PI) / (2 * Math.PI); // 0 to 1
// Combine angle with radius and value for complex patterns
const radiusNorm = radius / Math.max(centerX, centerY);
const spiralEffect =
(normalizedAngle + radiusNorm * 0.5 + Math.abs(value) * 0.02) % 1;
const polarValue = Math.sin(spiralEffect * Math.PI * 8) * 0.5 + 0.5; // Create wave pattern
processedValue = Math.floor(polarValue * 255);
break;
}
case 'distance': {
// Distance mode: concentric patterns with value-based frequency and phase
const distCenterX = width / 2;
const distCenterY = height / 2;
const distance = Math.sqrt(
(x - distCenterX) ** 2 + (y - distCenterY) ** 2
);
const maxDistance = Math.sqrt(distCenterX ** 2 + distCenterY ** 2);
const normalizedDistance = distance / maxDistance; // 0 to 1
// Create concentric waves with value-controlled frequency and phase
const frequency = 8 + Math.abs(value) * 0.1; // Variable frequency
const phase = Math.abs(value) * 0.05; // Value affects phase shift
const concentricWave =
Math.sin(normalizedDistance * Math.PI * frequency + phase) * 0.5 +
0.5;
// Add some radial falloff for more interesting patterns
const falloff = 1 - Math.pow(normalizedDistance, 0.8);
const distanceValue = concentricWave * falloff;
processedValue = Math.floor(distanceValue * 255);
break;
}
case 'wave': {
// Wave mode: interference patterns from multiple wave sources
const baseFreq = 0.08;
const valueScale = Math.abs(value) * 0.001 + 1; // Scale frequency by value
let waveSum = 0;
// Create wave sources at strategic positions for interesting interference
const sources = [
{ x: width * 0.3, y: height * 0.3 },
{ x: width * 0.7, y: height * 0.3 },
{ x: width * 0.5, y: height * 0.7 },
{ x: width * 0.2, y: height * 0.8 },
];
for (const source of sources) {
const dist = Math.sqrt((x - source.x) ** 2 + (y - source.y) ** 2);
const wave = Math.sin(
dist * baseFreq * valueScale + Math.abs(value) * 0.02
);
const amplitude = 1 / (1 + dist * 0.002); // Distance-based amplitude falloff
waveSum += wave * amplitude;
}
// Normalize and enhance contrast
const waveValue = Math.tanh(waveSum) * 0.5 + 0.5; // tanh for better contrast
processedValue = Math.floor(waveValue * 255);
break;
}
case 'fractal': {
// Fractal mode: recursive pattern generation
const scale = 0.01;
let fractalValue = 0;
let amplitude = 1;
const octaves = 4;
for (let i = 0; i < octaves; i++) {
const frequency = Math.pow(2, i) * scale;
const noise =
Math.sin((x + Math.abs(value) * 0.1) * frequency) *
Math.cos((y + Math.abs(value) * 0.1) * frequency);
fractalValue += noise * amplitude;
amplitude *= 0.5;
}
processedValue = Math.floor((fractalValue + 1) * 0.5 * 255);
break;
}
case 'cellular': {
// Cellular automata-inspired patterns
const cellSize = 16;
const cellX = Math.floor(x / 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;
}
default:
// Integer mode: treat value as 0-255 (original behavior)
processedValue = Math.abs(value) % 256;
break;
}
// Use direct calculation to support hue shift
return calculateColorDirect(processedValue, renderMode, hueShift);
}
private sanitizeCode(code: string): string {
// Auto-prefix Math functions
const mathFunctions = [
'abs',
'acos',
'asin',
'atan',
'atan2',
'ceil',
'cos',
'exp',
'floor',
'log',
'max',
'min',
'pow',
'random',
'round',
'sin',
'sqrt',
'tan',
'trunc',
'sign',
'cbrt',
'hypot',
'imul',
'fround',
'clz32',
'acosh',
'asinh',
'atanh',
'cosh',
'sinh',
'tanh',
'expm1',
'log1p',
'log10',
'log2',
];
let processedCode = code;
// Replace standalone math functions with Math.function
mathFunctions.forEach((func) => {
const regex = new RegExp(`\\b${func}\\(`, 'g');
processedCode = processedCode.replace(regex, `Math.${func}(`);
});
// Add Math constants
processedCode = processedCode.replace(/\bPI\b/g, 'Math.PI');
processedCode = processedCode.replace(/\bE\b/g, 'Math.E');
processedCode = processedCode.replace(/\bLN2\b/g, 'Math.LN2');
processedCode = processedCode.replace(/\bLN10\b/g, 'Math.LN10');
processedCode = processedCode.replace(/\bLOG2E\b/g, 'Math.LOG2E');
processedCode = processedCode.replace(/\bLOG10E\b/g, 'Math.LOG10E');
processedCode = processedCode.replace(/\bSQRT1_2\b/g, 'Math.SQRT1_2');
processedCode = processedCode.replace(/\bSQRT2\b/g, 'Math.SQRT2');
return processedCode;
}
private postMessage(response: WorkerResponse): void {
self.postMessage(response);
}
private postError(id: string, error: string): void {
this.postMessage({ id, type: 'error', success: false, error });
}
}
// Initialize worker
new ShaderWorker();

View File

@ -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 {

View File

@ -1,9 +1,10 @@
import React from 'react';
import React, { useState } from 'react';
import { useStore } from '@nanostores/react';
import { uiState, hideHelp } from '../stores/ui';
export function HelpPopup() {
const ui = useStore(uiState);
const [valueModeExpanded, setValueModeExpanded] = useState(false);
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
@ -52,26 +53,26 @@ export function HelpPopup() {
<strong>M</strong> - Cycle value mode
</p>
<p>
<strong>Space</strong> - Tap tempo (when editor not focused)
<strong>Space</strong> - Tap tempo
</p>
<p>
<strong>Arrow Left/Right</strong> - Adjust hue shift (when editor not focused)
<strong>Arrow Left/Right</strong> - Adjust hue shift
</p>
<p>
<strong>Arrow Up/Down</strong> - Cycle value mode (when editor not focused)
<strong>Arrow Up/Down</strong> - Cycle value mode
</p>
<p>
<strong>Shift+Arrow Up/Down</strong> - Cycle render mode (when editor not focused)
<strong>Shift+Arrow Up/Down</strong> - Cycle render mode
</p>
</div>
<div className="help-section">
<h4>Variables</h4>
<h4>Core Variables - Basics</h4>
<p>
<strong>x, y</strong> - Pixel coordinates
</p>
<p>
<strong>t</strong> - Time (enables animation)
<strong>t</strong> - Time (enables animation) - also available as t(n) for modulo wrapping
</p>
<p>
<strong>bpm</strong> - Current BPM from tap tempo (default: 120)
@ -97,11 +98,39 @@ export function HelpPopup() {
<p>
<strong>d</strong> - Manhattan distance from center
</p>
<p>
<strong>w, h</strong> - Canvas width and height (pixels)
</p>
</div>
<div className="help-section">
<h4>Core Variables - Advanced</h4>
<p>
<strong>bx, by</strong> - Block coordinates (16-pixel chunks, great for pixelated effects)
</p>
<p>
<strong>sx, sy</strong> - Signed coordinates (centered at origin, negative to positive)
</p>
<p>
<strong>qx, qy</strong> - Quarter-block coordinates (8-pixel chunks, finer than bx/by)
</p>
<p>
<strong>n</strong> - Noise value (0.0 to 1.0)
</p>
<p>
<strong>b</strong> - Previous frame&apos;s value (feedback)
<strong>p</strong> - Phase value (0 to 2π, cycles with time)
</p>
<p>
<strong>z</strong> - Pseudo-depth coordinate (oscillates with distance and time)
</p>
<p>
<strong>j</strong> - Per-pixel jitter/random value (0.0 to 1.0, deterministic)
</p>
<p>
<strong>o</strong> - Oscillation value (wave function based on time and distance)
</p>
<p>
<strong>g</strong> - Golden ratio constant (1.618... for natural spirals)
</p>
<p>
<strong>mouseX, mouseY</strong> - Mouse position (0.0 to 1.0)
@ -117,6 +146,34 @@ export function HelpPopup() {
</p>
</div>
<div className="help-section">
<h4>Feedback Variables</h4>
<p>
<strong>b</strong> - Previous frame's luminance at this pixel (0-255)
</p>
<p>
<strong>bn, bs, be, bw</strong> - Neighbor luminance (North, South, East, West)
</p>
<p>
<strong>m</strong> - Momentum/velocity: Detects motion and change between frames
</p>
<p>
<strong>l</strong> - Laplacian/diffusion: Creates natural spreading and heat diffusion
</p>
<p>
<strong>k</strong> - Curvature/contrast: Edge detection and gradient magnitude
</p>
<p>
<strong>s</strong> - State/memory: Persistent accumulator that remembers bright areas
</p>
<p>
<strong>e</strong> - Echo/history: Temporal snapshots that recall past brightness patterns
</p>
<p>
<em>Feedback uses actual displayed brightness with natural decay and frame-rate independence for stable, evolving patterns.</em>
</p>
</div>
<div className="help-section">
<h4>Touch & Gestures</h4>
<p>
@ -191,20 +248,41 @@ export function HelpPopup() {
<strong>sin, cos, tan</strong> - Trigonometric functions
</p>
<p>
<strong>abs, sqrt, pow</strong> - Absolute, square root, power
<strong>asin, acos, atan, atan2</strong> - Inverse trigonometric functions
</p>
<p>
<strong>floor, ceil, round</strong> - Rounding functions
<strong>abs, sqrt, cbrt, pow</strong> - Absolute, square root, cube root, power
</p>
<p>
<strong>min, max</strong> - Minimum and maximum
<strong>floor, ceil, round, trunc</strong> - Rounding functions
</p>
<p>
<strong>min, max, sign</strong> - Minimum, maximum, sign (-1/0/1)
</p>
<p>
<strong>log, log10, log2, exp</strong> - Logarithmic and exponential
</p>
<p>
<strong>clamp(val, min, max)</strong> - Constrain value between min and max
</p>
<p>
<strong>lerp(a, b, t)</strong> - Linear interpolation between a and b
</p>
<p>
<strong>smooth(edge, x)</strong> - Smooth step function for gradients
</p>
<p>
<strong>step(edge, x)</strong> - Step function (0 if x&lt;edge, 1 otherwise)
</p>
<p>
<strong>fract(x)</strong> - Fractional part (x - floor(x))
</p>
<p>
<strong>mix(a, b, t)</strong> - Alias for lerp
</p>
<p>
<strong>random</strong> - Random number 0-1
</p>
<p>
<strong>log, exp</strong> - Natural logarithm, exponential
</p>
<p>
<strong>PI, E</strong> - Math constants
</p>
@ -214,29 +292,6 @@ export function HelpPopup() {
</p>
</div>
<div className="help-section">
<h4>Value Modes</h4>
<p>
<strong>Integer (0-255):</strong> Traditional mode for large values
</p>
<p>
<strong>Float (0.0-1.0):</strong> Bitfield shader mode, inverts and
clamps values
</p>
<p>
<strong>Polar (angle-based):</strong> Spiral patterns combining
angle and radius
</p>
<p>
<strong>Distance (radial):</strong> Concentric wave rings with
variable frequency
</p>
<p>
<strong>Wave (ripple):</strong> Multi-source interference with
amplitude falloff
</p>
<p>Each mode transforms your expression differently!</p>
</div>
<div className="help-section">
<h4>Advanced Features</h4>
@ -261,40 +316,120 @@ export function HelpPopup() {
<div className="help-section">
<h4>Shader Library</h4>
<p>
Hover over the <strong>left edge</strong> of the screen to access
the shader library
<strong>Access:</strong> Hover over the left edge of the screen
</p>
<p>Save shaders with custom names and search through them</p>
<p>
Use <strong>edit</strong> to rename, <strong>del</strong> to delete
<strong>Save:</strong> Click the save icon to store current shader
</p>
<p>
<strong>Search:</strong> Filter saved shaders by name
</p>
<p>
<strong>Manage:</strong> Edit names or delete with the buttons
</p>
<p>
<strong>Load:</strong> Click any shader to apply it instantly
</p>
</div>
<div className="help-section">
<h4>Render Modes</h4>
<p>
<strong>Classic</strong> - Original colorful mode
</p>
<p>
<strong>Grayscale</strong> - Black and white
</p>
<p>
<strong>Red/Green/Blue</strong> - Single color channels
</p>
<p>
<strong>HSV</strong> - Hue-based coloring
</p>
<p>
<strong>Rainbow</strong> - Spectrum coloring
</p>
</div>
</div>
<div className="help-section">
<h4>Export</h4>
<p>
<strong>Export PNG</strong> - Save current frame as image
</p>
</div>
<div
className="help-section"
style={{
gridColumn: '1 / -1',
marginTop: '10px',
}}
>
<h4
style={{
cursor: 'pointer',
userSelect: 'none',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
onClick={() => setValueModeExpanded(!valueModeExpanded)}
>
Value Modes
<span style={{ fontSize: '0.8em' }}>
{valueModeExpanded ? '' : ''}
</span>
</h4>
{valueModeExpanded && (
<div
style={{
backgroundColor: '#000000',
padding: '15px',
marginTop: '10px',
borderRadius: '0',
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '10px',
}}
>
<p>
<strong>Integer:</strong> Traditional 0-255 mode with modulo wrapping
</p>
<p>
<strong>Spiral:</strong> Logarithmic spirals with value-controlled tightness
</p>
<p>
<strong>Float:</strong> Bitfield shader mode, clamps to 0-1, inverts, scales to 0-255
</p>
<p>
<strong>Turbulence:</strong> Multi-octave turbulence with chaos control
</p>
<p>
<strong>Polar:</strong> Spiral patterns combining angle and radius rotation
</p>
<p>
<strong>Crystal:</strong> Crystalline lattice patterns on grid structure
</p>
<p>
<strong>Distance:</strong> Concentric wave rings with variable frequency
</p>
<p>
<strong>Marble:</strong> Marble-like veining with turbulent noise
</p>
<p>
<strong>Wave:</strong> Multi-source interference with amplitude falloff
</p>
<p>
<strong>Quantum:</strong> Quantum uncertainty probability distributions
</p>
<p>
<strong>Fractal:</strong> Recursive patterns using multiple octaves of noise
</p>
<p>
<strong>Logarithmic:</strong> Simple logarithmic scaling transformation
</p>
<p>
<strong>Cellular:</strong> Cellular automata-inspired neighbor calculations
</p>
<p>
<strong>Mirror:</strong> Kaleidoscope effects with symmetrical patterns
</p>
<p>
<strong>Noise:</strong> Perlin-like noise using layered sine waves
</p>
<p>
<strong>Rings:</strong> Concentric rings with controlled spacing
</p>
<p>
<strong>Warp:</strong> Space deformation with barrel/lens distortion
</p>
<p>
<strong>Mesh:</strong> Grid patterns with density and rotation control
</p>
<p>
<strong>Flow:</strong> Fluid dynamics with flow sources and vortices
</p>
<p>
<strong>Glitch:</strong> Digital corruption effects with bit manipulation
</p>
</div>
)}
</div>
<div

View File

@ -1,7 +1,7 @@
import { useStore } from '@nanostores/react';
import { useState } from 'react';
import { $appSettings, updateAppSettings } from '../stores/appSettings';
import { VALUE_MODES, ValueMode } from '../utils/constants';
import { VALUE_MODES, ValueMode, RENDER_MODES } from '../utils/constants';
import {
uiState,
toggleMobileMenu,
@ -16,29 +16,13 @@ import { useAudio } from '../hooks/useAudio';
import { LucideIcon } from '../hooks/useLucideIcon';
function getValueModeLabel(mode: string): string {
const labels: Record<string, string> = {
integer: 'Integer (0-255)',
float: 'Float (0.0-1.0)',
polar: 'Polar (angle-based)',
distance: 'Distance (radial)',
wave: 'Wave (ripple)',
fractal: 'Fractal (recursive)',
cellular: 'Cellular (automata)',
noise: 'Noise (perlin-like)',
warp: 'Warp (space deformation)',
flow: 'Flow (fluid dynamics)',
spiral: 'Spiral (logarithmic)',
turbulence: 'Turbulence (chaos)',
crystal: 'Crystal (lattice)',
marble: 'Marble (veining)',
quantum: 'Quantum (uncertainty)',
logarithmic: 'Logarithmic (scaling)',
mirror: 'Mirror (symmetrical)',
rings: 'Rings (interference)',
mesh: 'Mesh (grid rotation)',
glitch: 'Glitch (corruption)',
};
return labels[mode] || mode;
// Automatically generate human-readable labels from mode names
return mode.charAt(0).toUpperCase() + mode.slice(1).replace(/_/g, ' ');
}
function getRenderModeLabel(mode: string): string {
// Automatically generate human-readable labels from render mode names
return mode.charAt(0).toUpperCase() + mode.slice(1).replace(/_/g, ' ');
}
export function TopBar() {
@ -116,162 +100,116 @@ export function TopBar() {
};
return (
<div id="topbar" className={ui.uiVisible ? '' : 'hidden'}>
<div
id="topbar"
className={ui.uiVisible ? '' : 'hidden'}>
<div className="title">Bitfielder</div>
<div className="controls">
<div className="controls-desktop">
<label
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
<select
value={settings.resolution}
onChange={(e) =>
updateAppSettings({ resolution: parseInt(e.target.value) })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
marginRight: '10px',
}}
>
Resolution:
<select
value={settings.resolution}
onChange={(e) =>
updateAppSettings({ resolution: parseInt(e.target.value) })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
}}
>
<option value="1">Full (1x)</option>
<option value="2">Half (2x)</option>
<option value="4">Quarter (4x)</option>
<option value="8">Eighth (8x)</option>
<option value="16">Sixteenth (16x)</option>
<option value="32">Thirty-second (32x)</option>
</select>
</label>
<option value="1">Full (1x)</option>
<option value="2">Half (2x)</option>
<option value="4">Quarter (4x)</option>
<option value="8">Eighth (8x)</option>
<option value="16">Sixteenth (16x)</option>
<option value="32">Thirty-second (32x)</option>
</select>
<label
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
<select
value={settings.fps}
onChange={(e) =>
updateAppSettings({ fps: parseInt(e.target.value) })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
marginRight: '10px',
}}
>
FPS:
<select
value={settings.fps}
onChange={(e) =>
updateAppSettings({ fps: parseInt(e.target.value) })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
}}
>
<option value="15">15 FPS</option>
<option value="30">30 FPS</option>
<option value="60">60 FPS</option>
</select>
</label>
<option value="15">15 FPS</option>
<option value="30">30 FPS</option>
<option value="60">60 FPS</option>
</select>
<label
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
<select
value={settings.valueMode}
onChange={(e) =>
updateAppSettings({ valueMode: e.target.value as ValueMode })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
marginRight: '10px',
}}
>
Value Mode:
<select
value={settings.valueMode}
onChange={(e) =>
updateAppSettings({ valueMode: e.target.value as ValueMode })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
}}
>
{VALUE_MODES.map((mode) => (
<option key={mode} value={mode}>
{getValueModeLabel(mode)}
</option>
))}
</select>
</label>
{VALUE_MODES.map((mode) => (
<option key={mode} value={mode}>
{getValueModeLabel(mode)}
</option>
))}
</select>
<label
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
<select
value={settings.renderMode}
onChange={(e) =>
updateAppSettings({ renderMode: e.target.value })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
marginRight: '10px',
}}
>
Render Mode:
<select
value={settings.renderMode}
onChange={(e) =>
updateAppSettings({ renderMode: e.target.value })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
}}
>
<option value="classic">Classic</option>
<option value="grayscale">Grayscale</option>
<option value="red">Red Channel</option>
<option value="green">Green Channel</option>
<option value="blue">Blue Channel</option>
<option value="rainbow">Rainbow</option>
<option value="thermal">Thermal</option>
<option value="neon">Neon</option>
<option value="sunset">Sunset</option>
<option value="ocean">Ocean</option>
<option value="forest">Forest</option>
<option value="copper">Copper</option>
<option value="dithered">Dithered</option>
<option value="palette">Palette</option>
<option value="vintage">Vintage</option>
<option value="infrared">Infrared</option>
<option value="fire">Fire</option>
<option value="ice">Ice</option>
<option value="plasma">Plasma</option>
<option value="xray">X-Ray</option>
<option value="spectrum">Spectrum</option>
</select>
</label>
{RENDER_MODES.map((mode) => (
<option key={mode} value={mode}>
{getRenderModeLabel(mode)}
</option>
))}
</select>
<label
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
>
Hue Shift:
<input
type="range"
min="0"
max="360"
value={settings.hueShift ?? 0}
onChange={(e) =>
updateAppSettings({ hueShift: parseInt(e.target.value) })
}
style={{ width: '80px', verticalAlign: 'middle' }}
/>
<span style={{ fontSize: '11px' }}>
{settings.hueShift ?? 0}°
</span>
</label>
<input
type="range"
min="0"
max="360"
value={settings.hueShift ?? 0}
onChange={(e) =>
updateAppSettings({ hueShift: parseInt(e.target.value) })
}
style={{ width: '80px', verticalAlign: 'middle', marginRight: '10px' }}
/>
<label
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
>
UI Opacity:
<input
type="range"
min="10"
max="100"
value={Math.round((settings.uiOpacity ?? 0.3) * 100)}
onChange={(e) =>
updateAppSettings({ uiOpacity: parseInt(e.target.value) / 100 })
}
style={{ width: '80px', verticalAlign: 'middle' }}
/>
<span style={{ fontSize: '11px' }}>
{Math.round((settings.uiOpacity ?? 0.3) * 100)}%
</span>
</label>
<input
type="range"
min="10"
max="100"
value={Math.round((settings.uiOpacity ?? 0.3) * 100)}
onChange={(e) =>
updateAppSettings({ uiOpacity: parseInt(e.target.value) / 100 })
}
style={{ width: '80px', verticalAlign: 'middle', marginRight: '10px' }}
/>
<button id="help-btn" onClick={showHelp}>
<LucideIcon name="help" />

80
src/hooks/useWebcam.ts Normal file
View File

@ -0,0 +1,80 @@
import { useCallback, useRef } from 'react';
import { $input } from '../stores/input';
export function useWebcam() {
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const pixelDataRef = useRef<Uint8ClampedArray | null>(null);
const setupWebcam = useCallback(async (): Promise<boolean> => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480 }
});
// Create video element
const video = document.createElement('video');
video.srcObject = stream;
video.autoplay = true;
video.playsInline = true;
videoRef.current = video;
// Create canvas for pixel extraction
const canvas = document.createElement('canvas');
canvasRef.current = canvas;
streamRef.current = stream;
$input.set({ ...$input.get(), webcamEnabled: true });
console.log('Webcam initialized successfully');
return true;
} catch (error) {
console.error('Failed to access webcam:', error);
$input.set({ ...$input.get(), webcamEnabled: false });
return false;
}
}, []);
const disableWebcam = useCallback(() => {
$input.set({ ...$input.get(), webcamEnabled: false });
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
}
videoRef.current = null;
canvasRef.current = null;
pixelDataRef.current = null;
}, []);
const getWebcamData = useCallback((width: number, height: number): Uint8ClampedArray | null => {
if (!videoRef.current || !canvasRef.current) {
return null;
}
const video = videoRef.current;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (!ctx || video.videoWidth === 0 || video.videoHeight === 0) {
return null;
}
// Set canvas size to match shader resolution
canvas.width = width;
canvas.height = height;
// Draw video frame scaled to shader resolution
ctx.drawImage(video, 0, 0, width, height);
// Extract pixel data
const imageData = ctx.getImageData(0, 0, width, height);
pixelDataRef.current = imageData.data;
return imageData.data;
}, []);
return { setupWebcam, disableWebcam, getWebcamData };
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,53 @@
import { LRUCache } from '../../utils/LRUCache';
import { ShaderFunction } from '../types';
import { PERFORMANCE } from '../../utils/constants';
/**
* Manages caching for compiled shaders and image data
*/
export class ShaderCache {
private imageDataCache: LRUCache<string, ImageData>;
private compilationCache: LRUCache<string, ShaderFunction>;
constructor() {
this.imageDataCache = new LRUCache(PERFORMANCE.IMAGE_DATA_CACHE_SIZE);
this.compilationCache = new LRUCache(PERFORMANCE.COMPILATION_CACHE_SIZE);
}
/**
* Gets cached ImageData or creates new one
*/
getOrCreateImageData(width: number, height: number): ImageData {
const key = `${width}x${height}`;
let imageData = this.imageDataCache.get(key);
if (!imageData) {
imageData = new ImageData(width, height);
this.imageDataCache.set(key, imageData);
}
return imageData;
}
/**
* Gets cached compiled shader function
*/
getCompiledShader(codeHash: string): ShaderFunction | undefined {
return this.compilationCache.get(codeHash);
}
/**
* Caches compiled shader function
*/
setCompiledShader(codeHash: string, compiledFunction: ShaderFunction): void {
this.compilationCache.set(codeHash, compiledFunction);
}
/**
* Clears all caches
*/
clear(): void {
this.imageDataCache.clear();
this.compilationCache.clear();
}
}

View File

@ -0,0 +1,145 @@
import { ShaderFunction } from '../types';
import { PERFORMANCE } from '../../utils/constants';
/**
* Handles shader code compilation and optimization
*/
export class ShaderCompiler {
/**
* Compiles shader code into an executable function
*/
static compile(code: string): ShaderFunction {
const safeCode = this.sanitizeCode(code);
// Check if expression is static (contains no variables)
const isStatic = this.isStaticExpression(safeCode);
if (isStatic) {
// Pre-compute static value
const staticValue = this.evaluateStaticExpression(safeCode);
return (_ctx) => staticValue;
}
return new Function(
'ctx',
`
// Destructure context for backward compatibility with existing shader code
const {
x, y, t, i, r, a, u, v, c, f, d, n, b, bn, bs, be, bw,
w, h, p, z, j, o, g, m, l, k, s, e, mouseX, mouseY,
mousePressed, mouseVX, mouseVY, mouseClickTime, touchCount,
touch0X, touch0Y, touch1X, touch1Y, pinchScale, pinchRotation,
accelX, accelY, accelZ, gyroX, gyroY, gyroZ, audioLevel,
bassLevel, midLevel, trebleLevel, bpm, _t, bx, by, sx, sy, qx, qy
} = ctx;
// Shader-specific helper functions
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
const lerp = (a, b, t) => a + (b - a) * t;
const smooth = (edge, x) => { const t = Math.min(Math.max((x - edge) / (1 - edge), 0), 1); return t * t * (3 - 2 * t); };
const step = (edge, x) => x < edge ? 0 : 1;
const fract = (x) => x - Math.floor(x);
const mix = (a, b, t) => a + (b - a) * t;
// Timeout protection
const startTime = performance.now();
let iterations = 0;
function checkTimeout() {
iterations++;
if (iterations % ${PERFORMANCE.TIMEOUT_CHECK_INTERVAL} === 0 && performance.now() - startTime > ${PERFORMANCE.MAX_SHADER_TIMEOUT_MS}) {
throw new Error('Shader timeout');
}
}
return (function() {
checkTimeout();
return ${safeCode};
})();
`
) as ShaderFunction;
}
/**
* Checks if shader code contains only static expressions (no variables)
*/
private static isStaticExpression(code: string): boolean {
// Check if code contains any variables using regex for better accuracy
const variablePattern = /\b(x|y|t|i|r|a|u|v|c|f|d|n|b|bn|bs|be|bw|m|l|k|s|e|w|h|p|z|j|o|g|bpm|bx|by|sx|sy|qx|qy|mouse[XY]|mousePressed|mouseV[XY]|mouseClickTime|touchCount|touch[01][XY]|pinchScale|pinchRotation|accel[XYZ]|gyro[XYZ]|audioLevel|bassLevel|midLevel|trebleLevel)\b/;
return !variablePattern.test(code);
}
/**
* Evaluates static expressions safely
*/
private static evaluateStaticExpression(code: string): number {
try {
// Safely evaluate numeric expression
const result = new Function(`return ${code}`)();
return isFinite(result) ? result : 0;
} catch (error) {
return 0;
}
}
/**
* Sanitizes shader code by auto-prefixing Math functions and constants
*/
private static sanitizeCode(code: string): string {
// Create a single regex pattern for all replacements
const mathFunctions = [
'abs', 'acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'exp',
'floor', 'log', 'max', 'min', 'pow', 'random', 'round', 'sin',
'sqrt', 'tan', 'trunc', 'sign', 'cbrt', 'hypot', 'imul', 'fround',
'clz32', 'acosh', 'asinh', 'atanh', 'cosh', 'sinh', 'tanh',
'expm1', 'log1p', 'log10', 'log2'
];
const mathConstants = {
'PI': 'Math.PI',
'E': 'Math.E',
'LN2': 'Math.LN2',
'LN10': 'Math.LN10',
'LOG2E': 'Math.LOG2E',
'LOG10E': 'Math.LOG10E',
'SQRT1_2': 'Math.SQRT1_2',
'SQRT2': 'Math.SQRT2'
};
// Build combined regex pattern
const functionPattern = mathFunctions.join('|');
const constantPattern = Object.keys(mathConstants).join('|');
const combinedPattern = new RegExp(
`\\b(${functionPattern})\\(|\\b(${constantPattern})\\b|\\bt\\s*\\(`,
'g'
);
// Single pass replacement
const processedCode = code.replace(combinedPattern, (match, func, constant) => {
if (func) {
return `Math.${func}(`;
} else if (constant) {
return mathConstants[constant as keyof typeof mathConstants];
} else if (match.startsWith('t')) {
return '_t(';
}
return match;
});
return processedCode;
}
/**
* Generates a hash for shader code caching
*/
static hashCode(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString(36);
}
}

View File

@ -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<number, ImageData> = 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<void> {
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();
}
}

15
src/shader/index.ts Normal file
View File

@ -0,0 +1,15 @@
/**
* Public API for the shader system
*/
// Export types
export type { ShaderContext, ShaderFunction, WorkerMessage, WorkerResponse } from './types';
// Export main classes
export { FakeShader } from '../FakeShader';
// Export utilities
export { ShaderCompiler } from './core/ShaderCompiler';
export { ShaderCache } from './core/ShaderCache';
export { FeedbackSystem } from './rendering/FeedbackSystem';
export { PixelRenderer } from './rendering/PixelRenderer';

View File

@ -0,0 +1,200 @@
import { LUMINANCE_WEIGHTS } from '../../utils/constants';
/**
* Manages feedback buffers for shader rendering
*/
export class FeedbackSystem {
private feedbackBuffer: Float32Array | null = null;
private previousFeedbackBuffer: Float32Array | null = null;
private stateBuffer: Float32Array | null = null;
private echoBuffers: Float32Array[] = [];
private echoFrameCounter: number = 0;
private echoInterval: number = 30; // Store echo every 30 frames (~0.5s at 60fps)
/**
* Initializes feedback buffers for given dimensions
*/
initializeBuffers(width: number, height: number): void {
const bufferSize = width * height;
if (!this.feedbackBuffer || this.feedbackBuffer.length !== bufferSize) {
this.feedbackBuffer = new Float32Array(bufferSize);
this.previousFeedbackBuffer = new Float32Array(bufferSize);
this.stateBuffer = new Float32Array(bufferSize);
// Initialize echo buffers (4 buffers for different time delays)
this.echoBuffers = [];
for (let i = 0; i < 4; i++) {
this.echoBuffers.push(new Float32Array(bufferSize));
}
}
}
/**
* Updates feedback value for a pixel
*/
updateFeedback(pixelIndex: number, r: number, g: number, b: number, deltaTime: number): void {
if (!this.feedbackBuffer) return;
// Use the actual displayed luminance as feedback (0-255 range)
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
// Simple mixing to prevent oscillation
const previousValue = this.feedbackBuffer[pixelIndex] || 0;
const mixRatio = Math.min(deltaTime * 10, 0.3); // Max 30% new value per frame
let newFeedback = luminance * mixRatio + previousValue * (1 - mixRatio);
newFeedback *= decayFactor;
// Clamp and store
this.feedbackBuffer[pixelIndex] = Math.max(0, Math.min(255, newFeedback));
}
/**
* Updates state buffer for a pixel
*/
updateState(pixelIndex: number, stateValue: number): void {
if (!this.stateBuffer) return;
this.stateBuffer[pixelIndex] = stateValue;
}
/**
* Gets feedback value for a pixel
*/
getFeedback(pixelIndex: number): number {
return this.feedbackBuffer ? this.feedbackBuffer[pixelIndex] || 0 : 0;
}
/**
* Gets neighbor feedback values
*/
getNeighborFeedback(_pixelIndex: number, x: number, y: number, width: number, height: number): {
north: number;
south: number;
east: number;
west: number;
} {
if (!this.feedbackBuffer) {
return { north: 0, south: 0, east: 0, west: 0 };
}
let north = 0, south = 0, east = 0, west = 0;
// North neighbor (bounds safe)
if (y > 0) north = this.feedbackBuffer[(y - 1) * width + x] || 0;
// South neighbor (bounds safe)
if (y < height - 1) south = this.feedbackBuffer[(y + 1) * width + x] || 0;
// East neighbor (bounds safe)
if (x < width - 1) east = this.feedbackBuffer[y * width + (x + 1)] || 0;
// West neighbor (bounds safe)
if (x > 0) west = this.feedbackBuffer[y * width + (x - 1)] || 0;
return { north, south, east, west };
}
/**
* Calculates momentum (change from previous frame)
*/
getMomentum(pixelIndex: number): number {
if (!this.feedbackBuffer || !this.previousFeedbackBuffer) return 0;
const currentValue = this.feedbackBuffer[pixelIndex] || 0;
const previousValue = this.previousFeedbackBuffer[pixelIndex] || 0;
return (currentValue - previousValue) * 0.5; // Scale for stability
}
/**
* Calculates laplacian/diffusion
*/
getLaplacian(pixelIndex: number, x: number, y: number, width: number, height: number): number {
if (!this.feedbackBuffer) return 0;
const neighbors = this.getNeighborFeedback(pixelIndex, x, y, width, height);
const currentValue = this.feedbackBuffer[pixelIndex] || 0;
return (neighbors.north + neighbors.south + neighbors.east + neighbors.west - currentValue * 4) * 0.25;
}
/**
* Calculates curvature/contrast
*/
getCurvature(pixelIndex: number, x: number, y: number, width: number, height: number): number {
if (!this.feedbackBuffer) return 0;
const neighbors = this.getNeighborFeedback(pixelIndex, x, y, width, height);
const gradientX = (neighbors.east - neighbors.west) * 0.5;
const gradientY = (neighbors.south - neighbors.north) * 0.5;
return Math.sqrt(gradientX * gradientX + gradientY * gradientY);
}
/**
* Gets/updates state value
*/
getState(pixelIndex: number, feedbackValue: number, deltaTime: number): number {
if (!this.stateBuffer) return 0;
let currentState = this.stateBuffer[pixelIndex] || 0;
// State accumulates when feedback is high, decays when low
if (feedbackValue > 128) {
currentState = Math.min(255, currentState + deltaTime * 200); // Accumulate
} else {
currentState = Math.max(0, currentState - deltaTime * 100); // Decay
}
return currentState;
}
/**
* Gets echo value
*/
getEcho(pixelIndex: number, time: number): number {
if (this.echoBuffers.length === 0) return 0;
// Cycle through different echo delays based on time
const echoIndex = Math.floor(time * 2) % this.echoBuffers.length; // Change every 0.5 seconds
const echoBuffer = this.echoBuffers[echoIndex];
return echoBuffer ? echoBuffer[pixelIndex] || 0 : 0;
}
/**
* Updates echo buffers at regular intervals
*/
updateEchoBuffers(): void {
if (!this.feedbackBuffer || this.echoBuffers.length === 0) return;
this.echoFrameCounter++;
if (this.echoFrameCounter >= this.echoInterval) {
this.echoFrameCounter = 0;
// Rotate echo buffers: shift all buffers forward and store current in first buffer
for (let i = this.echoBuffers.length - 1; i > 0; i--) {
if (this.echoBuffers[i] && this.echoBuffers[i - 1]) {
this.echoBuffers[i].set(this.echoBuffers[i - 1]);
}
}
// Store current feedback in first echo buffer
if (this.echoBuffers[0]) {
this.echoBuffers[0].set(this.feedbackBuffer);
}
}
}
/**
* Finalizes frame processing
*/
finalizeFrame(): void {
// Copy current feedback to previous for next frame momentum calculations
if (this.feedbackBuffer && this.previousFeedbackBuffer) {
this.previousFeedbackBuffer.set(this.feedbackBuffer);
}
// Update echo buffers
this.updateEchoBuffers();
}
}

View File

@ -0,0 +1,242 @@
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
*/
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();
}
/**
* Renders a single pixel
*/
renderPixel(
data: Uint8ClampedArray,
x: number,
y: number,
actualY: number,
width: number,
time: number,
renderMode: string,
valueMode: string,
message: WorkerMessage,
compiledFunction: ShaderFunction,
// Pre-calculated constants
centerX: number,
centerY: number,
_maxDistance: number,
invMaxDistance: number,
invFullWidth: number,
invFullHeight: number,
frameCount: number,
goldenRatio: number,
phase: number,
timeTwoPi: number,
fullWidthHalf: number,
fullHeightHalf: number,
deltaTime: number
): void {
const i = (y * width + x) * 4;
const pixelIndex = y * width + x;
try {
// 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) {
angle = dy >= 0 ? Math.PI / 2 : -Math.PI / 2;
} else if (dy === 0) {
angle = dx >= 0 ? 0 : Math.PI;
} 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);
// 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, canvasHeight);
const momentum = this.feedbackSystem.getMomentum(pixelIndex);
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 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;
const sx = x - fullWidthHalf;
const sy = actualY - fullHeightHalf;
const qx = x >> 3;
const qy = actualY >> 3;
// Populate context object efficiently by reusing existing object
const ctx = this.shaderContext;
ctx.x = x;
ctx.y = actualY;
ctx.t = time;
ctx.i = pixelIndex;
ctx.r = radius;
ctx.a = angle;
ctx.u = u;
ctx.v = v;
ctx.c = normalizedDistance;
ctx.f = frameCount;
ctx.d = manhattanDistance;
ctx.n = noise;
ctx.b = feedbackValue;
ctx.bn = neighbors.north;
ctx.bs = neighbors.south;
ctx.be = neighbors.east;
ctx.bw = neighbors.west;
ctx.w = canvasWidth;
ctx.h = canvasHeight;
ctx.p = phase;
ctx.z = pseudoZ;
ctx.j = jitter;
ctx.o = oscillation;
ctx.g = goldenRatio;
ctx.m = momentum;
ctx.l = laplacian;
ctx.k = curvature;
ctx.s = stateValue;
ctx.e = echoValue;
ctx.mouseX = message.mouseX || 0;
ctx.mouseY = message.mouseY || 0;
ctx.mousePressed = message.mousePressed ? 1 : 0;
ctx.mouseVX = message.mouseVX || 0;
ctx.mouseVY = message.mouseVY || 0;
ctx.mouseClickTime = message.mouseClickTime || 0;
ctx.touchCount = message.touchCount || 0;
ctx.touch0X = message.touch0X || 0;
ctx.touch0Y = message.touch0Y || 0;
ctx.touch1X = message.touch1X || 0;
ctx.touch1Y = message.touch1Y || 0;
ctx.pinchScale = message.pinchScale || 1;
ctx.pinchRotation = message.pinchRotation || 0;
ctx.accelX = message.accelX || 0;
ctx.accelY = message.accelY || 0;
ctx.accelZ = message.accelZ || 0;
ctx.gyroX = message.gyroX || 0;
ctx.gyroY = message.gyroY || 0;
ctx.gyroZ = message.gyroZ || 0;
ctx.audioLevel = message.audioLevel || 0;
ctx.bassLevel = message.bassLevel || 0;
ctx.midLevel = message.midLevel || 0;
ctx.trebleLevel = message.trebleLevel || 0;
ctx.bpm = message.bpm || 120;
ctx._t = (mod: number) => time % mod;
ctx.bx = bx;
ctx.by = by;
ctx.sx = sx;
ctx.sy = sy;
ctx.qx = qx;
ctx.qy = qy;
// Execute shader
const value = compiledFunction(ctx);
const safeValue = isFinite(value) ? value : 0;
// Calculate color
const color = this.calculateColor(
safeValue,
renderMode,
valueMode,
message.hueShift || 0,
x,
actualY,
canvasWidth,
canvasHeight
);
// Set pixel data
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, color[0], color[1], color[2], deltaTime);
this.feedbackSystem.updateState(pixelIndex, stateValue);
} catch (error) {
// Fill with black on error
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
data[i + 3] = 255;
}
}
/**
* Calculates color from shader value
*/
private calculateColor(
value: number,
renderMode: string,
valueMode: string = 'integer',
hueShift: number = 0,
x: number = 0,
y: number = 0,
width: number = 1,
height: number = 1
): [number, number, number] {
// Use optimized strategy pattern for ALL modes
const context: PixelContext = { x, y, width, height, value };
const processor = this.valueModeRegistry.getProcessor(valueMode);
let processedValue: number;
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);
}
}

View File

@ -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<string, ValueModeProcessor> = 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 },
];
}

View File

@ -0,0 +1,112 @@
/**
* Context object passed to shader functions containing all necessary variables
* and state for shader execution. This replaces the previous 57+ parameter approach.
*/
export interface ShaderContext {
// Core coordinates and indices
x: number;
y: number;
t: number;
i: number; // pixelIndex
// Geometric properties
r: number; // radius
a: number; // angle
u: number; // normalized x (0-1)
v: number; // normalized y (0-1)
c: number; // normalizedDistance
d: number; // manhattanDistance
// Canvas properties
w: number; // canvasWidth
h: number; // canvasHeight
// Time-based properties
f: number; // frameCount
p: number; // phase
// Noise and effects
n: number; // noise
z: number; // pseudoZ
j: number; // jitter
o: number; // oscillation
g: number; // goldenRatio
// Feedback system
b: number; // feedbackValue
m: number; // momentum
l: number; // laplacian
k: number; // curvature
s: number; // stateValue
e: number; // echoValue
// Neighbor feedback
bn: number; // north neighbor
bs: number; // south neighbor
be: number; // east neighbor
bw: number; // west neighbor
// Input devices
mouseX: number;
mouseY: number;
mousePressed: number;
mouseVX: number;
mouseVY: number;
mouseClickTime: number;
// Touch input
touchCount: number;
touch0X: number;
touch0Y: number;
touch1X: number;
touch1Y: number;
pinchScale: number;
pinchRotation: number;
// Device motion
accelX: number;
accelY: number;
accelZ: number;
gyroX: number;
gyroY: number;
gyroZ: number;
// Audio
audioLevel: number;
bassLevel: number;
midLevel: number;
trebleLevel: number;
bpm: number;
// Time function
_t: (mod: number) => number;
// Block coordinates
bx: number; // block x
by: number; // block y
sx: number; // signed x
sy: number; // signed y
qx: number; // quarter block x
qy: number; // quarter block y
}
/**
* Type definition for compiled shader functions
*/
export type ShaderFunction = (ctx: ShaderContext) => number;
/**
* Creates a default shader context with zero values
*/
export function createDefaultShaderContext(): ShaderContext {
return {
x: 0, y: 0, t: 0, i: 0, r: 0, a: 0, u: 0, v: 0, c: 0, f: 0, d: 0, n: 0, b: 0,
bn: 0, bs: 0, be: 0, bw: 0, w: 0, h: 0, p: 0, z: 0, j: 0, o: 0, g: 0, m: 0,
l: 0, k: 0, s: 0, e: 0, mouseX: 0, mouseY: 0, mousePressed: 0, 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, _t: (_mod: number) => 0, bx: 0, by: 0, sx: 0, sy: 0,
qx: 0, qy: 0
};
}

View File

@ -0,0 +1,56 @@
/**
* Message types for communication between main thread and shader workers
*/
/**
* Message sent from main thread to worker
*/
export interface WorkerMessage {
id: string;
type: 'compile' | 'render';
code?: string;
width?: number;
height?: number;
time?: number;
renderMode?: string;
valueMode?: string; // 'integer' or 'float'
hueShift?: number; // Hue shift in degrees (0-360)
startY?: number; // Y offset for tile rendering
fullWidth?: number; // Full canvas width for center calculations
fullHeight?: number; // Full canvas height for center calculations
mouseX?: number;
mouseY?: number;
mousePressed?: boolean;
mouseVX?: number;
mouseVY?: number;
mouseClickTime?: number;
touchCount?: number;
touch0X?: number;
touch0Y?: number;
touch1X?: number;
touch1Y?: number;
pinchScale?: number;
pinchRotation?: number;
accelX?: number;
accelY?: number;
accelZ?: number;
gyroX?: number;
gyroY?: number;
gyroZ?: number;
audioLevel?: number;
bassLevel?: number;
midLevel?: number;
trebleLevel?: number;
bpm?: number;
}
/**
* Response message sent from worker to main thread
*/
export interface WorkerResponse {
id: string;
type: 'compiled' | 'rendered' | 'error';
success: boolean;
imageData?: ImageData;
error?: string;
}

View File

@ -0,0 +1,7 @@
/**
* Type definitions for shader system
*/
export type { ShaderContext, ShaderFunction } from './ShaderContext';
export { createDefaultShaderContext } from './ShaderContext';
export type { WorkerMessage, WorkerResponse } from './WorkerMessage';

View File

@ -0,0 +1,317 @@
import { WorkerMessage, WorkerResponse, createDefaultShaderContext } from '../types';
import { ShaderCompiler } from '../core/ShaderCompiler';
import { ShaderCache } from '../core/ShaderCache';
import { FeedbackSystem } from '../rendering/FeedbackSystem';
import { PixelRenderer } from '../rendering/PixelRenderer';
import { PERFORMANCE } from '../../utils/constants';
/**
* Main shader worker class - handles compilation and rendering
*/
class ShaderWorker {
private compiledFunction: any = null;
private lastCode: string = '';
private cache: ShaderCache;
private feedbackSystem: FeedbackSystem;
private pixelRenderer: PixelRenderer;
private shaderContext = createDefaultShaderContext();
private lastFrameTime: number = 0;
constructor() {
this.cache = new ShaderCache();
this.feedbackSystem = new FeedbackSystem();
this.pixelRenderer = new PixelRenderer(this.feedbackSystem, this.shaderContext);
self.onmessage = (e: MessageEvent<WorkerMessage>) => {
this.handleMessage(e.data);
};
}
private handleMessage(message: WorkerMessage): void {
try {
switch (message.type) {
case 'compile':
this.compileShader(message.id, message.code!);
break;
case 'render':
this.renderShader(
message.id,
message.width!,
message.height!,
message.time!,
message.renderMode || 'classic',
message.valueMode || 'integer',
message,
message.startY || 0
);
break;
}
} catch (error) {
this.postError(
message.id,
error instanceof Error ? error.message : 'Unknown error'
);
}
}
private compileShader(id: string, code: string): void {
const codeHash = ShaderCompiler.hashCode(code);
if (code === this.lastCode && this.compiledFunction) {
this.postMessage({ id, type: 'compiled', success: true });
return;
}
// Check compilation cache
const cachedFunction = this.cache.getCompiledShader(codeHash);
if (cachedFunction) {
this.compiledFunction = cachedFunction;
this.lastCode = code;
this.postMessage({ id, type: 'compiled', success: true });
return;
}
try {
this.compiledFunction = ShaderCompiler.compile(code);
// Cache the compiled function
if (this.compiledFunction) {
this.cache.setCompiledShader(codeHash, this.compiledFunction);
}
this.lastCode = code;
this.postMessage({ id, type: 'compiled', success: true });
} catch (error) {
this.compiledFunction = null;
this.postError(
id,
error instanceof Error ? error.message : 'Compilation failed'
);
}
}
private renderShader(
id: string,
width: number,
height: number,
time: number,
renderMode: string,
valueMode: string,
message: WorkerMessage,
startY: number = 0
): void {
if (!this.compiledFunction) {
this.postError(id, 'No compiled shader');
return;
}
const imageData = this.cache.getOrCreateImageData(width, height);
const data = imageData.data;
const startTime = performance.now();
const maxRenderTime = PERFORMANCE.MAX_RENDER_TIME_MS;
// Initialize feedback buffers if needed
this.feedbackSystem.initializeBuffers(width, height);
// Update frame timing for frame rate independence
const deltaTime = time - this.lastFrameTime;
this.lastFrameTime = time;
try {
// Use tiled rendering for better timeout handling
this.renderTiled(
data,
width,
height,
time,
renderMode,
valueMode,
message,
startTime,
maxRenderTime,
startY,
deltaTime
);
// Finalize frame processing
this.feedbackSystem.finalizeFrame();
this.postMessage({ id, type: 'rendered', success: true, imageData });
} catch (error) {
this.postError(
id,
error instanceof Error ? error.message : 'Render failed'
);
}
}
private renderTiled(
data: Uint8ClampedArray,
width: number,
height: number,
time: number,
renderMode: string,
valueMode: string,
message: WorkerMessage,
startTime: number,
maxRenderTime: number,
yOffset: number = 0,
deltaTime: number = 0.016
): void {
const tileSize = PERFORMANCE.DEFAULT_TILE_SIZE;
const tilesX = Math.ceil(width / tileSize);
const tilesY = Math.ceil(height / tileSize);
// Pre-calculate constants outside the loop for performance
const fullWidth = message.fullWidth || width;
const fullHeight = message.fullHeight || message.height! + yOffset;
const centerX = fullWidth / 2;
const centerY = fullHeight / 2;
const maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2);
const invMaxDistance = 1 / maxDistance;
const invFullWidth = 1 / fullWidth;
const invFullHeight = 1 / fullHeight;
const frameCount = Math.floor(time * 60);
const goldenRatio = 1.618033988749;
const phase = (time * Math.PI * 2) % (Math.PI * 2);
const timeTwoPi = time * 2 * Math.PI;
const fullWidthHalf = fullWidth >> 1;
const fullHeightHalf = fullHeight >> 1;
for (let tileY = 0; tileY < tilesY; tileY++) {
for (let tileX = 0; tileX < tilesX; tileX++) {
// Check timeout before each tile
if (performance.now() - startTime > maxRenderTime) {
const startX = tileX * tileSize;
const startY = tileY * tileSize;
this.fillRemainingPixels(data, width, height, startY, startX);
return;
}
const tileStartX = tileX * tileSize;
const tileStartY = tileY * tileSize;
const tileEndX = Math.min(tileStartX + tileSize, width);
const tileEndY = Math.min(tileStartY + tileSize, height);
this.renderTile(
data,
width,
tileStartX,
tileStartY,
tileEndX,
tileEndY,
time,
renderMode,
valueMode,
message,
yOffset,
deltaTime,
// Pre-calculated constants
centerX,
centerY,
maxDistance,
invMaxDistance,
invFullWidth,
invFullHeight,
frameCount,
goldenRatio,
phase,
timeTwoPi,
fullWidthHalf,
fullHeightHalf
);
}
}
}
private renderTile(
data: Uint8ClampedArray,
width: number,
startX: number,
startY: number,
endX: number,
endY: number,
time: number,
renderMode: string,
valueMode: string,
message: WorkerMessage,
yOffset: number,
deltaTime: number,
// Pre-calculated constants
centerX: number,
centerY: number,
maxDistance: number,
invMaxDistance: number,
invFullWidth: number,
invFullHeight: number,
frameCount: number,
goldenRatio: number,
phase: number,
timeTwoPi: number,
fullWidthHalf: number,
fullHeightHalf: number
): void {
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
const actualY = y + yOffset;
this.pixelRenderer.renderPixel(
data,
x,
y,
actualY,
width,
time,
renderMode,
valueMode,
message,
this.compiledFunction,
// Pre-calculated constants
centerX,
centerY,
maxDistance,
invMaxDistance,
invFullWidth,
invFullHeight,
frameCount,
goldenRatio,
phase,
timeTwoPi,
fullWidthHalf,
fullHeightHalf,
deltaTime
);
}
}
}
private fillRemainingPixels(
data: Uint8ClampedArray,
width: number,
height: number,
startY: number,
startX: number
): void {
for (let remainingY = startY; remainingY < height; remainingY++) {
const xStart = remainingY === startY ? startX : 0;
for (let remainingX = xStart; remainingX < width; remainingX++) {
const i = (remainingY * width + remainingX) * 4;
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
data[i + 3] = 255;
}
}
}
private postMessage(response: WorkerResponse): void {
self.postMessage(response);
}
private postError(id: string, error: string): void {
this.postMessage({ id, type: 'error', success: false, error });
}
}
// Initialize worker
new ShaderWorker();

View File

@ -25,6 +25,7 @@ export interface InputState {
midLevel: number;
trebleLevel: number;
audioEnabled: boolean;
webcamEnabled: boolean;
}
export const defaultInputState: InputState = {
@ -52,6 +53,7 @@ export const defaultInputState: InputState = {
midLevel: 0,
trebleLevel: 0,
audioEnabled: false,
webcamEnabled: false,
};
export const $input = atom<InputState>(defaultInputState);

View File

@ -54,7 +54,7 @@ a:visited {
top: 0;
left: 0;
right: 0;
height: 40px;
height: 56px;
background: rgba(0, 0, 0, var(--ui-opacity));
border-bottom: 1px solid #333;
display: flex;
@ -117,11 +117,11 @@ a:visited {
#mobile-menu {
position: fixed;
top: 40px;
top: 56px;
right: -320px;
width: 320px;
max-width: 80vw;
height: calc(100vh - 40px);
height: calc(100vh - 56px);
background: rgba(0, 0, 0, var(--ui-opacity));
backdrop-filter: blur(3px);
border-left: 1px solid rgba(255, 255, 255, 0.1);
@ -517,10 +517,10 @@ button [data-lucide] {
#shader-library {
position: fixed;
top: 40px;
top: 56px;
left: -300px;
width: 300px;
height: calc(100vh - 40px);
height: calc(100vh - 56px);
background: rgba(0, 0, 0, calc(var(--ui-opacity) + 0.1));
border-right: 1px solid rgba(255, 255, 255, 0.1);
z-index: 90;
@ -533,10 +533,10 @@ button [data-lucide] {
#shader-library-trigger {
position: fixed;
top: 40px;
top: 56px;
left: 0;
width: 20px;
height: calc(100vh - 40px);
height: calc(100vh - 56px);
z-index: 91;
cursor: pointer;
}
@ -725,7 +725,7 @@ button [data-lucide] {
}
#topbar {
height: 40px;
height: 56px;
padding: 0 10px;
}
@ -778,8 +778,8 @@ button [data-lucide] {
#shader-library {
width: 100%;
left: -100%;
top: 40px;
height: calc(100vh - 40px);
top: 56px;
height: calc(100vh - 56px);
}
#shader-library-trigger {

View File

@ -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,108 +373,200 @@ 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 / RGB.MAX_VALUE;
const phase = t * Math.PI * 2;
const r = Math.sin(phase) * 0.5 + 0.5;
const g = Math.sin(phase + Math.PI * 0.66) * 0.5 + 0.5;
const b = Math.sin(phase + Math.PI * 1.33) * 0.5 + 0.5;
const intensity = Math.pow(t, 0.8);
const glow = Math.sin(t * Math.PI * 6) * 0.2 + 0.8;
return [
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
[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
[128, 64, 0], // Brown
[64, 128, 0], // Olive
[0, 64, 128], // Navy
[128, 0, 64], // Maroon
[64, 0, 128], // Indigo
[0, 128, 64], // Teal
[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 / RGB.MAX_VALUE) * (palette.length - 1));
return palette[index] as [number, number, number];
}
// Color palette registry - automatically maps render modes to color functions
const COLOR_PALETTE_REGISTRY: Record<string, (value: number) => [number, number, number]> = {
classic: (value) => [value, (value * 2) % 256, (value * 3) % 256],
grayscale: (value) => [value, value, value],
red: (value) => [value, 0, 0],
green: (value) => [0, value, 0],
blue: (value) => [0, 0, value],
rgb: (value) => [value, (value * 2) % 256, (value * 3) % 256], // Same as classic for now
forest: forestColor,
copper: copperColor,
rainbow: rainbowColor,
thermal: thermalColor,
neon: neonColor,
cyberpunk: cyberpunkColor,
vaporwave: plasmaColor, // Use plasma for vaporwave theme
sunset: sunsetColor,
ocean: oceanColor,
dithered: ditheredColor,
palette: paletteColor,
vintage: vintageColor,
plasma: plasmaColor,
fire: fireColor,
ice: iceColor,
infrared: infraredColor,
xray: xrayColor,
spectrum: spectrumColor,
acid: acidColor,
quantum: quantumColor,
neonstrike: neonStrikeColor,
eightbit: eightBitColor,
silk: silkColor,
binary: binaryColor,
palette16: palette16Color,
palette32: palette32Color,
};
export function calculateColorDirect(
absValue: number,
renderMode: string,
hueShift: number = 0
): [number, number, number] {
let color: [number, number, number];
switch (renderMode) {
case 'classic':
color = [absValue, (absValue * 2) % 256, (absValue * 3) % 256];
break;
case 'grayscale':
color = [absValue, absValue, absValue];
break;
case 'red':
color = [absValue, 0, 0];
break;
case 'green':
color = [0, absValue, 0];
break;
case 'blue':
color = [0, 0, absValue];
break;
case 'forest':
color = forestColor(absValue);
break;
case 'copper':
color = copperColor(absValue);
break;
case 'rainbow':
color = rainbowColor(absValue);
break;
case 'thermal':
color = thermalColor(absValue);
break;
case 'neon':
color = neonColor(absValue);
break;
case 'sunset':
color = sunsetColor(absValue);
break;
case 'ocean':
color = oceanColor(absValue);
break;
case 'dithered':
color = ditheredColor(absValue);
break;
case 'palette':
color = paletteColor(absValue);
break;
case 'vintage':
color = vintageColor(absValue);
break;
case 'plasma':
color = plasmaColor(absValue);
break;
case 'fire':
color = fireColor(absValue);
break;
case 'ice':
color = iceColor(absValue);
break;
case 'infrared':
color = infraredColor(absValue);
break;
case 'xray':
color = xrayColor(absValue);
break;
case 'spectrum':
color = spectrumColor(absValue);
break;
default:
color = [absValue, absValue, absValue];
break;
}
const colorFunction = COLOR_PALETTE_REGISTRY[renderMode];
const color = colorFunction ? colorFunction(absValue) : [absValue, absValue, absValue] as [number, number, number];
return applyHueShift(color, hueShift);
}

View File

@ -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',
@ -46,6 +129,14 @@ export const RENDER_MODES = [
'plasma',
'xray',
'spectrum',
'acid',
'quantum',
'neonstrike',
'eightbit',
'silk',
'binary',
'palette16',
'palette32',
] as const;
export type RenderMode = (typeof RENDER_MODES)[number];
@ -87,13 +178,49 @@ export const VALUE_MODES = [
'rings',
'mesh',
'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: 1,
RESOLUTION: 8,
FPS: 30,
RENDER_MODE: 'classic',
VALUE_MODE: 'integer' as ValueMode,