From d5c5b32bd74dac05900e692c3b83da1bb8c5341f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Forment?= Date: Sat, 5 Jul 2025 19:15:13 +0200 Subject: [PATCH] multiple interaction points --- coolshaders.md | 3 + index.html | 40 ++++++- src/FakeShader.ts | 97 ++++++++++++++++- src/ShaderWorker.ts | 45 +++++++- src/main.ts | 252 +++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 428 insertions(+), 9 deletions(-) create mode 100644 coolshaders.md 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

+ +
+

About

+

Bitfielder - Interactive bitfield shader editor

+

Created by BuboBubo (Raphaƫl Forment)

+

Website: raphaelforment.fr

+

Source: git.raphaelforment.fr

+

License: AGPL 3.0

+
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();