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); } }