Compare commits
1 Commits
fix/912
...
nexusburn/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7924fa3b10 |
80
playground/README.md
Normal file
80
playground/README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Sovereign Sound Playground
|
||||
|
||||
Interactive audio-visual experience — no servers, no dependencies, pure browser.
|
||||
|
||||
## Apps
|
||||
|
||||
### Playground v3 (Full Instrument)
|
||||
`playground.html` — The complete experience:
|
||||
- Visual piano keyboard (2 octaves)
|
||||
- 6 visualization modes: Waveform, Particles, Bars, Spiral, Gravity Well, Strobe
|
||||
- 5 color palettes: Cosmic, Sunset, Ocean, Forest, Neon
|
||||
- Ambient beat with chord progressions
|
||||
- Mouse/touch playback on visualizer
|
||||
- Chord detection
|
||||
- Recording to WAV
|
||||
- Export as PNG
|
||||
|
||||
### Synesthesia
|
||||
`synesthesia.html` — Paint with sound:
|
||||
- Click and drag to create colors and shapes
|
||||
- Each position generates a unique tone
|
||||
- Particles respond to your movement
|
||||
- Touch supported
|
||||
|
||||
### Ambient
|
||||
`ambient.html` — Evolving soundscape:
|
||||
- Automatic chord progressions
|
||||
- Floating orbs respond to audio
|
||||
- Reverb-drenched textures
|
||||
- Click to enter, let it wash over you
|
||||
|
||||
### Interactive
|
||||
`interactive.html` — 26 key-shape mappings:
|
||||
- Press A-Z to play notes
|
||||
- Each key has a unique shape and color
|
||||
- Shapes animate and fade
|
||||
- Visual keyboard at bottom
|
||||
- Touch supported
|
||||
|
||||
### Visualizer
|
||||
`visualizer.html` — WAV frequency visualization:
|
||||
- Load any audio file
|
||||
- 4 modes: Spectrum, Waveform, Spectrogram, Circular
|
||||
- Drag and drop support
|
||||
- Real-time frequency analysis
|
||||
|
||||
## Features
|
||||
|
||||
- Zero dependencies — just open in a browser
|
||||
- Local-first — no network requests
|
||||
- Touch support on all apps
|
||||
- Keyboard support
|
||||
- Recording and export
|
||||
- Multiple visualization modes
|
||||
- Color palettes
|
||||
|
||||
## Usage
|
||||
|
||||
Open any HTML file in a browser. That's it.
|
||||
|
||||
```bash
|
||||
# Quick start
|
||||
open playground/playground.html
|
||||
|
||||
# Or serve locally
|
||||
python3 -m http.server 8080 --directory playground
|
||||
```
|
||||
|
||||
## Keyboard Shortcuts (Playground v3)
|
||||
|
||||
- A-; (lower row): Play piano notes
|
||||
- Mouse drag on visualizer: Create sound
|
||||
- Click piano keys: Play notes
|
||||
|
||||
## Technical
|
||||
|
||||
- Web Audio API for sound generation
|
||||
- Canvas 2D for visualization
|
||||
- MediaRecorder for recording
|
||||
- No build step, no framework
|
||||
243
playground/ambient.html
Normal file
243
playground/ambient.html
Normal file
@@ -0,0 +1,243 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Ambient — Evolving Soundscape</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #0a0a0f; overflow: hidden; height: 100vh; }
|
||||
canvas { display: block; width: 100%; height: 100%; }
|
||||
.overlay {
|
||||
position: fixed; inset: 0; display: flex; align-items: center;
|
||||
justify-content: center; background: rgba(0,0,0,0.7);
|
||||
transition: opacity 0.5s; z-index: 10;
|
||||
}
|
||||
.overlay.hidden { opacity: 0; pointer-events: none; }
|
||||
.start-btn {
|
||||
background: rgba(100,50,150,0.5); border: 2px solid rgba(150,100,200,0.7);
|
||||
color: #e0e0e0; padding: 20px 40px; font-size: 18px; border-radius: 30px;
|
||||
cursor: pointer; font-family: sans-serif; letter-spacing: 2px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.start-btn:hover { background: rgba(150,100,200,0.5); transform: scale(1.05); }
|
||||
.title {
|
||||
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
|
||||
color: rgba(255,255,255,0.1); font-size: 24px; font-family: sans-serif;
|
||||
letter-spacing: 4px; text-transform: uppercase; pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="title">Ambient</div>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div class="overlay" id="overlay">
|
||||
<button class="start-btn" id="start-btn">Enter Soundscape</button>
|
||||
</div>
|
||||
<script>
|
||||
class AmbientSoundscape {
|
||||
constructor() {
|
||||
this.canvas = document.getElementById('canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.audioCtx = null;
|
||||
this.masterGain = null;
|
||||
this.analyser = null;
|
||||
this.isPlaying = false;
|
||||
this.currentChord = 0;
|
||||
this.time = 0;
|
||||
this.orbs = [];
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
|
||||
// Create orbs
|
||||
for (let i = 0; i < 15; i++) {
|
||||
this.orbs.push({
|
||||
x: Math.random() * this.canvas.width,
|
||||
y: Math.random() * this.canvas.height,
|
||||
radius: Math.random() * 50 + 20,
|
||||
speed: Math.random() * 0.5 + 0.2,
|
||||
angle: Math.random() * Math.PI * 2,
|
||||
color: [
|
||||
[167, 139, 250], [129, 140, 248], [99, 102, 241],
|
||||
[139, 92, 246], [124, 58, 237]
|
||||
][i % 5]
|
||||
});
|
||||
}
|
||||
|
||||
this.bindEvents();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
}
|
||||
|
||||
async initAudio() {
|
||||
if (this.audioCtx) return;
|
||||
|
||||
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
this.masterGain = this.audioCtx.createGain();
|
||||
this.masterGain.gain.value = 0.2;
|
||||
|
||||
this.analyser = this.audioCtx.createAnalyser();
|
||||
this.analyser.fftSize = 256;
|
||||
this.analyser.smoothingTimeConstant = 0.95;
|
||||
|
||||
this.masterGain.connect(this.analyser);
|
||||
this.analyser.connect(this.audioCtx.destination);
|
||||
|
||||
// Create reverb
|
||||
const convolver = this.audioCtx.createConvolver();
|
||||
const reverbTime = 3;
|
||||
const sampleRate = this.audioCtx.sampleRate;
|
||||
const length = sampleRate * reverbTime;
|
||||
const impulse = this.audioCtx.createBuffer(2, length, sampleRate);
|
||||
|
||||
for (let channel = 0; channel < 2; channel++) {
|
||||
const channelData = impulse.getChannelData(channel);
|
||||
for (let i = 0; i < length; i++) {
|
||||
channelData[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 2);
|
||||
}
|
||||
}
|
||||
|
||||
convolver.buffer = impulse;
|
||||
convolver.connect(this.masterGain);
|
||||
this.reverb = convolver;
|
||||
}
|
||||
|
||||
playChord() {
|
||||
if (!this.audioCtx || !this.isPlaying) return;
|
||||
|
||||
const progressions = [
|
||||
[[261.63, 329.63, 392.00], [392.00, 493.88, 587.33],
|
||||
[440.00, 523.25, 659.25], [349.23, 440.00, 523.25]],
|
||||
[[220.00, 277.18, 329.63], [329.63, 415.30, 493.88],
|
||||
[369.99, 466.16, 554.37], [293.66, 369.99, 440.00]]
|
||||
];
|
||||
|
||||
const prog = progressions[this.currentChord % 2];
|
||||
const chord = prog[Math.floor(this.time / 4) % prog.length];
|
||||
|
||||
chord.forEach((freq, i) => {
|
||||
const osc = this.audioCtx.createOscillator();
|
||||
const gain = this.audioCtx.createGain();
|
||||
const filter = this.audioCtx.createBiquadFilter();
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.value = freq * (1 + (Math.random() - 0.5) * 0.01);
|
||||
|
||||
filter.type = 'lowpass';
|
||||
filter.frequency.value = 800 + Math.sin(this.time * 0.1) * 400;
|
||||
|
||||
gain.gain.setValueAtTime(0, this.audioCtx.currentTime);
|
||||
gain.gain.linearRampToValueAtTime(0.15, this.audioCtx.currentTime + 0.5);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, this.audioCtx.currentTime + 4);
|
||||
|
||||
osc.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(this.reverb);
|
||||
|
||||
osc.start();
|
||||
osc.stop(this.audioCtx.currentTime + 4);
|
||||
});
|
||||
|
||||
// High texture
|
||||
const highOsc = this.audioCtx.createOscillator();
|
||||
const highGain = this.audioCtx.createGain();
|
||||
|
||||
highOsc.type = 'sine';
|
||||
highOsc.frequency.value = chord[0] * 4 + Math.random() * 50;
|
||||
|
||||
highGain.gain.setValueAtTime(0, this.audioCtx.currentTime);
|
||||
highGain.gain.linearRampToValueAtTime(0.03, this.audioCtx.currentTime + 1);
|
||||
highGain.gain.exponentialRampToValueAtTime(0.001, this.audioCtx.currentTime + 5);
|
||||
|
||||
highOsc.connect(highGain);
|
||||
highGain.connect(this.reverb);
|
||||
highOsc.start();
|
||||
highOsc.stop(this.audioCtx.currentTime + 5);
|
||||
|
||||
this.time += 4;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.isPlaying = true;
|
||||
document.getElementById('overlay').classList.add('hidden');
|
||||
|
||||
const loop = () => {
|
||||
if (!this.isPlaying) return;
|
||||
this.playChord();
|
||||
setTimeout(loop, 4000);
|
||||
};
|
||||
|
||||
loop();
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isPlaying = false;
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.ctx.fillStyle = 'rgba(10, 10, 15, 0.03)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
const freqData = this.analyser ? new Uint8Array(this.analyser.frequencyBinCount) : [];
|
||||
if (this.analyser) this.analyser.getByteFrequencyData(freqData);
|
||||
|
||||
const energy = freqData.length > 0 ? freqData[10] / 255 : 0;
|
||||
|
||||
// Move and draw orbs
|
||||
this.orbs.forEach((orb, i) => {
|
||||
orb.angle += orb.speed * 0.01;
|
||||
orb.x += Math.cos(orb.angle) * (1 + energy * 3);
|
||||
orb.y += Math.sin(orb.angle * 0.7) * (1 + energy * 3);
|
||||
|
||||
// Wrap around
|
||||
if (orb.x < -orb.radius) orb.x = this.canvas.width + orb.radius;
|
||||
if (orb.x > this.canvas.width + orb.radius) orb.x = -orb.radius;
|
||||
if (orb.y < -orb.radius) orb.y = this.canvas.height + orb.radius;
|
||||
if (orb.y > this.canvas.height + orb.radius) orb.y = -orb.radius;
|
||||
|
||||
// Draw glow
|
||||
const gradient = this.ctx.createRadialGradient(
|
||||
orb.x, orb.y, 0,
|
||||
orb.x, orb.y, orb.radius * (1 + energy * 0.5)
|
||||
);
|
||||
gradient.addColorStop(0, `rgba(${orb.color.join(',')}, 0.3)`);
|
||||
gradient.addColorStop(1, 'rgba(0,0,0,0)');
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(orb.x, orb.y, orb.radius * (1 + energy * 0.5), 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = gradient;
|
||||
this.ctx.fill();
|
||||
|
||||
// Inner glow
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(orb.x, orb.y, orb.radius * 0.3, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = `rgba(${orb.color.join(',')}, 0.5)`;
|
||||
this.ctx.fill();
|
||||
});
|
||||
|
||||
requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
document.getElementById('start-btn').addEventListener('click', async () => {
|
||||
await this.initAudio();
|
||||
this.start();
|
||||
});
|
||||
|
||||
document.addEventListener('click', async () => {
|
||||
if (!this.audioCtx) {
|
||||
await this.initAudio();
|
||||
}
|
||||
}, { once: true });
|
||||
}
|
||||
}
|
||||
|
||||
new AmbientSoundscape();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
294
playground/interactive.html
Normal file
294
playground/interactive.html
Normal file
@@ -0,0 +1,294 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Interactive — 26 Key Shapes</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #0a0a0f; overflow: hidden; height: 100vh; }
|
||||
canvas { display: block; width: 100%; height: 100%; }
|
||||
.title {
|
||||
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
|
||||
color: rgba(255,255,255,0.1); font-size: 24px; font-family: sans-serif;
|
||||
letter-spacing: 4px; text-transform: uppercase; pointer-events: none;
|
||||
}
|
||||
.key-hint {
|
||||
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
|
||||
color: rgba(255,255,255,0.3); font-size: 14px; font-family: monospace;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="title">Interactive</div>
|
||||
<div class="key-hint">Press A-Z to play</div>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script>
|
||||
class Interactive {
|
||||
constructor() {
|
||||
this.canvas = document.getElementById('canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.audioCtx = null;
|
||||
this.analyser = null;
|
||||
this.shapes = [];
|
||||
this.activeKeys = new Set();
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
|
||||
// 26 key mappings with unique shapes and frequencies
|
||||
this.keyMap = {
|
||||
'a': { freq: 261.63, shape: 'circle', color: [255,100,100] },
|
||||
'b': { freq: 277.18, shape: 'square', color: [255,150,100] },
|
||||
'c': { freq: 293.66, shape: 'triangle', color: [255,200,100] },
|
||||
'd': { freq: 311.13, shape: 'diamond', color: [255,255,100] },
|
||||
'e': { freq: 329.63, shape: 'star', color: [200,255,100] },
|
||||
'f': { freq: 349.23, shape: 'hexagon', color: [150,255,100] },
|
||||
'g': { freq: 369.99, shape: 'cross', color: [100,255,100] },
|
||||
'h': { freq: 392.00, shape: 'circle', color: [100,255,150] },
|
||||
'i': { freq: 415.30, shape: 'square', color: [100,255,200] },
|
||||
'j': { freq: 440.00, shape: 'triangle', color: [100,255,255] },
|
||||
'k': { freq: 466.16, shape: 'diamond', color: [100,200,255] },
|
||||
'l': { freq: 493.88, shape: 'star', color: [100,150,255] },
|
||||
'm': { freq: 523.25, shape: 'hexagon', color: [100,100,255] },
|
||||
'n': { freq: 554.37, shape: 'cross', color: [150,100,255] },
|
||||
'o': { freq: 587.33, shape: 'circle', color: [200,100,255] },
|
||||
'p': { freq: 622.25, shape: 'square', color: [255,100,255] },
|
||||
'q': { freq: 659.25, shape: 'triangle', color: [255,100,200] },
|
||||
'r': { freq: 698.46, shape: 'diamond', color: [255,100,150] },
|
||||
's': { freq: 739.99, shape: 'star', color: [255,120,120] },
|
||||
't': { freq: 783.99, shape: 'hexagon', color: [255,170,120] },
|
||||
'u': { freq: 830.61, shape: 'cross', color: [255,220,120] },
|
||||
'v': { freq: 880.00, shape: 'circle', color: [220,255,120] },
|
||||
'w': { freq: 932.33, shape: 'square', color: [170,255,120] },
|
||||
'x': { freq: 987.77, shape: 'triangle', color: [120,255,120] },
|
||||
'y': { freq: 1046.50, shape: 'diamond', color: [120,255,170] },
|
||||
'z': { freq: 1108.73, shape: 'star', color: [120,255,220] }
|
||||
};
|
||||
|
||||
this.bindEvents();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
}
|
||||
|
||||
async initAudio() {
|
||||
if (this.audioCtx) return;
|
||||
|
||||
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
this.analyser = this.audioCtx.createAnalyser();
|
||||
this.analyser.fftSize = 256;
|
||||
this.analyser.connect(this.audioCtx.destination);
|
||||
}
|
||||
|
||||
playKey(key) {
|
||||
if (!this.audioCtx || !this.keyMap[key]) return;
|
||||
|
||||
const { freq, shape, color } = this.keyMap[key];
|
||||
|
||||
// Play sound
|
||||
const osc = this.audioCtx.createOscillator();
|
||||
const gain = this.audioCtx.createGain();
|
||||
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.value = freq;
|
||||
gain.gain.setValueAtTime(0.3, this.audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, this.audioCtx.currentTime + 1);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.analyser);
|
||||
osc.start();
|
||||
osc.stop(this.audioCtx.currentTime + 1);
|
||||
|
||||
// Create shape
|
||||
const x = Math.random() * (this.canvas.width - 200) + 100;
|
||||
const y = Math.random() * (this.canvas.height - 200) + 100;
|
||||
|
||||
this.shapes.push({
|
||||
key, x, y, shape, color,
|
||||
size: 50,
|
||||
maxSize: 150,
|
||||
life: 1,
|
||||
rotation: 0
|
||||
});
|
||||
}
|
||||
|
||||
drawShape(shape) {
|
||||
const { x, y, size, rotation, shape: shapeType, color, life } = shape;
|
||||
|
||||
this.ctx.save();
|
||||
this.ctx.translate(x, y);
|
||||
this.ctx.rotate(rotation);
|
||||
this.ctx.globalAlpha = life;
|
||||
|
||||
this.ctx.strokeStyle = `rgb(${color.join(',')})`;
|
||||
this.ctx.lineWidth = 3;
|
||||
this.ctx.fillStyle = `rgba(${color.join(',')}, 0.2)`;
|
||||
|
||||
this.ctx.beginPath();
|
||||
|
||||
switch (shapeType) {
|
||||
case 'circle':
|
||||
this.ctx.arc(0, 0, size, 0, Math.PI * 2);
|
||||
break;
|
||||
|
||||
case 'square':
|
||||
this.ctx.rect(-size, -size, size * 2, size * 2);
|
||||
break;
|
||||
|
||||
case 'triangle':
|
||||
this.ctx.moveTo(0, -size);
|
||||
this.ctx.lineTo(size * 0.866, size * 0.5);
|
||||
this.ctx.lineTo(-size * 0.866, size * 0.5);
|
||||
this.ctx.closePath();
|
||||
break;
|
||||
|
||||
case 'diamond':
|
||||
this.ctx.moveTo(0, -size);
|
||||
this.ctx.lineTo(size * 0.7, 0);
|
||||
this.ctx.lineTo(0, size);
|
||||
this.ctx.lineTo(-size * 0.7, 0);
|
||||
this.ctx.closePath();
|
||||
break;
|
||||
|
||||
case 'star':
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const angle = (i * 4 * Math.PI) / 5 - Math.PI / 2;
|
||||
const method = i === 0 ? 'moveTo' : 'lineTo';
|
||||
this.ctx[method](Math.cos(angle) * size, Math.sin(angle) * size);
|
||||
}
|
||||
this.ctx.closePath();
|
||||
break;
|
||||
|
||||
case 'hexagon':
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const angle = (i * Math.PI) / 3;
|
||||
const method = i === 0 ? 'moveTo' : 'lineTo';
|
||||
this.ctx[method](Math.cos(angle) * size, Math.sin(angle) * size);
|
||||
}
|
||||
this.ctx.closePath();
|
||||
break;
|
||||
|
||||
case 'cross':
|
||||
const w = size * 0.3;
|
||||
this.ctx.moveTo(-w, -size);
|
||||
this.ctx.lineTo(w, -size);
|
||||
this.ctx.lineTo(w, -w);
|
||||
this.ctx.lineTo(size, -w);
|
||||
this.ctx.lineTo(size, w);
|
||||
this.ctx.lineTo(w, w);
|
||||
this.ctx.lineTo(w, size);
|
||||
this.ctx.lineTo(-w, size);
|
||||
this.ctx.lineTo(-w, w);
|
||||
this.ctx.lineTo(-size, w);
|
||||
this.ctx.lineTo(-size, -w);
|
||||
this.ctx.lineTo(-w, -w);
|
||||
this.ctx.closePath();
|
||||
break;
|
||||
}
|
||||
|
||||
this.ctx.fill();
|
||||
this.ctx.stroke();
|
||||
|
||||
// Draw key label
|
||||
this.ctx.fillStyle = `rgba(${color.join(',')}, ${life})`;
|
||||
this.ctx.font = `${size * 0.4}px monospace`;
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.textBaseline = 'middle';
|
||||
this.ctx.fillText(shape.key.toUpperCase(), 0, 0);
|
||||
|
||||
this.ctx.restore();
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.ctx.fillStyle = 'rgba(10, 10, 15, 0.1)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Update and draw shapes
|
||||
this.shapes = this.shapes.filter(shape => {
|
||||
shape.size += (shape.maxSize - shape.size) * 0.05;
|
||||
shape.rotation += 0.02;
|
||||
shape.life -= 0.01;
|
||||
|
||||
if (shape.life <= 0) return false;
|
||||
|
||||
this.drawShape(shape);
|
||||
return true;
|
||||
});
|
||||
|
||||
// Draw active key indicators
|
||||
const keyWidth = this.canvas.width / 13;
|
||||
const keyHeight = 40;
|
||||
const startY = this.canvas.height - 60;
|
||||
|
||||
let col = 0;
|
||||
for (const key of 'abcdefghijklmnopqrstuvwxyz') {
|
||||
const x = col * keyWidth + keyWidth / 2;
|
||||
const isActive = this.activeKeys.has(key);
|
||||
|
||||
this.ctx.fillStyle = isActive
|
||||
? `rgba(${this.keyMap[key].color.join(',')}, 0.5)`
|
||||
: 'rgba(255,255,255,0.05)';
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.roundRect(x - 15, startY, 30, keyHeight, 5);
|
||||
this.ctx.fill();
|
||||
|
||||
this.ctx.fillStyle = isActive ? '#fff' : 'rgba(255,255,255,0.3)';
|
||||
this.ctx.font = '12px monospace';
|
||||
this.ctx.textAlign = 'center';
|
||||
this.ctx.fillText(key.toUpperCase(), x, startY + 25);
|
||||
|
||||
col++;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
document.addEventListener('keydown', async (e) => {
|
||||
if (e.repeat) return;
|
||||
const key = e.key.toLowerCase();
|
||||
if (this.keyMap[key] && !this.activeKeys.has(key)) {
|
||||
await this.initAudio();
|
||||
this.activeKeys.add(key);
|
||||
this.playKey(key);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('keyup', (e) => {
|
||||
const key = e.key.toLowerCase();
|
||||
this.activeKeys.delete(key);
|
||||
});
|
||||
|
||||
// Touch support
|
||||
this.canvas.addEventListener('touchstart', async (e) => {
|
||||
e.preventDefault();
|
||||
await this.initAudio();
|
||||
|
||||
// Map touch area to key
|
||||
for (const touch of e.changedTouches) {
|
||||
const keyIndex = Math.floor(touch.clientX / (this.canvas.width / 26));
|
||||
const key = 'abcdefghijklmnopqrstuvwxyz'[keyIndex];
|
||||
if (key) {
|
||||
this.activeKeys.add(key);
|
||||
this.playKey(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.canvas.addEventListener('touchend', (e) => {
|
||||
e.preventDefault();
|
||||
this.activeKeys.clear();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new Interactive();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1216
playground/playground.html
Normal file
1216
playground/playground.html
Normal file
File diff suppressed because it is too large
Load Diff
198
playground/synesthesia.html
Normal file
198
playground/synesthesia.html
Normal file
@@ -0,0 +1,198 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Synesthesia — Paint with Sound</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #0a0a0f; overflow: hidden; height: 100vh; cursor: crosshair; }
|
||||
canvas { display: block; width: 100%; height: 100%; }
|
||||
.title {
|
||||
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
|
||||
color: rgba(255,255,255,0.15); font-size: 24px; font-family: sans-serif;
|
||||
letter-spacing: 4px; text-transform: uppercase; pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="title">Synesthesia</div>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script>
|
||||
class Synesthesia {
|
||||
constructor() {
|
||||
this.canvas = document.getElementById('canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.audioCtx = null;
|
||||
this.analyser = null;
|
||||
this.particles = [];
|
||||
this.trails = [];
|
||||
this.mouse = { x: 0, y: 0, down: false };
|
||||
this.lastNote = null;
|
||||
|
||||
this.colors = [
|
||||
[255, 100, 150], [100, 200, 255], [150, 255, 150],
|
||||
[255, 200, 100], [200, 150, 255]
|
||||
];
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
this.bindEvents();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
}
|
||||
|
||||
async initAudio() {
|
||||
if (this.audioCtx) return;
|
||||
this.audioCtx = new AudioContext();
|
||||
this.analyser = this.audioCtx.createAnalyser();
|
||||
this.analyser.fftSize = 256;
|
||||
this.analyser.connect(this.audioCtx.destination);
|
||||
}
|
||||
|
||||
playTone(x, y) {
|
||||
if (!this.audioCtx) return;
|
||||
|
||||
const freq = 200 + (x / this.canvas.width) * 600;
|
||||
const noteId = Math.round(freq / 20);
|
||||
|
||||
if (noteId === this.lastNote) return;
|
||||
this.lastNote = noteId;
|
||||
|
||||
const osc = this.audioCtx.createOscillator();
|
||||
const gain = this.audioCtx.createGain();
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.value = freq;
|
||||
gain.gain.setValueAtTime(0.2, this.audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, this.audioCtx.currentTime + 0.5);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.analyser);
|
||||
osc.start();
|
||||
osc.stop(this.audioCtx.currentTime + 0.5);
|
||||
|
||||
// Spawn particles
|
||||
const color = this.colors[Math.floor(Math.random() * this.colors.length)];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
this.particles.push({
|
||||
x, y,
|
||||
vx: (Math.random() - 0.5) * 8,
|
||||
vy: (Math.random() - 0.5) * 8,
|
||||
life: 1,
|
||||
color,
|
||||
size: Math.random() * 10 + 5
|
||||
});
|
||||
}
|
||||
|
||||
// Add trail
|
||||
this.trails.push({ x, y, color, size: 30, alpha: 0.5 });
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.ctx.fillStyle = 'rgba(10, 10, 15, 0.05)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Draw trails
|
||||
this.trails = this.trails.filter(t => {
|
||||
t.alpha -= 0.005;
|
||||
if (t.alpha <= 0) return false;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(t.x, t.y, t.size, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = `rgba(${t.color.join(',')}, ${t.alpha})`;
|
||||
this.ctx.fill();
|
||||
return true;
|
||||
});
|
||||
|
||||
// Draw particles
|
||||
this.particles = this.particles.filter(p => {
|
||||
p.x += p.vx;
|
||||
p.y += p.vy;
|
||||
p.life -= 0.02;
|
||||
p.vy += 0.1;
|
||||
|
||||
if (p.life <= 0) return false;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(p.x, p.y, p.size * p.life, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = `rgba(${p.color.join(',')}, ${p.life * 0.8})`;
|
||||
this.ctx.fill();
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Draw mouse trail
|
||||
if (this.mouse.down) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(this.mouse.x, this.mouse.y, 20, 0, Math.PI * 2);
|
||||
const color = this.colors[Date.now() % 5];
|
||||
this.ctx.fillStyle = `rgba(${color.join(',')}, 0.3)`;
|
||||
this.ctx.fill();
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.canvas.addEventListener('mousedown', async (e) => {
|
||||
await this.initAudio();
|
||||
this.mouse.down = true;
|
||||
this.mouse.x = e.clientX;
|
||||
this.mouse.y = e.clientY;
|
||||
this.playTone(e.clientX, e.clientY);
|
||||
});
|
||||
|
||||
this.canvas.addEventListener('mousemove', (e) => {
|
||||
this.mouse.x = e.clientX;
|
||||
this.mouse.y = e.clientY;
|
||||
if (this.mouse.down) {
|
||||
this.playTone(e.clientX, e.clientY);
|
||||
}
|
||||
});
|
||||
|
||||
this.canvas.addEventListener('mouseup', () => {
|
||||
this.mouse.down = false;
|
||||
this.lastNote = null;
|
||||
});
|
||||
|
||||
this.canvas.addEventListener('mouseleave', () => {
|
||||
this.mouse.down = false;
|
||||
this.lastNote = null;
|
||||
});
|
||||
|
||||
this.canvas.addEventListener('touchstart', async (e) => {
|
||||
e.preventDefault();
|
||||
await this.initAudio();
|
||||
this.mouse.down = true;
|
||||
const touch = e.touches[0];
|
||||
this.mouse.x = touch.clientX;
|
||||
this.mouse.y = touch.clientY;
|
||||
this.playTone(touch.clientX, touch.clientY);
|
||||
});
|
||||
|
||||
this.canvas.addEventListener('touchmove', (e) => {
|
||||
e.preventDefault();
|
||||
const touch = e.touches[0];
|
||||
this.mouse.x = touch.clientX;
|
||||
this.mouse.y = touch.clientY;
|
||||
if (this.mouse.down) {
|
||||
this.playTone(touch.clientX, touch.clientY);
|
||||
}
|
||||
});
|
||||
|
||||
this.canvas.addEventListener('touchend', () => {
|
||||
this.mouse.down = false;
|
||||
this.lastNote = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new Synesthesia();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
371
playground/visualizer.html
Normal file
371
playground/visualizer.html
Normal file
@@ -0,0 +1,371 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Visualizer — WAV Frequency</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { background: #0a0a0f; overflow: hidden; height: 100vh; }
|
||||
canvas { display: block; width: 100%; height: 100%; }
|
||||
.title {
|
||||
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
|
||||
color: rgba(255,255,255,0.1); font-size: 24px; font-family: sans-serif;
|
||||
letter-spacing: 4px; text-transform: uppercase; pointer-events: none;
|
||||
}
|
||||
.controls {
|
||||
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
|
||||
display: flex; gap: 10px; z-index: 10;
|
||||
}
|
||||
button, select {
|
||||
background: rgba(30,30,40,0.9); border: 1px solid rgba(100,100,120,0.3);
|
||||
color: #e0e0e0; padding: 10px 20px; border-radius: 8px; font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover, select:hover { background: rgba(50,50,70,0.9); }
|
||||
.drop-zone {
|
||||
position: fixed; inset: 0; display: flex; align-items: center;
|
||||
justify-content: center; pointer-events: none; z-index: 5;
|
||||
}
|
||||
.drop-zone.active {
|
||||
background: rgba(100,50,150,0.2);
|
||||
border: 3px dashed rgba(150,100,200,0.5);
|
||||
}
|
||||
.drop-text {
|
||||
color: rgba(255,255,255,0.3); font-size: 24px; font-family: sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="title">Visualizer</div>
|
||||
<div class="drop-zone" id="drop-zone">
|
||||
<span class="drop-text">Drop WAV file here or use controls</span>
|
||||
</div>
|
||||
<canvas id="canvas"></canvas>
|
||||
<div class="controls">
|
||||
<input type="file" id="file-input" accept="audio/*" style="display: none;">
|
||||
<button id="load-btn">Load Audio</button>
|
||||
<button id="play-btn" disabled>Play</button>
|
||||
<button id="stop-btn" disabled>Stop</button>
|
||||
<select id="viz-mode">
|
||||
<option value="spectrum">Spectrum</option>
|
||||
<option value="waveform">Waveform</option>
|
||||
<option value="spectrogram">Spectrogram</option>
|
||||
<option value="circular">Circular</option>
|
||||
</select>
|
||||
</div>
|
||||
<script>
|
||||
class AudioVisualizer {
|
||||
constructor() {
|
||||
this.canvas = document.getElementById('canvas');
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.audioCtx = null;
|
||||
this.analyser = null;
|
||||
this.source = null;
|
||||
this.audioBuffer = null;
|
||||
this.isPlaying = false;
|
||||
this.mode = 'spectrum';
|
||||
this.spectrogramData = [];
|
||||
this.startOffset = 0;
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', () => this.resize());
|
||||
this.bindEvents();
|
||||
this.animate();
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
}
|
||||
|
||||
async initAudio() {
|
||||
if (this.audioCtx) return;
|
||||
|
||||
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
this.analyser = this.audioCtx.createAnalyser();
|
||||
this.analyser.fftSize = 2048;
|
||||
this.analyser.smoothingTimeConstant = 0.8;
|
||||
this.analyser.connect(this.audioCtx.destination);
|
||||
}
|
||||
|
||||
async loadAudio(file) {
|
||||
await this.initAudio();
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
this.audioBuffer = await this.audioCtx.decodeAudioData(arrayBuffer);
|
||||
|
||||
document.getElementById('play-btn').disabled = false;
|
||||
document.getElementById('stop-btn').disabled = false;
|
||||
document.querySelector('.drop-text').textContent = file.name;
|
||||
}
|
||||
|
||||
play() {
|
||||
if (!this.audioBuffer || this.isPlaying) return;
|
||||
|
||||
this.source = this.audioCtx.createBufferSource();
|
||||
this.source.buffer = this.audioBuffer;
|
||||
this.source.connect(this.analyser);
|
||||
this.source.start(0, this.startOffset);
|
||||
this.isPlaying = true;
|
||||
|
||||
this.source.onended = () => {
|
||||
this.isPlaying = false;
|
||||
this.startOffset = 0;
|
||||
document.getElementById('play-btn').textContent = 'Play';
|
||||
};
|
||||
|
||||
document.getElementById('play-btn').textContent = 'Pause';
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.source && this.isPlaying) {
|
||||
this.source.stop();
|
||||
this.isPlaying = false;
|
||||
this.startOffset = 0;
|
||||
document.getElementById('play-btn').textContent = 'Play';
|
||||
}
|
||||
}
|
||||
|
||||
animate() {
|
||||
this.ctx.fillStyle = 'rgba(10, 10, 15, 0.15)';
|
||||
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
if (!this.analyser) {
|
||||
requestAnimationFrame(() => this.animate());
|
||||
return;
|
||||
}
|
||||
|
||||
const freqData = new Uint8Array(this.analyser.frequencyBinCount);
|
||||
const timeData = new Uint8Array(this.analyser.frequencyBinCount);
|
||||
this.analyser.getByteFrequencyData(freqData);
|
||||
this.analyser.getByteTimeDomainData(timeData);
|
||||
|
||||
switch (this.mode) {
|
||||
case 'spectrum': this.drawSpectrum(freqData); break;
|
||||
case 'waveform': this.drawWaveform(timeData); break;
|
||||
case 'spectrogram': this.drawSpectrogram(freqData); break;
|
||||
case 'circular': this.drawCircular(freqData, timeData); break;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => this.animate());
|
||||
}
|
||||
|
||||
drawSpectrum(data) {
|
||||
const w = this.canvas.width;
|
||||
const h = this.canvas.height;
|
||||
const barCount = 128;
|
||||
const barWidth = w / barCount;
|
||||
const step = Math.floor(data.length / barCount);
|
||||
|
||||
for (let i = 0; i < barCount; i++) {
|
||||
const value = data[i * step] / 255;
|
||||
const barHeight = value * h * 0.8;
|
||||
|
||||
const hue = (i / barCount) * 300;
|
||||
this.ctx.fillStyle = `hsla(${hue}, 80%, 60%, 0.8)`;
|
||||
|
||||
this.ctx.fillRect(
|
||||
i * barWidth,
|
||||
h - barHeight,
|
||||
barWidth - 1,
|
||||
barHeight
|
||||
);
|
||||
|
||||
// Mirror
|
||||
this.ctx.fillStyle = `hsla(${hue}, 80%, 60%, 0.2)`;
|
||||
this.ctx.fillRect(
|
||||
i * barWidth,
|
||||
0,
|
||||
barWidth - 1,
|
||||
barHeight * 0.3
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
drawWaveform(data) {
|
||||
const w = this.canvas.width;
|
||||
const h = this.canvas.height;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = '#a78bfa';
|
||||
this.ctx.lineWidth = 2;
|
||||
|
||||
const sliceWidth = w / data.length;
|
||||
let x = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const v = data[i] / 128.0;
|
||||
const y = (v * h) / 2;
|
||||
|
||||
if (i === 0) {
|
||||
this.ctx.moveTo(x, y);
|
||||
} else {
|
||||
this.ctx.lineTo(x, y);
|
||||
}
|
||||
x += sliceWidth;
|
||||
}
|
||||
|
||||
this.ctx.stroke();
|
||||
|
||||
// Glow
|
||||
this.ctx.strokeStyle = 'rgba(167,139,250,0.3)';
|
||||
this.ctx.lineWidth = 6;
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
drawSpectrogram(data) {
|
||||
const w = this.canvas.width;
|
||||
const h = this.canvas.height;
|
||||
|
||||
// Add new line
|
||||
this.spectrogramData.push([...data]);
|
||||
|
||||
// Keep last N lines
|
||||
const maxLines = Math.floor(w / 2);
|
||||
if (this.spectrogramData.length > maxLines) {
|
||||
this.spectrogramData.shift();
|
||||
}
|
||||
|
||||
// Draw
|
||||
const line_width = w / maxLines;
|
||||
|
||||
this.spectrogramData.forEach((line, x) => {
|
||||
const step = Math.floor(line.length / h);
|
||||
for (let y = 0; y < h; y++) {
|
||||
const value = line[y * step] || 0;
|
||||
const hue = 270 - (value / 255) * 200;
|
||||
const lightness = 20 + (value / 255) * 50;
|
||||
|
||||
this.ctx.fillStyle = `hsl(${hue}, 80%, ${lightness}%)`;
|
||||
this.ctx.fillRect(x * line_width, h - y, line_width, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
drawCircular(freqData, timeData) {
|
||||
const w = this.canvas.width;
|
||||
const h = this.canvas.height;
|
||||
const cx = w / 2;
|
||||
const cy = h / 2;
|
||||
const maxRadius = Math.min(w, h) * 0.35;
|
||||
|
||||
// Frequency ring
|
||||
const freqStep = Math.floor(freqData.length / 180);
|
||||
for (let i = 0; i < 180; i++) {
|
||||
const value = freqData[i * freqStep] / 255;
|
||||
const angle = (i / 180) * Math.PI * 2;
|
||||
const radius = maxRadius * 0.5 + value * maxRadius * 0.5;
|
||||
|
||||
const x = cx + Math.cos(angle) * radius;
|
||||
const y = cy + Math.sin(angle) * radius;
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(x, y, 3, 0, Math.PI * 2);
|
||||
|
||||
const hue = (i / 180) * 360;
|
||||
this.ctx.fillStyle = `hsla(${hue}, 80%, 60%, 0.8)`;
|
||||
this.ctx.fill();
|
||||
}
|
||||
|
||||
// Waveform circle
|
||||
this.ctx.beginPath();
|
||||
this.ctx.strokeStyle = 'rgba(255,255,255,0.3)';
|
||||
this.ctx.lineWidth = 1;
|
||||
|
||||
for (let i = 0; i < timeData.length; i++) {
|
||||
const v = timeData[i] / 128.0;
|
||||
const angle = (i / timeData.length) * Math.PI * 2;
|
||||
const radius = maxRadius * 0.3 * v;
|
||||
|
||||
const x = cx + Math.cos(angle) * radius;
|
||||
const y = cy + Math.sin(angle) * radius;
|
||||
|
||||
if (i === 0) {
|
||||
this.ctx.moveTo(x, y);
|
||||
} else {
|
||||
this.ctx.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
this.ctx.closePath();
|
||||
this.ctx.stroke();
|
||||
|
||||
// Center glow
|
||||
const gradient = this.ctx.createRadialGradient(cx, cy, 0, cx, cy, maxRadius * 0.3);
|
||||
gradient.addColorStop(0, 'rgba(167,139,250,0.3)');
|
||||
gradient.addColorStop(1, 'rgba(167,139,250,0)');
|
||||
|
||||
this.ctx.beginPath();
|
||||
this.ctx.arc(cx, cy, maxRadius * 0.3, 0, Math.PI * 2);
|
||||
this.ctx.fillStyle = gradient;
|
||||
this.ctx.fill();
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
document.getElementById('load-btn').addEventListener('click', () => {
|
||||
document.getElementById('file-input').click();
|
||||
});
|
||||
|
||||
document.getElementById('file-input').addEventListener('change', async (e) => {
|
||||
if (e.target.files[0]) {
|
||||
await this.loadAudio(e.target.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('play-btn').addEventListener('click', () => {
|
||||
if (this.isPlaying) {
|
||||
this.stop();
|
||||
} else {
|
||||
this.play();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('stop-btn').addEventListener('click', () => {
|
||||
this.stop();
|
||||
});
|
||||
|
||||
document.getElementById('viz-mode').addEventListener('change', (e) => {
|
||||
this.mode = e.target.value;
|
||||
this.spectrogramData = [];
|
||||
});
|
||||
|
||||
// Drag and drop
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
|
||||
document.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('active');
|
||||
});
|
||||
|
||||
document.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('active');
|
||||
});
|
||||
|
||||
document.addEventListener('drop', async (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('active');
|
||||
|
||||
const file = e.dataTransfer.files[0];
|
||||
if (file && file.type.startsWith('audio/')) {
|
||||
await this.loadAudio(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Touch support
|
||||
this.canvas.addEventListener('click', async () => {
|
||||
if (!this.audioCtx) {
|
||||
await this.initAudio();
|
||||
}
|
||||
|
||||
if (this.audioCtx.state === 'suspended') {
|
||||
this.audioCtx.resume();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new AudioVisualizer();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user