lots of updates
This commit is contained in:
@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user