essais
This commit is contained in:
@ -15,6 +15,7 @@ const CORE_ASSETS = [
|
|||||||
const DYNAMIC_ASSETS_PATTERNS = [
|
const DYNAMIC_ASSETS_PATTERNS = [
|
||||||
/\/src\/.+\.(ts|tsx|js|jsx)$/,
|
/\/src\/.+\.(ts|tsx|js|jsx)$/,
|
||||||
/\/src\/.+\.css$/,
|
/\/src\/.+\.css$/,
|
||||||
|
/\/assets\/.+\.(js|css)$/,
|
||||||
/fonts\.googleapis\.com/,
|
/fonts\.googleapis\.com/,
|
||||||
/fonts\.gstatic\.com/
|
/fonts\.gstatic\.com/
|
||||||
];
|
];
|
||||||
@ -30,6 +31,10 @@ self.addEventListener('install', event => {
|
|||||||
}),
|
}),
|
||||||
caches.open(DYNAMIC_CACHE).then(cache => {
|
caches.open(DYNAMIC_CACHE).then(cache => {
|
||||||
console.log('Dynamic cache initialized');
|
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(() => {
|
]).then(() => {
|
||||||
console.log('Service Worker installed successfully');
|
console.log('Service Worker installed successfully');
|
||||||
|
|||||||
@ -1,47 +1,4 @@
|
|||||||
interface WorkerMessage {
|
import { WorkerMessage, WorkerResponse } from './shader/types';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class FakeShader {
|
export class FakeShader {
|
||||||
private canvas: HTMLCanvasElement;
|
private canvas: HTMLCanvasElement;
|
||||||
@ -61,6 +18,48 @@ export class FakeShader {
|
|||||||
private timeSpeed: number = 1.0;
|
private timeSpeed: number = 1.0;
|
||||||
private currentBPM: number = 120;
|
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
|
// Multi-worker state
|
||||||
private tileResults: Map<number, ImageData> = new Map();
|
private tileResults: Map<number, ImageData> = new Map();
|
||||||
private tilesCompleted: number = 0;
|
private tilesCompleted: number = 0;
|
||||||
@ -137,7 +136,7 @@ export class FakeShader {
|
|||||||
private initializeWorkers(): void {
|
private initializeWorkers(): void {
|
||||||
// Create worker pool
|
// Create worker pool
|
||||||
for (let i = 0; i < this.workerCount; i++) {
|
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',
|
type: 'module',
|
||||||
});
|
});
|
||||||
worker.onmessage = (e: MessageEvent<WorkerResponse>) =>
|
worker.onmessage = (e: MessageEvent<WorkerResponse>) =>
|
||||||
@ -203,7 +202,7 @@ export class FakeShader {
|
|||||||
|
|
||||||
private compile(): void {
|
private compile(): void {
|
||||||
this.isCompiled = false;
|
this.isCompiled = false;
|
||||||
const id = `compile_${Date.now()}`;
|
const id = `compile_${++this.idCounter}`;
|
||||||
|
|
||||||
// Send compile message to all workers
|
// Send compile message to all workers
|
||||||
this.workers.forEach((worker) => {
|
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 {
|
private renderWithSingleWorker(id: string, currentTime: number): void {
|
||||||
this.worker.postMessage({
|
this.updateReusableMessage(id, currentTime, this.canvas.width, this.canvas.height, this.canvas.width, this.canvas.height, 0);
|
||||||
id,
|
this.worker.postMessage(this.reusableMessage);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderWithMultipleWorkers(id: string, currentTime: number): void {
|
private renderWithMultipleWorkers(id: string, currentTime: number): void {
|
||||||
@ -288,45 +291,18 @@ export class FakeShader {
|
|||||||
|
|
||||||
if (startY >= height) return; // Skip if tile is outside canvas
|
if (startY >= height) return; // Skip if tile is outside canvas
|
||||||
|
|
||||||
worker.postMessage({
|
// Update reusable message with worker-specific values
|
||||||
id: `${id}_tile_${index}`,
|
this.updateReusableMessage(
|
||||||
type: 'render',
|
`${id}_tile_${index}`,
|
||||||
width: width,
|
currentTime,
|
||||||
height: endY - startY,
|
width,
|
||||||
// Pass the Y offset for correct coordinate calculation
|
endY - startY,
|
||||||
startY: startY,
|
width,
|
||||||
// Pass full canvas dimensions for center calculations
|
height,
|
||||||
fullWidth: width,
|
startY
|
||||||
fullHeight: height,
|
);
|
||||||
time: currentTime,
|
|
||||||
renderMode: this.renderMode,
|
worker.postMessage(this.reusableMessage);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,7 +332,7 @@ export class FakeShader {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderId = `render_${Date.now()}_${Math.random()}`;
|
const renderId = `render_${++this.idCounter}`;
|
||||||
|
|
||||||
// Add to pending renders queue
|
// Add to pending renders queue
|
||||||
this.pendingRenders.push(renderId);
|
this.pendingRenders.push(renderId);
|
||||||
@ -607,9 +583,10 @@ export class FakeShader {
|
|||||||
const ops = ['^', '&', '|', '+', '-', '*', '%', '**', '%'];
|
const ops = ['^', '&', '|', '+', '-', '*', '%', '**', '%'];
|
||||||
const shifts = ['<<', '>>'];
|
const shifts = ['<<', '>>'];
|
||||||
|
|
||||||
const numbers = [];
|
const numbers: number[] = [];
|
||||||
for (let i = 0; i < Math.random(200); i++) {
|
const numCount = Math.floor(Math.random() * 20) + 10; // Generate 10-30 numbers
|
||||||
numbers.push(Math.floor(Math.random(400)))
|
for (let i = 0; i < numCount; i++) {
|
||||||
|
numbers.push(Math.floor(Math.random() * 400))
|
||||||
}
|
}
|
||||||
const randomChoice = <T>(arr: T[]): T =>
|
const randomChoice = <T>(arr: T[]): T =>
|
||||||
arr[Math.floor(Math.random() * arr.length)];
|
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,14 +116,12 @@ export function TopBar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="topbar" className={ui.uiVisible ? '' : 'hidden'}>
|
<div
|
||||||
|
id="topbar"
|
||||||
|
className={ui.uiVisible ? '' : 'hidden'}>
|
||||||
<div className="title">Bitfielder</div>
|
<div className="title">Bitfielder</div>
|
||||||
<div className="controls">
|
<div className="controls">
|
||||||
<div className="controls-desktop">
|
<div className="controls-desktop">
|
||||||
<label
|
|
||||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
|
||||||
>
|
|
||||||
Resolution:
|
|
||||||
<select
|
<select
|
||||||
value={settings.resolution}
|
value={settings.resolution}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@ -135,6 +133,7 @@ export function TopBar() {
|
|||||||
color: '#fff',
|
color: '#fff',
|
||||||
padding: '4px',
|
padding: '4px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
|
marginRight: '10px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="1">Full (1x)</option>
|
<option value="1">Full (1x)</option>
|
||||||
@ -144,12 +143,7 @@ export function TopBar() {
|
|||||||
<option value="16">Sixteenth (16x)</option>
|
<option value="16">Sixteenth (16x)</option>
|
||||||
<option value="32">Thirty-second (32x)</option>
|
<option value="32">Thirty-second (32x)</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label
|
|
||||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
|
||||||
>
|
|
||||||
FPS:
|
|
||||||
<select
|
<select
|
||||||
value={settings.fps}
|
value={settings.fps}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@ -161,18 +155,14 @@ export function TopBar() {
|
|||||||
color: '#fff',
|
color: '#fff',
|
||||||
padding: '4px',
|
padding: '4px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
|
marginRight: '10px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="15">15 FPS</option>
|
<option value="15">15 FPS</option>
|
||||||
<option value="30">30 FPS</option>
|
<option value="30">30 FPS</option>
|
||||||
<option value="60">60 FPS</option>
|
<option value="60">60 FPS</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label
|
|
||||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
|
||||||
>
|
|
||||||
Value Mode:
|
|
||||||
<select
|
<select
|
||||||
value={settings.valueMode}
|
value={settings.valueMode}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@ -184,6 +174,7 @@ export function TopBar() {
|
|||||||
color: '#fff',
|
color: '#fff',
|
||||||
padding: '4px',
|
padding: '4px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
|
marginRight: '10px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{VALUE_MODES.map((mode) => (
|
{VALUE_MODES.map((mode) => (
|
||||||
@ -192,12 +183,7 @@ export function TopBar() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label
|
|
||||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
|
||||||
>
|
|
||||||
Render Mode:
|
|
||||||
<select
|
<select
|
||||||
value={settings.renderMode}
|
value={settings.renderMode}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@ -209,6 +195,7 @@ export function TopBar() {
|
|||||||
color: '#fff',
|
color: '#fff',
|
||||||
padding: '4px',
|
padding: '4px',
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
|
marginRight: '10px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<option value="classic">Classic</option>
|
<option value="classic">Classic</option>
|
||||||
@ -233,12 +220,7 @@ export function TopBar() {
|
|||||||
<option value="xray">X-Ray</option>
|
<option value="xray">X-Ray</option>
|
||||||
<option value="spectrum">Spectrum</option>
|
<option value="spectrum">Spectrum</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label
|
|
||||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
|
||||||
>
|
|
||||||
Hue Shift:
|
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="0"
|
min="0"
|
||||||
@ -247,17 +229,9 @@ export function TopBar() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateAppSettings({ hueShift: parseInt(e.target.value) })
|
updateAppSettings({ hueShift: parseInt(e.target.value) })
|
||||||
}
|
}
|
||||||
style={{ width: '80px', verticalAlign: 'middle' }}
|
style={{ width: '80px', verticalAlign: 'middle', marginRight: '10px' }}
|
||||||
/>
|
/>
|
||||||
<span style={{ fontSize: '11px' }}>
|
|
||||||
{settings.hueShift ?? 0}°
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label
|
|
||||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
|
||||||
>
|
|
||||||
UI Opacity:
|
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="10"
|
min="10"
|
||||||
@ -266,12 +240,8 @@ export function TopBar() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
updateAppSettings({ uiOpacity: parseInt(e.target.value) / 100 })
|
updateAppSettings({ uiOpacity: parseInt(e.target.value) / 100 })
|
||||||
}
|
}
|
||||||
style={{ width: '80px', verticalAlign: 'middle' }}
|
style={{ width: '80px', verticalAlign: 'middle', marginRight: '10px' }}
|
||||||
/>
|
/>
|
||||||
<span style={{ fontSize: '11px' }}>
|
|
||||||
{Math.round((settings.uiOpacity ?? 0.3) * 100)}%
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button id="help-btn" onClick={showHelp}>
|
<button id="help-btn" onClick={showHelp}>
|
||||||
<LucideIcon name="help" />
|
<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;
|
midLevel: number;
|
||||||
trebleLevel: number;
|
trebleLevel: number;
|
||||||
audioEnabled: boolean;
|
audioEnabled: boolean;
|
||||||
|
webcamEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultInputState: InputState = {
|
export const defaultInputState: InputState = {
|
||||||
@ -52,6 +53,7 @@ export const defaultInputState: InputState = {
|
|||||||
midLevel: 0,
|
midLevel: 0,
|
||||||
trebleLevel: 0,
|
trebleLevel: 0,
|
||||||
audioEnabled: false,
|
audioEnabled: false,
|
||||||
|
webcamEnabled: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const $input = atom<InputState>(defaultInputState);
|
export const $input = atom<InputState>(defaultInputState);
|
||||||
|
|||||||
@ -54,7 +54,7 @@ a:visited {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
height: 40px;
|
height: 56px;
|
||||||
background: rgba(0, 0, 0, var(--ui-opacity));
|
background: rgba(0, 0, 0, var(--ui-opacity));
|
||||||
border-bottom: 1px solid #333;
|
border-bottom: 1px solid #333;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -117,11 +117,11 @@ a:visited {
|
|||||||
|
|
||||||
#mobile-menu {
|
#mobile-menu {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 40px;
|
top: 56px;
|
||||||
right: -320px;
|
right: -320px;
|
||||||
width: 320px;
|
width: 320px;
|
||||||
max-width: 80vw;
|
max-width: 80vw;
|
||||||
height: calc(100vh - 40px);
|
height: calc(100vh - 56px);
|
||||||
background: rgba(0, 0, 0, var(--ui-opacity));
|
background: rgba(0, 0, 0, var(--ui-opacity));
|
||||||
backdrop-filter: blur(3px);
|
backdrop-filter: blur(3px);
|
||||||
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
border-left: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
@ -517,10 +517,10 @@ button [data-lucide] {
|
|||||||
|
|
||||||
#shader-library {
|
#shader-library {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 40px;
|
top: 56px;
|
||||||
left: -300px;
|
left: -300px;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
height: calc(100vh - 40px);
|
height: calc(100vh - 56px);
|
||||||
background: rgba(0, 0, 0, calc(var(--ui-opacity) + 0.1));
|
background: rgba(0, 0, 0, calc(var(--ui-opacity) + 0.1));
|
||||||
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
z-index: 90;
|
z-index: 90;
|
||||||
@ -533,10 +533,10 @@ button [data-lucide] {
|
|||||||
|
|
||||||
#shader-library-trigger {
|
#shader-library-trigger {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 40px;
|
top: 56px;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: calc(100vh - 40px);
|
height: calc(100vh - 56px);
|
||||||
z-index: 91;
|
z-index: 91;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@ -725,7 +725,7 @@ button [data-lucide] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#topbar {
|
#topbar {
|
||||||
height: 40px;
|
height: 56px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -778,8 +778,8 @@ button [data-lucide] {
|
|||||||
#shader-library {
|
#shader-library {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: -100%;
|
left: -100%;
|
||||||
top: 40px;
|
top: 56px;
|
||||||
height: calc(100vh - 40px);
|
height: calc(100vh - 56px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#shader-library-trigger {
|
#shader-library-trigger {
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export type ValueMode = (typeof VALUE_MODES)[number];
|
|||||||
|
|
||||||
// Default Values
|
// Default Values
|
||||||
export const DEFAULTS = {
|
export const DEFAULTS = {
|
||||||
RESOLUTION: 1,
|
RESOLUTION: 8,
|
||||||
FPS: 30,
|
FPS: 30,
|
||||||
RENDER_MODE: 'classic',
|
RENDER_MODE: 'classic',
|
||||||
VALUE_MODE: 'integer' as ValueMode,
|
VALUE_MODE: 'integer' as ValueMode,
|
||||||
|
|||||||
Reference in New Issue
Block a user