This commit is contained in:
2025-07-14 21:08:21 +02:00
parent 2cf306ee8c
commit 431966d498
17 changed files with 1856 additions and 1442 deletions

View File

@ -15,6 +15,7 @@ const CORE_ASSETS = [
const DYNAMIC_ASSETS_PATTERNS = [
/\/src\/.+\.(ts|tsx|js|jsx)$/,
/\/src\/.+\.css$/,
/\/assets\/.+\.(js|css)$/,
/fonts\.googleapis\.com/,
/fonts\.gstatic\.com/
];
@ -30,6 +31,10 @@ self.addEventListener('install', event => {
}),
caches.open(DYNAMIC_CACHE).then(cache => {
console.log('Dynamic cache initialized');
// Pre-cache critical assets if they exist
return cache.addAll([]).catch(() => {
console.log('No additional assets to pre-cache');
});
})
]).then(() => {
console.log('Service Worker installed successfully');

View File

@ -1,47 +1,4 @@
interface WorkerMessage {
id: string;
type: 'compile' | 'render';
code?: string;
width?: number;
height?: number;
time?: number;
renderMode?: string;
valueMode?: string;
hueShift?: number;
startY?: number; // Y offset for tile rendering
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 { WorkerMessage, WorkerResponse } from './shader/types';
export class FakeShader {
private canvas: HTMLCanvasElement;
@ -61,6 +18,48 @@ export class FakeShader {
private timeSpeed: number = 1.0;
private currentBPM: number = 120;
// ID generation optimization
private idCounter: number = 0;
// Reusable message object to avoid allocations
private reusableMessage: WorkerMessage = {
id: '',
type: 'render',
width: 0,
height: 0,
fullWidth: 0,
fullHeight: 0,
time: 0,
renderMode: 'classic',
valueMode: 'integer',
hueShift: 0,
mouseX: 0,
mouseY: 0,
mousePressed: false,
mouseVX: 0,
mouseVY: 0,
mouseClickTime: 0,
touchCount: 0,
touch0X: 0,
touch0Y: 0,
touch1X: 0,
touch1Y: 0,
pinchScale: 1,
pinchRotation: 0,
accelX: 0,
accelY: 0,
accelZ: 0,
gyroX: 0,
gyroY: 0,
gyroZ: 0,
audioLevel: 0,
bassLevel: 0,
midLevel: 0,
trebleLevel: 0,
bpm: 120,
startY: 0,
};
// Multi-worker state
private tileResults: Map<number, ImageData> = new Map();
private tilesCompleted: number = 0;
@ -137,7 +136,7 @@ export class FakeShader {
private initializeWorkers(): void {
// Create worker pool
for (let i = 0; i < this.workerCount; i++) {
const worker = new Worker(new URL('./ShaderWorker.ts', import.meta.url), {
const worker = new Worker(new URL('./shader/worker/ShaderWorker.ts', import.meta.url), {
type: 'module',
});
worker.onmessage = (e: MessageEvent<WorkerResponse>) =>
@ -203,7 +202,7 @@ export class FakeShader {
private compile(): void {
this.isCompiled = false;
const id = `compile_${Date.now()}`;
const id = `compile_${++this.idCounter}`;
// Send compile message to all workers
this.workers.forEach((worker) => {
@ -232,43 +231,47 @@ export class FakeShader {
}
}
private updateReusableMessage(id: string, currentTime: number, width: number, height: number, fullWidth: number, fullHeight: number, startY: number = 0): void {
this.reusableMessage.id = id;
this.reusableMessage.type = 'render';
this.reusableMessage.width = width;
this.reusableMessage.height = height;
this.reusableMessage.fullWidth = fullWidth;
this.reusableMessage.fullHeight = fullHeight;
this.reusableMessage.time = currentTime;
this.reusableMessage.renderMode = this.renderMode;
this.reusableMessage.valueMode = this.valueMode;
this.reusableMessage.hueShift = this.hueShift;
this.reusableMessage.startY = startY;
this.reusableMessage.mouseX = this.mouseX;
this.reusableMessage.mouseY = this.mouseY;
this.reusableMessage.mousePressed = this.mousePressed;
this.reusableMessage.mouseVX = this.mouseVX;
this.reusableMessage.mouseVY = this.mouseVY;
this.reusableMessage.mouseClickTime = this.mouseClickTime;
this.reusableMessage.touchCount = this.touchCount;
this.reusableMessage.touch0X = this.touch0X;
this.reusableMessage.touch0Y = this.touch0Y;
this.reusableMessage.touch1X = this.touch1X;
this.reusableMessage.touch1Y = this.touch1Y;
this.reusableMessage.pinchScale = this.pinchScale;
this.reusableMessage.pinchRotation = this.pinchRotation;
this.reusableMessage.accelX = this.accelX;
this.reusableMessage.accelY = this.accelY;
this.reusableMessage.accelZ = this.accelZ;
this.reusableMessage.gyroX = this.gyroX;
this.reusableMessage.gyroY = this.gyroY;
this.reusableMessage.gyroZ = this.gyroZ;
this.reusableMessage.audioLevel = this.audioLevel;
this.reusableMessage.bassLevel = this.bassLevel;
this.reusableMessage.midLevel = this.midLevel;
this.reusableMessage.trebleLevel = this.trebleLevel;
this.reusableMessage.bpm = this.currentBPM;
}
private renderWithSingleWorker(id: string, currentTime: number): void {
this.worker.postMessage({
id,
type: 'render',
width: this.canvas.width,
height: this.canvas.height,
fullWidth: this.canvas.width,
fullHeight: this.canvas.height,
time: currentTime,
renderMode: this.renderMode,
valueMode: this.valueMode,
hueShift: this.hueShift,
mouseX: this.mouseX,
mouseY: this.mouseY,
mousePressed: this.mousePressed,
mouseVX: this.mouseVX,
mouseVY: this.mouseVY,
mouseClickTime: this.mouseClickTime,
touchCount: this.touchCount,
touch0X: this.touch0X,
touch0Y: this.touch0Y,
touch1X: this.touch1X,
touch1Y: this.touch1Y,
pinchScale: this.pinchScale,
pinchRotation: this.pinchRotation,
accelX: this.accelX,
accelY: this.accelY,
accelZ: this.accelZ,
gyroX: this.gyroX,
gyroY: this.gyroY,
gyroZ: this.gyroZ,
audioLevel: this.audioLevel,
bassLevel: this.bassLevel,
midLevel: this.midLevel,
trebleLevel: this.trebleLevel,
bpm: this.currentBPM,
} as WorkerMessage);
this.updateReusableMessage(id, currentTime, this.canvas.width, this.canvas.height, this.canvas.width, this.canvas.height, 0);
this.worker.postMessage(this.reusableMessage);
}
private renderWithMultipleWorkers(id: string, currentTime: number): void {
@ -288,45 +291,18 @@ export class FakeShader {
if (startY >= height) return; // Skip if tile is outside canvas
worker.postMessage({
id: `${id}_tile_${index}`,
type: 'render',
width: width,
height: endY - startY,
// Pass the Y offset for correct coordinate calculation
startY: startY,
// Pass full canvas dimensions for center calculations
fullWidth: width,
fullHeight: height,
time: currentTime,
renderMode: this.renderMode,
valueMode: this.valueMode,
hueShift: this.hueShift,
mouseX: this.mouseX,
mouseY: this.mouseY,
mousePressed: this.mousePressed,
mouseVX: this.mouseVX,
mouseVY: this.mouseVY,
mouseClickTime: this.mouseClickTime,
touchCount: this.touchCount,
touch0X: this.touch0X,
touch0Y: this.touch0Y,
touch1X: this.touch1X,
touch1Y: this.touch1Y,
pinchScale: this.pinchScale,
pinchRotation: this.pinchRotation,
accelX: this.accelX,
accelY: this.accelY,
accelZ: this.accelZ,
gyroX: this.gyroX,
gyroY: this.gyroY,
gyroZ: this.gyroZ,
audioLevel: this.audioLevel,
bassLevel: this.bassLevel,
midLevel: this.midLevel,
trebleLevel: this.trebleLevel,
bpm: this.currentBPM,
} as WorkerMessage);
// Update reusable message with worker-specific values
this.updateReusableMessage(
`${id}_tile_${index}`,
currentTime,
width,
endY - startY,
width,
height,
startY
);
worker.postMessage(this.reusableMessage);
});
}
@ -356,7 +332,7 @@ export class FakeShader {
return;
}
const renderId = `render_${Date.now()}_${Math.random()}`;
const renderId = `render_${++this.idCounter}`;
// Add to pending renders queue
this.pendingRenders.push(renderId);
@ -607,9 +583,10 @@ export class FakeShader {
const ops = ['^', '&', '|', '+', '-', '*', '%', '**', '%'];
const shifts = ['<<', '>>'];
const numbers = [];
for (let i = 0; i < Math.random(200); i++) {
numbers.push(Math.floor(Math.random(400)))
const numbers: number[] = [];
const numCount = Math.floor(Math.random() * 20) + 10; // Generate 10-30 numbers
for (let i = 0; i < numCount; i++) {
numbers.push(Math.floor(Math.random() * 400))
}
const randomChoice = <T>(arr: T[]): T =>
arr[Math.floor(Math.random() * arr.length)];

File diff suppressed because it is too large Load Diff

View File

@ -116,162 +116,132 @@ export function TopBar() {
};
return (
<div id="topbar" className={ui.uiVisible ? '' : 'hidden'}>
<div
id="topbar"
className={ui.uiVisible ? '' : 'hidden'}>
<div className="title">Bitfielder</div>
<div className="controls">
<div className="controls-desktop">
<label
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
<select
value={settings.resolution}
onChange={(e) =>
updateAppSettings({ resolution: parseInt(e.target.value) })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
marginRight: '10px',
}}
>
Resolution:
<select
value={settings.resolution}
onChange={(e) =>
updateAppSettings({ resolution: parseInt(e.target.value) })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
}}
>
<option value="1">Full (1x)</option>
<option value="2">Half (2x)</option>
<option value="4">Quarter (4x)</option>
<option value="8">Eighth (8x)</option>
<option value="16">Sixteenth (16x)</option>
<option value="32">Thirty-second (32x)</option>
</select>
</label>
<option value="1">Full (1x)</option>
<option value="2">Half (2x)</option>
<option value="4">Quarter (4x)</option>
<option value="8">Eighth (8x)</option>
<option value="16">Sixteenth (16x)</option>
<option value="32">Thirty-second (32x)</option>
</select>
<label
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
<select
value={settings.fps}
onChange={(e) =>
updateAppSettings({ fps: parseInt(e.target.value) })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
marginRight: '10px',
}}
>
FPS:
<select
value={settings.fps}
onChange={(e) =>
updateAppSettings({ fps: parseInt(e.target.value) })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
}}
>
<option value="15">15 FPS</option>
<option value="30">30 FPS</option>
<option value="60">60 FPS</option>
</select>
</label>
<option value="15">15 FPS</option>
<option value="30">30 FPS</option>
<option value="60">60 FPS</option>
</select>
<label
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
<select
value={settings.valueMode}
onChange={(e) =>
updateAppSettings({ valueMode: e.target.value as ValueMode })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
marginRight: '10px',
}}
>
Value Mode:
<select
value={settings.valueMode}
onChange={(e) =>
updateAppSettings({ valueMode: e.target.value as ValueMode })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
}}
>
{VALUE_MODES.map((mode) => (
<option key={mode} value={mode}>
{getValueModeLabel(mode)}
</option>
))}
</select>
</label>
{VALUE_MODES.map((mode) => (
<option key={mode} value={mode}>
{getValueModeLabel(mode)}
</option>
))}
</select>
<label
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
<select
value={settings.renderMode}
onChange={(e) =>
updateAppSettings({ renderMode: e.target.value })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
marginRight: '10px',
}}
>
Render Mode:
<select
value={settings.renderMode}
onChange={(e) =>
updateAppSettings({ renderMode: e.target.value })
}
style={{
background: 'rgba(255,255,255,0.1)',
border: '1px solid #555',
color: '#fff',
padding: '4px',
borderRadius: '4px',
}}
>
<option value="classic">Classic</option>
<option value="grayscale">Grayscale</option>
<option value="red">Red Channel</option>
<option value="green">Green Channel</option>
<option value="blue">Blue Channel</option>
<option value="rainbow">Rainbow</option>
<option value="thermal">Thermal</option>
<option value="neon">Neon</option>
<option value="sunset">Sunset</option>
<option value="ocean">Ocean</option>
<option value="forest">Forest</option>
<option value="copper">Copper</option>
<option value="dithered">Dithered</option>
<option value="palette">Palette</option>
<option value="vintage">Vintage</option>
<option value="infrared">Infrared</option>
<option value="fire">Fire</option>
<option value="ice">Ice</option>
<option value="plasma">Plasma</option>
<option value="xray">X-Ray</option>
<option value="spectrum">Spectrum</option>
</select>
</label>
<option value="classic">Classic</option>
<option value="grayscale">Grayscale</option>
<option value="red">Red Channel</option>
<option value="green">Green Channel</option>
<option value="blue">Blue Channel</option>
<option value="rainbow">Rainbow</option>
<option value="thermal">Thermal</option>
<option value="neon">Neon</option>
<option value="sunset">Sunset</option>
<option value="ocean">Ocean</option>
<option value="forest">Forest</option>
<option value="copper">Copper</option>
<option value="dithered">Dithered</option>
<option value="palette">Palette</option>
<option value="vintage">Vintage</option>
<option value="infrared">Infrared</option>
<option value="fire">Fire</option>
<option value="ice">Ice</option>
<option value="plasma">Plasma</option>
<option value="xray">X-Ray</option>
<option value="spectrum">Spectrum</option>
</select>
<label
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
>
Hue Shift:
<input
type="range"
min="0"
max="360"
value={settings.hueShift ?? 0}
onChange={(e) =>
updateAppSettings({ hueShift: parseInt(e.target.value) })
}
style={{ width: '80px', verticalAlign: 'middle' }}
/>
<span style={{ fontSize: '11px' }}>
{settings.hueShift ?? 0}°
</span>
</label>
<input
type="range"
min="0"
max="360"
value={settings.hueShift ?? 0}
onChange={(e) =>
updateAppSettings({ hueShift: parseInt(e.target.value) })
}
style={{ width: '80px', verticalAlign: 'middle', marginRight: '10px' }}
/>
<label
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
>
UI Opacity:
<input
type="range"
min="10"
max="100"
value={Math.round((settings.uiOpacity ?? 0.3) * 100)}
onChange={(e) =>
updateAppSettings({ uiOpacity: parseInt(e.target.value) / 100 })
}
style={{ width: '80px', verticalAlign: 'middle' }}
/>
<span style={{ fontSize: '11px' }}>
{Math.round((settings.uiOpacity ?? 0.3) * 100)}%
</span>
</label>
<input
type="range"
min="10"
max="100"
value={Math.round((settings.uiOpacity ?? 0.3) * 100)}
onChange={(e) =>
updateAppSettings({ uiOpacity: parseInt(e.target.value) / 100 })
}
style={{ width: '80px', verticalAlign: 'middle', marginRight: '10px' }}
/>
<button id="help-btn" onClick={showHelp}>
<LucideIcon name="help" />

80
src/hooks/useWebcam.ts Normal file
View File

@ -0,0 +1,80 @@
import { useCallback, useRef } from 'react';
import { $input } from '../stores/input';
export function useWebcam() {
const videoRef = useRef<HTMLVideoElement | null>(null);
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const streamRef = useRef<MediaStream | null>(null);
const pixelDataRef = useRef<Uint8ClampedArray | null>(null);
const setupWebcam = useCallback(async (): Promise<boolean> => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480 }
});
// Create video element
const video = document.createElement('video');
video.srcObject = stream;
video.autoplay = true;
video.playsInline = true;
videoRef.current = video;
// Create canvas for pixel extraction
const canvas = document.createElement('canvas');
canvasRef.current = canvas;
streamRef.current = stream;
$input.set({ ...$input.get(), webcamEnabled: true });
console.log('Webcam initialized successfully');
return true;
} catch (error) {
console.error('Failed to access webcam:', error);
$input.set({ ...$input.get(), webcamEnabled: false });
return false;
}
}, []);
const disableWebcam = useCallback(() => {
$input.set({ ...$input.get(), webcamEnabled: false });
if (streamRef.current) {
streamRef.current.getTracks().forEach(track => track.stop());
streamRef.current = null;
}
videoRef.current = null;
canvasRef.current = null;
pixelDataRef.current = null;
}, []);
const getWebcamData = useCallback((width: number, height: number): Uint8ClampedArray | null => {
if (!videoRef.current || !canvasRef.current) {
return null;
}
const video = videoRef.current;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
if (!ctx || video.videoWidth === 0 || video.videoHeight === 0) {
return null;
}
// Set canvas size to match shader resolution
canvas.width = width;
canvas.height = height;
// Draw video frame scaled to shader resolution
ctx.drawImage(video, 0, 0, width, height);
// Extract pixel data
const imageData = ctx.getImageData(0, 0, width, height);
pixelDataRef.current = imageData.data;
return imageData.data;
}, []);
return { setupWebcam, disableWebcam, getWebcamData };
}

View File

@ -0,0 +1,53 @@
import { LRUCache } from '../../utils/LRUCache';
import { ShaderFunction } from '../types';
import { PERFORMANCE } from '../../utils/constants';
/**
* Manages caching for compiled shaders and image data
*/
export class ShaderCache {
private imageDataCache: LRUCache<string, ImageData>;
private compilationCache: LRUCache<string, ShaderFunction>;
constructor() {
this.imageDataCache = new LRUCache(PERFORMANCE.IMAGE_DATA_CACHE_SIZE);
this.compilationCache = new LRUCache(PERFORMANCE.COMPILATION_CACHE_SIZE);
}
/**
* Gets cached ImageData or creates new one
*/
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;
}
/**
* Gets cached compiled shader function
*/
getCompiledShader(codeHash: string): ShaderFunction | undefined {
return this.compilationCache.get(codeHash);
}
/**
* Caches compiled shader function
*/
setCompiledShader(codeHash: string, compiledFunction: ShaderFunction): void {
this.compilationCache.set(codeHash, compiledFunction);
}
/**
* Clears all caches
*/
clear(): void {
this.imageDataCache.clear();
this.compilationCache.clear();
}
}

View File

@ -0,0 +1,145 @@
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);
}
}

