refactor help

This commit is contained in:
2025-07-14 12:19:41 +02:00
parent 9bf5d40171
commit 80537a4a30
3 changed files with 252 additions and 89 deletions

View File

@ -507,7 +507,7 @@ export class FakeShader {
try {
const bitmapPromises: Promise<ImageBitmap>[] = [];
const positions: number[] = [];
for (let i = 0; i < this.workerCount; i++) {
const tileData = this.tileResults.get(i);
if (tileData) {
@ -515,9 +515,9 @@ export class FakeShader {
positions.push(i * tileHeight);
}
}
const bitmaps = await Promise.all(bitmapPromises);
for (let i = 0; i < bitmaps.length; i++) {
this.ctx.drawImage(bitmaps[i], 0, positions[i]);
bitmaps[i].close(); // Free memory
@ -549,7 +549,7 @@ 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) {
@ -576,17 +576,21 @@ export class FakeShader {
'x^y',
'x&y',
'x|y',
'(x*y)%256',
'a|d|r',
'x|n*t^b*(t % 1.0)',
'(x+y+t*10)%256',
'((x>>4)^(y>>4))<<4',
'(x^y^(x*y))%256',
'd * t / 2.0',
'((x&y)|(x^y))%256',
'(x+y)&255',
'a^d * [b, r**t][floor(t%2.0)]',
'x%y',
'(x^(y<<2))%256',
'((x*t)^y)%256',
'(x&(y|t*8))%256',
'((x>>2)|(y<<2))%256',
'a+d*t',
'n*t*400',
'((x>>2)|(y<<2))%88',
'(x*y*t)%256',
'(x+y*t)%256',
'(x^y^(t*16))%256',
@ -599,11 +603,14 @@ export class FakeShader {
'((x|t)^(y|t))%256',
];
const vars = ['x', 'y', 't', 'i'];
const ops = ['^', '&', '|', '+', '-', '*', '%'];
const vars = ['x', 'y', 't', 'i', 'a', 'd', 'n', 'r', 'u', 'v', 'd', 'b'];
const ops = ['^', '&', '|', '+', '-', '*', '%', '**', '%'];
const shifts = ['<<', '>>'];
const numbers = ['2', '4', '8', '16', '32', '64', '128', '256'];
const numbers = [];
for (let i = 0; i < Math.random(200); i++) {
numbers.push(Math.floor(Math.random(400)))
}
const randomChoice = <T>(arr: T[]): T =>
arr[Math.floor(Math.random() * arr.length)];
@ -618,8 +625,7 @@ export class FakeShader {
() => `${randomChoice(vars)}^${randomChoice(vars)}^${randomChoice(vars)}`,
];
// 70% chance to pick from presets, 30% chance to generate dynamic
if (Math.random() < 0.7) {
if (Math.random() < 0.5) {
return randomChoice(presets);
} else {
return randomChoice(dynamicExpressions)();

View File

@ -62,6 +62,12 @@ class ShaderWorker {
PERFORMANCE.COMPILATION_CACHE_SIZE
);
private feedbackBuffer: Float32Array | null = null;
private previousFeedbackBuffer: Float32Array | null = null;
private stateBuffer: Float32Array | null = null;
private echoBuffers: Float32Array[] = [];
private echoFrameCounter: number = 0;
private echoInterval: number = 30; // Store echo every 30 frames (~0.5s at 60fps)
private lastFrameTime: number = 0;
constructor() {
self.onmessage = (e: MessageEvent<WorkerMessage>) => {
@ -139,6 +145,22 @@ class ShaderWorker {
'd',
'n',
'b',
'bn',
'bs',
'be',
'bw',
'w',
'h',
'p',
'z',
'j',
'o',
'g',
'm',
'l',
'k',
's',
'e',
'mouseX',
'mouseY',
'mousePressed',
@ -201,8 +223,8 @@ 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|bpm|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|bn|bs|be|bw|m|l|k|s|e|w|h|p|z|j|o|g|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);
}
@ -246,11 +268,23 @@ class ShaderWorker {
const startTime = performance.now();
const maxRenderTime = PERFORMANCE.MAX_RENDER_TIME_MS;
// Initialize feedback buffer if needed
// Initialize feedback buffers if needed
if (!this.feedbackBuffer || this.feedbackBuffer.length !== width * height) {
this.feedbackBuffer = new Float32Array(width * height);
this.previousFeedbackBuffer = new Float32Array(width * height);
this.stateBuffer = new Float32Array(width * height);
// Initialize echo buffers (4 buffers for different time delays)
this.echoBuffers = [];
for (let i = 0; i < 4; i++) {
this.echoBuffers.push(new Float32Array(width * height));
}
}
// Update frame timing for frame rate independence
const deltaTime = time - this.lastFrameTime;
this.lastFrameTime = time;
try {
// Use tiled rendering for better timeout handling
this.renderTiled(
@ -263,8 +297,33 @@ class ShaderWorker {
message,
startTime,
maxRenderTime,
startY
startY,
deltaTime
);
// Copy current feedback to previous for next frame momentum calculations
if (this.feedbackBuffer && this.previousFeedbackBuffer) {
this.previousFeedbackBuffer.set(this.feedbackBuffer);
}
// Update echo buffers at regular intervals
this.echoFrameCounter++;
if (this.echoFrameCounter >= this.echoInterval && this.echoBuffers.length > 0) {
this.echoFrameCounter = 0;
// Rotate echo buffers: shift all buffers forward and store current in first buffer
for (let i = this.echoBuffers.length - 1; i > 0; i--) {
if (this.echoBuffers[i] && this.echoBuffers[i - 1]) {
this.echoBuffers[i].set(this.echoBuffers[i - 1]);
}
}
// Store current feedback in first echo buffer
if (this.feedbackBuffer && this.echoBuffers[0]) {
this.echoBuffers[0].set(this.feedbackBuffer);
}
}
this.postMessage({ id, type: 'rendered', success: true, imageData });
} catch (error) {
this.postError(
@ -284,7 +343,8 @@ class ShaderWorker {
message: WorkerMessage,
startTime: number,
maxRenderTime: number,
yOffset: number = 0
yOffset: number = 0,
deltaTime: number = 0.016
): void {
const tileSize = PERFORMANCE.DEFAULT_TILE_SIZE;
const tilesX = Math.ceil(width / tileSize);
@ -316,7 +376,8 @@ class ShaderWorker {
renderMode,
valueMode,
message,
yOffset
yOffset,
deltaTime
);
}
}
@ -333,7 +394,8 @@ class ShaderWorker {
renderMode: string,
valueMode: string,
message: WorkerMessage,
yOffset: number = 0
yOffset: number = 0,
deltaTime: number = 0.016
): void {
// Get full canvas dimensions for special modes (use provided full dimensions or fall back)
const fullWidth = message.fullWidth || width;
@ -362,9 +424,63 @@ class ShaderWorker {
const manhattanDistance =
Math.abs(x - centerX) + Math.abs(actualY - centerY);
const noise = (Math.sin(x * 0.1) * Math.cos(actualY * 0.1) + 1) * 0.5;
const feedbackValue = this.feedbackBuffer
? this.feedbackBuffer[pixelIndex] || 0
: 0;
// Simple, efficient feedback system
const currentFeedback = this.feedbackBuffer ? this.feedbackBuffer[pixelIndex] || 0 : 0;
const feedbackValue = currentFeedback;
// Simple neighbor feedback with bounds checking
let fbn = 0, fbs = 0, fbe = 0, fbw = 0;
if (this.feedbackBuffer) {
// North neighbor (bounds safe)
if (y > 0) fbn = this.feedbackBuffer[(y - 1) * width + x] || 0;
// South neighbor (bounds safe)
if (y < fullHeight - 1) fbs = this.feedbackBuffer[(y + 1) * width + x] || 0;
// East neighbor (bounds safe)
if (x < width - 1) fbe = this.feedbackBuffer[y * width + (x + 1)] || 0;
// West neighbor (bounds safe)
if (x > 0) fbw = this.feedbackBuffer[y * width + (x - 1)] || 0;
}
// Calculate feedback-based operators
// m - Momentum/Velocity (change from previous frame)
const previousValue = this.previousFeedbackBuffer ? this.previousFeedbackBuffer[pixelIndex] || 0 : 0;
const momentum = (feedbackValue - previousValue) * 0.5; // Scale for stability
// l - Laplacian/Diffusion (spatial derivative)
const laplacian = (fbn + fbs + fbe + fbw - feedbackValue * 4) * 0.25;
// k - Curvature/Contrast (gradient magnitude)
const gradientX = (fbe - fbw) * 0.5;
const gradientY = (fbs - fbn) * 0.5;
const curvature = Math.sqrt(gradientX * gradientX + gradientY * gradientY);
// s - State/Memory (persistent accumulator)
let currentState = this.stateBuffer ? this.stateBuffer[pixelIndex] || 0 : 0;
// State accumulates when feedback is high, decays when low
if (feedbackValue > 128) {
currentState = Math.min(255, currentState + deltaTime * 200); // Accumulate
} else {
currentState = Math.max(0, currentState - deltaTime * 100); // Decay
}
const stateValue = currentState;
// e - Echo/History (temporal snapshots)
let echoValue = 0;
if (this.echoBuffers.length > 0) {
// Cycle through different echo delays based on time
const echoIndex = Math.floor(time * 2) % this.echoBuffers.length; // Change every 0.5 seconds
const echoBuffer = this.echoBuffers[echoIndex];
echoValue = echoBuffer ? echoBuffer[pixelIndex] || 0 : 0;
}
// Calculate other variables
const canvasWidth = fullWidth;
const canvasHeight = fullHeight;
const phase = (time * Math.PI * 2) % (Math.PI * 2); // 0 to 2π cycling
const pseudoZ = Math.sin(radius * 0.01 + time) * 50; // depth based on radius and time
const jitter = ((x * 73856093 + actualY * 19349663) % 256) / 255; // deterministic per-pixel random
const oscillation = Math.sin(time * 2 * Math.PI + radius * 0.1); // wave oscillation
const goldenRatio = 1.618033988749; // golden ratio constant
const value = this.compiledFunction!(
x,
@ -380,6 +496,22 @@ class ShaderWorker {
manhattanDistance,
noise,
feedbackValue,
canvasWidth,
canvasHeight,
phase,
pseudoZ,
jitter,
oscillation,
goldenRatio,
momentum,
laplacian,
curvature,
stateValue,
echoValue,
fbn,
fbs,
fbe,
fbw,
message.mouseX || 0,
message.mouseY || 0,
message.mousePressed ? 1 : 0,
@ -422,9 +554,28 @@ class ShaderWorker {
data[i + 2] = b;
data[i + 3] = 255;
// Update feedback buffer with current processed value
// Store feedback as luminance of displayed color for consistency
if (this.feedbackBuffer) {
this.feedbackBuffer[pixelIndex] = safeValue;
// Use the actual displayed luminance as feedback (0-255 range)
const luminance = (r * 0.299 + g * 0.587 + b * 0.114);
// Frame rate independent decay
const decayFactor = Math.pow(0.95, deltaTime * 60); // 5% decay at 60fps
// Simple mixing to prevent oscillation
const previousValue = this.feedbackBuffer[pixelIndex] || 0;
const mixRatio = Math.min(deltaTime * 10, 0.3); // Max 30% new value per frame
let newFeedback = luminance * mixRatio + previousValue * (1 - mixRatio);
newFeedback *= decayFactor;
// Clamp and store
this.feedbackBuffer[pixelIndex] = Math.max(0, Math.min(255, newFeedback));
}
// Update state buffer
if (this.stateBuffer) {
this.stateBuffer[pixelIndex] = stateValue;
}
} catch (error) {
data[i] = 0;
@ -584,8 +735,8 @@ class ShaderWorker {
case 'cellular': {
// Cellular automata-inspired patterns
const cellSize = 16;
const cellX = Math.floor(x / cellSize);
const cellY = Math.floor(y / cellSize);
const cellX = Math.floor(x / centerX);
const cellY = Math.floor(y / centerY);
const cellHash =
(cellX * 73856093) ^ (cellY * 19349663) ^ Math.floor(Math.abs(value));
@ -802,8 +953,8 @@ class ShaderWorker {
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);
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;
}
@ -812,7 +963,7 @@ class ShaderWorker {
// 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;
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;
@ -884,12 +1035,12 @@ class ShaderWorker {
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;
}

View File

@ -52,21 +52,21 @@ export function HelpPopup() {
<strong>M</strong> - Cycle value mode
</p>
<p>
<strong>Space</strong> - Tap tempo (when editor not focused)
<strong>Space</strong> - Tap tempo
</p>
<p>
<strong>Arrow Left/Right</strong> - Adjust hue shift (when editor not focused)
<strong>Arrow Left/Right</strong> - Adjust hue shift
</p>
<p>
<strong>Arrow Up/Down</strong> - Cycle value mode (when editor not focused)
<strong>Arrow Up/Down</strong> - Cycle value mode
</p>
<p>
<strong>Shift+Arrow Up/Down</strong> - Cycle render mode (when editor not focused)
<strong>Shift+Arrow Up/Down</strong> - Cycle render mode
</p>
</div>
<div className="help-section">
<h4>Variables</h4>
<h4>Core Variables - Basics</h4>
<p>
<strong>x, y</strong> - Pixel coordinates
</p>
@ -97,11 +97,30 @@ export function HelpPopup() {
<p>
<strong>d</strong> - Manhattan distance from center
</p>
<p>
<strong>w, h</strong> - Canvas width and height (pixels)
</p>
</div>
<div className="help-section">
<h4>Core Variables - Advanced</h4>
<p>
<strong>n</strong> - Noise value (0.0 to 1.0)
</p>
<p>
<strong>b</strong> - Previous frame&apos;s value (feedback)
<strong>p</strong> - Phase value (0 to 2π, cycles with time)
</p>
<p>
<strong>z</strong> - Pseudo-depth coordinate (oscillates with distance and time)
</p>
<p>
<strong>j</strong> - Per-pixel jitter/random value (0.0 to 1.0, deterministic)
</p>
<p>
<strong>o</strong> - Oscillation value (wave function based on time and distance)
</p>
<p>
<strong>g</strong> - Golden ratio constant (1.618... for natural spirals)
</p>
<p>
<strong>mouseX, mouseY</strong> - Mouse position (0.0 to 1.0)
@ -117,6 +136,34 @@ export function HelpPopup() {
</p>
</div>
<div className="help-section">
<h4>Feedback Variables</h4>
<p>
<strong>b</strong> - Previous frame's luminance at this pixel (0-255)
</p>
<p>
<strong>bn, bs, be, bw</strong> - Neighbor luminance (North, South, East, West)
</p>
<p>
<strong>m</strong> - Momentum/velocity: Detects motion and change between frames
</p>
<p>
<strong>l</strong> - Laplacian/diffusion: Creates natural spreading and heat diffusion
</p>
<p>
<strong>k</strong> - Curvature/contrast: Edge detection and gradient magnitude
</p>
<p>
<strong>s</strong> - State/memory: Persistent accumulator that remembers bright areas
</p>
<p>
<strong>e</strong> - Echo/history: Temporal snapshots that recall past brightness patterns
</p>
<p>
<em>Feedback uses actual displayed brightness with natural decay and frame-rate independence for stable, evolving patterns.</em>
</p>
</div>
<div className="help-section">
<h4>Touch & Gestures</h4>
<p>
@ -214,29 +261,6 @@ export function HelpPopup() {
</p>
</div>
<div className="help-section">
<h4>Value Modes</h4>
<p>
<strong>Integer (0-255):</strong> Traditional mode for large values
</p>
<p>
<strong>Float (0.0-1.0):</strong> Bitfield shader mode, inverts and
clamps values
</p>
<p>
<strong>Polar (angle-based):</strong> Spiral patterns combining
angle and radius
</p>
<p>
<strong>Distance (radial):</strong> Concentric wave rings with
variable frequency
</p>
<p>
<strong>Wave (ripple):</strong> Multi-source interference with
amplitude falloff
</p>
<p>Each mode transforms your expression differently!</p>
</div>
<div className="help-section">
<h4>Advanced Features</h4>
@ -261,40 +285,22 @@ export function HelpPopup() {
<div className="help-section">
<h4>Shader Library</h4>
<p>
Hover over the <strong>left edge</strong> of the screen to access
the shader library
<strong>Access:</strong> Hover over the left edge of the screen
</p>
<p>Save shaders with custom names and search through them</p>
<p>
Use <strong>edit</strong> to rename, <strong>del</strong> to delete
<strong>Save:</strong> Click the save icon to store current shader
</p>
<p>
<strong>Search:</strong> Filter saved shaders by name
</p>
<p>
<strong>Manage:</strong> Edit names or delete with the buttons
</p>
<p>
<strong>Load:</strong> Click any shader to apply it instantly
</p>
</div>
<div className="help-section">
<h4>Render Modes</h4>
<p>
<strong>Classic</strong> - Original colorful mode
</p>
<p>
<strong>Grayscale</strong> - Black and white
</p>
<p>
<strong>Red/Green/Blue</strong> - Single color channels
</p>
<p>
<strong>HSV</strong> - Hue-based coloring
</p>
<p>
<strong>Rainbow</strong> - Spectrum coloring
</p>
</div>
<div className="help-section">
<h4>Export</h4>
<p>
<strong>Export PNG</strong> - Save current frame as image
</p>
</div>
</div>
<div