145 lines
4.8 KiB
TypeScript
145 lines
4.8 KiB
TypeScript
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);
|
|
}
|
|
} |