15
src/shader/index.ts Normal file
View File

@ -0,0 +1,15 @@
/**
* Public API for the shader system
*/
// Export types
export type { ShaderContext, ShaderFunction, WorkerMessage, WorkerResponse } from './types';
// Export main classes
export { FakeShader } from '../FakeShader';
// Export utilities
export { ShaderCompiler } from './core/ShaderCompiler';
export { ShaderCache } from './core/ShaderCache';
export { FeedbackSystem } from './rendering/FeedbackSystem';
export { PixelRenderer } from './rendering/PixelRenderer';

View File

@ -0,0 +1,198 @@
/**
* Manages feedback buffers for shader rendering
*/
export class FeedbackSystem {
private feedbackBuffer: Float32Array | null = null;
private previousFeedbackBuffer: Float32Array | null = null;
private stateBuffer: Float32Array | null = null;
private echoBuffers: Float32Array[] = [];
private echoFrameCounter: number = 0;
private echoInterval: number = 30; // Store echo every 30 frames (~0.5s at 60fps)
/**
* Initializes feedback buffers for given dimensions
*/
initializeBuffers(width: number, height: number): void {
const bufferSize = width * height;
if (!this.feedbackBuffer || this.feedbackBuffer.length !== bufferSize) {
this.feedbackBuffer = new Float32Array(bufferSize);
this.previousFeedbackBuffer = new Float32Array(bufferSize);
this.stateBuffer = new Float32Array(bufferSize);
// Initialize echo buffers (4 buffers for different time delays)
this.echoBuffers = [];
for (let i = 0; i < 4; i++) {
this.echoBuffers.push(new Float32Array(bufferSize));
}
}
}
/**
* Updates feedback value for a pixel
*/
updateFeedback(pixelIndex: number, r: number, g: number, b: number, deltaTime: number): void {
if (!this.feedbackBuffer) return;
// Use the actual displayed luminance as feedback (0-255 range)
const luminance = (r * 0.299 + g * 0.587 + b * 0.114);
// Frame rate independent decay
const decayFactor = Math.pow(0.95, deltaTime * 60); // 5% decay at 60fps
// Simple mixing to prevent oscillation
const previousValue = this.feedbackBuffer[pixelIndex] || 0;
const mixRatio = Math.min(deltaTime * 10, 0.3); // Max 30% new value per frame
let newFeedback = luminance * mixRatio + previousValue * (1 - mixRatio);
newFeedback *= decayFactor;
// Clamp and store
this.feedbackBuffer[pixelIndex] = Math.max(0, Math.min(255, newFeedback));
}
/**
* Updates state buffer for a pixel
*/
updateState(pixelIndex: number, stateValue: number): void {
if (!this.stateBuffer) return;
this.stateBuffer[pixelIndex] = stateValue;
}
/**
* Gets feedback value for a pixel
*/
getFeedback(pixelIndex: number): number {
return this.feedbackBuffer ? this.feedbackBuffer[pixelIndex] || 0 : 0;
}
/**
* Gets neighbor feedback values
*/
getNeighborFeedback(_pixelIndex: number, x: number, y: number, width: number, height: number): {
north: number;
south: number;
east: number;
west: number;
} {
if (!this.feedbackBuffer) {
return { north: 0, south: 0, east: 0, west: 0 };
}
let north = 0, south = 0, east = 0, west = 0;
// North neighbor (bounds safe)
if (y > 0) north = this.feedbackBuffer[(y - 1) * width + x] || 0;
// South neighbor (bounds safe)
if (y < height - 1) south = this.feedbackBuffer[(y + 1) * width + x] || 0;
// East neighbor (bounds safe)
if (x < width - 1) east = this.feedbackBuffer[y * width + (x + 1)] || 0;
// West neighbor (bounds safe)
if (x > 0) west = this.feedbackBuffer[y * width + (x - 1)] || 0;
return { north, south, east, west };
}
/**
* Calculates momentum (change from previous frame)
*/
getMomentum(pixelIndex: number): number {
if (!this.feedbackBuffer || !this.previousFeedbackBuffer) return 0;
const currentValue = this.feedbackBuffer[pixelIndex] || 0;
const previousValue = this.previousFeedbackBuffer[pixelIndex] || 0;
return (currentValue - previousValue) * 0.5; // Scale for stability
}
/**
* Calculates laplacian/diffusion
*/
getLaplacian(pixelIndex: number, x: number, y: number, width: number, height: number): number {
if (!this.feedbackBuffer) return 0;
const neighbors = this.getNeighborFeedback(pixelIndex, x, y, width, height);
const currentValue = this.feedbackBuffer[pixelIndex] || 0;
return (neighbors.north + neighbors.south + neighbors.east + neighbors.west - currentValue * 4) * 0.25;
}
/**
* Calculates curvature/contrast
*/
getCurvature(pixelIndex: number, x: number, y: number, width: number, height: number): number {
if (!this.feedbackBuffer) return 0;
const neighbors = this.getNeighborFeedback(pixelIndex, x, y, width, height);
const gradientX = (neighbors.east - neighbors.west) * 0.5;
const gradientY = (neighbors.south - neighbors.north) * 0.5;
return Math.sqrt(gradientX * gradientX + gradientY * gradientY);
}
/**
* Gets/updates state value
*/
getState(pixelIndex: number, feedbackValue: number, deltaTime: number): number {
if (!this.stateBuffer) return 0;
let currentState = this.stateBuffer[pixelIndex] || 0;
// State accumulates when feedback is high, decays when low
if (feedbackValue > 128) {
currentState = Math.min(255, currentState + deltaTime * 200); // Accumulate
} else {
currentState = Math.max(0, currentState - deltaTime * 100); // Decay
}
return currentState;
}
/**
* Gets echo value
*/
getEcho(pixelIndex: number, time: number): number {
if (this.echoBuffers.length === 0) return 0;
// Cycle through different echo delays based on time
const echoIndex = Math.floor(time * 2) % this.echoBuffers.length; // Change every 0.5 seconds
const echoBuffer = this.echoBuffers[echoIndex];
return echoBuffer ? echoBuffer[pixelIndex] || 0 : 0;
}
/**
* Updates echo buffers at regular intervals
*/
updateEchoBuffers(): void {
if (!this.feedbackBuffer || this.echoBuffers.length === 0) return;
this.echoFrameCounter++;
if (this.echoFrameCounter >= this.echoInterval) {
this.echoFrameCounter = 0;
// Rotate echo buffers: shift all buffers forward and store current in first buffer
for (let i = this.echoBuffers.length - 1; i > 0; i--) {
if (this.echoBuffers[i] && this.echoBuffers[i - 1]) {
this.echoBuffers[i].set(this.echoBuffers[i - 1]);
}
}
// Store current feedback in first echo buffer
if (this.echoBuffers[0]) {
this.echoBuffers[0].set(this.feedbackBuffer);
}
}
}
/**
* Finalizes frame processing
*/
finalizeFrame(): void {
// Copy current feedback to previous for next frame momentum calculations
if (this.feedbackBuffer && this.previousFeedbackBuffer) {
this.previousFeedbackBuffer.set(this.feedbackBuffer);
}
// Update echo buffers
this.updateEchoBuffers();
}
}

