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() { updateAppSettings({ valueMode: e.target.value as ValueMode })} + onChange={(e) => + updateAppSettings({ valueMode: e.target.value as ValueMode }) + } style={{ background: 'rgba(255,255,255,0.1)', border: '1px solid #555', diff --git a/src/components/WelcomePopup.tsx b/src/components/WelcomePopup.tsx index ce3a8d7..ffdf6ff 100644 --- a/src/components/WelcomePopup.tsx +++ b/src/components/WelcomePopup.tsx @@ -32,11 +32,17 @@ export const WelcomePopup: React.FC = () => {

Welcome to BitFielder

-

BitFielder is an experimental lofi bitfield shader editor made by BuboBubo. Use it to create visual compositions through code. I use it for fun :)

+

+ BitFielder is an experimental lofi bitfield shader editor made by{' '} + BuboBubo. Use it to create + visual compositions through code. I use it for fun :){' '} +

Getting Started

    -
  • Edit the shader code and press Eval or Ctrl+Enter
  • +
  • + Edit the shader code and press Eval or Ctrl+Enter +
  • Use special variables to create reactive effects
  • Explore/store shaders in the library (left pane)
  • Export your creations as images or sharable links
  • @@ -44,13 +50,24 @@ export const WelcomePopup: React.FC = () => {

    Key Features

      -
    • Real-time editing: See your changes instantly
    • -
    • Motion and touch: Mouse, touchscreen support
    • -
    • Audio reactive: Synchronize with a sound signal
    • -
    • Export capabilities: Save and share your work
    • +
    • + Real-time editing: See your changes instantly +
    • +
    • + Motion and touch: Mouse, touchscreen support +
    • +
    • + Audio reactive: Synchronize with a sound signal +
    • +
    • + Export capabilities: Save and share your work +
    -

    Press ? anytime to view keyboard shortcuts and detailed help.

    +

    + Press ? anytime to view keyboard shortcuts and detailed + help. +

    Press any key to dismiss this message

diff --git a/src/hooks/useKeyboardShortcuts.ts b/src/hooks/useKeyboardShortcuts.ts index 9ffd403..8910e20 100644 --- a/src/hooks/useKeyboardShortcuts.ts +++ b/src/hooks/useKeyboardShortcuts.ts @@ -74,4 +74,4 @@ function shareURL() { .catch(() => { console.log('Copy failed'); }); -} \ No newline at end of file +} diff --git a/src/main.tsx b/src/main.tsx index 6c7551b..12b70f0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -13,11 +13,20 @@ $appSettings.set(savedSettings); function loadFromURL() { if (window.location.hash) { try { - const decoded = atob(window.location.hash.substring(1)); + const hash = window.location.hash.substring(1); + console.log('Loading from URL hash:', hash); + + const decoded = atob(hash); + console.log('Decoded data:', decoded); try { const shareData = JSON.parse(decoded); - setShaderCode(shareData.code); + console.log('Parsed share data:', shareData); + + if (shareData.code) { + setShaderCode(shareData.code); + } + $appSettings.set({ resolution: shareData.resolution || savedSettings.resolution, fps: shareData.fps || savedSettings.fps, @@ -28,7 +37,10 @@ function loadFromURL() { ? shareData.uiOpacity : savedSettings.uiOpacity, }); + + console.log('Settings updated from URL'); } catch (jsonError) { + console.log('JSON parse failed, falling back to old format'); // Fall back to old format (just code as string) setShaderCode(decoded); } diff --git a/src/stores/appSettings.ts b/src/stores/appSettings.ts index 4b8584e..44e51c7 100644 --- a/src/stores/appSettings.ts +++ b/src/stores/appSettings.ts @@ -31,9 +31,9 @@ export function cycleValueMode() { const currentIndex = VALUE_MODES.indexOf(currentMode); const nextIndex = (currentIndex + 1) % VALUE_MODES.length; const nextMode = VALUE_MODES[nextIndex]; - + updateAppSettings({ valueMode: nextMode }); - + // Return the new mode for UI feedback return nextMode; } diff --git a/src/stores/input.ts b/src/stores/input.ts index f966dff..d1b3268 100644 --- a/src/stores/input.ts +++ b/src/stores/input.ts @@ -56,6 +56,16 @@ export const defaultInputState: InputState = { export const $input = atom(defaultInputState); +let mouseUpdatePending = false; +let pendingMouseData = { + x: 0, + y: 0, + pressed: false, + vx: 0, + vy: 0, + clickTime: 0, +}; + export function updateMousePosition( x: number, y: number, @@ -64,17 +74,37 @@ export function updateMousePosition( vy: number, clickTime: number ) { - $input.set({ - ...$input.get(), - mouseX: x, - mouseY: y, - mousePressed: pressed, - mouseVX: vx, - mouseVY: vy, - mouseClickTime: clickTime, - }); + pendingMouseData = { x, y, pressed, vx, vy, clickTime }; + + if (!mouseUpdatePending) { + mouseUpdatePending = true; + requestAnimationFrame(() => { + const current = $input.get(); + $input.set({ + ...current, + mouseX: pendingMouseData.x, + mouseY: pendingMouseData.y, + mousePressed: pendingMouseData.pressed, + mouseVX: pendingMouseData.vx, + mouseVY: pendingMouseData.vy, + mouseClickTime: pendingMouseData.clickTime, + }); + mouseUpdatePending = false; + }); + } } +let touchUpdatePending = false; +let pendingTouchData = { + count: 0, + x0: 0, + y0: 0, + x1: 0, + y1: 0, + scale: 1, + rotation: 0, +}; + export function updateTouchPosition( count: number, x0: number, @@ -84,16 +114,25 @@ export function updateTouchPosition( scale: number, rotation: number ) { - $input.set({ - ...$input.get(), - touchCount: count, - touch0X: x0, - touch0Y: y0, - touch1X: x1, - touch1Y: y1, - pinchScale: scale, - pinchRotation: rotation, - }); + pendingTouchData = { count, x0, y0, x1, y1, scale, rotation }; + + if (!touchUpdatePending) { + touchUpdatePending = true; + requestAnimationFrame(() => { + const current = $input.get(); + $input.set({ + ...current, + touchCount: pendingTouchData.count, + touch0X: pendingTouchData.x0, + touch0Y: pendingTouchData.y0, + touch1X: pendingTouchData.x1, + touch1Y: pendingTouchData.y1, + pinchScale: pendingTouchData.scale, + pinchRotation: pendingTouchData.rotation, + }); + touchUpdatePending = false; + }); + } } export function updateDeviceMotion( diff --git a/src/stores/ui.ts b/src/stores/ui.ts index 317cc7f..afbc0de 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -21,7 +21,10 @@ export const defaultUIState: UIState = { export const uiState = atom(defaultUIState); export function toggleMobileMenu() { - uiState.set({ ...uiState.get(), mobileMenuOpen: !uiState.get().mobileMenuOpen }); + uiState.set({ + ...uiState.get(), + mobileMenuOpen: !uiState.get().mobileMenuOpen, + }); } export function closeMobileMenu() { @@ -37,7 +40,10 @@ export function hideHelp() { } export function toggleShaderLibrary() { - uiState.set({ ...uiState.get(), shaderLibraryOpen: !uiState.get().shaderLibraryOpen }); + uiState.set({ + ...uiState.get(), + shaderLibraryOpen: !uiState.get().shaderLibraryOpen, + }); } export function toggleUI() { diff --git a/src/styles/main.css b/src/styles/main.css index e594c71..e4d5814 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -462,7 +462,7 @@ button [data-lucide] { } .welcome-content li:before { - content: "▸"; + content: '▸'; position: absolute; left: 0; color: #999; diff --git a/src/utils/LRUCache.ts b/src/utils/LRUCache.ts index fd98f5d..3ae088b 100644 --- a/src/utils/LRUCache.ts +++ b/src/utils/LRUCache.ts @@ -69,4 +69,4 @@ export class LRUCache { this.cache.delete(leastUsed); } } -} \ No newline at end of file +} diff --git a/src/utils/colorModes.ts b/src/utils/colorModes.ts index ee8f0b5..425e570 100644 --- a/src/utils/colorModes.ts +++ b/src/utils/colorModes.ts @@ -1,4 +1,8 @@ -export function hsvToRgb(h: number, s: number, v: number): [number, number, number] { +export function hsvToRgb( + h: number, + s: number, + v: number +): [number, number, number] { const c = v * s; const x = c * (1 - Math.abs(((h * 6) % 2) - 1)); const m = v - c; @@ -223,4 +227,4 @@ export function calculateColorDirect( default: return [absValue, absValue, absValue]; } -} \ No newline at end of file +} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index f1c4773..41cad5f 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -7,18 +7,47 @@ export const UI_HEIGHTS = { // Performance Constants export const PERFORMANCE = { - DEFAULT_TILE_SIZE: 64, + DEFAULT_TILE_SIZE: 128, MAX_RENDER_TIME_MS: 50, MAX_SHADER_TIMEOUT_MS: 5, TIMEOUT_CHECK_INTERVAL: 1000, MAX_SAVED_SHADERS: 50, - IMAGE_DATA_CACHE_SIZE: 5, - COMPILATION_CACHE_SIZE: 20, + IMAGE_DATA_CACHE_SIZE: 10, + COMPILATION_CACHE_SIZE: 30, } as const; // Color Constants export const COLOR_TABLE_SIZE = 256; +// Render Mode Constants - Keep in sync with color modes +export const RENDER_MODES = [ + 'classic', + 'grayscale', + 'red', + 'green', + 'blue', + 'rgb', + 'hsv', + 'rainbow', + 'thermal', + 'neon', + 'cyberpunk', + 'vaporwave', + 'dithered', + 'palette', +] as const; + +export type RenderMode = (typeof RENDER_MODES)[number]; + +// Create a mapping from render mode to index for O(1) lookups +export const RENDER_MODE_INDEX: Record = RENDER_MODES.reduce( + (acc, mode, index) => { + acc[mode] = index; + return acc; + }, + {} as Record +); + // Storage Keys export const STORAGE_KEYS = { SHADERS: 'bitfielder_shaders', @@ -28,7 +57,7 @@ export const STORAGE_KEYS = { // Value Modes export const VALUE_MODES = [ 'integer', - 'float', + 'float', 'polar', 'distance', 'wave', @@ -36,10 +65,10 @@ export const VALUE_MODES = [ 'cellular', 'noise', 'warp', - 'flow' + 'flow', ] as const; -export type ValueMode = typeof VALUE_MODES[number]; +export type ValueMode = (typeof VALUE_MODES)[number]; // Default Values export const DEFAULTS = { @@ -49,4 +78,4 @@ export const DEFAULTS = { VALUE_MODE: 'integer' as ValueMode, UI_OPACITY: 0.3, SHADER_CODE: 'x^y', -} as const; \ No newline at end of file +} as const;