multiple interaction points
This commit is contained in:
3
coolshaders.md
Normal file
3
coolshaders.md
Normal file
@ -0,0 +1,3 @@
|
||||
x<<127*y*t
|
||||
x<<20*t*80*y^8
|
||||
x**10*y^200*t+20
|
||||
40
index.html
40
index.html
@ -772,6 +772,7 @@
|
||||
<button id="fullscreen-btn">Fullscreen</button>
|
||||
<button id="hide-ui-btn">Hide UI</button>
|
||||
<button id="random-btn">Random</button>
|
||||
<button id="audio-btn">Enable Audio</button>
|
||||
<button id="share-btn">Share</button>
|
||||
<button id="export-png-btn">Export PNG</button>
|
||||
</div>
|
||||
@ -827,6 +828,7 @@
|
||||
<div class="mobile-menu-buttons">
|
||||
<button id="help-btn-mobile"><span class="icon"></span> Help</button>
|
||||
<button id="fullscreen-btn-mobile"><span class="icon"></span> Fullscreen</button>
|
||||
<button id="audio-btn-mobile"><span class="icon"></span> Enable Audio</button>
|
||||
<button id="share-btn-mobile"><span class="icon"></span> Share</button>
|
||||
<button id="export-png-btn-mobile"><span class="icon"></span> Export PNG</button>
|
||||
</div>
|
||||
@ -834,7 +836,7 @@
|
||||
</div>
|
||||
|
||||
<div id="editor-panel">
|
||||
<textarea id="editor" placeholder="Enter shader code... (x, y, t, i, mouseX, mouseY)" spellcheck="false">x^y</textarea>
|
||||
<textarea id="editor" placeholder="Enter shader code... (x, y, t, i, mouseX, mouseY, mousePressed, touchCount, accelX, audioLevel, bassLevel...)" spellcheck="false">x^y</textarea>
|
||||
<button id="eval-btn">Eval</button>
|
||||
</div>
|
||||
|
||||
@ -876,6 +878,33 @@
|
||||
<p><strong>t</strong> - Time (enables animation)</p>
|
||||
<p><strong>i</strong> - Pixel index</p>
|
||||
<p><strong>mouseX, mouseY</strong> - Mouse position (0.0 to 1.0)</p>
|
||||
<p><strong>mousePressed</strong> - Mouse button down (true/false)</p>
|
||||
<p><strong>mouseVX, mouseVY</strong> - Mouse velocity</p>
|
||||
<p><strong>mouseClickTime</strong> - Time since last click (ms)</p>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h4>Touch & Gestures</h4>
|
||||
<p><strong>touchCount</strong> - Number of active touches</p>
|
||||
<p><strong>touch0X, touch0Y</strong> - Primary touch position</p>
|
||||
<p><strong>touch1X, touch1Y</strong> - Secondary touch position</p>
|
||||
<p><strong>pinchScale</strong> - Pinch zoom scale factor</p>
|
||||
<p><strong>pinchRotation</strong> - Pinch rotation angle</p>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h4>Device Motion</h4>
|
||||
<p><strong>accelX, accelY, accelZ</strong> - Accelerometer data</p>
|
||||
<p><strong>gyroX, gyroY, gyroZ</strong> - Gyroscope rotation rates</p>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h4>Audio Reactive</h4>
|
||||
<p><strong>audioLevel</strong> - Overall audio volume (0.0-1.0)</p>
|
||||
<p><strong>bassLevel</strong> - Low frequencies (0.0-1.0)</p>
|
||||
<p><strong>midLevel</strong> - Mid frequencies (0.0-1.0)</p>
|
||||
<p><strong>trebleLevel</strong> - High frequencies (0.0-1.0)</p>
|
||||
<p>Click "Enable Audio" to activate microphone</p>
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
@ -906,6 +935,15 @@
|
||||
<p><strong>Export PNG</strong> - Save current frame as image</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-section" style="grid-column: 1 / -1; margin-top: 20px; text-align: center; padding-top: 20px; border-bottom: none;">
|
||||
<h4>About</h4>
|
||||
<p><strong>Bitfielder</strong> - Interactive bitfield shader editor</p>
|
||||
<p>Created by <strong>BuboBubo</strong> (Raphaël Forment)</p>
|
||||
<p>Website: <a href="https://raphaelforment.fr" target="_blank" style="color: #4A9EFF;">raphaelforment.fr</a></p>
|
||||
<p>Source: <a href="https://git.raphaelforment.fr" target="_blank" style="color: #4A9EFF;">git.raphaelforment.fr</a></p>
|
||||
<p>License: <strong>AGPL 3.0</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="performance-warning">
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)) {
|
||||
|
||||
252
src/main.ts
252
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<void> {
|
||||
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 = '<span class="icon"></span> Disable Audio';
|
||||
} catch (error) {
|
||||
console.error('Failed to enable audio:', error);
|
||||
audioBtn.textContent = 'Audio Failed';
|
||||
audioBtnMobile.innerHTML = '<span class="icon"></span> Audio Failed';
|
||||
}
|
||||
} else {
|
||||
this.disableAudio();
|
||||
audioBtn.textContent = 'Enable Audio';
|
||||
audioBtnMobile.innerHTML = '<span class="icon"></span> 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<void> {
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user