ok le merge

This commit is contained in:
2025-07-07 15:32:51 +00:00
16 changed files with 325 additions and 197 deletions

View File

@ -476,21 +476,36 @@ export class FakeShader {
} }
} }
private compositeTiles(): void { private async compositeTiles(): Promise<void> {
const width = this.canvas.width;
const height = this.canvas.height; const height = this.canvas.height;
const tileHeight = Math.ceil(height / this.workerCount); const tileHeight = Math.ceil(height / this.workerCount);
// Clear main canvas // Use ImageBitmap for faster compositing if available
this.ctx.clearRect(0, 0, width, height); if (typeof createImageBitmap !== 'undefined') {
try {
const bitmapPromises: Promise<ImageBitmap>[] = [];
const positions: number[] = [];
// Composite all tiles directly on main canvas for (let i = 0; i < this.workerCount; i++) {
for (let i = 0; i < this.workerCount; i++) { const tileData = this.tileResults.get(i);
const tileData = this.tileResults.get(i); if (tileData) {
if (tileData) { bitmapPromises.push(createImageBitmap(tileData));
const startY = i * tileHeight; positions.push(i * tileHeight);
this.ctx.putImageData(tileData, 0, startY); }
}
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 // 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 // Simplified method - kept for backward compatibility but always uses all cores
setMultiWorkerMode(_enabled: boolean, _workerCount?: number): void { setMultiWorkerMode(_enabled: boolean, _workerCount?: number): void {
// Always use all available cores, ignore the enabled parameter // Always use all available cores, ignore the enabled parameter

View File

@ -46,16 +46,20 @@ 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 } from './utils/constants'; import { PERFORMANCE, COLOR_TABLE_SIZE, RENDER_MODES, RENDER_MODE_INDEX } from './utils/constants';
type ShaderFunction = (...args: number[]) => number; type ShaderFunction = (...args: number[]) => number;
class ShaderWorker { class ShaderWorker {
private compiledFunction: ShaderFunction | null = null; private compiledFunction: ShaderFunction | null = null;
private lastCode: string = ''; private lastCode: string = '';
private imageDataCache: LRUCache<string, ImageData> = new LRUCache(PERFORMANCE.IMAGE_DATA_CACHE_SIZE); private imageDataCache: LRUCache<string, ImageData> = new LRUCache(
private compilationCache: LRUCache<string, ShaderFunction> = new LRUCache(PERFORMANCE.COMPILATION_CACHE_SIZE); PERFORMANCE.IMAGE_DATA_CACHE_SIZE
private colorTables: Map<string, Uint8Array> = new Map(); );
private compilationCache: LRUCache<string, ShaderFunction> = new LRUCache(
PERFORMANCE.COMPILATION_CACHE_SIZE
);
private colorTables: Uint8Array[] = [];
private feedbackBuffer: Float32Array | null = null; private feedbackBuffer: Float32Array | null = null;
constructor() { constructor() {
@ -66,23 +70,14 @@ class ShaderWorker {
this.initializeColorTables(); this.initializeColorTables();
} }
private initializeColorTables(): void { private initializeColorTables(): void {
const tableSize = COLOR_TABLE_SIZE; const tableSize = COLOR_TABLE_SIZE;
// Pre-compute color tables for each render mode // Pre-compute color tables for each render mode using array indexing
const modes = [ this.colorTables = new Array(RENDER_MODES.length);
'classic',
'grayscale',
'red',
'green',
'blue',
'rgb',
'hsv',
'rainbow',
];
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 const colorTable = new Uint8Array(tableSize * 3); // RGB triplets
for (let i = 0; i < tableSize; i++) { for (let i = 0; i < tableSize; i++) {
@ -92,11 +87,10 @@ class ShaderWorker {
colorTable[i * 3 + 2] = b; colorTable[i * 3 + 2] = b;
} }
this.colorTables.set(mode, colorTable); this.colorTables[modeIndex] = colorTable;
} }
} }
private handleMessage(message: WorkerMessage): void { private handleMessage(message: WorkerMessage): void {
try { try {
switch (message.type) { switch (message.type) {
@ -226,44 +220,10 @@ class ShaderWorker {
} }
private isStaticExpression(code: string): boolean { private isStaticExpression(code: string): boolean {
// Check if code contains any variables // Check if code contains any variables using regex for better accuracy
const variables = [ 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/;
'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) { return !variablePattern.test(code);
if (code.includes(variable)) {
return false;
}
}
return true;
} }
private evaluateStaticExpression(code: string): number { private evaluateStaticExpression(code: string): number {
@ -412,14 +372,19 @@ class ShaderWorker {
const v = actualY / fullHeight; const v = actualY / fullHeight;
const centerX = fullWidth / 2; const centerX = fullWidth / 2;
const centerY = fullHeight / 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 angle = Math.atan2(actualY - centerY, x - centerX);
const maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2); const maxDistance = Math.sqrt(centerX ** 2 + centerY ** 2);
const normalizedDistance = radius / maxDistance; const normalizedDistance = radius / maxDistance;
const frameCount = Math.floor(time * 60); 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 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!( const value = this.compiledFunction!(
x, x,
@ -515,7 +480,6 @@ class ShaderWorker {
if (!imageData) { if (!imageData) {
imageData = new ImageData(width, height); imageData = new ImageData(width, height);
this.imageDataCache.set(key, imageData); this.imageDataCache.set(key, imageData);
} }
return imageData; return imageData;
@ -623,13 +587,14 @@ class ShaderWorker {
for (let i = 0; i < octaves; i++) { for (let i = 0; i < octaves; i++) {
const frequency = Math.pow(2, i) * scale; const frequency = Math.pow(2, i) * scale;
const noise = Math.sin((x + Math.abs(value) * 0.1) * frequency) * const noise =
Math.cos((y + Math.abs(value) * 0.1) * frequency); Math.sin((x + Math.abs(value) * 0.1) * frequency) *
Math.cos((y + Math.abs(value) * 0.1) * frequency);
fractalValue += noise * amplitude; fractalValue += noise * amplitude;
amplitude *= 0.5; amplitude *= 0.5;
} }
processedValue = Math.floor(((fractalValue + 1) * 0.5) * 255); processedValue = Math.floor((fractalValue + 1) * 0.5 * 255);
break; break;
} }
@ -638,20 +603,24 @@ class ShaderWorker {
const cellSize = 16; const cellSize = 16;
const cellX = Math.floor(x / cellSize); const cellX = Math.floor(x / cellSize);
const cellY = Math.floor(y / 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 // Generate cellular pattern based on neighbors
let neighbors = 0; let neighbors = 0;
for (let dx = -1; dx <= 1; dx++) { for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) { for (let dy = -1; dy <= 1; dy++) {
if (dx === 0 && dy === 0) continue; if (dx === 0 && dy === 0) continue;
const neighborHash = ((cellX + dx) * 73856093) ^ ((cellY + dy) * 19349663) ^ Math.floor(Math.abs(value)); const neighborHash =
if ((neighborHash % 256) > 128) neighbors++; ((cellX + dx) * 73856093) ^
((cellY + dy) * 19349663) ^
Math.floor(Math.abs(value));
if (neighborHash % 256 > 128) neighbors++;
} }
} }
const cellState = (cellHash % 256) > 128 ? 1 : 0; const cellState = cellHash % 256 > 128 ? 1 : 0;
const evolution = (neighbors >= 3 && neighbors <= 5) ? 1 : cellState; const evolution = neighbors >= 3 && neighbors <= 5 ? 1 : cellState;
processedValue = evolution * 255; processedValue = evolution * 255;
break; break;
} }
@ -668,7 +637,7 @@ class ShaderWorker {
const noise3 = Math.sin(nx * 25.12) * Math.cos(ny * 25.12) * 0.25; const noise3 = Math.sin(nx * 25.12) * Math.cos(ny * 25.12) * 0.25;
const combinedNoise = (noise1 + noise2 + noise3) / 1.75; const combinedNoise = (noise1 + noise2 + noise3) / 1.75;
processedValue = Math.floor(((combinedNoise + 1) * 0.5) * 255); processedValue = Math.floor((combinedNoise + 1) * 0.5 * 255);
break; break;
} }
@ -682,8 +651,12 @@ class ShaderWorker {
const warpFreq = 0.02; const warpFreq = 0.02;
// Calculate warped coordinates // Calculate warped coordinates
const warpX = x + Math.sin(y * warpFreq + Math.abs(value) * 0.01) * warpStrength * 100; const warpX =
const warpY = y + Math.cos(x * warpFreq + Math.abs(value) * 0.01) * warpStrength * 100; 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 // Create barrel/lens distortion
const dx = warpX - centerX; const dx = warpX - centerX;
@ -693,7 +666,8 @@ class ShaderWorker {
const normDist = dist / maxDist; const normDist = dist / maxDist;
// Apply non-linear space deformation // 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 deformedX = centerX + dx * deform;
const deformedY = centerY + dy * deform; const deformedY = centerY + dy * deform;
@ -713,18 +687,18 @@ class ShaderWorker {
{ {
x: centerX + Math.sin(Math.abs(value) * 0.01) * 200, x: centerX + Math.sin(Math.abs(value) * 0.01) * 200,
y: centerY + Math.cos(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, x: centerX + Math.cos(Math.abs(value) * 0.015) * 150,
y: centerY + Math.sin(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, x: centerX + Math.sin(Math.abs(value) * 0.008) * 300,
y: centerY + Math.cos(Math.abs(value) * 0.012) * 250, 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 // Calculate flow field at this point
@ -746,8 +720,8 @@ class ShaderWorker {
// Curl component (rotation) - creates vortices // Curl component (rotation) - creates vortices
const curlStrength = source.strength * 0.5; const curlStrength = source.strength * 0.5;
flowX += (-dy / normalizedDist) * curlStrength / normalizedDist; flowX += ((-dy / normalizedDist) * curlStrength) / normalizedDist;
flowY += (dx / normalizedDist) * curlStrength / normalizedDist; flowY += ((dx / normalizedDist) * curlStrength) / normalizedDist;
} }
// Add global flow influenced by value // Add global flow influenced by value
@ -757,9 +731,10 @@ class ShaderWorker {
// Add turbulence // Add turbulence
const turbScale = 0.05; const turbScale = 0.05;
const turbulence = Math.sin(x * turbScale + Math.abs(value) * 0.01) * const turbulence =
Math.cos(y * turbScale + Math.abs(value) * 0.015) * Math.sin(x * turbScale + Math.abs(value) * 0.01) *
(Math.abs(value) * 0.02); Math.cos(y * turbScale + Math.abs(value) * 0.015) *
(Math.abs(value) * 0.02);
flowX += turbulence; flowX += turbulence;
flowY += turbulence * 0.7; flowY += turbulence * 0.7;
@ -786,8 +761,10 @@ class ShaderWorker {
// Curl // Curl
const curlStrength = source.strength * 0.5; const curlStrength = source.strength * 0.5;
localFlowX += (-dy / normalizedDist) * curlStrength / normalizedDist; localFlowX +=
localFlowY += (dx / normalizedDist) * curlStrength / normalizedDist; ((-dy / normalizedDist) * curlStrength) / normalizedDist;
localFlowY +=
((dx / normalizedDist) * curlStrength) / normalizedDist;
} }
// Move particle // Move particle
@ -798,11 +775,14 @@ class ShaderWorker {
// Calculate final value based on particle's final position and flow magnitude // Calculate final value based on particle's final position and flow magnitude
const flowMagnitude = Math.sqrt(flowX * flowX + flowY * flowY); 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 // Combine flow magnitude with particle trajectory
const flowValue = (flowMagnitude * 10 + particleDistance * 2) % 256; 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); processedValue = Math.floor(enhanced * 255);
break; break;
@ -814,18 +794,18 @@ class ShaderWorker {
break; break;
} }
// Use pre-computed color table if available // Use pre-computed color table with O(1) array indexing
const colorTable = this.colorTables.get(renderMode); const modeIndex = RENDER_MODE_INDEX[renderMode];
if (colorTable) { if (modeIndex !== undefined && this.colorTables[modeIndex]) {
const colorTable = this.colorTables[modeIndex];
const index = Math.floor(processedValue) * 3; const index = Math.floor(processedValue) * 3;
return [colorTable[index], colorTable[index + 1], colorTable[index + 2]]; 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); return calculateColorDirect(processedValue, renderMode);
} }
private sanitizeCode(code: string): string { private sanitizeCode(code: string): string {
// Auto-prefix Math functions // Auto-prefix Math functions
const mathFunctions = [ const mathFunctions = [

View File

@ -1,5 +1,10 @@
import { AppSettings } from './stores/appSettings'; 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 { export interface SavedShader {
id: string; id: string;

View File

@ -80,7 +80,7 @@ export function HelpPopup() {
<strong>n</strong> - Noise value (0.0 to 1.0) <strong>n</strong> - Noise value (0.0 to 1.0)
</p> </p>
<p> <p>
<strong>b</strong> - Previous frame's value (feedback) <strong>b</strong> - Previous frame&apos;s value (feedback)
</p> </p>
<p> <p>
<strong>mouseX, mouseY</strong> - Mouse position (0.0 to 1.0) <strong>mouseX, mouseY</strong> - Mouse position (0.0 to 1.0)
@ -139,7 +139,7 @@ export function HelpPopup() {
<p> <p>
<strong>trebleLevel</strong> - High frequencies (0.0-1.0) <strong>trebleLevel</strong> - High frequencies (0.0-1.0)
</p> </p>
<p>Click "Enable Audio" to activate microphone</p> <p>Click &quot;Enable Audio&quot; to activate microphone</p>
</div> </div>
<div className="help-section"> <div className="help-section">
@ -295,10 +295,7 @@ export function HelpPopup() {
</p> </p>
<p> <p>
Website:{' '} Website:{' '}
<a <a href="https://raphaelforment.fr" target="_blank" rel="noreferrer">
href="https://raphaelforment.fr"
target="_blank"
>
raphaelforment.fr raphaelforment.fr
</a> </a>
</p> </p>
@ -307,6 +304,7 @@ export function HelpPopup() {
<a <a
href="https://git.raphaelforment.fr" href="https://git.raphaelforment.fr"
target="_blank" target="_blank"
rel="noreferrer"
> >
git.raphaelforment.fr git.raphaelforment.fr
</a> </a>

View File

@ -112,7 +112,9 @@ export function MobileMenu() {
<label>Value Mode</label> <label>Value Mode</label>
<select <select
value={settings.valueMode} value={settings.valueMode}
onChange={(e) => updateAppSettings({ valueMode: e.target.value as ValueMode })} onChange={(e) =>
updateAppSettings({ valueMode: e.target.value as ValueMode })
}
> >
{VALUE_MODES.map((mode) => ( {VALUE_MODES.map((mode) => (
<option key={mode} value={mode}> <option key={mode} value={mode}>

View File

@ -60,17 +60,24 @@ export function TopBar() {
uiOpacity: settings.uiOpacity, uiOpacity: settings.uiOpacity,
}; };
const encoded = btoa(JSON.stringify(shareData)); try {
window.location.hash = encoded; const encoded = btoa(JSON.stringify(shareData));
const url = `${window.location.origin}${window.location.pathname}#${encoded}`;
navigator.clipboard console.log('Sharing URL:', url);
.writeText(window.location.href) console.log('Share data:', shareData);
.then(() => {
console.log('URL copied to clipboard'); navigator.clipboard
}) .writeText(url)
.catch(() => { .then(() => {
console.log('Copy failed'); console.log('URL copied to clipboard');
}); })
.catch(() => {
console.log('Copy failed');
});
} catch (error) {
console.error('Failed to create share URL:', error);
}
}; };
const handleExportPNG = () => { const handleExportPNG = () => {
@ -151,7 +158,9 @@ export function TopBar() {
Value Mode: Value Mode:
<select <select
value={settings.valueMode} value={settings.valueMode}
onChange={(e) => updateAppSettings({ valueMode: e.target.value as ValueMode })} onChange={(e) =>
updateAppSettings({ valueMode: e.target.value as ValueMode })
}
style={{ style={{
background: 'rgba(255,255,255,0.1)', background: 'rgba(255,255,255,0.1)',
border: '1px solid #555', border: '1px solid #555',

View File

@ -32,11 +32,17 @@ export const WelcomePopup: React.FC = () => {
<h2 className="welcome-title">Welcome to BitFielder</h2> <h2 className="welcome-title">Welcome to BitFielder</h2>
<div className="welcome-content"> <div className="welcome-content">
<p>BitFielder is an experimental lofi bitfield shader editor made by <a href="https://raphaelforment.fr">BuboBubo</a>. Use it to create visual compositions through code. I use it for fun :) </p> <p>
BitFielder is an experimental lofi bitfield shader editor made by{' '}
<a href="https://raphaelforment.fr">BuboBubo</a>. Use it to create
visual compositions through code. I use it for fun :){' '}
</p>
<h3>Getting Started</h3> <h3>Getting Started</h3>
<ul> <ul>
<li>Edit the shader code and press <i>Eval</i> or <i>Ctrl+Enter</i></li> <li>
Edit the shader code and press <i>Eval</i> or <i>Ctrl+Enter</i>
</li>
<li>Use special variables to create reactive effects</li> <li>Use special variables to create reactive effects</li>
<li>Explore/store shaders in the library (left pane)</li> <li>Explore/store shaders in the library (left pane)</li>
<li>Export your creations as images or sharable links</li> <li>Export your creations as images or sharable links</li>
@ -44,13 +50,24 @@ export const WelcomePopup: React.FC = () => {
<h3>Key Features</h3> <h3>Key Features</h3>
<ul> <ul>
<li><strong>Real-time editing:</strong> See your changes instantly</li> <li>
<li><strong>Motion and touch:</strong> Mouse, touchscreen support</li> <strong>Real-time editing:</strong> See your changes instantly
<li><strong>Audio reactive:</strong> Synchronize with a sound signal</li> </li>
<li><strong>Export capabilities:</strong> Save and share your work</li> <li>
<strong>Motion and touch:</strong> Mouse, touchscreen support
</li>
<li>
<strong>Audio reactive:</strong> Synchronize with a sound signal
</li>
<li>
<strong>Export capabilities:</strong> Save and share your work
</li>
</ul> </ul>
<p className="help-hint">Press <kbd>?</kbd> anytime to view keyboard shortcuts and detailed help.</p> <p className="help-hint">
Press <kbd>?</kbd> anytime to view keyboard shortcuts and detailed
help.
</p>
<p className="dismiss-hint">Press any key to dismiss this message</p> <p className="dismiss-hint">Press any key to dismiss this message</p>
</div> </div>

View File

@ -13,11 +13,20 @@ $appSettings.set(savedSettings);
function loadFromURL() { function loadFromURL() {
if (window.location.hash) { if (window.location.hash) {
try { 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 { try {
const shareData = JSON.parse(decoded); const shareData = JSON.parse(decoded);
setShaderCode(shareData.code); console.log('Parsed share data:', shareData);
if (shareData.code) {
setShaderCode(shareData.code);
}
$appSettings.set({ $appSettings.set({
resolution: shareData.resolution || savedSettings.resolution, resolution: shareData.resolution || savedSettings.resolution,
fps: shareData.fps || savedSettings.fps, fps: shareData.fps || savedSettings.fps,
@ -28,7 +37,10 @@ function loadFromURL() {
? shareData.uiOpacity ? shareData.uiOpacity
: savedSettings.uiOpacity, : savedSettings.uiOpacity,
}); });
console.log('Settings updated from URL');
} catch (jsonError) { } catch (jsonError) {
console.log('JSON parse failed, falling back to old format');
// Fall back to old format (just code as string) // Fall back to old format (just code as string)
setShaderCode(decoded); setShaderCode(decoded);
} }

View File

@ -56,6 +56,16 @@ export const defaultInputState: InputState = {
export const $input = atom<InputState>(defaultInputState); export const $input = atom<InputState>(defaultInputState);
let mouseUpdatePending = false;
let pendingMouseData = {
x: 0,
y: 0,
pressed: false,
vx: 0,
vy: 0,
clickTime: 0,
};
export function updateMousePosition( export function updateMousePosition(
x: number, x: number,
y: number, y: number,
@ -64,17 +74,37 @@ export function updateMousePosition(
vy: number, vy: number,
clickTime: number clickTime: number
) { ) {
$input.set({ pendingMouseData = { x, y, pressed, vx, vy, clickTime };
...$input.get(),
mouseX: x, if (!mouseUpdatePending) {
mouseY: y, mouseUpdatePending = true;
mousePressed: pressed, requestAnimationFrame(() => {
mouseVX: vx, const current = $input.get();
mouseVY: vy, $input.set({
mouseClickTime: clickTime, ...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( export function updateTouchPosition(
count: number, count: number,
x0: number, x0: number,
@ -84,16 +114,25 @@ export function updateTouchPosition(
scale: number, scale: number,
rotation: number rotation: number
) { ) {
$input.set({ pendingTouchData = { count, x0, y0, x1, y1, scale, rotation };
...$input.get(),
touchCount: count, if (!touchUpdatePending) {
touch0X: x0, touchUpdatePending = true;
touch0Y: y0, requestAnimationFrame(() => {
touch1X: x1, const current = $input.get();
touch1Y: y1, $input.set({
pinchScale: scale, ...current,
pinchRotation: rotation, 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( export function updateDeviceMotion(

View File

@ -21,7 +21,10 @@ export const defaultUIState: UIState = {
export const uiState = atom<UIState>(defaultUIState); export const uiState = atom<UIState>(defaultUIState);
export function toggleMobileMenu() { export function toggleMobileMenu() {
uiState.set({ ...uiState.get(), mobileMenuOpen: !uiState.get().mobileMenuOpen }); uiState.set({
...uiState.get(),
mobileMenuOpen: !uiState.get().mobileMenuOpen,
});
} }
export function closeMobileMenu() { export function closeMobileMenu() {
@ -37,7 +40,10 @@ export function hideHelp() {
} }
export function toggleShaderLibrary() { export function toggleShaderLibrary() {
uiState.set({ ...uiState.get(), shaderLibraryOpen: !uiState.get().shaderLibraryOpen }); uiState.set({
...uiState.get(),
shaderLibraryOpen: !uiState.get().shaderLibraryOpen,
});
} }
export function toggleUI() { export function toggleUI() {

View File

@ -462,7 +462,7 @@ button [data-lucide] {
} }
.welcome-content li:before { .welcome-content li:before {
content: "▸"; content: '▸';
position: absolute; position: absolute;
left: 0; left: 0;
color: #999; color: #999;

View File

@ -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 c = v * s;
const x = c * (1 - Math.abs(((h * 6) % 2) - 1)); const x = c * (1 - Math.abs(((h * 6) % 2) - 1));
const m = v - c; const m = v - c;

View File

@ -7,18 +7,47 @@ export const UI_HEIGHTS = {
// Performance Constants // Performance Constants
export const PERFORMANCE = { export const PERFORMANCE = {
DEFAULT_TILE_SIZE: 64, DEFAULT_TILE_SIZE: 128,
MAX_RENDER_TIME_MS: 50, MAX_RENDER_TIME_MS: 50,
MAX_SHADER_TIMEOUT_MS: 5, MAX_SHADER_TIMEOUT_MS: 5,
TIMEOUT_CHECK_INTERVAL: 1000, TIMEOUT_CHECK_INTERVAL: 1000,
MAX_SAVED_SHADERS: 50, MAX_SAVED_SHADERS: 50,
IMAGE_DATA_CACHE_SIZE: 5, IMAGE_DATA_CACHE_SIZE: 10,
COMPILATION_CACHE_SIZE: 20, COMPILATION_CACHE_SIZE: 30,
} as const; } as const;
// Color Constants // Color Constants
export const COLOR_TABLE_SIZE = 256; 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<string, number> = RENDER_MODES.reduce(
(acc, mode, index) => {
acc[mode] = index;
return acc;
},
{} as Record<string, number>
);
// Storage Keys // Storage Keys
export const STORAGE_KEYS = { export const STORAGE_KEYS = {
SHADERS: 'bitfielder_shaders', SHADERS: 'bitfielder_shaders',
@ -36,10 +65,10 @@ export const VALUE_MODES = [
'cellular', 'cellular',
'noise', 'noise',
'warp', 'warp',
'flow' 'flow',
] as const; ] as const;
export type ValueMode = typeof VALUE_MODES[number]; export type ValueMode = (typeof VALUE_MODES)[number];
// Default Values // Default Values
export const DEFAULTS = { export const DEFAULTS = {