Files
bitfielder/src/ShaderWorker.ts

979 lines
31 KiB
TypeScript

// 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();