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