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="fullscreen-btn">Fullscreen</button>
|
||||||
<button id="hide-ui-btn">Hide UI</button>
|
<button id="hide-ui-btn">Hide UI</button>
|
||||||
<button id="random-btn">Random</button>
|
<button id="random-btn">Random</button>
|
||||||
|
<button id="audio-btn">Enable Audio</button>
|
||||||
<button id="share-btn">Share</button>
|
<button id="share-btn">Share</button>
|
||||||
<button id="export-png-btn">Export PNG</button>
|
<button id="export-png-btn">Export PNG</button>
|
||||||
</div>
|
</div>
|
||||||
@ -827,6 +828,7 @@
|
|||||||
<div class="mobile-menu-buttons">
|
<div class="mobile-menu-buttons">
|
||||||
<button id="help-btn-mobile"><span class="icon"></span> Help</button>
|
<button id="help-btn-mobile"><span class="icon"></span> Help</button>
|
||||||
<button id="fullscreen-btn-mobile"><span class="icon"></span> Fullscreen</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="share-btn-mobile"><span class="icon"></span> Share</button>
|
||||||
<button id="export-png-btn-mobile"><span class="icon"></span> Export PNG</button>
|
<button id="export-png-btn-mobile"><span class="icon"></span> Export PNG</button>
|
||||||
</div>
|
</div>
|
||||||
@ -834,7 +836,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="editor-panel">
|
<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>
|
<button id="eval-btn">Eval</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -876,6 +878,33 @@
|
|||||||
<p><strong>t</strong> - Time (enables animation)</p>
|
<p><strong>t</strong> - Time (enables animation)</p>
|
||||||
<p><strong>i</strong> - Pixel index</p>
|
<p><strong>i</strong> - Pixel index</p>
|
||||||
<p><strong>mouseX, mouseY</strong> - Mouse position (0.0 to 1.0)</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>
|
||||||
|
|
||||||
<div class="help-section">
|
<div class="help-section">
|
||||||
@ -906,6 +935,15 @@
|
|||||||
<p><strong>Export PNG</strong> - Save current frame as image</p>
|
<p><strong>Export PNG</strong> - Save current frame as image</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
|
||||||
<div id="performance-warning">
|
<div id="performance-warning">
|
||||||
|
|||||||
@ -8,6 +8,27 @@ interface WorkerMessage {
|
|||||||
renderMode?: string;
|
renderMode?: string;
|
||||||
mouseX?: number;
|
mouseX?: number;
|
||||||
mouseY?: 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 {
|
interface WorkerResponse {
|
||||||
@ -31,6 +52,27 @@ export class FakeShader {
|
|||||||
private renderMode: string = 'classic';
|
private renderMode: string = 'classic';
|
||||||
private mouseX: number = 0;
|
private mouseX: number = 0;
|
||||||
private mouseY: 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
|
// Frame rate limiting
|
||||||
private targetFPS: number = 30;
|
private targetFPS: number = 30;
|
||||||
@ -120,7 +162,28 @@ export class FakeShader {
|
|||||||
time: currentTime,
|
time: currentTime,
|
||||||
renderMode: this.renderMode,
|
renderMode: this.renderMode,
|
||||||
mouseX: this.mouseX,
|
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);
|
} as WorkerMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,9 +255,39 @@ export class FakeShader {
|
|||||||
this.renderMode = mode;
|
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.mouseX = x;
|
||||||
this.mouseY = y;
|
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 {
|
destroy(): void {
|
||||||
|
|||||||
@ -9,6 +9,27 @@ interface WorkerMessage {
|
|||||||
renderMode?: string;
|
renderMode?: string;
|
||||||
mouseX?: number;
|
mouseX?: number;
|
||||||
mouseY?: 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 {
|
interface WorkerResponse {
|
||||||
@ -36,7 +57,7 @@ class ShaderWorker {
|
|||||||
this.compileShader(message.id, message.code!);
|
this.compileShader(message.id, message.code!);
|
||||||
break;
|
break;
|
||||||
case 'render':
|
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;
|
break;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -52,7 +73,7 @@ class ShaderWorker {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const safeCode = this.sanitizeCode(code);
|
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
|
// Timeout protection
|
||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
let iterations = 0;
|
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) {
|
if (!this.compiledFunction) {
|
||||||
this.postError(id, 'No compiled shader');
|
this.postError(id, 'No compiled shader');
|
||||||
return;
|
return;
|
||||||
@ -111,7 +132,20 @@ class ShaderWorker {
|
|||||||
const pixelIndex = y * width + x;
|
const pixelIndex = y * width + x;
|
||||||
|
|
||||||
try {
|
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 safeValue = isFinite(value) ? value : 0;
|
||||||
const [r, g, b] = this.calculateColor(safeValue, renderMode);
|
const [r, g, b] = this.calculateColor(safeValue, renderMode);
|
||||||
|
|
||||||
@ -222,7 +256,8 @@ class ShaderWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sanitizeCode(code: string): string {
|
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]+$/;
|
const allowedPattern = /^[0-9a-zA-Z\s\+\-\*\/\%\^\&\|\(\)\<\>\~\?:,\.xyti]+$/;
|
||||||
|
|
||||||
if (!allowedPattern.test(code)) {
|
if (!allowedPattern.test(code)) {
|
||||||
|
|||||||
252
src/main.ts
252
src/main.ts
@ -11,6 +11,35 @@ class BitfielderApp {
|
|||||||
private performanceWarning: HTMLElement;
|
private performanceWarning: HTMLElement;
|
||||||
private mouseX: number = 0;
|
private mouseX: number = 0;
|
||||||
private mouseY: 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() {
|
constructor() {
|
||||||
this.canvas = document.getElementById('canvas') as HTMLCanvasElement;
|
this.canvas = document.getElementById('canvas') as HTMLCanvasElement;
|
||||||
@ -55,6 +84,7 @@ class BitfielderApp {
|
|||||||
const hideUiBtn = document.getElementById('hide-ui-btn')!;
|
const hideUiBtn = document.getElementById('hide-ui-btn')!;
|
||||||
const showUiBtn = document.getElementById('show-ui-btn')!;
|
const showUiBtn = document.getElementById('show-ui-btn')!;
|
||||||
const randomBtn = document.getElementById('random-btn')!;
|
const randomBtn = document.getElementById('random-btn')!;
|
||||||
|
const audioBtn = document.getElementById('audio-btn')!;
|
||||||
const shareBtn = document.getElementById('share-btn')!;
|
const shareBtn = document.getElementById('share-btn')!;
|
||||||
const exportPngBtn = document.getElementById('export-png-btn')!;
|
const exportPngBtn = document.getElementById('export-png-btn')!;
|
||||||
const resolutionSelect = document.getElementById('resolution-select') as HTMLSelectElement;
|
const resolutionSelect = document.getElementById('resolution-select') as HTMLSelectElement;
|
||||||
@ -81,6 +111,7 @@ class BitfielderApp {
|
|||||||
// Mobile menu buttons
|
// Mobile menu buttons
|
||||||
const helpBtnMobile = document.getElementById('help-btn-mobile')!;
|
const helpBtnMobile = document.getElementById('help-btn-mobile')!;
|
||||||
const fullscreenBtnMobile = document.getElementById('fullscreen-btn-mobile')!;
|
const fullscreenBtnMobile = document.getElementById('fullscreen-btn-mobile')!;
|
||||||
|
const audioBtnMobile = document.getElementById('audio-btn-mobile')!;
|
||||||
const shareBtnMobile = document.getElementById('share-btn-mobile')!;
|
const shareBtnMobile = document.getElementById('share-btn-mobile')!;
|
||||||
const exportPngBtnMobile = document.getElementById('export-png-btn-mobile')!;
|
const exportPngBtnMobile = document.getElementById('export-png-btn-mobile')!;
|
||||||
|
|
||||||
@ -94,6 +125,7 @@ class BitfielderApp {
|
|||||||
hideUiBtn.addEventListener('click', () => this.toggleUI());
|
hideUiBtn.addEventListener('click', () => this.toggleUI());
|
||||||
showUiBtn.addEventListener('click', () => this.showUI());
|
showUiBtn.addEventListener('click', () => this.showUI());
|
||||||
randomBtn.addEventListener('click', () => this.generateRandom());
|
randomBtn.addEventListener('click', () => this.generateRandom());
|
||||||
|
audioBtn.addEventListener('click', () => this.toggleAudio());
|
||||||
shareBtn.addEventListener('click', () => this.shareURL());
|
shareBtn.addEventListener('click', () => this.shareURL());
|
||||||
exportPngBtn.addEventListener('click', () => this.exportPNG());
|
exportPngBtn.addEventListener('click', () => this.exportPNG());
|
||||||
resolutionSelect.addEventListener('change', () => this.updateResolution());
|
resolutionSelect.addEventListener('change', () => this.updateResolution());
|
||||||
@ -146,6 +178,10 @@ class BitfielderApp {
|
|||||||
this.closeMobileMenu();
|
this.closeMobileMenu();
|
||||||
this.toggleFullscreen();
|
this.toggleFullscreen();
|
||||||
});
|
});
|
||||||
|
audioBtnMobile.addEventListener('click', () => {
|
||||||
|
this.closeMobileMenu();
|
||||||
|
this.toggleAudio();
|
||||||
|
});
|
||||||
shareBtnMobile.addEventListener('click', () => {
|
shareBtnMobile.addEventListener('click', () => {
|
||||||
this.closeMobileMenu();
|
this.closeMobileMenu();
|
||||||
this.shareURL();
|
this.shareURL();
|
||||||
@ -208,10 +244,69 @@ class BitfielderApp {
|
|||||||
|
|
||||||
// Mouse tracking
|
// Mouse tracking
|
||||||
window.addEventListener('mousemove', (e) => {
|
window.addEventListener('mousemove', (e) => {
|
||||||
|
this.lastMouseX = this.mouseX;
|
||||||
|
this.lastMouseY = this.mouseY;
|
||||||
this.mouseX = e.clientX / window.innerWidth;
|
this.mouseX = e.clientX / window.innerWidth;
|
||||||
this.mouseY = 1.0 - (e.clientY / window.innerHeight); // Invert Y to match shader coordinates
|
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 {
|
private render(): void {
|
||||||
@ -600,6 +695,161 @@ class BitfielderApp {
|
|||||||
this.shader.setCode(randomCode);
|
this.shader.setCode(randomCode);
|
||||||
this.render();
|
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();
|
const app = new BitfielderApp();
|
||||||
|
|||||||
Reference in New Issue
Block a user