Compare commits
6 Commits
9bf5d40171
...
2cee4084c0
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cee4084c0 | |||
| 3eeafc1277 | |||
| 431966d498 | |||
| 2cf306ee8c | |||
| d64b3839e8 | |||
| 80537a4a30 |
@ -15,6 +15,7 @@ const CORE_ASSETS = [
|
|||||||
const DYNAMIC_ASSETS_PATTERNS = [
|
const DYNAMIC_ASSETS_PATTERNS = [
|
||||||
/\/src\/.+\.(ts|tsx|js|jsx)$/,
|
/\/src\/.+\.(ts|tsx|js|jsx)$/,
|
||||||
/\/src\/.+\.css$/,
|
/\/src\/.+\.css$/,
|
||||||
|
/\/assets\/.+\.(js|css)$/,
|
||||||
/fonts\.googleapis\.com/,
|
/fonts\.googleapis\.com/,
|
||||||
/fonts\.gstatic\.com/
|
/fonts\.gstatic\.com/
|
||||||
];
|
];
|
||||||
@ -30,6 +31,10 @@ self.addEventListener('install', event => {
|
|||||||
}),
|
}),
|
||||||
caches.open(DYNAMIC_CACHE).then(cache => {
|
caches.open(DYNAMIC_CACHE).then(cache => {
|
||||||
console.log('Dynamic cache initialized');
|
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(() => {
|
]).then(() => {
|
||||||
console.log('Service Worker installed successfully');
|
console.log('Service Worker installed successfully');
|
||||||
|
|||||||
@ -1,47 +1,5 @@
|
|||||||
interface WorkerMessage {
|
import { WorkerMessage, WorkerResponse } from './shader/types';
|
||||||
id: string;
|
import { TIMING, WORKER, DEFAULTS } from './utils/constants';
|
||||||
type: 'compile' | 'render';
|
|
||||||
code?: string;
|
|
||||||
width?: number;
|
|
||||||
height?: number;
|
|
||||||
time?: number;
|
|
||||||
renderMode?: string;
|
|
||||||
valueMode?: string;
|
|
||||||
hueShift?: number;
|
|
||||||
startY?: number; // Y offset for tile rendering
|
|
||||||
mouseX?: number;
|
|
||||||
mouseY?: number;
|
|
||||||
mousePressed?: boolean;
|
|
||||||
mouseVX?: number;
|
|
||||||
mouseVY?: number;
|
|
||||||
mouseClickTime?: number;
|
|
||||||
touchCount?: number;
|
|
||||||
touch0X?: number;
|
|
||||||
touch0Y?: number;
|
|
||||||
touch1X?: number;
|
|
||||||
touch1Y?: number;
|
|
||||||
pinchScale?: number;
|
|
||||||
pinchRotation?: number;
|
|
||||||
accelX?: number;
|
|
||||||
accelY?: number;
|
|
||||||
accelZ?: number;
|
|
||||||
gyroX?: number;
|
|
||||||
gyroY?: number;
|
|
||||||
gyroZ?: number;
|
|
||||||
audioLevel?: number;
|
|
||||||
bassLevel?: number;
|
|
||||||
midLevel?: number;
|
|
||||||
trebleLevel?: number;
|
|
||||||
bpm?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorkerResponse {
|
|
||||||
id: string;
|
|
||||||
type: 'compiled' | 'rendered' | 'error';
|
|
||||||
success: boolean;
|
|
||||||
imageData?: ImageData;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FakeShader {
|
export class FakeShader {
|
||||||
private canvas: HTMLCanvasElement;
|
private canvas: HTMLCanvasElement;
|
||||||
@ -58,8 +16,50 @@ export class FakeShader {
|
|||||||
private renderMode: string = 'classic';
|
private renderMode: string = 'classic';
|
||||||
private valueMode: string = 'integer';
|
private valueMode: string = 'integer';
|
||||||
private hueShift: number = 0;
|
private hueShift: number = 0;
|
||||||
private timeSpeed: number = 1.0;
|
private timeSpeed: number = DEFAULTS.TIME_SPEED;
|
||||||
private currentBPM: number = 120;
|
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
|
// Multi-worker state
|
||||||
private tileResults: Map<number, ImageData> = new Map();
|
private tileResults: Map<number, ImageData> = new Map();
|
||||||
@ -77,7 +77,7 @@ export class FakeShader {
|
|||||||
private touch0Y: number = 0;
|
private touch0Y: number = 0;
|
||||||
private touch1X: number = 0;
|
private touch1X: number = 0;
|
||||||
private touch1Y: number = 0;
|
private touch1Y: number = 0;
|
||||||
private pinchScale: number = 1;
|
private pinchScale: number = WORKER.DEFAULT_PINCH_SCALE;
|
||||||
private pinchRotation: number = 0;
|
private pinchRotation: number = 0;
|
||||||
private accelX: number = 0;
|
private accelX: number = 0;
|
||||||
private accelY: number = 0;
|
private accelY: number = 0;
|
||||||
@ -91,8 +91,8 @@ export class FakeShader {
|
|||||||
private trebleLevel: number = 0;
|
private trebleLevel: number = 0;
|
||||||
|
|
||||||
// Frame rate limiting
|
// Frame rate limiting
|
||||||
private targetFPS: number = 30;
|
private targetFPS: number = TIMING.DEFAULT_FPS;
|
||||||
private frameInterval: number = 1000 / this.targetFPS;
|
private frameInterval: number = TIMING.MILLISECONDS_PER_SECOND / this.targetFPS;
|
||||||
private lastFrameTime: number = 0;
|
private lastFrameTime: number = 0;
|
||||||
|
|
||||||
constructor(canvas: HTMLCanvasElement, code: string = 'x^y') {
|
constructor(canvas: HTMLCanvasElement, code: string = 'x^y') {
|
||||||
@ -104,10 +104,10 @@ export class FakeShader {
|
|||||||
this.initializeOffscreenCanvas();
|
this.initializeOffscreenCanvas();
|
||||||
|
|
||||||
// Always use maximum available cores
|
// 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
|
// Some browsers report logical processors (hyperthreading), which is good
|
||||||
// But cap at a reasonable maximum to avoid overhead
|
// 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(
|
console.log(
|
||||||
`Auto-detected ${this.workerCount} CPU cores, using all for maximum performance`
|
`Auto-detected ${this.workerCount} CPU cores, using all for maximum performance`
|
||||||
);
|
);
|
||||||
@ -137,7 +137,7 @@ export class FakeShader {
|
|||||||
private initializeWorkers(): void {
|
private initializeWorkers(): void {
|
||||||
// Create worker pool
|
// Create worker pool
|
||||||
for (let i = 0; i < this.workerCount; i++) {
|
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',
|
type: 'module',
|
||||||
});
|
});
|
||||||
worker.onmessage = (e: MessageEvent<WorkerResponse>) =>
|
worker.onmessage = (e: MessageEvent<WorkerResponse>) =>
|
||||||
@ -203,7 +203,7 @@ export class FakeShader {
|
|||||||
|
|
||||||
private compile(): void {
|
private compile(): void {
|
||||||
this.isCompiled = false;
|
this.isCompiled = false;
|
||||||
const id = `compile_${Date.now()}`;
|
const id = `compile_${++this.idCounter}`;
|
||||||
|
|
||||||
// Send compile message to all workers
|
// Send compile message to all workers
|
||||||
this.workers.forEach((worker) => {
|
this.workers.forEach((worker) => {
|
||||||
@ -222,7 +222,7 @@ export class FakeShader {
|
|||||||
|
|
||||||
this.isRendering = true;
|
this.isRendering = true;
|
||||||
// this._currentRenderID = id; // Removed unused property
|
// 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
|
// Always use multiple workers if available
|
||||||
if (this.workerCount > 1) {
|
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 {
|
private renderWithSingleWorker(id: string, currentTime: number): void {
|
||||||
this.worker.postMessage({
|
this.updateReusableMessage(id, currentTime, this.canvas.width, this.canvas.height, this.canvas.width, this.canvas.height, 0);
|
||||||
id,
|
this.worker.postMessage(this.reusableMessage);
|
||||||
type: 'render',
|
|
||||||
width: this.canvas.width,
|
|
||||||
height: this.canvas.height,
|
|
||||||
fullWidth: this.canvas.width,
|
|
||||||
fullHeight: this.canvas.height,
|
|
||||||
time: currentTime,
|
|
||||||
renderMode: this.renderMode,
|
|
||||||
valueMode: this.valueMode,
|
|
||||||
hueShift: this.hueShift,
|
|
||||||
mouseX: this.mouseX,
|
|
||||||
mouseY: this.mouseY,
|
|
||||||
mousePressed: this.mousePressed,
|
|
||||||
mouseVX: this.mouseVX,
|
|
||||||
mouseVY: this.mouseVY,
|
|
||||||
mouseClickTime: this.mouseClickTime,
|
|
||||||
touchCount: this.touchCount,
|
|
||||||
touch0X: this.touch0X,
|
|
||||||
touch0Y: this.touch0Y,
|
|
||||||
touch1X: this.touch1X,
|
|
||||||
touch1Y: this.touch1Y,
|
|
||||||
pinchScale: this.pinchScale,
|
|
||||||
pinchRotation: this.pinchRotation,
|
|
||||||
accelX: this.accelX,
|
|
||||||
accelY: this.accelY,
|
|
||||||
accelZ: this.accelZ,
|
|
||||||
gyroX: this.gyroX,
|
|
||||||
gyroY: this.gyroY,
|
|
||||||
gyroZ: this.gyroZ,
|
|
||||||
audioLevel: this.audioLevel,
|
|
||||||
bassLevel: this.bassLevel,
|
|
||||||
midLevel: this.midLevel,
|
|
||||||
trebleLevel: this.trebleLevel,
|
|
||||||
bpm: this.currentBPM,
|
|
||||||
} as WorkerMessage);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderWithMultipleWorkers(id: string, currentTime: number): void {
|
private renderWithMultipleWorkers(id: string, currentTime: number): void {
|
||||||
@ -288,45 +292,18 @@ export class FakeShader {
|
|||||||
|
|
||||||
if (startY >= height) return; // Skip if tile is outside canvas
|
if (startY >= height) return; // Skip if tile is outside canvas
|
||||||
|
|
||||||
worker.postMessage({
|
// Update reusable message with worker-specific values
|
||||||
id: `${id}_tile_${index}`,
|
this.updateReusableMessage(
|
||||||
type: 'render',
|
`${id}_tile_${index}`,
|
||||||
width: width,
|
currentTime,
|
||||||
height: endY - startY,
|
width,
|
||||||
// Pass the Y offset for correct coordinate calculation
|
endY - startY,
|
||||||
startY: startY,
|
width,
|
||||||
// Pass full canvas dimensions for center calculations
|
height,
|
||||||
fullWidth: width,
|
startY
|
||||||
fullHeight: height,
|
);
|
||||||
time: currentTime,
|
|
||||||
renderMode: this.renderMode,
|
worker.postMessage(this.reusableMessage);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,7 +333,7 @@ export class FakeShader {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderId = `render_${Date.now()}_${Math.random()}`;
|
const renderId = `render_${++this.idCounter}`;
|
||||||
|
|
||||||
// Add to pending renders queue
|
// Add to pending renders queue
|
||||||
this.pendingRenders.push(renderId);
|
this.pendingRenders.push(renderId);
|
||||||
@ -390,8 +367,8 @@ export class FakeShader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setTargetFPS(fps: number): void {
|
setTargetFPS(fps: number): void {
|
||||||
this.targetFPS = Math.max(1, Math.min(120, fps)); // Clamp between 1-120 FPS
|
this.targetFPS = Math.max(TIMING.MIN_FPS, Math.min(TIMING.MAX_FPS, fps)); // Clamp between 1-120 FPS
|
||||||
this.frameInterval = 1000 / this.targetFPS;
|
this.frameInterval = TIMING.MILLISECONDS_PER_SECOND / this.targetFPS;
|
||||||
}
|
}
|
||||||
|
|
||||||
setRenderMode(mode: string): void {
|
setRenderMode(mode: string): void {
|
||||||
@ -507,7 +484,7 @@ export class FakeShader {
|
|||||||
try {
|
try {
|
||||||
const bitmapPromises: Promise<ImageBitmap>[] = [];
|
const bitmapPromises: Promise<ImageBitmap>[] = [];
|
||||||
const positions: number[] = [];
|
const positions: number[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < this.workerCount; i++) {
|
for (let i = 0; i < this.workerCount; i++) {
|
||||||
const tileData = this.tileResults.get(i);
|
const tileData = this.tileResults.get(i);
|
||||||
if (tileData) {
|
if (tileData) {
|
||||||
@ -515,9 +492,9 @@ export class FakeShader {
|
|||||||
positions.push(i * tileHeight);
|
positions.push(i * tileHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bitmaps = await Promise.all(bitmapPromises);
|
const bitmaps = await Promise.all(bitmapPromises);
|
||||||
|
|
||||||
for (let i = 0; i < bitmaps.length; i++) {
|
for (let i = 0; i < bitmaps.length; i++) {
|
||||||
this.ctx.drawImage(bitmaps[i], 0, positions[i]);
|
this.ctx.drawImage(bitmaps[i], 0, positions[i]);
|
||||||
bitmaps[i].close(); // Free memory
|
bitmaps[i].close(); // Free memory
|
||||||
@ -549,7 +526,7 @@ export class FakeShader {
|
|||||||
|
|
||||||
private fallbackCompositeTiles(): void {
|
private fallbackCompositeTiles(): void {
|
||||||
const tileHeight = Math.ceil(this.canvas.height / this.workerCount);
|
const tileHeight = Math.ceil(this.canvas.height / this.workerCount);
|
||||||
|
|
||||||
for (let i = 0; i < this.workerCount; i++) {
|
for (let i = 0; i < this.workerCount; i++) {
|
||||||
const tileData = this.tileResults.get(i);
|
const tileData = this.tileResults.get(i);
|
||||||
if (tileData) {
|
if (tileData) {
|
||||||
@ -576,17 +553,21 @@ export class FakeShader {
|
|||||||
'x^y',
|
'x^y',
|
||||||
'x&y',
|
'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+y+t*10)%256',
|
||||||
'((x>>4)^(y>>4))<<4',
|
'((x>>4)^(y>>4))<<4',
|
||||||
'(x^y^(x*y))%256',
|
'(x^y^(x*y))%256',
|
||||||
|
'd * t / 2.0',
|
||||||
'((x&y)|(x^y))%256',
|
'((x&y)|(x^y))%256',
|
||||||
'(x+y)&255',
|
'(x+y)&255',
|
||||||
|
'a^d * [b, r**t][floor(t%2.0)]',
|
||||||
'x%y',
|
'x%y',
|
||||||
'(x^(y<<2))%256',
|
|
||||||
'((x*t)^y)%256',
|
'((x*t)^y)%256',
|
||||||
'(x&(y|t*8))%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)%256',
|
'(x+y*t)%256',
|
||||||
'(x^y^(t*16))%256',
|
'(x^y^(t*16))%256',
|
||||||
@ -599,11 +580,15 @@ export class FakeShader {
|
|||||||
'((x|t)^(y|t))%256',
|
'((x|t)^(y|t))%256',
|
||||||
];
|
];
|
||||||
|
|
||||||
const vars = ['x', 'y', 't', 'i'];
|
const vars = ['x', 'y', 't', 'i', 'a', 'd', 'n', 'r', 'u', 'v', 'd', 'b'];
|
||||||
const ops = ['^', '&', '|', '+', '-', '*', '%'];
|
const ops = ['^', '&', '|', '+', '-', '*', '%', '**', '%'];
|
||||||
const shifts = ['<<', '>>'];
|
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 =>
|
const randomChoice = <T>(arr: T[]): T =>
|
||||||
arr[Math.floor(Math.random() * arr.length)];
|
arr[Math.floor(Math.random() * arr.length)];
|
||||||
|
|
||||||
@ -618,8 +603,7 @@ export class FakeShader {
|
|||||||
() => `${randomChoice(vars)}^${randomChoice(vars)}^${randomChoice(vars)}`,
|
() => `${randomChoice(vars)}^${randomChoice(vars)}^${randomChoice(vars)}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
// 70% chance to pick from presets, 30% chance to generate dynamic
|
if (Math.random() < 0.5) {
|
||||||
if (Math.random() < 0.7) {
|
|
||||||
return randomChoice(presets);
|
return randomChoice(presets);
|
||||||
} else {
|
} else {
|
||||||
return randomChoice(dynamicExpressions)();
|
return randomChoice(dynamicExpressions)();
|
||||||
|
|||||||
281
src/RefactoredShader.ts
Normal file
281
src/RefactoredShader.ts
Normal 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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
|
||||||
@ -3,6 +3,7 @@ import {
|
|||||||
STORAGE_KEYS,
|
STORAGE_KEYS,
|
||||||
PERFORMANCE,
|
PERFORMANCE,
|
||||||
DEFAULTS,
|
DEFAULTS,
|
||||||
|
FORMAT,
|
||||||
ValueMode,
|
ValueMode,
|
||||||
} from './utils/constants';
|
} from './utils/constants';
|
||||||
|
|
||||||
@ -146,12 +147,12 @@ export class Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static generateId(): string {
|
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 {
|
static exportShaders(): string {
|
||||||
const shaders = this.getShaders();
|
const shaders = this.getShaders();
|
||||||
return JSON.stringify(shaders, null, 2);
|
return JSON.stringify(shaders, null, FORMAT.JSON_INDENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
static importShaders(jsonData: string): boolean {
|
static importShaders(jsonData: string): boolean {
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { uiState, hideHelp } from '../stores/ui';
|
import { uiState, hideHelp } from '../stores/ui';
|
||||||
|
|
||||||
export function HelpPopup() {
|
export function HelpPopup() {
|
||||||
const ui = useStore(uiState);
|
const ui = useStore(uiState);
|
||||||
|
const [valueModeExpanded, setValueModeExpanded] = useState(false);
|
||||||
|
|
||||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
@ -52,26 +53,26 @@ export function HelpPopup() {
|
|||||||
<strong>M</strong> - Cycle value mode
|
<strong>M</strong> - Cycle value mode
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Space</strong> - Tap tempo (when editor not focused)
|
<strong>Space</strong> - Tap tempo
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Arrow Left/Right</strong> - Adjust hue shift (when editor not focused)
|
<strong>Arrow Left/Right</strong> - Adjust hue shift
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Arrow Up/Down</strong> - Cycle value mode (when editor not focused)
|
<strong>Arrow Up/Down</strong> - Cycle value mode
|
||||||
</p>
|
</p>
|
||||||
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="help-section">
|
<div className="help-section">
|
||||||
<h4>Variables</h4>
|
<h4>Core Variables - Basics</h4>
|
||||||
<p>
|
<p>
|
||||||
<strong>x, y</strong> - Pixel coordinates
|
<strong>x, y</strong> - Pixel coordinates
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>t</strong> - Time (enables animation)
|
<strong>t</strong> - Time (enables animation) - also available as t(n) for modulo wrapping
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>bpm</strong> - Current BPM from tap tempo (default: 120)
|
<strong>bpm</strong> - Current BPM from tap tempo (default: 120)
|
||||||
@ -97,11 +98,39 @@ export function HelpPopup() {
|
|||||||
<p>
|
<p>
|
||||||
<strong>d</strong> - Manhattan distance from center
|
<strong>d</strong> - Manhattan distance from center
|
||||||
</p>
|
</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>
|
<p>
|
||||||
<strong>n</strong> - Noise value (0.0 to 1.0)
|
<strong>n</strong> - Noise value (0.0 to 1.0)
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>b</strong> - Previous frame'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>
|
||||||
<p>
|
<p>
|
||||||
<strong>mouseX, mouseY</strong> - Mouse position (0.0 to 1.0)
|
<strong>mouseX, mouseY</strong> - Mouse position (0.0 to 1.0)
|
||||||
@ -117,6 +146,34 @@ export function HelpPopup() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div className="help-section">
|
||||||
<h4>Touch & Gestures</h4>
|
<h4>Touch & Gestures</h4>
|
||||||
<p>
|
<p>
|
||||||
@ -191,20 +248,41 @@ export function HelpPopup() {
|
|||||||
<strong>sin, cos, tan</strong> - Trigonometric functions
|
<strong>sin, cos, tan</strong> - Trigonometric functions
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>abs, sqrt, pow</strong> - Absolute, square root, power
|
<strong>asin, acos, atan, atan2</strong> - Inverse trigonometric functions
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>floor, ceil, round</strong> - Rounding functions
|
<strong>abs, sqrt, cbrt, pow</strong> - Absolute, square root, cube root, power
|
||||||
</p>
|
</p>
|
||||||
<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<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>
|
||||||
<p>
|
<p>
|
||||||
<strong>random</strong> - Random number 0-1
|
<strong>random</strong> - Random number 0-1
|
||||||
</p>
|
</p>
|
||||||
<p>
|
|
||||||
<strong>log, exp</strong> - Natural logarithm, exponential
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
<strong>PI, E</strong> - Math constants
|
<strong>PI, E</strong> - Math constants
|
||||||
</p>
|
</p>
|
||||||
@ -214,29 +292,6 @@ export function HelpPopup() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div className="help-section">
|
||||||
<h4>Advanced Features</h4>
|
<h4>Advanced Features</h4>
|
||||||
@ -261,40 +316,120 @@ export function HelpPopup() {
|
|||||||
<div className="help-section">
|
<div className="help-section">
|
||||||
<h4>Shader Library</h4>
|
<h4>Shader Library</h4>
|
||||||
<p>
|
<p>
|
||||||
Hover over the <strong>left edge</strong> of the screen to access
|
<strong>Access:</strong> Hover over the left edge of the screen
|
||||||
the shader library
|
|
||||||
</p>
|
</p>
|
||||||
<p>Save shaders with custom names and search through them</p>
|
|
||||||
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="help-section">
|
</div>
|
||||||
<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 className="help-section">
|
<div
|
||||||
<h4>Export</h4>
|
className="help-section"
|
||||||
<p>
|
style={{
|
||||||
<strong>Export PNG</strong> - Save current frame as image
|
gridColumn: '1 / -1',
|
||||||
</p>
|
marginTop: '10px',
|
||||||
</div>
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { $appSettings, updateAppSettings } from '../stores/appSettings';
|
import { $appSettings, updateAppSettings } from '../stores/appSettings';
|
||||||
import { VALUE_MODES, ValueMode } from '../utils/constants';
|
import { VALUE_MODES, ValueMode, RENDER_MODES } from '../utils/constants';
|
||||||
import {
|
import {
|
||||||
uiState,
|
uiState,
|
||||||
toggleMobileMenu,
|
toggleMobileMenu,
|
||||||
@ -16,29 +16,13 @@ import { useAudio } from '../hooks/useAudio';
|
|||||||
import { LucideIcon } from '../hooks/useLucideIcon';
|
import { LucideIcon } from '../hooks/useLucideIcon';
|
||||||
|
|
||||||
function getValueModeLabel(mode: string): string {
|
function getValueModeLabel(mode: string): string {
|
||||||
const labels: Record<string, string> = {
|
// Automatically generate human-readable labels from mode names
|
||||||
integer: 'Integer (0-255)',
|
return mode.charAt(0).toUpperCase() + mode.slice(1).replace(/_/g, ' ');
|
||||||
float: 'Float (0.0-1.0)',
|
}
|
||||||
polar: 'Polar (angle-based)',
|
|
||||||
distance: 'Distance (radial)',
|
function getRenderModeLabel(mode: string): string {
|
||||||
wave: 'Wave (ripple)',
|
// Automatically generate human-readable labels from render mode names
|
||||||
fractal: 'Fractal (recursive)',
|
return mode.charAt(0).toUpperCase() + mode.slice(1).replace(/_/g, ' ');
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TopBar() {
|
export function TopBar() {
|
||||||
@ -116,162 +100,116 @@ export function TopBar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="topbar" className={ui.uiVisible ? '' : 'hidden'}>
|
<div
|
||||||
|
id="topbar"
|
||||||
|
className={ui.uiVisible ? '' : 'hidden'}>
|
||||||
<div className="title">Bitfielder</div>
|
<div className="title">Bitfielder</div>
|
||||||
<div className="controls">
|
<div className="controls">
|
||||||
<div className="controls-desktop">
|
<div className="controls-desktop">
|
||||||
<label
|
<select
|
||||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
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:
|
<option value="1">Full (1x)</option>
|
||||||
<select
|
<option value="2">Half (2x)</option>
|
||||||
value={settings.resolution}
|
<option value="4">Quarter (4x)</option>
|
||||||
onChange={(e) =>
|
<option value="8">Eighth (8x)</option>
|
||||||
updateAppSettings({ resolution: parseInt(e.target.value) })
|
<option value="16">Sixteenth (16x)</option>
|
||||||
}
|
<option value="32">Thirty-second (32x)</option>
|
||||||
style={{
|
</select>
|
||||||
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>
|
|
||||||
|
|
||||||
<label
|
<select
|
||||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
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:
|
<option value="15">15 FPS</option>
|
||||||
<select
|
<option value="30">30 FPS</option>
|
||||||
value={settings.fps}
|
<option value="60">60 FPS</option>
|
||||||
onChange={(e) =>
|
</select>
|
||||||
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>
|
|
||||||
|
|
||||||
<label
|
<select
|
||||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
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:
|
{VALUE_MODES.map((mode) => (
|
||||||
<select
|
<option key={mode} value={mode}>
|
||||||
value={settings.valueMode}
|
{getValueModeLabel(mode)}
|
||||||
onChange={(e) =>
|
</option>
|
||||||
updateAppSettings({ valueMode: e.target.value as ValueMode })
|
))}
|
||||||
}
|
</select>
|
||||||
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>
|
|
||||||
|
|
||||||
<label
|
<select
|
||||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
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:
|
{RENDER_MODES.map((mode) => (
|
||||||
<select
|
<option key={mode} value={mode}>
|
||||||
value={settings.renderMode}
|
{getRenderModeLabel(mode)}
|
||||||
onChange={(e) =>
|
</option>
|
||||||
updateAppSettings({ renderMode: e.target.value })
|
))}
|
||||||
}
|
</select>
|
||||||
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>
|
|
||||||
|
|
||||||
<label
|
<input
|
||||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
type="range"
|
||||||
>
|
min="0"
|
||||||
Hue Shift:
|
max="360"
|
||||||
<input
|
value={settings.hueShift ?? 0}
|
||||||
type="range"
|
onChange={(e) =>
|
||||||
min="0"
|
updateAppSettings({ hueShift: parseInt(e.target.value) })
|
||||||
max="360"
|
}
|
||||||
value={settings.hueShift ?? 0}
|
style={{ width: '80px', verticalAlign: 'middle', marginRight: '10px' }}
|
||||||
onChange={(e) =>
|
/>
|
||||||
updateAppSettings({ hueShift: parseInt(e.target.value) })
|
|
||||||
}
|
|
||||||
style={{ width: '80px', verticalAlign: 'middle' }}
|
|
||||||
/>
|
|
||||||
<span style={{ fontSize: '11px' }}>
|
|
||||||
{settings.hueShift ?? 0}°
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label
|
<input
|
||||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
type="range"
|
||||||
>
|
min="10"
|
||||||
UI Opacity:
|
max="100"
|
||||||
<input
|
value={Math.round((settings.uiOpacity ?? 0.3) * 100)}
|
||||||
type="range"
|
onChange={(e) =>
|
||||||
min="10"
|
updateAppSettings({ uiOpacity: parseInt(e.target.value) / 100 })
|
||||||
max="100"
|
}
|
||||||
value={Math.round((settings.uiOpacity ?? 0.3) * 100)}
|
style={{ width: '80px', verticalAlign: 'middle', marginRight: '10px' }}
|
||||||
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>
|
|
||||||
|
|
||||||
<button id="help-btn" onClick={showHelp}>
|
<button id="help-btn" onClick={showHelp}>
|
||||||
<LucideIcon name="help" />
|
<LucideIcon name="help" />
|
||||||
|
|||||||
80
src/hooks/useWebcam.ts
Normal file
80
src/hooks/useWebcam.ts
Normal 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 };
|
||||||
|
}
|
||||||
188
src/shader/core/InputManager.ts
Normal file
188
src/shader/core/InputManager.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/shader/core/RenderController.ts
Normal file
114
src/shader/core/RenderController.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/shader/core/ShaderCache.ts
Normal file
53
src/shader/core/ShaderCache.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/shader/core/ShaderCompiler.ts
Normal file
145
src/shader/core/ShaderCompiler.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
188
src/shader/core/WorkerPool.ts
Normal file
188
src/shader/core/WorkerPool.ts
Normal 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
15
src/shader/index.ts
Normal 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';
|
||||||
200
src/shader/rendering/FeedbackSystem.ts
Normal file
200
src/shader/rendering/FeedbackSystem.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
242
src/shader/rendering/PixelRenderer.ts
Normal file
242
src/shader/rendering/PixelRenderer.ts
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
872
src/shader/rendering/ValueModeProcessor.ts
Normal file
872
src/shader/rendering/ValueModeProcessor.ts
Normal 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 },
|
||||||
|
];
|
||||||
|
}
|
||||||
112
src/shader/types/ShaderContext.ts
Normal file
112
src/shader/types/ShaderContext.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
56
src/shader/types/WorkerMessage.ts
Normal file
56
src/shader/types/WorkerMessage.ts
Normal 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;
|
||||||
|
}
|
||||||
7
src/shader/types/index.ts
Normal file
7
src/shader/types/index.ts
Normal 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';
|
||||||
317
src/shader/worker/ShaderWorker.ts
Normal file
317
src/shader/worker/ShaderWorker.ts
Normal 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();
|
||||||
@ -25,6 +25,7 @@ export interface InputState {
|
|||||||
midLevel: number;
|
midLevel: number;
|
||||||
trebleLevel: number;
|
trebleLevel: number;
|
||||||
audioEnabled: boolean;
|
audioEnabled: boolean;
|
||||||
|
webcamEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultInputState: InputState = {
|
export const defaultInputState: InputState = {
|
||||||
@ -52,6 +53,7 @@ export const defaultInputState: InputState = {
|
|||||||
midLevel: 0,
|
midLevel: 0,
|
||||||
trebleLevel: 0,
|
trebleLevel: 0,
|
||||||
audioEnabled: false,
|
audioEnabled: false,
|
||||||
|
webcamEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const $input = atom<InputState>(defaultInputState);
|
export const $input = atom<InputState>(defaultInputState);
|
||||||
|
|||||||
@ -54,7 +54,7 @@ a:visited {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 40px;
|
height: 56px;
|
||||||
background: rgba(0, 0, 0, var(--ui-opacity));
|
background: rgba(0, 0, 0, var(--ui-opacity));
|
||||||
border-bottom: 1px solid #333;
|
border-bottom: 1px solid #333;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -117,11 +117,11 @@ a:visited {
|
|||||||
|
|
||||||
#mobile-menu {
|
#mobile-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 40px;
|
top: 56px;
|
||||||
right: -320px;
|
right: -320px;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
max-width: 80vw;
|
max-width: 80vw;
|
||||||
height: calc(100vh - 40px);
|
height: calc(100vh - 56px);
|
||||||
background: rgba(0, 0, 0, var(--ui-opacity));
|
background: rgba(0, 0, 0, var(--ui-opacity));
|
||||||
backdrop-filter: blur(3px);
|
backdrop-filter: blur(3px);
|
||||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
@ -517,10 +517,10 @@ button [data-lucide] {
|
|||||||
|
|
||||||
#shader-library {
|
#shader-library {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 40px;
|
top: 56px;
|
||||||
left: -300px;
|
left: -300px;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: calc(100vh - 40px);
|
height: calc(100vh - 56px);
|
||||||
background: rgba(0, 0, 0, calc(var(--ui-opacity) + 0.1));
|
background: rgba(0, 0, 0, calc(var(--ui-opacity) + 0.1));
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
z-index: 90;
|
z-index: 90;
|
||||||
@ -533,10 +533,10 @@ button [data-lucide] {
|
|||||||
|
|
||||||
#shader-library-trigger {
|
#shader-library-trigger {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 40px;
|
top: 56px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: calc(100vh - 40px);
|
height: calc(100vh - 56px);
|
||||||
z-index: 91;
|
z-index: 91;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -725,7 +725,7 @@ button [data-lucide] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#topbar {
|
#topbar {
|
||||||
height: 40px;
|
height: 56px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -778,8 +778,8 @@ button [data-lucide] {
|
|||||||
#shader-library {
|
#shader-library {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: -100%;
|
left: -100%;
|
||||||
top: 40px;
|
top: 56px;
|
||||||
height: calc(100vh - 40px);
|
height: calc(100vh - 56px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#shader-library-trigger {
|
#shader-library-trigger {
|
||||||
|
|||||||
@ -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] {
|
export function rgbToHsv(r: number, g: number, b: number): [number, number, number] {
|
||||||
r /= 255;
|
r /= RGB.MAX_VALUE;
|
||||||
g /= 255;
|
g /= RGB.MAX_VALUE;
|
||||||
b /= 255;
|
b /= RGB.MAX_VALUE;
|
||||||
|
|
||||||
const max = Math.max(r, g, b);
|
const max = Math.max(r, g, b);
|
||||||
const min = Math.min(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 (delta !== 0) {
|
||||||
if (max === r) {
|
if (max === r) {
|
||||||
h = ((g - b) / delta) % 6;
|
h = ((g - b) / delta) % HSV.HUE_SECTORS;
|
||||||
} else if (max === g) {
|
} else if (max === g) {
|
||||||
h = (b - r) / delta + 2;
|
h = (b - r) / delta + HSV.SECTOR_OFFSETS.GREEN;
|
||||||
} else {
|
} 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;
|
if (h < 0) h += 1;
|
||||||
@ -33,30 +40,30 @@ export function hsvToRgb(
|
|||||||
v: number
|
v: number
|
||||||
): [number, number, number] {
|
): [number, number, number] {
|
||||||
const c = v * s;
|
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;
|
const m = v - c;
|
||||||
|
|
||||||
let r = 0,
|
let r = 0,
|
||||||
g = 0,
|
g = 0,
|
||||||
b = 0;
|
b = 0;
|
||||||
|
|
||||||
if (h < 1 / 6) {
|
if (h < HSV.SECTOR_BOUNDARIES.SIXTH) {
|
||||||
r = c;
|
r = c;
|
||||||
g = x;
|
g = x;
|
||||||
b = 0;
|
b = 0;
|
||||||
} else if (h < 2 / 6) {
|
} else if (h < HSV.SECTOR_BOUNDARIES.THIRD) {
|
||||||
r = x;
|
r = x;
|
||||||
g = c;
|
g = c;
|
||||||
b = 0;
|
b = 0;
|
||||||
} else if (h < 3 / 6) {
|
} else if (h < HSV.SECTOR_BOUNDARIES.HALF) {
|
||||||
r = 0;
|
r = 0;
|
||||||
g = c;
|
g = c;
|
||||||
b = x;
|
b = x;
|
||||||
} else if (h < 4 / 6) {
|
} else if (h < HSV.SECTOR_BOUNDARIES.TWO_THIRDS) {
|
||||||
r = 0;
|
r = 0;
|
||||||
g = x;
|
g = x;
|
||||||
b = c;
|
b = c;
|
||||||
} else if (h < 5 / 6) {
|
} else if (h < HSV.SECTOR_BOUNDARIES.FIVE_SIXTHS) {
|
||||||
r = x;
|
r = x;
|
||||||
g = 0;
|
g = 0;
|
||||||
b = c;
|
b = c;
|
||||||
@ -67,9 +74,9 @@ export function hsvToRgb(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Math.round((r + m) * 255),
|
Math.round((r + m) * RGB.MAX_VALUE),
|
||||||
Math.round((g + m) * 255),
|
Math.round((g + m) * RGB.MAX_VALUE),
|
||||||
Math.round((b + m) * 255),
|
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] {
|
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 segment = Math.floor(phase);
|
||||||
const remainder = phase - segment;
|
const remainder = phase - segment;
|
||||||
const t = remainder;
|
const t = remainder;
|
||||||
@ -95,112 +102,142 @@ export function rainbowColor(value: number): [number, number, number] {
|
|||||||
|
|
||||||
switch (segment % 6) {
|
switch (segment % 6) {
|
||||||
case 0:
|
case 0:
|
||||||
return [255, Math.round(t * 255), 0];
|
return [RGB.MAX_VALUE, Math.round(t * RGB.MAX_VALUE), 0];
|
||||||
case 1:
|
case 1:
|
||||||
return [Math.round(q * 255), 255, 0];
|
return [Math.round(q * RGB.MAX_VALUE), RGB.MAX_VALUE, 0];
|
||||||
case 2:
|
case 2:
|
||||||
return [0, 255, Math.round(t * 255)];
|
return [0, RGB.MAX_VALUE, Math.round(t * RGB.MAX_VALUE)];
|
||||||
case 3:
|
case 3:
|
||||||
return [0, Math.round(q * 255), 255];
|
return [0, Math.round(q * RGB.MAX_VALUE), RGB.MAX_VALUE];
|
||||||
case 4:
|
case 4:
|
||||||
return [Math.round(t * 255), 0, 255];
|
return [Math.round(t * RGB.MAX_VALUE), 0, RGB.MAX_VALUE];
|
||||||
case 5:
|
case 5:
|
||||||
return [255, 0, Math.round(q * 255)];
|
return [RGB.MAX_VALUE, 0, Math.round(q * RGB.MAX_VALUE)];
|
||||||
default:
|
default:
|
||||||
return [255, 255, 255];
|
return [RGB.MAX_VALUE, RGB.MAX_VALUE, RGB.MAX_VALUE];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function thermalColor(value: number): [number, number, number] {
|
export function thermalColor(value: number): [number, number, number] {
|
||||||
const t = value / 255.0;
|
const t = value / RGB.MAX_VALUE;
|
||||||
if (t < 0.25) {
|
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) {
|
} 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) {
|
} else if (t < 0.75) {
|
||||||
return [
|
return [
|
||||||
Math.round((t - 0.5) * 4 * 255),
|
Math.round((t - 0.5) * 4 * RGB.MAX_VALUE),
|
||||||
255,
|
RGB.MAX_VALUE,
|
||||||
Math.round((0.75 - t) * 4 * 255),
|
Math.round((0.75 - t) * 4 * RGB.MAX_VALUE),
|
||||||
];
|
];
|
||||||
} else {
|
} 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] {
|
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 intensity = Math.pow(Math.sin(t * Math.PI), 2);
|
||||||
const glow = Math.pow(intensity, 0.5);
|
const glow = Math.pow(intensity, 0.5);
|
||||||
return [
|
return [
|
||||||
Math.round(glow * 255),
|
Math.round(glow * RGB.MAX_VALUE),
|
||||||
Math.round(intensity * 255),
|
Math.round(intensity * RGB.MAX_VALUE),
|
||||||
Math.round(Math.pow(intensity, 2) * 255),
|
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] {
|
export function sunsetColor(value: number): [number, number, number] {
|
||||||
const t = value / 255.0;
|
const t = value / RGB.MAX_VALUE;
|
||||||
if (t < 0.3) {
|
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) {
|
} else if (t < 0.6) {
|
||||||
const p = (t - 0.3) / 0.3;
|
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 {
|
} else {
|
||||||
const p = (t - 0.6) / 0.4;
|
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] {
|
export function oceanColor(value: number): [number, number, number] {
|
||||||
const t = value / 255.0;
|
const t = value / RGB.MAX_VALUE;
|
||||||
if (t < 0.25) {
|
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) {
|
} else if (t < 0.5) {
|
||||||
const p = (t - 0.25) / 0.25;
|
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) {
|
} else if (t < 0.75) {
|
||||||
const p = (t - 0.5) / 0.25;
|
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 {
|
} else {
|
||||||
const p = (t - 0.75) / 0.25;
|
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] {
|
export function forestColor(value: number): [number, number, number] {
|
||||||
const t = value / 255.0;
|
const t = value / RGB.MAX_VALUE;
|
||||||
if (t < 0.3) {
|
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) {
|
} else if (t < 0.6) {
|
||||||
const p = (t - 0.3) / 0.3;
|
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 {
|
} else {
|
||||||
const p = (t - 0.6) / 0.4;
|
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] {
|
export function copperColor(value: number): [number, number, number] {
|
||||||
const t = value / 255.0;
|
const t = value / RGB.MAX_VALUE;
|
||||||
if (t < 0.4) {
|
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) {
|
} else if (t < 0.7) {
|
||||||
const p = (t - 0.4) / 0.3;
|
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 {
|
} else {
|
||||||
const p = (t - 0.7) / 0.3;
|
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] {
|
export function ditheredColor(value: number): [number, number, number] {
|
||||||
const levels = 4;
|
const levels = COLOR_MODE_CONSTANTS.DITHER_LEVELS;
|
||||||
const step = 255 / (levels - 1);
|
const step = RGB.MAX_VALUE / (levels - 1);
|
||||||
const quantized = Math.round(value / step) * step;
|
const quantized = Math.round(value / step) * step;
|
||||||
const error = value - quantized;
|
const error = value - quantized;
|
||||||
const dither = (Math.random() - 0.5) * 32;
|
const dither = (Math.random() - 0.5) * COLOR_MODE_CONSTANTS.DITHER_NOISE_AMPLITUDE;
|
||||||
const final = Math.max(0, Math.min(255, quantized + error + dither));
|
const final = Math.max(RGB.MIN_VALUE, Math.min(RGB.MAX_VALUE, quantized + error + dither));
|
||||||
return [final, final, final];
|
return [final, final, final];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,12 +247,12 @@ export function paletteColor(value: number): [number, number, number] {
|
|||||||
[87, 29, 149],
|
[87, 29, 149],
|
||||||
[191, 82, 177],
|
[191, 82, 177],
|
||||||
[249, 162, 162],
|
[249, 162, 162],
|
||||||
[255, 241, 165],
|
[RGB.MAX_VALUE, 241, 165],
|
||||||
[134, 227, 206],
|
[134, 227, 206],
|
||||||
[29, 161, 242],
|
[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];
|
return palette[index] as [number, number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,12 +267,12 @@ export function vintageColor(value: number): [number, number, number] {
|
|||||||
[166, 124, 82],
|
[166, 124, 82],
|
||||||
[245, 222, 179],
|
[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];
|
return palette[index] as [number, number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function plasmaColor(value: number): [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 freq = 2.4;
|
||||||
const phase1 = 0.0;
|
const phase1 = 0.0;
|
||||||
const phase2 = 2.094;
|
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;
|
const b = Math.sin(freq * t + phase3) * 0.5 + 0.5;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Math.round(r * 255),
|
Math.round(r * RGB.MAX_VALUE),
|
||||||
Math.round(g * 255),
|
Math.round(g * RGB.MAX_VALUE),
|
||||||
Math.round(b * 255)
|
Math.round(b * RGB.MAX_VALUE)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fireColor(value: number): [number, number, number] {
|
export function fireColor(value: number): [number, number, number] {
|
||||||
const t = value / 255.0;
|
const t = value / RGB.MAX_VALUE;
|
||||||
if (t < 0.2) {
|
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) {
|
} else if (t < 0.5) {
|
||||||
const p = (t - 0.2) / 0.3;
|
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) {
|
} else if (t < 0.8) {
|
||||||
const p = (t - 0.5) / 0.3;
|
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 {
|
} else {
|
||||||
const p = (t - 0.8) / 0.2;
|
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] {
|
export function iceColor(value: number): [number, number, number] {
|
||||||
const t = value / 255.0;
|
const t = value / RGB.MAX_VALUE;
|
||||||
if (t < 0.25) {
|
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) {
|
} else if (t < 0.5) {
|
||||||
const p = (t - 0.25) / 0.25;
|
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) {
|
} else if (t < 0.75) {
|
||||||
const p = (t - 0.5) / 0.25;
|
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 {
|
} else {
|
||||||
const p = (t - 0.75) / 0.25;
|
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] {
|
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 intensity = Math.pow(t, 0.6);
|
||||||
const heat = Math.sin(t * Math.PI * 1.5) * 0.5 + 0.5;
|
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 g = Math.round(128 * heat * intensity);
|
||||||
const b = Math.round(64 * (1 - intensity) * heat);
|
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] {
|
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 inverted = 1.0 - t;
|
||||||
const contrast = Math.pow(inverted, 1.8);
|
const contrast = Math.pow(inverted, 1.8);
|
||||||
const glow = Math.sin(t * Math.PI) * 0.3;
|
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 cyan = Math.round((contrast + glow) * 180);
|
||||||
const blue = Math.round((contrast + glow * 0.5) * 120);
|
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] {
|
export function spectrumColor(value: number): [number, number, number] {
|
||||||
const t = value / 255.0;
|
const t = value / RGB.MAX_VALUE;
|
||||||
const hue = t * 360;
|
const hue = t * 360;
|
||||||
const saturation = 0.7;
|
const saturation = 0.7;
|
||||||
const lightness = 0.6 + (Math.sin(t * Math.PI * 4) * 0.2);
|
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 [
|
return [
|
||||||
Math.round((r + m) * 255),
|
Math.round((r + m) * RGB.MAX_VALUE),
|
||||||
Math.round((g + m) * 255),
|
Math.round((g + m) * RGB.MAX_VALUE),
|
||||||
Math.round((b + m) * 255)
|
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(
|
export function calculateColorDirect(
|
||||||
absValue: number,
|
absValue: number,
|
||||||
renderMode: string,
|
renderMode: string,
|
||||||
hueShift: number = 0
|
hueShift: number = 0
|
||||||
): [number, number, number] {
|
): [number, number, number] {
|
||||||
let color: [number, number, number];
|
const colorFunction = COLOR_PALETTE_REGISTRY[renderMode];
|
||||||
|
const color = colorFunction ? colorFunction(absValue) : [absValue, absValue, absValue] as [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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return applyHueShift(color, hueShift);
|
return applyHueShift(color, hueShift);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,15 +19,98 @@ export const PERFORMANCE = {
|
|||||||
// Color Constants
|
// Color Constants
|
||||||
export const COLOR_TABLE_SIZE = 256;
|
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
|
// Render Mode Constants - Keep in sync with color modes
|
||||||
export const RENDER_MODES = [
|
export const RENDER_MODES = [
|
||||||
'classic',
|
'classic',
|
||||||
'grayscale',
|
'grayscale',
|
||||||
'red',
|
'red',
|
||||||
'green',
|
'green',
|
||||||
'blue',
|
'blue',
|
||||||
'rgb',
|
'rgb',
|
||||||
'hsv',
|
|
||||||
'rainbow',
|
'rainbow',
|
||||||
'thermal',
|
'thermal',
|
||||||
'neon',
|
'neon',
|
||||||
@ -46,6 +129,14 @@ export const RENDER_MODES = [
|
|||||||
'plasma',
|
'plasma',
|
||||||
'xray',
|
'xray',
|
||||||
'spectrum',
|
'spectrum',
|
||||||
|
'acid',
|
||||||
|
'quantum',
|
||||||
|
'neonstrike',
|
||||||
|
'eightbit',
|
||||||
|
'silk',
|
||||||
|
'binary',
|
||||||
|
'palette16',
|
||||||
|
'palette32',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type RenderMode = (typeof RENDER_MODES)[number];
|
export type RenderMode = (typeof RENDER_MODES)[number];
|
||||||
@ -87,13 +178,49 @@ export const VALUE_MODES = [
|
|||||||
'rings',
|
'rings',
|
||||||
'mesh',
|
'mesh',
|
||||||
'glitch',
|
'glitch',
|
||||||
|
'diffusion',
|
||||||
|
'cascade',
|
||||||
|
'echo',
|
||||||
|
'mosh',
|
||||||
|
'fold',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ValueMode = (typeof VALUE_MODES)[number];
|
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
|
// Default Values
|
||||||
export const DEFAULTS = {
|
export const DEFAULTS = {
|
||||||
RESOLUTION: 1,
|
RESOLUTION: 8,
|
||||||
FPS: 30,
|
FPS: 30,
|
||||||
RENDER_MODE: 'classic',
|
RENDER_MODE: 'classic',
|
||||||
VALUE_MODE: 'integer' as ValueMode,
|
VALUE_MODE: 'integer' as ValueMode,
|
||||||
|
|||||||
Reference in New Issue
Block a user