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}
/>
+
+ G - Randomize visual settings (hue, modes)
+
S - Share current shader (copy URL)
? - Show this help
+
+ M - Cycle value mode
+
+
+ Space - Tap tempo (when editor not focused)
+
+
+ Arrow Left/Right - Adjust hue shift (when editor not focused)
+
+
+ Arrow Up/Down - Cycle value mode (when editor not focused)
+
+
+ Shift+Arrow Up/Down - Cycle render mode (when editor not focused)
+
@@ -55,6 +73,9 @@ export function HelpPopup() {
t - Time (enables animation)
+
+ bpm - Current BPM from tap tempo (default: 120)
+
i - Pixel index
diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx
index 8d19d69..31403e2 100644
--- a/src/components/MobileMenu.tsx
+++ b/src/components/MobileMenu.tsx
@@ -18,6 +18,16 @@ function getValueModeLabel(mode: string): string {
noise: 'Noise (perlin-like)',
warp: 'Warp (space deformation)',
flow: 'Flow (fluid dynamics)',
+ spiral: 'Spiral (logarithmic)',
+ turbulence: 'Turbulence (chaos)',
+ crystal: 'Crystal (lattice)',
+ marble: 'Marble (veining)',
+ quantum: 'Quantum (uncertainty)',
+ logarithmic: 'Logarithmic (scaling)',
+ mirror: 'Mirror (symmetrical)',
+ rings: 'Rings (interference)',
+ mesh: 'Mesh (grid rotation)',
+ glitch: 'Glitch (corruption)',
};
return labels[mode] || mode;
}
@@ -146,9 +156,31 @@ export function MobileMenu() {
+
+
+
+
+
+
+
+
+
+
+ updateAppSettings({ hueShift: parseInt(e.target.value) })
+ }
+ />
+
+
+
+