View File

@ -0,0 +1,639 @@
import { ShaderFunction, ShaderContext, WorkerMessage } from '../types';
import { FeedbackSystem } from './FeedbackSystem';
import { calculateColorDirect } from '../../utils/colorModes';
/**
* Handles pixel-level rendering operations
*/
export class PixelRenderer {
private feedbackSystem: FeedbackSystem;
private shaderContext: ShaderContext;
constructor(feedbackSystem: FeedbackSystem, shaderContext: ShaderContext) {
this.feedbackSystem = feedbackSystem;
this.shaderContext = shaderContext;
}
/**
* Renders a single pixel
*/
renderPixel(
data: Uint8ClampedArray,
x: number,
y: number,
actualY: number,
width: number,
time: number,
renderMode: string,
valueMode: string,
message: WorkerMessage,
compiledFunction: ShaderFunction,
// Pre-calculated constants
centerX: number,
centerY: number,
_maxDistance: number,
invMaxDistance: number,
invFullWidth: number,
invFullHeight: number,
frameCount: number,
goldenRatio: number,
phase: number,
timeTwoPi: number,
fullWidthHalf: number,
fullHeightHalf: number,
deltaTime: number
): void {
const i = (y * width + x) * 4;
const pixelIndex = y * width + x;
try {
// Calculate coordinate variables with optimized math
const u = x * invFullWidth;
const v = actualY * invFullHeight;
// Pre-calculate deltas for reuse
const dx = x - centerX;
const dy = actualY - centerY;
// Use more efficient radius calculation
const radiusSquared = dx * dx + dy * dy;
const radius = Math.sqrt(radiusSquared);
// Optimize angle calculation - avoid atan2 for common cases
let angle: number;
if (dx === 0) {
angle = dy >= 0 ? Math.PI / 2 : -Math.PI / 2;
} else if (dy === 0) {
angle = dx >= 0 ? 0 : Math.PI;
} else {
angle = Math.atan2(dy, dx);
}
// Use pre-computed max distance inverse to avoid division
const normalizedDistance = radius * invMaxDistance;
// Optimize Manhattan distance using absolute values of pre-computed deltas
const manhattanDistance = Math.abs(dx) + Math.abs(dy);
// Optimize noise calculation with cached sin/cos values
const noise = (Math.sin(x * 0.1) * Math.cos(actualY * 0.1) + 1) * 0.5;
// Get feedback values
const feedbackValue = this.feedbackSystem.getFeedback(pixelIndex);
const neighbors = this.feedbackSystem.getNeighborFeedback(pixelIndex, x, y, width, message.fullHeight || message.height! + (message.startY || 0));
const momentum = this.feedbackSystem.getMomentum(pixelIndex);
const laplacian = this.feedbackSystem.getLaplacian(pixelIndex, x, y, width, message.fullHeight || message.height! + (message.startY || 0));
const curvature = this.feedbackSystem.getCurvature(pixelIndex, x, y, width, message.fullHeight || message.height! + (message.startY || 0));
const stateValue = this.feedbackSystem.getState(pixelIndex, feedbackValue, deltaTime);
const echoValue = this.feedbackSystem.getEcho(pixelIndex, time);
// Calculate other variables
const canvasWidth = message.fullWidth || width;
const canvasHeight = message.fullHeight || message.height! + (message.startY || 0);
const pseudoZ = Math.sin(radius * 0.01 + time) * 50;
const jitter = ((x * 73856093 + actualY * 19349663) % 256) / 255;
const oscillation = Math.sin(timeTwoPi + radius * 0.1);
// Calculate block coordinates
const bx = x >> 4;
const by = actualY >> 4;
const sx = x - fullWidthHalf;
const sy = actualY - fullHeightHalf;
const qx = x >> 3;
const qy = actualY >> 3;
// Populate context object efficiently by reusing existing object
const ctx = this.shaderContext;
ctx.x = x;
ctx.y = actualY;
ctx.t = time;
ctx.i = pixelIndex;
ctx.r = radius;
ctx.a = angle;
ctx.u = u;
ctx.v = v;
ctx.c = normalizedDistance;
ctx.f = frameCount;
ctx.d = manhattanDistance;
ctx.n = noise;
ctx.b = feedbackValue;
ctx.bn = neighbors.north;
ctx.bs = neighbors.south;
ctx.be = neighbors.east;
ctx.bw = neighbors.west;
ctx.w = canvasWidth;
ctx.h = canvasHeight;
ctx.p = phase;
ctx.z = pseudoZ;
ctx.j = jitter;
ctx.o = oscillation;
ctx.g = goldenRatio;
ctx.m = momentum;
ctx.l = laplacian;
ctx.k = curvature;
ctx.s = stateValue;
ctx.e = echoValue;
ctx.mouseX = message.mouseX || 0;
ctx.mouseY = message.mouseY || 0;
ctx.mousePressed = message.mousePressed ? 1 : 0;
ctx.mouseVX = message.mouseVX || 0;
ctx.mouseVY = message.mouseVY || 0;
ctx.mouseClickTime = message.mouseClickTime || 0;
ctx.touchCount = message.touchCount || 0;
ctx.touch0X = message.touch0X || 0;
ctx.touch0Y = message.touch0Y || 0;
ctx.touch1X = message.touch1X || 0;
ctx.touch1Y = message.touch1Y || 0;
ctx.pinchScale = message.pinchScale || 1;
ctx.pinchRotation = message.pinchRotation || 0;
ctx.accelX = message.accelX || 0;
ctx.accelY = message.accelY || 0;
ctx.accelZ = message.accelZ || 0;
ctx.gyroX = message.gyroX || 0;
ctx.gyroY = message.gyroY || 0;
ctx.gyroZ = message.gyroZ || 0;
ctx.audioLevel = message.audioLevel || 0;
ctx.bassLevel = message.bassLevel || 0;
ctx.midLevel = message.midLevel || 0;
ctx.trebleLevel = message.trebleLevel || 0;
ctx.bpm = message.bpm || 120;
ctx._t = (mod: number) => time % mod;
ctx.bx = bx;
ctx.by = by;
ctx.sx = sx;
ctx.sy = sy;
ctx.qx = qx;
ctx.qy = qy;
// Execute shader
const value = compiledFunction(ctx);
const safeValue = isFinite(value) ? value : 0;
// Calculate color
const [r, g, b] = this.calculateColor(
safeValue,
renderMode,
valueMode,
message.hueShift || 0,
x,
actualY,
canvasWidth,
canvasHeight
);
// Set pixel data
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
data[i + 3] = 255;
// Update feedback system
this.feedbackSystem.updateFeedback(pixelIndex, r, g, b, deltaTime);
this.feedbackSystem.updateState(pixelIndex, stateValue);
} catch (error) {
// Fill with black on error
data[i] = 0;
data[i + 1] = 0;
data[i + 2] = 0;
data[i + 3] = 255;
}
}
/**
* Calculates color from shader value
*/
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;
}
return calculateColorDirect(processedValue, renderMode, hueShift);
}
}

