From fb2d5c1b4c2b7ac9218c908c06dbf3eee7c70972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Mon, 7 Jul 2025 21:40:19 +0200 Subject: [PATCH] small size update with tons of rendering modes, palettes and keybindings --- src/FakeShader.ts | 24 ++- src/ShaderWorker.ts | 170 +++++++++++++---- src/components/App.tsx | 80 +++++++- src/components/EditorPanel.tsx | 2 +- src/components/HelpPopup.tsx | 21 ++ src/components/MobileMenu.tsx | 32 ++++ src/components/ShaderCanvas.tsx | 5 +- src/components/TopBar.tsx | 36 ++++ src/hooks/useKeyboardShortcuts.ts | 4 +- src/stores/appSettings.ts | 125 +++++++++++- src/utils/colorModes.ts | 307 +++++++++++++++++++++++++----- src/utils/constants.ts | 22 +++ 12 files changed, 737 insertions(+), 91 deletions(-) diff --git a/src/FakeShader.ts b/src/FakeShader.ts index e1f50ad..59f77c8 100644 --- a/src/FakeShader.ts +++ b/src/FakeShader.ts @@ -6,6 +6,8 @@ interface WorkerMessage { height?: number; time?: number; renderMode?: string; + valueMode?: string; + hueShift?: number; startY?: number; // Y offset for tile rendering mouseX?: number; mouseY?: number; @@ -30,6 +32,7 @@ interface WorkerMessage { bassLevel?: number; midLevel?: number; trebleLevel?: number; + bpm?: number; } interface WorkerResponse { @@ -54,6 +57,9 @@ export class FakeShader { private pendingRenders: string[] = []; private renderMode: string = 'classic'; private valueMode: string = 'integer'; + private hueShift: number = 0; + private timeSpeed: number = 1.0; + private currentBPM: number = 120; // Multi-worker state private tileResults: Map = new Map(); @@ -216,7 +222,7 @@ export class FakeShader { this.isRendering = true; // this._currentRenderID = id; // Removed unused property - const currentTime = (Date.now() - this.startTime) / 1000; + const currentTime = (Date.now() - this.startTime) / 1000 * this.timeSpeed; // Always use multiple workers if available if (this.workerCount > 1) { @@ -237,6 +243,7 @@ export class FakeShader { time: currentTime, renderMode: this.renderMode, valueMode: this.valueMode, + hueShift: this.hueShift, mouseX: this.mouseX, mouseY: this.mouseY, mousePressed: this.mousePressed, @@ -260,6 +267,7 @@ export class FakeShader { bassLevel: this.bassLevel, midLevel: this.midLevel, trebleLevel: this.trebleLevel, + bpm: this.currentBPM, } as WorkerMessage); } @@ -293,6 +301,7 @@ export class FakeShader { time: currentTime, renderMode: this.renderMode, valueMode: this.valueMode, + hueShift: this.hueShift, mouseX: this.mouseX, mouseY: this.mouseY, mousePressed: this.mousePressed, @@ -316,6 +325,7 @@ export class FakeShader { bassLevel: this.bassLevel, midLevel: this.midLevel, trebleLevel: this.trebleLevel, + bpm: this.currentBPM, } as WorkerMessage); }); } @@ -392,6 +402,18 @@ export class FakeShader { this.valueMode = mode; } + setHueShift(shift: number): void { + this.hueShift = shift; + } + + setTimeSpeed(speed: number): void { + this.timeSpeed = speed; + } + + setBPM(bpm: number): void { + this.currentBPM = bpm; + } + setMousePosition( x: number, y: number, diff --git a/src/ShaderWorker.ts b/src/ShaderWorker.ts index 0c0780b..df4a6fd 100644 --- a/src/ShaderWorker.ts +++ b/src/ShaderWorker.ts @@ -8,6 +8,7 @@ interface WorkerMessage { 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 @@ -34,6 +35,7 @@ interface WorkerMessage { bassLevel?: number; midLevel?: number; trebleLevel?: number; + bpm?: number; } interface WorkerResponse { @@ -46,7 +48,7 @@ interface WorkerResponse { import { LRUCache } from './utils/LRUCache'; import { calculateColorDirect } from './utils/colorModes'; -import { PERFORMANCE, COLOR_TABLE_SIZE, RENDER_MODES, RENDER_MODE_INDEX } from './utils/constants'; +import { PERFORMANCE } from './utils/constants'; type ShaderFunction = (...args: number[]) => number; @@ -59,7 +61,6 @@ class ShaderWorker { private compilationCache: LRUCache = new LRUCache( PERFORMANCE.COMPILATION_CACHE_SIZE ); - private colorTables: Uint8Array[] = []; private feedbackBuffer: Float32Array | null = null; constructor() { @@ -67,28 +68,6 @@ class ShaderWorker { this.handleMessage(e.data); }; - this.initializeColorTables(); - } - - private initializeColorTables(): void { - const tableSize = COLOR_TABLE_SIZE; - - // Pre-compute color tables for each render mode using array indexing - this.colorTables = new Array(RENDER_MODES.length); - - 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++) { - const [r, g, b] = calculateColorDirect(i, mode); - colorTable[i * 3] = r; - colorTable[i * 3 + 1] = g; - colorTable[i * 3 + 2] = b; - } - - this.colorTables[modeIndex] = colorTable; - } } private handleMessage(message: WorkerMessage): void { @@ -183,6 +162,7 @@ class ShaderWorker { 'bassLevel', 'midLevel', 'trebleLevel', + 'bpm', ` // Timeout protection const startTime = performance.now(); @@ -221,7 +201,7 @@ class ShaderWorker { private 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|mouse[XY]|mousePressed|mouseV[XY]|mouseClickTime|touchCount|touch[01][XY]|pinchScale|pinchRotation|accel[XYZ]|gyro[XYZ]|audioLevel|bassLevel|midLevel|trebleLevel)\b/; + const variablePattern = /\b(x|y|t|i|r|a|u|v|c|f|d|n|b|bpm|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); } @@ -422,13 +402,15 @@ class ShaderWorker { message.audioLevel || 0, message.bassLevel || 0, message.midLevel || 0, - message.trebleLevel || 0 + message.trebleLevel || 0, + message.bpm || 120 ); const safeValue = isFinite(value) ? value : 0; const [r, g, b] = this.calculateColor( safeValue, renderMode, valueMode, + message.hueShift || 0, x, actualY, fullWidth, @@ -489,6 +471,7 @@ class ShaderWorker { value: number, renderMode: string, valueMode: string = 'integer', + hueShift: number = 0, x: number = 0, y: number = 0, width: number = 1, @@ -788,22 +771,137 @@ class ShaderWorker { 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; } - // 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 for unknown render modes - return calculateColorDirect(processedValue, renderMode); + // Use direct calculation to support hue shift + return calculateColorDirect(processedValue, renderMode, hueShift); } private sanitizeCode(code: string): string { diff --git a/src/components/App.tsx b/src/components/App.tsx index c03a972..33fd0d1 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -9,7 +9,7 @@ import { WelcomePopup } from './WelcomePopup'; import { ShaderCanvas } from './ShaderCanvas'; import { PerformanceWarning } from './PerformanceWarning'; import { uiState, showUI } from '../stores/ui'; -import { $appSettings } from '../stores/appSettings'; +import { $appSettings, updateAppSettings, cycleValueMode, cycleRenderMode, handleTapTempo } from '../stores/appSettings'; import { $shader } from '../stores/shader'; import { loadShaders } from '../stores/library'; import { Storage } from '../Storage'; @@ -43,6 +43,82 @@ export function App() { ); }, [settings.uiOpacity]); + // Keyboard controls for hue shift and value mode when editor not focused + useEffect(() => { + let lastKeyTime = 0; + const DEBOUNCE_DELAY = 150; // ms between key presses + + const handleKeyDown = (e: KeyboardEvent) => { + // Only activate if editor is not focused and no control/meta/alt keys are pressed + const editorElement = document.getElementById('editor') as HTMLTextAreaElement; + const isEditorFocused = editorElement && document.activeElement === editorElement; + + if (isEditorFocused || e.ctrlKey || e.metaKey || e.altKey) { + return; + } + + // Debounce rapid key repeats + const now = Date.now(); + if (now - lastKeyTime < DEBOUNCE_DELAY) { + e.preventDefault(); + return; + } + lastKeyTime = now; + + switch (e.key) { + case 'ArrowLeft': + e.preventDefault(); + // Decrease hue shift by 10 degrees (wrapping at 0) + const currentHue = settings.hueShift ?? 0; + const newHueLeft = currentHue - 10; + updateAppSettings({ hueShift: newHueLeft < 0 ? 360 + newHueLeft : newHueLeft }); + break; + + case 'ArrowRight': + e.preventDefault(); + // Increase hue shift by 10 degrees (wrapping at 360) + const currentHueRight = settings.hueShift ?? 0; + const newHueRight = (currentHueRight + 10) % 360; + updateAppSettings({ hueShift: newHueRight }); + break; + + case 'ArrowUp': + e.preventDefault(); + if (e.shiftKey) { + // Shift + Up: Cycle to previous render mode (color palette) + cycleRenderMode('backward'); + } else { + // Up: Cycle to previous value mode + cycleValueMode('backward'); + } + break; + + case 'ArrowDown': + e.preventDefault(); + if (e.shiftKey) { + // Shift + Down: Cycle to next render mode (color palette) + cycleRenderMode('forward'); + } else { + // Down: Cycle to next value mode + cycleValueMode('forward'); + } + break; + + case ' ': + e.preventDefault(); + // Spacebar: Tap tempo to control time speed + handleTapTempo(); + break; + } + }; + + document.addEventListener('keydown', handleKeyDown); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [settings.hueShift]); + // Save settings changes to localStorage useEffect(() => { Storage.saveSettings({ @@ -51,6 +127,8 @@ export function App() { renderMode: settings.renderMode, valueMode: settings.valueMode, uiOpacity: settings.uiOpacity, + hueShift: settings.hueShift, + timeSpeed: settings.timeSpeed, lastShaderCode: shader.code, }); }, [settings, shader.code]); diff --git a/src/components/EditorPanel.tsx b/src/components/EditorPanel.tsx index d253ee0..300b49f 100644 --- a/src/components/EditorPanel.tsx +++ b/src/components/EditorPanel.tsx @@ -44,7 +44,7 @@ export function EditorPanel({ minimal = false }: EditorPanelProps) { value={localCode} onChange={handleCodeChange} onKeyDown={handleKeyDown} - placeholder="Enter shader code... (x, y, t, i, mouseX, mouseY, mousePressed, touchCount, accelX, audioLevel, bassLevel...)" + placeholder="Enter shader code... (x, y, t, i, bpm, mouseX, mouseY, mousePressed, touchCount, accelX, audioLevel, bassLevel...)" spellCheck={false} />