diff --git a/src/FakeShader.ts b/src/FakeShader.ts
index d69b492..e1f50ad 100644
--- a/src/FakeShader.ts
+++ b/src/FakeShader.ts
@@ -476,21 +476,36 @@ export class FakeShader {
}
}
- private compositeTiles(): void {
- const width = this.canvas.width;
+ private async compositeTiles(): Promise {
const height = this.canvas.height;
const tileHeight = Math.ceil(height / this.workerCount);
- // Clear main canvas
- this.ctx.clearRect(0, 0, width, height);
-
- // Composite all tiles directly on main canvas
- for (let i = 0; i < this.workerCount; i++) {
- const tileData = this.tileResults.get(i);
- if (tileData) {
- const startY = i * tileHeight;
- this.ctx.putImageData(tileData, 0, startY);
+ // Use ImageBitmap for faster compositing if available
+ if (typeof createImageBitmap !== 'undefined') {
+ try {
+ const bitmapPromises: Promise[] = [];
+ const positions: number[] = [];
+
+ for (let i = 0; i < this.workerCount; i++) {
+ const tileData = this.tileResults.get(i);
+ if (tileData) {
+ bitmapPromises.push(createImageBitmap(tileData));
+ positions.push(i * tileHeight);
+ }
+ }
+
+ const bitmaps = await Promise.all(bitmapPromises);
+
+ for (let i = 0; i < bitmaps.length; i++) {
+ this.ctx.drawImage(bitmaps[i], 0, positions[i]);
+ bitmaps[i].close(); // Free memory
+ }
+ } catch (error) {
+ // Fallback to putImageData if ImageBitmap fails
+ this.fallbackCompositeTiles();
}
+ } else {
+ this.fallbackCompositeTiles();
}
// Clear tile results
@@ -510,6 +525,18 @@ export class FakeShader {
}
}
+ private fallbackCompositeTiles(): void {
+ const tileHeight = Math.ceil(this.canvas.height / this.workerCount);
+
+ for (let i = 0; i < this.workerCount; i++) {
+ const tileData = this.tileResults.get(i);
+ if (tileData) {
+ const startY = i * tileHeight;
+ this.ctx.putImageData(tileData, 0, startY);
+ }
+ }
+ }
+
// Simplified method - kept for backward compatibility but always uses all cores
setMultiWorkerMode(_enabled: boolean, _workerCount?: number): void {
// Always use all available cores, ignore the enabled parameter
diff --git a/src/ShaderWorker.ts b/src/ShaderWorker.ts
index 2deeb49..0c0780b 100644
--- a/src/ShaderWorker.ts
+++ b/src/ShaderWorker.ts
@@ -46,16 +46,20 @@ interface WorkerResponse {
import { LRUCache } from './utils/LRUCache';
import { calculateColorDirect } from './utils/colorModes';
-import { PERFORMANCE, COLOR_TABLE_SIZE } from './utils/constants';
+import { PERFORMANCE, COLOR_TABLE_SIZE, RENDER_MODES, RENDER_MODE_INDEX } from './utils/constants';
type ShaderFunction = (...args: number[]) => number;
class ShaderWorker {
private compiledFunction: ShaderFunction | null = null;
private lastCode: string = '';
- private imageDataCache: LRUCache = new LRUCache(PERFORMANCE.IMAGE_DATA_CACHE_SIZE);
- private compilationCache: LRUCache = new LRUCache(PERFORMANCE.COMPILATION_CACHE_SIZE);
- private colorTables: Map = new Map();
+ private imageDataCache: LRUCache = new LRUCache(
+ PERFORMANCE.IMAGE_DATA_CACHE_SIZE
+ );
+ private compilationCache: LRUCache = new LRUCache(
+ PERFORMANCE.COMPILATION_CACHE_SIZE
+ );
+ private colorTables: Uint8Array[] = [];
private feedbackBuffer: Float32Array | null = null;
constructor() {
@@ -66,23 +70,14 @@ class ShaderWorker {
this.initializeColorTables();
}
-
private initializeColorTables(): void {
const tableSize = COLOR_TABLE_SIZE;
- // Pre-compute color tables for each render mode
- const modes = [
- 'classic',
- 'grayscale',
- 'red',
- 'green',
- 'blue',
- 'rgb',
- 'hsv',
- 'rainbow',
- ];
+ // Pre-compute color tables for each render mode using array indexing
+ this.colorTables = new Array(RENDER_MODES.length);
- for (const mode of modes) {
+ for (let modeIndex = 0; modeIndex < RENDER_MODES.length; modeIndex++) {
+ const mode = RENDER_MODES[modeIndex];
const colorTable = new Uint8Array(tableSize * 3); // RGB triplets
for (let i = 0; i < tableSize; i++) {
@@ -92,11 +87,10 @@ class ShaderWorker {
colorTable[i * 3 + 2] = b;
}
- this.colorTables.set(mode, colorTable);
+ this.colorTables[modeIndex] = colorTable;
}
}
-
private handleMessage(message: WorkerMessage): void {
try {
switch (message.type) {
@@ -226,44 +220,10 @@ class ShaderWorker {
}
private isStaticExpression(code: string): boolean {
- // Check if code contains any variables
- const variables = [
- 'x',
- 'y',
- 't',
- 'i',
- 'mouseX',
- 'mouseY',
- 'mousePressed',
- 'mouseVX',
- 'mouseVY',
- 'mouseClickTime',
- 'touchCount',
- 'touch0X',
- 'touch0Y',
- 'touch1X',
- 'touch1Y',
- 'pinchScale',
- 'pinchRotation',
- 'accelX',
- 'accelY',
- 'accelZ',
- 'gyroX',
- 'gyroY',
- 'gyroZ',
- 'audioLevel',
- 'bassLevel',
- 'midLevel',
- 'trebleLevel',
- ];
-
- for (const variable of variables) {
- if (code.includes(variable)) {
- return false;
- }
- }
-
- return true;
+ // 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|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);
}
private evaluateStaticExpression(code: string): number {
@@ -305,7 +265,7 @@ class ShaderWorker {
const data = imageData.data;
const startTime = performance.now();
const maxRenderTime = PERFORMANCE.MAX_RENDER_TIME_MS;
-
+
// Initialize feedback buffer if needed
if (!this.feedbackBuffer || this.feedbackBuffer.length !== width * height) {
this.feedbackBuffer = new Float32Array(width * height);
@@ -412,15 +372,20 @@ class ShaderWorker {
const v = actualY / fullHeight;
const centerX = fullWidth / 2;
const centerY = fullHeight / 2;
- const radius = Math.sqrt((x - centerX) ** 2 + (actualY - centerY) ** 2);
+ const radius = Math.sqrt(
+ (x - centerX) ** 2 + (actualY - centerY) ** 2
+ );
const angle = Math.atan2(actualY - centerY, x - centerX);
const maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2);
const normalizedDistance = radius / maxDistance;
const frameCount = Math.floor(time * 60);
- const manhattanDistance = Math.abs(x - centerX) + Math.abs(actualY - centerY);
+ const manhattanDistance =
+ Math.abs(x - centerX) + Math.abs(actualY - centerY);
const noise = (Math.sin(x * 0.1) * Math.cos(actualY * 0.1) + 1) * 0.5;
- const feedbackValue = this.feedbackBuffer ? this.feedbackBuffer[pixelIndex] || 0 : 0;
-
+ const feedbackValue = this.feedbackBuffer
+ ? this.feedbackBuffer[pixelIndex] || 0
+ : 0;
+
const value = this.compiledFunction!(
x,
actualY,
@@ -474,7 +439,7 @@ class ShaderWorker {
data[i + 1] = g;
data[i + 2] = b;
data[i + 3] = 255;
-
+
// Update feedback buffer with current processed value
if (this.feedbackBuffer) {
this.feedbackBuffer[pixelIndex] = safeValue;
@@ -515,7 +480,6 @@ class ShaderWorker {
if (!imageData) {
imageData = new ImageData(width, height);
this.imageDataCache.set(key, imageData);
-
}
return imageData;
@@ -620,16 +584,17 @@ class ShaderWorker {
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);
+ 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);
+
+ processedValue = Math.floor((fractalValue + 1) * 0.5 * 255);
break;
}
@@ -638,20 +603,24 @@ class ShaderWorker {
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));
-
+ 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 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;
+
+ const cellState = cellHash % 256 > 128 ? 1 : 0;
+ const evolution = neighbors >= 3 && neighbors <= 5 ? 1 : cellState;
processedValue = evolution * 255;
break;
}
@@ -661,14 +630,14 @@ class ShaderWorker {
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);
+ processedValue = Math.floor((combinedNoise + 1) * 0.5 * 255);
break;
}
@@ -676,27 +645,32 @@ class ShaderWorker {
// 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;
-
+ 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 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));
@@ -707,103 +681,109 @@ class ShaderWorker {
// 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
+ 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
+ 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
- }
+ 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;
+ 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);
-
+ 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;
+ 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));
-
+ 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;
-
+ const enhanced =
+ Math.sin(flowValue * 0.05 + Math.abs(value) * 0.01) * 0.5 + 0.5;
+
processedValue = Math.floor(enhanced * 255);
break;
}
@@ -814,18 +794,18 @@ class ShaderWorker {
break;
}
- // Use pre-computed color table if available
- const colorTable = this.colorTables.get(renderMode);
- if (colorTable) {
+ // Use pre-computed color table with O(1) array indexing
+ const modeIndex = RENDER_MODE_INDEX[renderMode];
+ if (modeIndex !== undefined && this.colorTables[modeIndex]) {
+ const colorTable = this.colorTables[modeIndex];
const index = Math.floor(processedValue) * 3;
return [colorTable[index], colorTable[index + 1], colorTable[index + 2]];
}
- // Fallback to direct calculation
+ // Fallback to direct calculation for unknown render modes
return calculateColorDirect(processedValue, renderMode);
}
-
private sanitizeCode(code: string): string {
// Auto-prefix Math functions
const mathFunctions = [
diff --git a/src/Storage.ts b/src/Storage.ts
index 2473fc1..873131d 100644
--- a/src/Storage.ts
+++ b/src/Storage.ts
@@ -1,5 +1,10 @@
import { AppSettings } from './stores/appSettings';
-import { STORAGE_KEYS, PERFORMANCE, DEFAULTS, ValueMode } from './utils/constants';
+import {
+ STORAGE_KEYS,
+ PERFORMANCE,
+ DEFAULTS,
+ ValueMode,
+} from './utils/constants';
export interface SavedShader {
id: string;
diff --git a/src/components/HelpPopup.tsx b/src/components/HelpPopup.tsx
index d96db3f..28bda42 100644
--- a/src/components/HelpPopup.tsx
+++ b/src/components/HelpPopup.tsx
@@ -80,7 +80,7 @@ export function HelpPopup() {
n - Noise value (0.0 to 1.0)
- b - Previous frame's value (feedback)
+ b - Previous frame's value (feedback)
mouseX, mouseY - Mouse position (0.0 to 1.0)
@@ -139,7 +139,7 @@ export function HelpPopup() {
trebleLevel - High frequencies (0.0-1.0)
- Click "Enable Audio" to activate microphone
+ Click "Enable Audio" to activate microphone
@@ -295,10 +295,7 @@ export function HelpPopup() {
Website:{' '}
-
+
raphaelforment.fr
@@ -307,6 +304,7 @@ export function HelpPopup() {
git.raphaelforment.fr
diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx
index 8cda42a..8d19d69 100644
--- a/src/components/MobileMenu.tsx
+++ b/src/components/MobileMenu.tsx
@@ -112,7 +112,9 @@ export function MobileMenu() {