essais
This commit is contained in:
@ -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');
|
||||
|
||||
@ -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)];
|
||||
|
||||
1162
src/ShaderWorker.ts
1162
src/ShaderWorker.ts
File diff suppressed because it is too large
Load Diff
@ -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
80
src/hooks/useWebcam.ts
Normal 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 };
|
||||
}
|
||||
53
src/shader/core/ShaderCache.ts
Normal file
53
src/shader/core/ShaderCache.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
145
src/shader/core/ShaderCompiler.ts
Normal file
145
src/shader/core/ShaderCompiler.ts
Normal 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
15
src/shader/index.ts
Normal 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';
|
||||
198
src/shader/rendering/FeedbackSystem.ts
Normal file
198
src/shader/rendering/FeedbackSystem.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
639
src/shader/rendering/PixelRenderer.ts
Normal file
639
src/shader/rendering/PixelRenderer.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
112
src/shader/types/ShaderContext.ts
Normal file
112
src/shader/types/ShaderContext.ts
Normal 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
|
||||
};
|
||||
}
|
||||
56
src/shader/types/WorkerMessage.ts
Normal file
56
src/shader/types/WorkerMessage.ts
Normal 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;
|
||||
}
|
||||
7
src/shader/types/index.ts
Normal file
7
src/shader/types/index.ts
Normal 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';
|
||||
317
src/shader/worker/ShaderWorker.ts
Normal file
317
src/shader/worker/ShaderWorker.ts
Normal 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();
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user