diff --git a/coolshaders.md b/coolshaders.md
new file mode 100644
index 0000000..226e173
--- /dev/null
+++ b/coolshaders.md
@@ -0,0 +1,3 @@
+x<<127*y*t
+x<<20*t*80*y^8
+x**10*y^200*t+20
diff --git a/index.html b/index.html
index dda1c9a..c380cb3 100644
--- a/index.html
+++ b/index.html
@@ -772,6 +772,7 @@
+
@@ -827,6 +828,7 @@
@@ -834,7 +836,7 @@
-
+
@@ -876,6 +878,33 @@
t - Time (enables animation)
i - Pixel index
mouseX, mouseY - Mouse position (0.0 to 1.0)
+ mousePressed - Mouse button down (true/false)
+ mouseVX, mouseVY - Mouse velocity
+ mouseClickTime - Time since last click (ms)
+
+
+
+
Touch & Gestures
+
touchCount - Number of active touches
+
touch0X, touch0Y - Primary touch position
+
touch1X, touch1Y - Secondary touch position
+
pinchScale - Pinch zoom scale factor
+
pinchRotation - Pinch rotation angle
+
+
+
+
Device Motion
+
accelX, accelY, accelZ - Accelerometer data
+
gyroX, gyroY, gyroZ - Gyroscope rotation rates
+
+
+
+
Audio Reactive
+
audioLevel - Overall audio volume (0.0-1.0)
+
bassLevel - Low frequencies (0.0-1.0)
+
midLevel - Mid frequencies (0.0-1.0)
+
trebleLevel - High frequencies (0.0-1.0)
+
Click "Enable Audio" to activate microphone
@@ -906,6 +935,15 @@
Export PNG - Save current frame as image
+
+
diff --git a/src/FakeShader.ts b/src/FakeShader.ts
index d01ff34..61bb69d 100644
--- a/src/FakeShader.ts
+++ b/src/FakeShader.ts
@@ -8,6 +8,27 @@ interface WorkerMessage {
renderMode?: string;
mouseX?: number;
mouseY?: number;
+ mousePressed?: boolean;
+ mouseVX?: number;
+ mouseVY?: number;
+ mouseClickTime?: number;
+ touchCount?: number;
+ touch0X?: number;
+ touch0Y?: number;
+ touch1X?: number;
+ touch1Y?: number;
+ pinchScale?: number;
+ pinchRotation?: number;
+ accelX?: number;
+ accelY?: number;
+ accelZ?: number;
+ gyroX?: number;
+ gyroY?: number;
+ gyroZ?: number;
+ audioLevel?: number;
+ bassLevel?: number;
+ midLevel?: number;
+ trebleLevel?: number;
}
interface WorkerResponse {
@@ -31,6 +52,27 @@ export class FakeShader {
private renderMode: string = 'classic';
private mouseX: number = 0;
private mouseY: number = 0;
+ private mousePressed: boolean = false;
+ private mouseVX: number = 0;
+ private mouseVY: number = 0;
+ private mouseClickTime: number = 0;
+ private touchCount: number = 0;
+ private touch0X: number = 0;
+ private touch0Y: number = 0;
+ private touch1X: number = 0;
+ private touch1Y: number = 0;
+ private pinchScale: number = 1;
+ private pinchRotation: number = 0;
+ private accelX: number = 0;
+ private accelY: number = 0;
+ private accelZ: number = 0;
+ private gyroX: number = 0;
+ private gyroY: number = 0;
+ private gyroZ: number = 0;
+ private audioLevel: number = 0;
+ private bassLevel: number = 0;
+ private midLevel: number = 0;
+ private trebleLevel: number = 0;
// Frame rate limiting
private targetFPS: number = 30;
@@ -120,7 +162,28 @@ export class FakeShader {
time: currentTime,
renderMode: this.renderMode,
mouseX: this.mouseX,
- mouseY: this.mouseY
+ mouseY: this.mouseY,
+ mousePressed: this.mousePressed,
+ mouseVX: this.mouseVX,
+ mouseVY: this.mouseVY,
+ mouseClickTime: this.mouseClickTime,
+ touchCount: this.touchCount,
+ touch0X: this.touch0X,
+ touch0Y: this.touch0Y,
+ touch1X: this.touch1X,
+ touch1Y: this.touch1Y,
+ pinchScale: this.pinchScale,
+ pinchRotation: this.pinchRotation,
+ accelX: this.accelX,
+ accelY: this.accelY,
+ accelZ: this.accelZ,
+ gyroX: this.gyroX,
+ gyroY: this.gyroY,
+ gyroZ: this.gyroZ,
+ audioLevel: this.audioLevel,
+ bassLevel: this.bassLevel,
+ midLevel: this.midLevel,
+ trebleLevel: this.trebleLevel
} as WorkerMessage);
}
@@ -192,9 +255,39 @@ export class FakeShader {
this.renderMode = mode;
}
- setMousePosition(x: number, y: number): void {
+ setMousePosition(x: number, y: number, pressed: boolean = false, vx: number = 0, vy: number = 0, clickTime: number = 0): void {
this.mouseX = x;
this.mouseY = y;
+ this.mousePressed = pressed;
+ this.mouseVX = vx;
+ this.mouseVY = vy;
+ this.mouseClickTime = clickTime;
+ }
+
+ setTouchPosition(count: number, x0: number = 0, y0: number = 0, x1: number = 0, y1: number = 0, scale: number = 1, rotation: number = 0): void {
+ this.touchCount = count;
+ this.touch0X = x0;
+ this.touch0Y = y0;
+ this.touch1X = x1;
+ this.touch1Y = y1;
+ this.pinchScale = scale;
+ this.pinchRotation = rotation;
+ }
+
+ setDeviceMotion(ax: number, ay: number, az: number, gx: number, gy: number, gz: number): void {
+ this.accelX = ax;
+ this.accelY = ay;
+ this.accelZ = az;
+ this.gyroX = gx;
+ this.gyroY = gy;
+ this.gyroZ = gz;
+ }
+
+ setAudioData(level: number, bass: number, mid: number, treble: number): void {
+ this.audioLevel = level;
+ this.bassLevel = bass;
+ this.midLevel = mid;
+ this.trebleLevel = treble;
}
destroy(): void {
diff --git a/src/ShaderWorker.ts b/src/ShaderWorker.ts
index 9d027a0..c17a90e 100644
--- a/src/ShaderWorker.ts
+++ b/src/ShaderWorker.ts
@@ -9,6 +9,27 @@ interface WorkerMessage {
renderMode?: string;
mouseX?: number;
mouseY?: number;
+ mousePressed?: boolean;
+ mouseVX?: number;
+ mouseVY?: number;
+ mouseClickTime?: number;
+ touchCount?: number;
+ touch0X?: number;
+ touch0Y?: number;
+ touch1X?: number;
+ touch1Y?: number;
+ pinchScale?: number;
+ pinchRotation?: number;
+ accelX?: number;
+ accelY?: number;
+ accelZ?: number;
+ gyroX?: number;
+ gyroY?: number;
+ gyroZ?: number;
+ audioLevel?: number;
+ bassLevel?: number;
+ midLevel?: number;
+ trebleLevel?: number;
}
interface WorkerResponse {
@@ -36,7 +57,7 @@ class ShaderWorker {
this.compileShader(message.id, message.code!);
break;
case 'render':
- this.renderShader(message.id, message.width!, message.height!, message.time!, message.renderMode || 'classic', message.mouseX || 0, message.mouseY || 0);
+ this.renderShader(message.id, message.width!, message.height!, message.time!, message.renderMode || 'classic', message);
break;
}
} catch (error) {
@@ -52,7 +73,7 @@ class ShaderWorker {
try {
const safeCode = this.sanitizeCode(code);
- this.compiledFunction = new Function('x', 'y', 't', 'i', 'mouseX', 'mouseY', `
+ this.compiledFunction = new Function('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', `
// Timeout protection
const startTime = performance.now();
let iterations = 0;
@@ -78,7 +99,7 @@ class ShaderWorker {
}
}
- private renderShader(id: string, width: number, height: number, time: number, renderMode: string, mouseX: number, mouseY: number): void {
+ private renderShader(id: string, width: number, height: number, time: number, renderMode: string, message: WorkerMessage): void {
if (!this.compiledFunction) {
this.postError(id, 'No compiled shader');
return;
@@ -111,7 +132,20 @@ class ShaderWorker {
const pixelIndex = y * width + x;
try {
- const value = this.compiledFunction(x, y, time, pixelIndex, mouseX, mouseY);
+ const value = this.compiledFunction(
+ x, y, time, pixelIndex,
+ message.mouseX || 0, message.mouseY || 0,
+ message.mousePressed || false,
+ message.mouseVX || 0, message.mouseVY || 0,
+ message.mouseClickTime || 0,
+ message.touchCount || 0,
+ message.touch0X || 0, message.touch0Y || 0,
+ message.touch1X || 0, message.touch1Y || 0,
+ message.pinchScale || 1, message.pinchRotation || 0,
+ message.accelX || 0, message.accelY || 0, message.accelZ || 0,
+ message.gyroX || 0, message.gyroY || 0, message.gyroZ || 0,
+ message.audioLevel || 0, message.bassLevel || 0, message.midLevel || 0, message.trebleLevel || 0
+ );
const safeValue = isFinite(value) ? value : 0;
const [r, g, b] = this.calculateColor(safeValue, renderMode);
@@ -222,7 +256,8 @@ class ShaderWorker {
}
private sanitizeCode(code: string): string {
- // Strict whitelist approach
+ // Strict whitelist approach - extended to include new interaction variables
+ // Variables: x, y, t, i, mouseX, mouseY, mousePressed, mouseVX, mouseVY, mouseClickTime, touchCount, touch0X, touch0Y, touch1X, touch1Y, pinchScale, pinchRotation, accelX, accelY, accelZ, gyroX, gyroY, gyroZ
const allowedPattern = /^[0-9a-zA-Z\s\+\-\*\/\%\^\&\|\(\)\<\>\~\?:,\.xyti]+$/;
if (!allowedPattern.test(code)) {
diff --git a/src/main.ts b/src/main.ts
index 153d766..30a5ae4 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -11,6 +11,35 @@ class BitfielderApp {
private performanceWarning: HTMLElement;
private mouseX: number = 0;
private mouseY: number = 0;
+ private mousePressed: boolean = false;
+ private mouseVX: number = 0;
+ private mouseVY: number = 0;
+ private mouseClickTime: number = 0;
+ private lastMouseX: number = 0;
+ private lastMouseY: number = 0;
+ private touchCount: number = 0;
+ private touch0X: number = 0;
+ private touch0Y: number = 0;
+ private touch1X: number = 0;
+ private touch1Y: number = 0;
+ private pinchScale: number = 1;
+ private pinchRotation: number = 0;
+ private initialPinchDistance: number = 0;
+ private initialPinchAngle: number = 0;
+ private accelX: number = 0;
+ private accelY: number = 0;
+ private accelZ: number = 0;
+ private gyroX: number = 0;
+ private gyroY: number = 0;
+ private gyroZ: number = 0;
+ private audioLevel: number = 0;
+ private bassLevel: number = 0;
+ private midLevel: number = 0;
+ private trebleLevel: number = 0;
+ private audioContext: AudioContext | null = null;
+ private analyser: AnalyserNode | null = null;
+ private microphone: MediaStreamAudioSourceNode | null = null;
+ private audioEnabled: boolean = false;
constructor() {
this.canvas = document.getElementById('canvas') as HTMLCanvasElement;
@@ -55,6 +84,7 @@ class BitfielderApp {
const hideUiBtn = document.getElementById('hide-ui-btn')!;
const showUiBtn = document.getElementById('show-ui-btn')!;
const randomBtn = document.getElementById('random-btn')!;
+ const audioBtn = document.getElementById('audio-btn')!;
const shareBtn = document.getElementById('share-btn')!;
const exportPngBtn = document.getElementById('export-png-btn')!;
const resolutionSelect = document.getElementById('resolution-select') as HTMLSelectElement;
@@ -81,6 +111,7 @@ class BitfielderApp {
// Mobile menu buttons
const helpBtnMobile = document.getElementById('help-btn-mobile')!;
const fullscreenBtnMobile = document.getElementById('fullscreen-btn-mobile')!;
+ const audioBtnMobile = document.getElementById('audio-btn-mobile')!;
const shareBtnMobile = document.getElementById('share-btn-mobile')!;
const exportPngBtnMobile = document.getElementById('export-png-btn-mobile')!;
@@ -94,6 +125,7 @@ class BitfielderApp {
hideUiBtn.addEventListener('click', () => this.toggleUI());
showUiBtn.addEventListener('click', () => this.showUI());
randomBtn.addEventListener('click', () => this.generateRandom());
+ audioBtn.addEventListener('click', () => this.toggleAudio());
shareBtn.addEventListener('click', () => this.shareURL());
exportPngBtn.addEventListener('click', () => this.exportPNG());
resolutionSelect.addEventListener('change', () => this.updateResolution());
@@ -146,6 +178,10 @@ class BitfielderApp {
this.closeMobileMenu();
this.toggleFullscreen();
});
+ audioBtnMobile.addEventListener('click', () => {
+ this.closeMobileMenu();
+ this.toggleAudio();
+ });
shareBtnMobile.addEventListener('click', () => {
this.closeMobileMenu();
this.shareURL();
@@ -208,10 +244,69 @@ class BitfielderApp {
// Mouse tracking
window.addEventListener('mousemove', (e) => {
+ this.lastMouseX = this.mouseX;
+ this.lastMouseY = this.mouseY;
this.mouseX = e.clientX / window.innerWidth;
this.mouseY = 1.0 - (e.clientY / window.innerHeight); // Invert Y to match shader coordinates
- this.shader.setMousePosition(this.mouseX, this.mouseY);
+ this.mouseVX = this.mouseX - this.lastMouseX;
+ this.mouseVY = this.mouseY - this.lastMouseY;
+ this.shader.setMousePosition(this.mouseX, this.mouseY, this.mousePressed, this.mouseVX, this.mouseVY, this.mouseClickTime);
});
+
+ window.addEventListener('mousedown', () => {
+ this.mousePressed = true;
+ this.mouseClickTime = Date.now();
+ this.shader.setMousePosition(this.mouseX, this.mouseY, this.mousePressed, this.mouseVX, this.mouseVY, this.mouseClickTime);
+ });
+
+ window.addEventListener('mouseup', () => {
+ this.mousePressed = false;
+ this.shader.setMousePosition(this.mouseX, this.mouseY, this.mousePressed, this.mouseVX, this.mouseVY, this.mouseClickTime);
+ });
+
+ // Touch tracking
+ window.addEventListener('touchstart', (e) => {
+ e.preventDefault();
+ this.touchCount = e.touches.length;
+ this.updateTouchPositions(e.touches);
+ this.initializePinchGesture(e.touches);
+ });
+
+ window.addEventListener('touchmove', (e) => {
+ e.preventDefault();
+ this.touchCount = e.touches.length;
+ this.updateTouchPositions(e.touches);
+ this.updatePinchGesture(e.touches);
+ });
+
+ window.addEventListener('touchend', (e) => {
+ e.preventDefault();
+ this.touchCount = e.touches.length;
+ if (this.touchCount === 0) {
+ this.touch0X = this.touch0Y = this.touch1X = this.touch1Y = 0;
+ this.pinchScale = 1;
+ this.pinchRotation = 0;
+ } else {
+ this.updateTouchPositions(e.touches);
+ }
+ });
+
+ // Device motion tracking
+ if (window.DeviceMotionEvent) {
+ window.addEventListener('devicemotion', (e) => {
+ if (e.acceleration) {
+ this.accelX = e.acceleration.x || 0;
+ this.accelY = e.acceleration.y || 0;
+ this.accelZ = e.acceleration.z || 0;
+ }
+ if (e.rotationRate) {
+ this.gyroX = e.rotationRate.alpha || 0;
+ this.gyroY = e.rotationRate.beta || 0;
+ this.gyroZ = e.rotationRate.gamma || 0;
+ }
+ this.shader.setDeviceMotion(this.accelX, this.accelY, this.accelZ, this.gyroX, this.gyroY, this.gyroZ);
+ });
+ }
}
private render(): void {
@@ -600,6 +695,161 @@ class BitfielderApp {
this.shader.setCode(randomCode);
this.render();
}
+
+ private async toggleAudio(): Promise {
+ const audioBtn = document.getElementById('audio-btn')!;
+ const audioBtnMobile = document.getElementById('audio-btn-mobile')!;
+
+ if (!this.audioEnabled) {
+ try {
+ await this.setupAudio();
+ audioBtn.textContent = 'Disable Audio';
+ audioBtnMobile.innerHTML = ' Disable Audio';
+ } catch (error) {
+ console.error('Failed to enable audio:', error);
+ audioBtn.textContent = 'Audio Failed';
+ audioBtnMobile.innerHTML = ' Audio Failed';
+ }
+ } else {
+ this.disableAudio();
+ audioBtn.textContent = 'Enable Audio';
+ audioBtnMobile.innerHTML = ' Enable Audio';
+ }
+ }
+
+ private disableAudio(): void {
+ this.audioEnabled = false;
+ if (this.microphone) {
+ this.microphone.disconnect();
+ }
+ if (this.audioContext) {
+ this.audioContext.close();
+ }
+ this.audioLevel = 0;
+ this.bassLevel = 0;
+ this.midLevel = 0;
+ this.trebleLevel = 0;
+ this.shader.setAudioData(0, 0, 0, 0);
+ }
+
+ private updateTouchPositions(touches: TouchList): void {
+ if (touches.length > 0) {
+ this.touch0X = touches[0].clientX / window.innerWidth;
+ this.touch0Y = 1.0 - (touches[0].clientY / window.innerHeight);
+ }
+ if (touches.length > 1) {
+ this.touch1X = touches[1].clientX / window.innerWidth;
+ this.touch1Y = 1.0 - (touches[1].clientY / window.innerHeight);
+ }
+ this.shader.setTouchPosition(this.touchCount, this.touch0X, this.touch0Y, this.touch1X, this.touch1Y, this.pinchScale, this.pinchRotation);
+ }
+
+ private initializePinchGesture(touches: TouchList): void {
+ if (touches.length === 2) {
+ const dx = touches[1].clientX - touches[0].clientX;
+ const dy = touches[1].clientY - touches[0].clientY;
+ this.initialPinchDistance = Math.sqrt(dx * dx + dy * dy);
+ this.initialPinchAngle = Math.atan2(dy, dx);
+ this.pinchScale = 1;
+ this.pinchRotation = 0;
+ }
+ }
+
+ private updatePinchGesture(touches: TouchList): void {
+ if (touches.length === 2 && this.initialPinchDistance > 0) {
+ const dx = touches[1].clientX - touches[0].clientX;
+ const dy = touches[1].clientY - touches[0].clientY;
+ const distance = Math.sqrt(dx * dx + dy * dy);
+ const angle = Math.atan2(dy, dx);
+
+ this.pinchScale = distance / this.initialPinchDistance;
+ this.pinchRotation = angle - this.initialPinchAngle;
+ this.shader.setTouchPosition(this.touchCount, this.touch0X, this.touch0Y, this.touch1X, this.touch1Y, this.pinchScale, this.pinchRotation);
+ }
+ }
+
+ private async setupAudio(): Promise {
+ try {
+ // Check if Web Audio API is supported
+ if (!window.AudioContext && !(window as any).webkitAudioContext) {
+ console.warn('Web Audio API not supported');
+ return;
+ }
+
+ // Request microphone access
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+
+ // Create audio context
+ this.audioContext = new (window.AudioContext || (window as any).webkitAudioContext)();
+
+ // Create analyser node
+ this.analyser = this.audioContext.createAnalyser();
+ this.analyser.fftSize = 256; // 128 frequency bins
+ this.analyser.smoothingTimeConstant = 0.8;
+
+ // Create microphone source
+ this.microphone = this.audioContext.createMediaStreamSource(stream);
+ this.microphone.connect(this.analyser);
+
+ this.audioEnabled = true;
+ console.log('Audio analysis enabled');
+
+ // Start audio analysis loop
+ this.updateAudioData();
+ } catch (error) {
+ console.warn('Failed to setup audio:', error);
+ this.audioEnabled = false;
+ }
+ }
+
+ private updateAudioData(): void {
+ if (!this.analyser || !this.audioEnabled) return;
+
+ const bufferLength = this.analyser.frequencyBinCount;
+ const dataArray = new Uint8Array(bufferLength);
+ this.analyser.getByteFrequencyData(dataArray);
+
+ // Calculate overall audio level (RMS)
+ let sum = 0;
+ for (let i = 0; i < bufferLength; i++) {
+ sum += dataArray[i] * dataArray[i];
+ }
+ this.audioLevel = Math.sqrt(sum / bufferLength) / 255;
+
+ // Calculate frequency bands
+ // Low: 0-85Hz (bins 0-10 at 44.1kHz sample rate)
+ // Mid: 85-5500Hz (bins 11-85)
+ // High: 5500Hz+ (bins 86-127)
+ const lowEnd = Math.floor(bufferLength * 0.08); // ~10 bins
+ const midEnd = Math.floor(bufferLength * 0.67); // ~85 bins
+
+ // Bass (low frequencies)
+ let bassSum = 0;
+ for (let i = 0; i < lowEnd; i++) {
+ bassSum += dataArray[i];
+ }
+ this.bassLevel = (bassSum / lowEnd) / 255;
+
+ // Mid frequencies
+ let midSum = 0;
+ for (let i = lowEnd; i < midEnd; i++) {
+ midSum += dataArray[i];
+ }
+ this.midLevel = (midSum / (midEnd - lowEnd)) / 255;
+
+ // Treble (high frequencies)
+ let trebleSum = 0;
+ for (let i = midEnd; i < bufferLength; i++) {
+ trebleSum += dataArray[i];
+ }
+ this.trebleLevel = (trebleSum / (bufferLength - midEnd)) / 255;
+
+ // Update shader with audio data
+ this.shader.setAudioData(this.audioLevel, this.bassLevel, this.midLevel, this.trebleLevel);
+
+ // Continue the analysis loop
+ requestAnimationFrame(() => this.updateAudioData());
+ }
}
const app = new BitfielderApp();