small size update with tons of rendering modes, palettes and keybindings
This commit is contained in:
@ -6,6 +6,8 @@ interface WorkerMessage {
|
|||||||
height?: number;
|
height?: number;
|
||||||
time?: number;
|
time?: number;
|
||||||
renderMode?: string;
|
renderMode?: string;
|
||||||
|
valueMode?: string;
|
||||||
|
hueShift?: number;
|
||||||
startY?: number; // Y offset for tile rendering
|
startY?: number; // Y offset for tile rendering
|
||||||
mouseX?: number;
|
mouseX?: number;
|
||||||
mouseY?: number;
|
mouseY?: number;
|
||||||
@ -30,6 +32,7 @@ interface WorkerMessage {
|
|||||||
bassLevel?: number;
|
bassLevel?: number;
|
||||||
midLevel?: number;
|
midLevel?: number;
|
||||||
trebleLevel?: number;
|
trebleLevel?: number;
|
||||||
|
bpm?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkerResponse {
|
interface WorkerResponse {
|
||||||
@ -54,6 +57,9 @@ export class FakeShader {
|
|||||||
private pendingRenders: string[] = [];
|
private pendingRenders: string[] = [];
|
||||||
private renderMode: string = 'classic';
|
private renderMode: string = 'classic';
|
||||||
private valueMode: string = 'integer';
|
private valueMode: string = 'integer';
|
||||||
|
private hueShift: number = 0;
|
||||||
|
private timeSpeed: number = 1.0;
|
||||||
|
private currentBPM: number = 120;
|
||||||
|
|
||||||
// Multi-worker state
|
// Multi-worker state
|
||||||
private tileResults: Map<number, ImageData> = new Map();
|
private tileResults: Map<number, ImageData> = new Map();
|
||||||
@ -216,7 +222,7 @@ export class FakeShader {
|
|||||||
|
|
||||||
this.isRendering = true;
|
this.isRendering = true;
|
||||||
// this._currentRenderID = id; // Removed unused property
|
// 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
|
// Always use multiple workers if available
|
||||||
if (this.workerCount > 1) {
|
if (this.workerCount > 1) {
|
||||||
@ -237,6 +243,7 @@ export class FakeShader {
|
|||||||
time: currentTime,
|
time: currentTime,
|
||||||
renderMode: this.renderMode,
|
renderMode: this.renderMode,
|
||||||
valueMode: this.valueMode,
|
valueMode: this.valueMode,
|
||||||
|
hueShift: this.hueShift,
|
||||||
mouseX: this.mouseX,
|
mouseX: this.mouseX,
|
||||||
mouseY: this.mouseY,
|
mouseY: this.mouseY,
|
||||||
mousePressed: this.mousePressed,
|
mousePressed: this.mousePressed,
|
||||||
@ -260,6 +267,7 @@ export class FakeShader {
|
|||||||
bassLevel: this.bassLevel,
|
bassLevel: this.bassLevel,
|
||||||
midLevel: this.midLevel,
|
midLevel: this.midLevel,
|
||||||
trebleLevel: this.trebleLevel,
|
trebleLevel: this.trebleLevel,
|
||||||
|
bpm: this.currentBPM,
|
||||||
} as WorkerMessage);
|
} as WorkerMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -293,6 +301,7 @@ export class FakeShader {
|
|||||||
time: currentTime,
|
time: currentTime,
|
||||||
renderMode: this.renderMode,
|
renderMode: this.renderMode,
|
||||||
valueMode: this.valueMode,
|
valueMode: this.valueMode,
|
||||||
|
hueShift: this.hueShift,
|
||||||
mouseX: this.mouseX,
|
mouseX: this.mouseX,
|
||||||
mouseY: this.mouseY,
|
mouseY: this.mouseY,
|
||||||
mousePressed: this.mousePressed,
|
mousePressed: this.mousePressed,
|
||||||
@ -316,6 +325,7 @@ export class FakeShader {
|
|||||||
bassLevel: this.bassLevel,
|
bassLevel: this.bassLevel,
|
||||||
midLevel: this.midLevel,
|
midLevel: this.midLevel,
|
||||||
trebleLevel: this.trebleLevel,
|
trebleLevel: this.trebleLevel,
|
||||||
|
bpm: this.currentBPM,
|
||||||
} as WorkerMessage);
|
} as WorkerMessage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -392,6 +402,18 @@ export class FakeShader {
|
|||||||
this.valueMode = mode;
|
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(
|
setMousePosition(
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ interface WorkerMessage {
|
|||||||
time?: number;
|
time?: number;
|
||||||
renderMode?: string;
|
renderMode?: string;
|
||||||
valueMode?: string; // 'integer' or 'float'
|
valueMode?: string; // 'integer' or 'float'
|
||||||
|
hueShift?: number; // Hue shift in degrees (0-360)
|
||||||
startY?: number; // Y offset for tile rendering
|
startY?: number; // Y offset for tile rendering
|
||||||
fullWidth?: number; // Full canvas width for center calculations
|
fullWidth?: number; // Full canvas width for center calculations
|
||||||
fullHeight?: number; // Full canvas height for center calculations
|
fullHeight?: number; // Full canvas height for center calculations
|
||||||
@ -34,6 +35,7 @@ interface WorkerMessage {
|
|||||||
bassLevel?: number;
|
bassLevel?: number;
|
||||||
midLevel?: number;
|
midLevel?: number;
|
||||||
trebleLevel?: number;
|
trebleLevel?: number;
|
||||||
|
bpm?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkerResponse {
|
interface WorkerResponse {
|
||||||
@ -46,7 +48,7 @@ interface WorkerResponse {
|
|||||||
|
|
||||||
import { LRUCache } from './utils/LRUCache';
|
import { LRUCache } from './utils/LRUCache';
|
||||||
import { calculateColorDirect } from './utils/colorModes';
|
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;
|
type ShaderFunction = (...args: number[]) => number;
|
||||||
|
|
||||||
@ -59,7 +61,6 @@ class ShaderWorker {
|
|||||||
private compilationCache: LRUCache<string, ShaderFunction> = new LRUCache(
|
private compilationCache: LRUCache<string, ShaderFunction> = new LRUCache(
|
||||||
PERFORMANCE.COMPILATION_CACHE_SIZE
|
PERFORMANCE.COMPILATION_CACHE_SIZE
|
||||||
);
|
);
|
||||||
private colorTables: Uint8Array[] = [];
|
|
||||||
private feedbackBuffer: Float32Array | null = null;
|
private feedbackBuffer: Float32Array | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -67,28 +68,6 @@ class ShaderWorker {
|
|||||||
this.handleMessage(e.data);
|
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 {
|
private handleMessage(message: WorkerMessage): void {
|
||||||
@ -183,6 +162,7 @@ class ShaderWorker {
|
|||||||
'bassLevel',
|
'bassLevel',
|
||||||
'midLevel',
|
'midLevel',
|
||||||
'trebleLevel',
|
'trebleLevel',
|
||||||
|
'bpm',
|
||||||
`
|
`
|
||||||
// Timeout protection
|
// Timeout protection
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
@ -221,7 +201,7 @@ class ShaderWorker {
|
|||||||
|
|
||||||
private isStaticExpression(code: string): boolean {
|
private isStaticExpression(code: string): boolean {
|
||||||
// Check if code contains any variables using regex for better accuracy
|
// 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);
|
return !variablePattern.test(code);
|
||||||
}
|
}
|
||||||
@ -422,13 +402,15 @@ class ShaderWorker {
|
|||||||
message.audioLevel || 0,
|
message.audioLevel || 0,
|
||||||
message.bassLevel || 0,
|
message.bassLevel || 0,
|
||||||
message.midLevel || 0,
|
message.midLevel || 0,
|
||||||
message.trebleLevel || 0
|
message.trebleLevel || 0,
|
||||||
|
message.bpm || 120
|
||||||
);
|
);
|
||||||
const safeValue = isFinite(value) ? value : 0;
|
const safeValue = isFinite(value) ? value : 0;
|
||||||
const [r, g, b] = this.calculateColor(
|
const [r, g, b] = this.calculateColor(
|
||||||
safeValue,
|
safeValue,
|
||||||
renderMode,
|
renderMode,
|
||||||
valueMode,
|
valueMode,
|
||||||
|
message.hueShift || 0,
|
||||||
x,
|
x,
|
||||||
actualY,
|
actualY,
|
||||||
fullWidth,
|
fullWidth,
|
||||||
@ -489,6 +471,7 @@ class ShaderWorker {
|
|||||||
value: number,
|
value: number,
|
||||||
renderMode: string,
|
renderMode: string,
|
||||||
valueMode: string = 'integer',
|
valueMode: string = 'integer',
|
||||||
|
hueShift: number = 0,
|
||||||
x: number = 0,
|
x: number = 0,
|
||||||
y: number = 0,
|
y: number = 0,
|
||||||
width: number = 1,
|
width: number = 1,
|
||||||
@ -788,22 +771,137 @@ class ShaderWorker {
|
|||||||
break;
|
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:
|
default:
|
||||||
// Integer mode: treat value as 0-255 (original behavior)
|
// Integer mode: treat value as 0-255 (original behavior)
|
||||||
processedValue = Math.abs(value) % 256;
|
processedValue = Math.abs(value) % 256;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use pre-computed color table with O(1) array indexing
|
// Use direct calculation to support hue shift
|
||||||
const modeIndex = RENDER_MODE_INDEX[renderMode];
|
return calculateColorDirect(processedValue, renderMode, hueShift);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private sanitizeCode(code: string): string {
|
private sanitizeCode(code: string): string {
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { WelcomePopup } from './WelcomePopup';
|
|||||||
import { ShaderCanvas } from './ShaderCanvas';
|
import { ShaderCanvas } from './ShaderCanvas';
|
||||||
import { PerformanceWarning } from './PerformanceWarning';
|
import { PerformanceWarning } from './PerformanceWarning';
|
||||||
import { uiState, showUI } from '../stores/ui';
|
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 { $shader } from '../stores/shader';
|
||||||
import { loadShaders } from '../stores/library';
|
import { loadShaders } from '../stores/library';
|
||||||
import { Storage } from '../Storage';
|
import { Storage } from '../Storage';
|
||||||
@ -43,6 +43,82 @@ export function App() {
|
|||||||
);
|
);
|
||||||
}, [settings.uiOpacity]);
|
}, [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
|
// Save settings changes to localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Storage.saveSettings({
|
Storage.saveSettings({
|
||||||
@ -51,6 +127,8 @@ export function App() {
|
|||||||
renderMode: settings.renderMode,
|
renderMode: settings.renderMode,
|
||||||
valueMode: settings.valueMode,
|
valueMode: settings.valueMode,
|
||||||
uiOpacity: settings.uiOpacity,
|
uiOpacity: settings.uiOpacity,
|
||||||
|
hueShift: settings.hueShift,
|
||||||
|
timeSpeed: settings.timeSpeed,
|
||||||
lastShaderCode: shader.code,
|
lastShaderCode: shader.code,
|
||||||
});
|
});
|
||||||
}, [settings, shader.code]);
|
}, [settings, shader.code]);
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export function EditorPanel({ minimal = false }: EditorPanelProps) {
|
|||||||
value={localCode}
|
value={localCode}
|
||||||
onChange={handleCodeChange}
|
onChange={handleCodeChange}
|
||||||
onKeyDown={handleKeyDown}
|
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}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@ -39,12 +39,30 @@ export function HelpPopup() {
|
|||||||
<p>
|
<p>
|
||||||
<strong>R</strong> - Generate random shader
|
<strong>R</strong> - Generate random shader
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>G</strong> - Randomize visual settings (hue, modes)
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>S</strong> - Share current shader (copy URL)
|
<strong>S</strong> - Share current shader (copy URL)
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>?</strong> - Show this help
|
<strong>?</strong> - Show this help
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>M</strong> - Cycle value mode
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Space</strong> - Tap tempo (when editor not focused)
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Arrow Left/Right</strong> - Adjust hue shift (when editor not focused)
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Arrow Up/Down</strong> - Cycle value mode (when editor not focused)
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Shift+Arrow Up/Down</strong> - Cycle render mode (when editor not focused)
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="help-section">
|
<div className="help-section">
|
||||||
@ -55,6 +73,9 @@ export function HelpPopup() {
|
|||||||
<p>
|
<p>
|
||||||
<strong>t</strong> - Time (enables animation)
|
<strong>t</strong> - Time (enables animation)
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>bpm</strong> - Current BPM from tap tempo (default: 120)
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>i</strong> - Pixel index
|
<strong>i</strong> - Pixel index
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -18,6 +18,16 @@ function getValueModeLabel(mode: string): string {
|
|||||||
noise: 'Noise (perlin-like)',
|
noise: 'Noise (perlin-like)',
|
||||||
warp: 'Warp (space deformation)',
|
warp: 'Warp (space deformation)',
|
||||||
flow: 'Flow (fluid dynamics)',
|
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;
|
return labels[mode] || mode;
|
||||||
}
|
}
|
||||||
@ -146,9 +156,31 @@ export function MobileMenu() {
|
|||||||
<option value="copper">Copper</option>
|
<option value="copper">Copper</option>
|
||||||
<option value="dithered">Dithered</option>
|
<option value="dithered">Dithered</option>
|
||||||
<option value="palette">Palette</option>
|
<option value="palette">Palette</option>
|
||||||
|
<option value="vintage">Vintage</option>
|
||||||
|
<option value="infrared">Infrared</option>
|
||||||
|
<option value="fire">Fire</option>
|
||||||
|
<option value="ice">Ice</option>
|
||||||
|
<option value="plasma">Plasma</option>
|
||||||
|
<option value="xray">X-Ray</option>
|
||||||
|
<option value="spectrum">Spectrum</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mobile-menu-item">
|
||||||
|
<label>
|
||||||
|
Hue Shift: {settings.hueShift ?? 0}°
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="360"
|
||||||
|
value={settings.hueShift ?? 0}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateAppSettings({ hueShift: parseInt(e.target.value) })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mobile-menu-item">
|
<div className="mobile-menu-item">
|
||||||
<label>
|
<label>
|
||||||
UI Opacity: {Math.round((settings.uiOpacity ?? 0.3) * 100)}%
|
UI Opacity: {Math.round((settings.uiOpacity ?? 0.3) * 100)}%
|
||||||
|
|||||||
@ -62,9 +62,12 @@ export function ShaderCanvas() {
|
|||||||
if (shaderRef.current) {
|
if (shaderRef.current) {
|
||||||
shaderRef.current.setRenderMode(settings.renderMode);
|
shaderRef.current.setRenderMode(settings.renderMode);
|
||||||
shaderRef.current.setValueMode(settings.valueMode ?? 'integer');
|
shaderRef.current.setValueMode(settings.valueMode ?? 'integer');
|
||||||
|
shaderRef.current.setHueShift(settings.hueShift ?? 0);
|
||||||
|
shaderRef.current.setTimeSpeed(settings.timeSpeed ?? 1.0);
|
||||||
|
shaderRef.current.setBPM(settings.currentBPM ?? 120);
|
||||||
shaderRef.current.setTargetFPS(settings.fps);
|
shaderRef.current.setTargetFPS(settings.fps);
|
||||||
}
|
}
|
||||||
}, [settings.renderMode, settings.valueMode, settings.fps]);
|
}, [settings.renderMode, settings.valueMode, settings.hueShift, settings.timeSpeed, settings.currentBPM, settings.fps]);
|
||||||
|
|
||||||
// Handle canvas resize when resolution or UI visibility changes
|
// Handle canvas resize when resolution or UI visibility changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -26,6 +26,16 @@ function getValueModeLabel(mode: string): string {
|
|||||||
noise: 'Noise (perlin-like)',
|
noise: 'Noise (perlin-like)',
|
||||||
warp: 'Warp (space deformation)',
|
warp: 'Warp (space deformation)',
|
||||||
flow: 'Flow (fluid dynamics)',
|
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;
|
return labels[mode] || mode;
|
||||||
}
|
}
|
||||||
@ -208,9 +218,35 @@ export function TopBar() {
|
|||||||
<option value="copper">Copper</option>
|
<option value="copper">Copper</option>
|
||||||
<option value="dithered">Dithered</option>
|
<option value="dithered">Dithered</option>
|
||||||
<option value="palette">Palette</option>
|
<option value="palette">Palette</option>
|
||||||
|
<option value="vintage">Vintage</option>
|
||||||
|
<option value="infrared">Infrared</option>
|
||||||
|
<option value="fire">Fire</option>
|
||||||
|
<option value="ice">Ice</option>
|
||||||
|
<option value="plasma">Plasma</option>
|
||||||
|
<option value="xray">X-Ray</option>
|
||||||
|
<option value="spectrum">Spectrum</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label
|
||||||
|
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
||||||
|
>
|
||||||
|
Hue Shift:
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="360"
|
||||||
|
value={settings.hueShift ?? 0}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateAppSettings({ hueShift: parseInt(e.target.value) })
|
||||||
|
}
|
||||||
|
style={{ width: '80px', verticalAlign: 'middle' }}
|
||||||
|
/>
|
||||||
|
<span style={{ fontSize: '11px' }}>
|
||||||
|
{settings.hueShift ?? 0}°
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
style={{ color: '#ccc', fontSize: '12px', marginRight: '10px' }}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { uiState } from '../stores/ui';
|
import { uiState } from '../stores/ui';
|
||||||
import { $shader, setShaderCode } from '../stores/shader';
|
import { $shader, setShaderCode } from '../stores/shader';
|
||||||
import { $appSettings, cycleValueMode } from '../stores/appSettings';
|
import { $appSettings, cycleValueMode, randomizeVisualSettings } from '../stores/appSettings';
|
||||||
import { FakeShader } from '../FakeShader';
|
import { FakeShader } from '../FakeShader';
|
||||||
|
|
||||||
export function useKeyboardShortcuts() {
|
export function useKeyboardShortcuts() {
|
||||||
@ -31,6 +31,8 @@ export function useKeyboardShortcuts() {
|
|||||||
} else if (e.key === '?') {
|
} else if (e.key === '?') {
|
||||||
const ui = uiState.get();
|
const ui = uiState.get();
|
||||||
uiState.set({ ...ui, helpPopupOpen: true });
|
uiState.set({ ...ui, helpPopupOpen: true });
|
||||||
|
} else if (e.key === 'g' || e.key === 'G') {
|
||||||
|
randomizeVisualSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { atom } from 'nanostores';
|
import { atom } from 'nanostores';
|
||||||
import { DEFAULTS, VALUE_MODES, ValueMode } from '../utils/constants';
|
import { DEFAULTS, VALUE_MODES, ValueMode, RENDER_MODES } from '../utils/constants';
|
||||||
|
|
||||||
export interface AppSettings {
|
export interface AppSettings {
|
||||||
resolution: number;
|
resolution: number;
|
||||||
@ -7,7 +7,10 @@ export interface AppSettings {
|
|||||||
renderMode: string;
|
renderMode: string;
|
||||||
valueMode?: ValueMode;
|
valueMode?: ValueMode;
|
||||||
uiOpacity?: number;
|
uiOpacity?: number;
|
||||||
|
hueShift?: number;
|
||||||
lastShaderCode?: string;
|
lastShaderCode?: string;
|
||||||
|
timeSpeed?: number;
|
||||||
|
currentBPM?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultSettings: AppSettings = {
|
export const defaultSettings: AppSettings = {
|
||||||
@ -16,7 +19,10 @@ export const defaultSettings: AppSettings = {
|
|||||||
renderMode: DEFAULTS.RENDER_MODE,
|
renderMode: DEFAULTS.RENDER_MODE,
|
||||||
valueMode: DEFAULTS.VALUE_MODE,
|
valueMode: DEFAULTS.VALUE_MODE,
|
||||||
uiOpacity: DEFAULTS.UI_OPACITY,
|
uiOpacity: DEFAULTS.UI_OPACITY,
|
||||||
|
hueShift: 0,
|
||||||
lastShaderCode: DEFAULTS.SHADER_CODE,
|
lastShaderCode: DEFAULTS.SHADER_CODE,
|
||||||
|
timeSpeed: DEFAULTS.TIME_SPEED,
|
||||||
|
currentBPM: 120,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const $appSettings = atom<AppSettings>(defaultSettings);
|
export const $appSettings = atom<AppSettings>(defaultSettings);
|
||||||
@ -25,11 +31,24 @@ export function updateAppSettings(settings: Partial<AppSettings>) {
|
|||||||
$appSettings.set({ ...$appSettings.get(), ...settings });
|
$appSettings.set({ ...$appSettings.get(), ...settings });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cycleValueMode() {
|
export function cycleValueMode(direction: 'forward' | 'backward' = 'forward') {
|
||||||
const currentSettings = $appSettings.get();
|
const currentSettings = $appSettings.get();
|
||||||
const currentMode = currentSettings.valueMode || DEFAULTS.VALUE_MODE;
|
const currentMode = currentSettings.valueMode || DEFAULTS.VALUE_MODE;
|
||||||
const currentIndex = VALUE_MODES.indexOf(currentMode);
|
const currentIndex = VALUE_MODES.indexOf(currentMode);
|
||||||
const nextIndex = (currentIndex + 1) % VALUE_MODES.length;
|
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
// Fall back to first mode if current mode not found
|
||||||
|
updateAppSettings({ valueMode: VALUE_MODES[0] });
|
||||||
|
return VALUE_MODES[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextIndex: number;
|
||||||
|
if (direction === 'forward') {
|
||||||
|
nextIndex = (currentIndex + 1) % VALUE_MODES.length;
|
||||||
|
} else {
|
||||||
|
nextIndex = (currentIndex - 1 + VALUE_MODES.length) % VALUE_MODES.length;
|
||||||
|
}
|
||||||
|
|
||||||
const nextMode = VALUE_MODES[nextIndex];
|
const nextMode = VALUE_MODES[nextIndex];
|
||||||
|
|
||||||
updateAppSettings({ valueMode: nextMode });
|
updateAppSettings({ valueMode: nextMode });
|
||||||
@ -37,3 +56,103 @@ export function cycleValueMode() {
|
|||||||
// Return the new mode for UI feedback
|
// Return the new mode for UI feedback
|
||||||
return nextMode;
|
return nextMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function cycleRenderMode(direction: 'forward' | 'backward' = 'forward') {
|
||||||
|
const currentSettings = $appSettings.get();
|
||||||
|
const currentMode = currentSettings.renderMode || DEFAULTS.RENDER_MODE;
|
||||||
|
const currentIndex = RENDER_MODES.indexOf(currentMode as any);
|
||||||
|
|
||||||
|
if (currentIndex === -1) {
|
||||||
|
// Fall back to first mode if current mode not found
|
||||||
|
updateAppSettings({ renderMode: RENDER_MODES[0] });
|
||||||
|
return RENDER_MODES[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextIndex: number;
|
||||||
|
if (direction === 'forward') {
|
||||||
|
nextIndex = (currentIndex + 1) % RENDER_MODES.length;
|
||||||
|
} else {
|
||||||
|
nextIndex = (currentIndex - 1 + RENDER_MODES.length) % RENDER_MODES.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextMode = RENDER_MODES[nextIndex];
|
||||||
|
|
||||||
|
updateAppSettings({ renderMode: nextMode });
|
||||||
|
|
||||||
|
// Return the new mode for UI feedback
|
||||||
|
return nextMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tap tempo state and functions
|
||||||
|
interface TapTempoState {
|
||||||
|
taps: number[];
|
||||||
|
maxTaps: number;
|
||||||
|
timeoutMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tapTempoState: TapTempoState = {
|
||||||
|
taps: [],
|
||||||
|
maxTaps: 4,
|
||||||
|
timeoutMs: 2500,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function handleTapTempo(): void {
|
||||||
|
const now = Date.now();
|
||||||
|
const state = tapTempoState;
|
||||||
|
|
||||||
|
// Clear old taps that are outside the timeout window
|
||||||
|
state.taps = state.taps.filter(tap => now - tap < state.timeoutMs);
|
||||||
|
|
||||||
|
// Add new tap
|
||||||
|
state.taps.push(now);
|
||||||
|
|
||||||
|
// Keep only the most recent taps
|
||||||
|
if (state.taps.length > state.maxTaps) {
|
||||||
|
state.taps.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate BPM and update time speed if we have enough taps
|
||||||
|
if (state.taps.length >= 2) {
|
||||||
|
const intervals = [];
|
||||||
|
for (let i = 1; i < state.taps.length; i++) {
|
||||||
|
intervals.push(state.taps[i] - state.taps[i - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
|
||||||
|
const bpm = 60000 / avgInterval;
|
||||||
|
|
||||||
|
// Map BPM to time speed (120 BPM = 1.0x speed)
|
||||||
|
const targetBPM = 120;
|
||||||
|
const timeSpeed = Math.max(0.1, Math.min(5.0, bpm / targetBPM));
|
||||||
|
|
||||||
|
updateAppSettings({ timeSpeed, currentBPM: bpm });
|
||||||
|
|
||||||
|
console.log(`Tap tempo: ${bpm.toFixed(1)} BPM, ${timeSpeed.toFixed(2)}x speed`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetTapTempo(): void {
|
||||||
|
tapTempoState.taps = [];
|
||||||
|
updateAppSettings({ timeSpeed: 1.0, currentBPM: 120 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function randomizeVisualSettings(): void {
|
||||||
|
// Random hue shift (0-359 degrees)
|
||||||
|
const randomHue = Math.floor(Math.random() * 360);
|
||||||
|
|
||||||
|
// Random value mode
|
||||||
|
const randomValueIndex = Math.floor(Math.random() * VALUE_MODES.length);
|
||||||
|
const randomValueMode = VALUE_MODES[randomValueIndex];
|
||||||
|
|
||||||
|
// Random render mode
|
||||||
|
const randomRenderIndex = Math.floor(Math.random() * RENDER_MODES.length);
|
||||||
|
const randomRenderMode = RENDER_MODES[randomRenderIndex];
|
||||||
|
|
||||||
|
updateAppSettings({
|
||||||
|
hueShift: randomHue,
|
||||||
|
valueMode: randomValueMode,
|
||||||
|
renderMode: randomRenderMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Randomized visuals: Hue ${randomHue}°, Value mode: ${randomValueMode}, Render mode: ${randomRenderMode}`);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,32 @@
|
|||||||
|
export function rgbToHsv(r: number, g: number, b: number): [number, number, number] {
|
||||||
|
r /= 255;
|
||||||
|
g /= 255;
|
||||||
|
b /= 255;
|
||||||
|
|
||||||
|
const max = Math.max(r, g, b);
|
||||||
|
const min = Math.min(r, g, b);
|
||||||
|
const delta = max - min;
|
||||||
|
|
||||||
|
let h = 0;
|
||||||
|
const s = max === 0 ? 0 : delta / max;
|
||||||
|
const v = max;
|
||||||
|
|
||||||
|
if (delta !== 0) {
|
||||||
|
if (max === r) {
|
||||||
|
h = ((g - b) / delta) % 6;
|
||||||
|
} else if (max === g) {
|
||||||
|
h = (b - r) / delta + 2;
|
||||||
|
} else {
|
||||||
|
h = (r - g) / delta + 4;
|
||||||
|
}
|
||||||
|
h /= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (h < 0) h += 1;
|
||||||
|
|
||||||
|
return [h, s, v];
|
||||||
|
}
|
||||||
|
|
||||||
export function hsvToRgb(
|
export function hsvToRgb(
|
||||||
h: number,
|
h: number,
|
||||||
s: number,
|
s: number,
|
||||||
@ -44,6 +73,19 @@ export function hsvToRgb(
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function applyHueShift(rgb: [number, number, number], hueShiftDegrees: number): [number, number, number] {
|
||||||
|
if (hueShiftDegrees === 0) return rgb;
|
||||||
|
|
||||||
|
const [r, g, b] = rgb;
|
||||||
|
const [h, s, v] = rgbToHsv(r, g, b);
|
||||||
|
|
||||||
|
let newHue = h + (hueShiftDegrees / 360);
|
||||||
|
if (newHue > 1) newHue -= 1;
|
||||||
|
if (newHue < 0) newHue += 1;
|
||||||
|
|
||||||
|
return hsvToRgb(newHue, s, v);
|
||||||
|
}
|
||||||
|
|
||||||
export function rainbowColor(value: number): [number, number, number] {
|
export function rainbowColor(value: number): [number, number, number] {
|
||||||
const phase = (value / 255.0) * 6;
|
const phase = (value / 255.0) * 6;
|
||||||
const segment = Math.floor(phase);
|
const segment = Math.floor(phase);
|
||||||
@ -177,54 +219,225 @@ export function paletteColor(value: number): [number, number, number] {
|
|||||||
return palette[index] as [number, number, number];
|
return palette[index] as [number, number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateColorDirect(
|
export function vintageColor(value: number): [number, number, number] {
|
||||||
absValue: number,
|
const palette = [
|
||||||
renderMode: string
|
[25, 20, 15],
|
||||||
): [number, number, number] {
|
[75, 54, 33],
|
||||||
switch (renderMode) {
|
[124, 88, 56],
|
||||||
case 'classic':
|
[173, 129, 80],
|
||||||
return [absValue, (absValue * 2) % 256, (absValue * 3) % 256];
|
[222, 179, 120],
|
||||||
|
[194, 154, 108],
|
||||||
|
[166, 124, 82],
|
||||||
|
[245, 222, 179],
|
||||||
|
];
|
||||||
|
const index = Math.floor((value / 255.0) * (palette.length - 1));
|
||||||
|
return palette[index] as [number, number, number];
|
||||||
|
}
|
||||||
|
|
||||||
case 'grayscale':
|
export function plasmaColor(value: number): [number, number, number] {
|
||||||
return [absValue, absValue, absValue];
|
const t = value / 255.0;
|
||||||
|
const freq = 2.4;
|
||||||
|
const phase1 = 0.0;
|
||||||
|
const phase2 = 2.094;
|
||||||
|
const phase3 = 4.188;
|
||||||
|
|
||||||
|
const r = Math.sin(freq * t + phase1) * 0.5 + 0.5;
|
||||||
|
const g = Math.sin(freq * t + phase2) * 0.5 + 0.5;
|
||||||
|
const b = Math.sin(freq * t + phase3) * 0.5 + 0.5;
|
||||||
|
|
||||||
|
return [
|
||||||
|
Math.round(r * 255),
|
||||||
|
Math.round(g * 255),
|
||||||
|
Math.round(b * 255)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
case 'red':
|
export function fireColor(value: number): [number, number, number] {
|
||||||
return [absValue, 0, 0];
|
const t = value / 255.0;
|
||||||
|
if (t < 0.2) {
|
||||||
case 'green':
|
return [Math.round(t * 5 * 255), 0, 0];
|
||||||
return [0, absValue, 0];
|
} else if (t < 0.5) {
|
||||||
|
const p = (t - 0.2) / 0.3;
|
||||||
case 'blue':
|
return [255, Math.round(p * 165), 0];
|
||||||
return [0, 0, absValue];
|
} else if (t < 0.8) {
|
||||||
|
const p = (t - 0.5) / 0.3;
|
||||||
case 'forest':
|
return [255, Math.round(165 + p * 90), Math.round(p * 100)];
|
||||||
return forestColor(absValue);
|
} else {
|
||||||
|
const p = (t - 0.8) / 0.2;
|
||||||
case 'copper':
|
return [255, 255, Math.round(100 + p * 155)];
|
||||||
return copperColor(absValue);
|
|
||||||
|
|
||||||
case 'rainbow':
|
|
||||||
return rainbowColor(absValue);
|
|
||||||
|
|
||||||
case 'thermal':
|
|
||||||
return thermalColor(absValue);
|
|
||||||
|
|
||||||
case 'neon':
|
|
||||||
return neonColor(absValue);
|
|
||||||
|
|
||||||
case 'sunset':
|
|
||||||
return sunsetColor(absValue);
|
|
||||||
|
|
||||||
case 'ocean':
|
|
||||||
return oceanColor(absValue);
|
|
||||||
|
|
||||||
case 'dithered':
|
|
||||||
return ditheredColor(absValue);
|
|
||||||
|
|
||||||
case 'palette':
|
|
||||||
return paletteColor(absValue);
|
|
||||||
|
|
||||||
default:
|
|
||||||
return [absValue, absValue, absValue];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function iceColor(value: number): [number, number, number] {
|
||||||
|
const t = value / 255.0;
|
||||||
|
if (t < 0.25) {
|
||||||
|
return [Math.round(t * 2 * 255), Math.round(t * 3 * 255), 255];
|
||||||
|
} else if (t < 0.5) {
|
||||||
|
const p = (t - 0.25) / 0.25;
|
||||||
|
return [Math.round(128 + p * 127), Math.round(192 + p * 63), 255];
|
||||||
|
} else if (t < 0.75) {
|
||||||
|
const p = (t - 0.5) / 0.25;
|
||||||
|
return [255, 255, Math.round(255 - p * 100)];
|
||||||
|
} else {
|
||||||
|
const p = (t - 0.75) / 0.25;
|
||||||
|
return [255, 255, Math.round(155 + p * 100)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function infraredColor(value: number): [number, number, number] {
|
||||||
|
const t = value / 255.0;
|
||||||
|
const intensity = Math.pow(t, 0.6);
|
||||||
|
const heat = Math.sin(t * Math.PI * 1.5) * 0.5 + 0.5;
|
||||||
|
|
||||||
|
const r = Math.round(255 * intensity);
|
||||||
|
const g = Math.round(128 * heat * intensity);
|
||||||
|
const b = Math.round(64 * (1 - intensity) * heat);
|
||||||
|
|
||||||
|
return [r, g, b];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function xrayColor(value: number): [number, number, number] {
|
||||||
|
const t = value / 255.0;
|
||||||
|
const inverted = 1.0 - t;
|
||||||
|
const contrast = Math.pow(inverted, 1.8);
|
||||||
|
const glow = Math.sin(t * Math.PI) * 0.3;
|
||||||
|
|
||||||
|
const intensity = Math.round(contrast * 255);
|
||||||
|
const cyan = Math.round((contrast + glow) * 180);
|
||||||
|
const blue = Math.round((contrast + glow * 0.5) * 120);
|
||||||
|
|
||||||
|
return [intensity, cyan, blue];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spectrumColor(value: number): [number, number, number] {
|
||||||
|
const t = value / 255.0;
|
||||||
|
const hue = t * 360;
|
||||||
|
const saturation = 0.7;
|
||||||
|
const lightness = 0.6 + (Math.sin(t * Math.PI * 4) * 0.2);
|
||||||
|
|
||||||
|
const c = (1 - Math.abs(2 * lightness - 1)) * saturation;
|
||||||
|
const x = c * (1 - Math.abs((hue / 60) % 2 - 1));
|
||||||
|
const m = lightness - c / 2;
|
||||||
|
|
||||||
|
let r = 0, g = 0, b = 0;
|
||||||
|
|
||||||
|
if (hue < 60) {
|
||||||
|
r = c; g = x; b = 0;
|
||||||
|
} else if (hue < 120) {
|
||||||
|
r = x; g = c; b = 0;
|
||||||
|
} else if (hue < 180) {
|
||||||
|
r = 0; g = c; b = x;
|
||||||
|
} else if (hue < 240) {
|
||||||
|
r = 0; g = x; b = c;
|
||||||
|
} else if (hue < 300) {
|
||||||
|
r = x; g = 0; b = c;
|
||||||
|
} else {
|
||||||
|
r = c; g = 0; b = x;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
Math.round((r + m) * 255),
|
||||||
|
Math.round((g + m) * 255),
|
||||||
|
Math.round((b + m) * 255)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateColorDirect(
|
||||||
|
absValue: number,
|
||||||
|
renderMode: string,
|
||||||
|
hueShift: number = 0
|
||||||
|
): [number, number, number] {
|
||||||
|
let color: [number, number, number];
|
||||||
|
|
||||||
|
switch (renderMode) {
|
||||||
|
case 'classic':
|
||||||
|
color = [absValue, (absValue * 2) % 256, (absValue * 3) % 256];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'grayscale':
|
||||||
|
color = [absValue, absValue, absValue];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'red':
|
||||||
|
color = [absValue, 0, 0];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'green':
|
||||||
|
color = [0, absValue, 0];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'blue':
|
||||||
|
color = [0, 0, absValue];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'forest':
|
||||||
|
color = forestColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'copper':
|
||||||
|
color = copperColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'rainbow':
|
||||||
|
color = rainbowColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'thermal':
|
||||||
|
color = thermalColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'neon':
|
||||||
|
color = neonColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'sunset':
|
||||||
|
color = sunsetColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ocean':
|
||||||
|
color = oceanColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'dithered':
|
||||||
|
color = ditheredColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'palette':
|
||||||
|
color = paletteColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'vintage':
|
||||||
|
color = vintageColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'plasma':
|
||||||
|
color = plasmaColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'fire':
|
||||||
|
color = fireColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ice':
|
||||||
|
color = iceColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'infrared':
|
||||||
|
color = infraredColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'xray':
|
||||||
|
color = xrayColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'spectrum':
|
||||||
|
color = spectrumColor(absValue);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
color = [absValue, absValue, absValue];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return applyHueShift(color, hueShift);
|
||||||
|
}
|
||||||
|
|||||||
@ -35,6 +35,17 @@ export const RENDER_MODES = [
|
|||||||
'vaporwave',
|
'vaporwave',
|
||||||
'dithered',
|
'dithered',
|
||||||
'palette',
|
'palette',
|
||||||
|
'sunset',
|
||||||
|
'ocean',
|
||||||
|
'forest',
|
||||||
|
'copper',
|
||||||
|
'vintage',
|
||||||
|
'infrared',
|
||||||
|
'fire',
|
||||||
|
'ice',
|
||||||
|
'plasma',
|
||||||
|
'xray',
|
||||||
|
'spectrum',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type RenderMode = (typeof RENDER_MODES)[number];
|
export type RenderMode = (typeof RENDER_MODES)[number];
|
||||||
@ -66,6 +77,16 @@ export const VALUE_MODES = [
|
|||||||
'noise',
|
'noise',
|
||||||
'warp',
|
'warp',
|
||||||
'flow',
|
'flow',
|
||||||
|
'spiral',
|
||||||
|
'turbulence',
|
||||||
|
'crystal',
|
||||||
|
'marble',
|
||||||
|
'quantum',
|
||||||
|
'logarithmic',
|
||||||
|
'mirror',
|
||||||
|
'rings',
|
||||||
|
'mesh',
|
||||||
|
'glitch',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type ValueMode = (typeof VALUE_MODES)[number];
|
export type ValueMode = (typeof VALUE_MODES)[number];
|
||||||
@ -78,4 +99,5 @@ export const DEFAULTS = {
|
|||||||
VALUE_MODE: 'integer' as ValueMode,
|
VALUE_MODE: 'integer' as ValueMode,
|
||||||
UI_OPACITY: 0.3,
|
UI_OPACITY: 0.3,
|
||||||
SHADER_CODE: 'x^y',
|
SHADER_CODE: 'x^y',
|
||||||
|
TIME_SPEED: 1.0,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user