View File

@ -0,0 +1,112 @@
/**
* Context object passed to shader functions containing all necessary variables
* and state for shader execution. This replaces the previous 57+ parameter approach.
*/
export interface ShaderContext {
// Core coordinates and indices
x: number;
y: number;
t: number;
i: number; // pixelIndex
// Geometric properties
r: number; // radius
a: number; // angle
u: number; // normalized x (0-1)
v: number; // normalized y (0-1)
c: number; // normalizedDistance
d: number; // manhattanDistance
// Canvas properties
w: number; // canvasWidth
h: number; // canvasHeight
// Time-based properties
f: number; // frameCount
p: number; // phase
// Noise and effects
n: number; // noise
z: number; // pseudoZ
j: number; // jitter
o: number; // oscillation
g: number; // goldenRatio
// Feedback system
b: number; // feedbackValue
m: number; // momentum
l: number; // laplacian
k: number; // curvature
s: number; // stateValue
e: number; // echoValue
// Neighbor feedback
bn: number; // north neighbor
bs: number; // south neighbor
be: number; // east neighbor
bw: number; // west neighbor
// Input devices
mouseX: number;
mouseY: number;
mousePressed: number;
mouseVX: number;
mouseVY: number;
mouseClickTime: number;
// Touch input
touchCount: number;
touch0X: number;
touch0Y: number;
touch1X: number;
touch1Y: number;
pinchScale: number;
pinchRotation: number;
// Device motion
accelX: number;
accelY: number;
accelZ: number;
gyroX: number;
gyroY: number;
gyroZ: number;
// Audio
audioLevel: number;
bassLevel: number;
midLevel: number;
trebleLevel: number;
bpm: number;
// Time function
_t: (mod: number) => number;
// Block coordinates
bx: number; // block x
by: number; // block y
sx: number; // signed x
sy: number; // signed y
qx: number; // quarter block x
qy: number; // quarter block y
}
/**
* Type definition for compiled shader functions
*/
export type ShaderFunction = (ctx: ShaderContext) => number;
/**
* Creates a default shader context with zero values
*/
export function createDefaultShaderContext(): ShaderContext {
return {
x: 0, y: 0, t: 0, i: 0, r: 0, a: 0, u: 0, v: 0, c: 0, f: 0, d: 0, n: 0, b: 0,
bn: 0, bs: 0, be: 0, bw: 0, w: 0, h: 0, p: 0, z: 0, j: 0, o: 0, g: 0, m: 0,
l: 0, k: 0, s: 0, e: 0, mouseX: 0, mouseY: 0, mousePressed: 0, mouseVX: 0,
mouseVY: 0, mouseClickTime: 0, touchCount: 0, touch0X: 0, touch0Y: 0, touch1X: 0,
touch1Y: 0, pinchScale: 1, pinchRotation: 0, accelX: 0, accelY: 0, accelZ: 0,
gyroX: 0, gyroY: 0, gyroZ: 0, audioLevel: 0, bassLevel: 0, midLevel: 0,
trebleLevel: 0, bpm: 120, _t: (_mod: number) => 0, bx: 0, by: 0, sx: 0, sy: 0,
qx: 0, qy: 0
};
}

