lots of updates

This commit is contained in:
2025-07-06 01:14:43 +02:00
parent 96af50ee6b
commit f84b515523
6 changed files with 382 additions and 53 deletions

View File

@ -7,7 +7,10 @@ interface WorkerMessage {
height?: number;
time?: number;
renderMode?: string;
valueMode?: string; // 'integer' or 'float'
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;
@ -115,7 +118,7 @@ class ShaderWorker {
this.compileShader(message.id, message.code!);
break;
case 'render':
this.renderShader(message.id, message.width!, message.height!, message.time!, message.renderMode || 'classic', message, message.startY || 0);
this.renderShader(message.id, message.width!, message.height!, message.time!, message.renderMode || 'classic', message.valueMode || 'integer', message, message.startY || 0);
break;
}
} catch (error) {
@ -220,7 +223,7 @@ class ShaderWorker {
return hash.toString(36);
}
private renderShader(id: string, width: number, height: number, time: number, renderMode: string, message: WorkerMessage, startY: number = 0): void {
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;
@ -233,14 +236,14 @@ class ShaderWorker {
try {
// Use tiled rendering for better timeout handling
this.renderTiled(data, width, height, time, renderMode, message, startTime, maxRenderTime, startY);
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, message: WorkerMessage, startTime: number, maxRenderTime: number, yOffset: number = 0): void {
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 = 64; // 64x64 tiles for better granularity
const tilesX = Math.ceil(width / tileSize);
const tilesY = Math.ceil(height / tileSize);
@ -260,12 +263,15 @@ class ShaderWorker {
const tileEndX = Math.min(tileStartX + tileSize, width);
const tileEndY = Math.min(tileStartY + tileSize, height);
this.renderTile(data, width, tileStartX, tileStartY, tileEndX, tileEndY, time, renderMode, message, yOffset);
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, message: WorkerMessage, yOffset: number = 0): void {
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;
@ -290,7 +296,7 @@ class ShaderWorker {
message.audioLevel || 0, message.bassLevel || 0, message.midLevel || 0, message.trebleLevel || 0
);
const safeValue = isFinite(value) ? value : 0;
const [r, g, b] = this.calculateColor(safeValue, renderMode);
const [r, g, b] = this.calculateColor(safeValue, renderMode, valueMode, x, actualY, fullWidth, fullHeight);
data[i] = r;
data[i + 1] = g;
@ -446,18 +452,96 @@ class ShaderWorker {
return imageData;
}
private calculateColor(value: number, renderMode: string): [number, number, number] {
const absValue = Math.abs(value) % 256;
private calculateColor(value: number, renderMode: string, valueMode: string = 'integer', 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;
default:
// Integer mode: treat value as 0-255 (original behavior)
processedValue = Math.abs(value) % 256;
break;
}
// Use pre-computed color table if available
const colorTable = this.colorTables.get(renderMode);
if (colorTable) {
const index = Math.floor(absValue) * 3;
const index = Math.floor(processedValue) * 3;
return [colorTable[index], colorTable[index + 1], colorTable[index + 2]];
}
// Fallback to direct calculation
return this.calculateColorDirect(absValue, renderMode);
return this.calculateColorDirect(processedValue, renderMode);
}
private calculateColorDirect(absValue: number, renderMode: string): [number, number, number] {
@ -639,42 +723,34 @@ class ShaderWorker {
}
private sanitizeCode(code: string): string {
// Strict whitelist approach - extended to include new interaction variables
// Variables: x, y, t, i, mouseX, mouseY, mousePressed, mouseVX, mouseVY, mouseClickTime, touchCount, touch0X, touch0Y, touch1X, touch1Y, pinchScale, pinchRotation, accelX, accelY, accelZ, gyroX, gyroY, gyroZ
const allowedPattern = /^[0-9a-zA-Z\s\+\-\*\/\%\^\&\|\(\)\<\>\~\?:,\.xyti]+$/;
if (!allowedPattern.test(code)) {
throw new Error('Invalid characters in shader code');
}
// Check for dangerous keywords
const dangerousKeywords = [
'eval', 'Function', 'constructor', 'prototype', '__proto__',
'window', 'document', 'global', 'process', 'require',
'import', 'export', 'class', 'function', 'var', 'let', 'const',
'while', 'for', 'do', 'if', 'else', 'switch', 'case', 'break',
'continue', 'return', 'throw', 'try', 'catch', 'finally'
// 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'
];
const codeWords = code.toLowerCase().split(/[^a-z]/);
for (const keyword of dangerousKeywords) {
if (codeWords.includes(keyword)) {
throw new Error(`Forbidden keyword: ${keyword}`);
}
}
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}(`);
});
// Limit expression complexity
const complexity = (code.match(/[\(\)]/g) || []).length;
if (complexity > 20) {
throw new Error('Expression too complex');
}
// 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');
// Limit code length
if (code.length > 200) {
throw new Error('Code too long');
}
return code;
return processedCode;
}
private postMessage(response: WorkerResponse): void {