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 -

-