View File

@ -0,0 +1,56 @@
/**
* Message types for communication between main thread and shader workers
*/
/**
* Message sent from main thread to worker
*/
export 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;
}
/**
* Response message sent from worker to main thread
*/
export interface WorkerResponse {
id: string;
type: 'compiled' | 'rendered' | 'error';
success: boolean;
imageData?: ImageData;
error?: string;
}

View File

@ -0,0 +1,7 @@
/**
* Type definitions for shader system
*/
export type { ShaderContext, ShaderFunction } from './ShaderContext';
export { createDefaultShaderContext } from './ShaderContext';
export type { WorkerMessage, WorkerResponse } from './WorkerMessage';

View File

@ -0,0 +1,317 @@
import { WorkerMessage, WorkerResponse, createDefaultShaderContext } from '../types';
import { ShaderCompiler } from '../core/ShaderCompiler';
import { ShaderCache } from '../core/ShaderCache';
import { FeedbackSystem } from '../rendering/FeedbackSystem';
import { PixelRenderer } from '../rendering/PixelRenderer';
import { PERFORMANCE } from '../../utils/constants';
/**
* Main shader worker class - handles compilation and rendering
*/
class ShaderWorker {
private compiledFunction: any = null;
private lastCode: string = '';
private cache: ShaderCache;
private feedbackSystem: FeedbackSystem;
private pixelRenderer: PixelRenderer;
private shaderContext = createDefaultShaderContext();
private lastFrameTime: number = 0;
constructor() {
this.cache = new ShaderCache();
this.feedbackSystem = new FeedbackSystem();
this.pixelRenderer = new PixelRenderer(this.feedbackSystem, this.shaderContext);
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 = ShaderCompiler.hashCode(code);
if (code === this.lastCode && this.compiledFunction) {
this.postMessage({ id, type: 'compiled', success: true });
return;
}
// Check compilation cache
const cachedFunction = this.cache.getCompiledShader(codeHash);
if (cachedFunction) {
this.compiledFunction = cachedFunction;
this.lastCode = code;
this.postMessage({ id, type: 'compiled', success: true });
return;
}
try {
this.compiledFunction = ShaderCompiler.compile(code);
// Cache the compiled function
if (this.compiledFunction) {
this.cache.setCompiledShader(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 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.cache.getOrCreateImageData(width, height);
const data = imageData.data;
const startTime = performance.now();
const maxRenderTime = PERFORMANCE.MAX_RENDER_TIME_MS;
// Initialize feedback buffers if needed
this.feedbackSystem.initializeBuffers(width, height);
// Update frame timing for frame rate independence
const deltaTime = time - this.lastFrameTime;
this.lastFrameTime = time;
try {
// Use tiled rendering for better timeout handling
this.renderTiled(
data,
width,
height,
time,
renderMode,
valueMode,
message,
startTime,
maxRenderTime,
startY,
deltaTime
);
// Finalize frame processing
this.feedbackSystem.finalizeFrame();
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,
deltaTime: number = 0.016
): void {
const tileSize = PERFORMANCE.DEFAULT_TILE_SIZE;
const tilesX = Math.ceil(width / tileSize);
const tilesY = Math.ceil(height / tileSize);
// Pre-calculate constants outside the loop for performance
const fullWidth = message.fullWidth || width;
const fullHeight = message.fullHeight || message.height! + yOffset;
const centerX = fullWidth / 2;
const centerY = fullHeight / 2;
const maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2);
const invMaxDistance = 1 / maxDistance;
const invFullWidth = 1 / fullWidth;
const invFullHeight = 1 / fullHeight;
const frameCount = Math.floor(time * 60);
const goldenRatio = 1.618033988749;
const phase = (time * Math.PI * 2) % (Math.PI * 2);
const timeTwoPi = time * 2 * Math.PI;
const fullWidthHalf = fullWidth >> 1;
const fullHeightHalf = fullHeight >> 1;
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,
deltaTime,
// Pre-calculated constants
centerX,
centerY,
maxDistance,
invMaxDistance,
invFullWidth,
invFullHeight,
frameCount,
goldenRatio,
phase,
timeTwoPi,
fullWidthHalf,
fullHeightHalf
);
}
}
}
private renderTile(
data: Uint8ClampedArray,
width: number,
startX: number,
startY: number,
endX: number,
endY: number,
time: number,
renderMode: string,
valueMode: string,
message: WorkerMessage,
yOffset: number,
deltaTime: number,
// Pre-calculated constants
centerX: number,
centerY: number,
maxDistance: number,
invMaxDistance: number,
invFullWidth: number,
invFullHeight: number,
frameCount: number,
goldenRatio: number,
phase: number,
timeTwoPi: number,
fullWidthHalf: number,
fullHeightHalf: number
): void {
for (let y = startY; y < endY; y++) {
for (let x = startX; x < endX; x++) {
const actualY = y + yOffset;
this.pixelRenderer.renderPixel(
data,
x,
y,
actualY,
width,
time,
renderMode,
valueMode,
message,
this.compiledFunction,
// Pre-calculated constants
centerX,
centerY,
maxDistance,
invMaxDistance,
invFullWidth,
invFullHeight,
frameCount,
goldenRatio,
phase,
timeTwoPi,
fullWidthHalf,
fullHeightHalf,
deltaTime
);
}
}
}
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 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();

View File

@ -25,6 +25,7 @@ export interface InputState {
midLevel: number;
trebleLevel: number;
audioEnabled: boolean;
webcamEnabled: boolean;
}
export const defaultInputState: InputState = {
@ -52,6 +53,7 @@ export const defaultInputState: InputState = {
midLevel: 0,
trebleLevel: 0,
audioEnabled: false,
webcamEnabled: false,
};
export const $input = atom<InputState>(defaultInputState);

View File

@ -54,7 +54,7 @@ a:visited {
top: 0;
left: 0;
right: 0;
height: 40px;
height: 56px;
background: rgba(0, 0, 0, var(--ui-opacity));
border-bottom: 1px solid #333;
display: flex;
@ -117,11 +117,11 @@ a:visited {
#mobile-menu {
position: fixed;
top: 40px;
top: 56px;
right: -320px;
width: 320px;
max-width: 80vw;
height: calc(100vh - 40px);
height: calc(100vh - 56px);
background: rgba(0, 0, 0, var(--ui-opacity));
backdrop-filter: blur(3px);
border-left: 1px solid rgba(255, 255, 255, 0.1);
@ -517,10 +517,10 @@ button [data-lucide] {
#shader-library {
position: fixed;
top: 40px;
top: 56px;
left: -300px;
width: 300px;
height: calc(100vh - 40px);
height: calc(100vh - 56px);
background: rgba(0, 0, 0, calc(var(--ui-opacity) + 0.1));
border-right: 1px solid rgba(255, 255, 255, 0.1);
z-index: 90;
@ -533,10 +533,10 @@ button [data-lucide] {
#shader-library-trigger {
position: fixed;
top: 40px;
top: 56px;
left: 0;
width: 20px;
height: calc(100vh - 40px);
height: calc(100vh - 56px);
z-index: 91;
cursor: pointer;
}
@ -725,7 +725,7 @@ button [data-lucide] {
}
#topbar {
height: 40px;
height: 56px;
padding: 0 10px;
}
@ -778,8 +778,8 @@ button [data-lucide] {
#shader-library {
width: 100%;
left: -100%;
top: 40px;
height: calc(100vh - 40px);
top: 56px;
height: calc(100vh - 56px);
}
#shader-library-trigger {

View File

@ -93,7 +93,7 @@ export type ValueMode = (typeof VALUE_MODES)[number];
// Default Values
export const DEFAULTS = {
RESOLUTION: 1,
RESOLUTION: 8,
FPS: 30,
RENDER_MODE: 'classic',
VALUE_MODE: 'integer' as ValueMode,