979 lines
31 KiB
TypeScript
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();
|