Loading Symphony...

Particle Symphony

Particle Type

Musical Scale

Controls

3
1.0
50

Physics Engine

Forces

0.1
0
0.98

Effects

🎨 Click and drag to create particles
🎵 Each particle generates a musical note
🎹 Different colors create unique sounds
playRecording() { if (this.recordedSequence.length === 0) return; const startTime = this.recordedSequence[0].time; this.recordedSequence.forEach(event => { const delay = event.time - startTime; setTimeout(() => { this.createParticle(event.x, event.y); }, delay); }); } clearAll() { this.particles = []; this.recordedSequence = []; } saveComposition() { // If we have video chunks, save as video if (this.recordedChunks.length > 0) { this.saveAsVideo(); } else { // Otherwise, save as JSON this.saveAsJSON(); } } async saveAsVideo() { try { // Create a blob from recorded chunks const blob = new Blob(this.recordedChunks, { type: 'video/webm' }); // Convert to MP4 if possible (using browser's built-in capabilities) let finalBlob = blob; let filename = `particle-symphony-${Date.now()}.webm`; // For true MP4 conversion, we would need a library like ffmpeg.js // For now, we'll save as WebM which most modern players can handle // If you need true MP4, you can use a service or library to convert // Create download link const url = URL.createObjectURL(finalBlob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); // Clear recorded chunks this.recordedChunks = []; // Show success message this.showNotification('Video exported successfully! 🎬'); } catch (error) { console.error('Failed to save video:', error); this.showNotification('Failed to export video. Saving as JSON instead...', 'error'); this.saveAsJSON(); } } saveAsJSON() { const data = { version: '1.0', timestamp: new Date().toISOString(), sequence: this.recordedSequence, settings: { particleType: this.particleType, scale: this.scale, particleSize: this.particleSize, particleSpeed: this.particleSpeed, gravity: this.gravity, wind: this.wind, friction: this.friction, effect: this.effect } }; const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `particle-symphony-${Date.now()}.json`; a.click(); URL.revokeObjectURL(url); this.showNotification('Composition exported as JSON! 📄'); } showNotification(message, type = 'success') { // Create notification element const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 80px; left: 50%; transform: translateX(-50%); padding: 16px 24px; background: ${type === 'success' ? 'var(--accent-green)' : 'var(--accent-red)'}; color: white; border-radius: 12px; font-weight: 500; z-index: 10000; animation: slideDown 0.3s ease-out; `; notification.textContent = message; document.body.appendChild(notification); // Remove after 3 seconds setTimeout(() => { notification.style.animation = 'slideUp 0.3s ease-out'; setTimeout(() => notification.remove(), 300); }, 3000); } startAnimation() { const animate = () => { this.updateParticles(); this.drawParticles(); this.drawWaveform(); requestAnimationFrame(animate); }; animate(); // Hide loading screen after a short delay setTimeout(() => { const loadingScreen = document.getElementById('loadingScreen'); if (loadingScreen) { loadingScreen.classList.add('hidden'); } }, 1000); } playNote(frequency, color) { if (!this.audioContext) return; try { const oscillator = this.audioContext.createOscillator(); const gainNode = this.audioContext.createGain(); // Create a more complex sound with harmonics const harmonic1 = this.audioContext.createOscillator(); const harmonic2 = this.audioContext.createOscillator(); const harmonicGain1 = this.audioContext.createGain(); const harmonicGain2 = this.audioContext.createGain(); // Set frequencies oscillator.frequency.setValueAtTime(frequency, this.audioContext.currentTime); harmonic1.frequency.setValueAtTime(frequency * 2, this.audioContext.currentTime); harmonic2.frequency.setValueAtTime(frequency * 3, this.audioContext.currentTime); // Set wave types oscillator.type = this.getWaveType(color); harmonic1.type = 'sine'; harmonic2.type = 'sine'; // Set harmonic gains (lower than fundamental) harmonicGain1.gain.setValueAtTime(0.3, this.audioContext.currentTime); harmonicGain2.gain.setValueAtTime(0.15, this.audioContext.currentTime); // Connect harmonics harmonic1.connect(harmonicGain1); harmonic2.connect(harmonicGain2); harmonicGain1.connect(gainNode); harmonicGain2.connect(gainNode); // Connect main oscillator oscillator.connect(gainNode); // Connect to master gain (which is connected to both speakers and recording) gainNode.connect(this.masterGain); // Get volume from slider const volume = (document.getElementById('volume').value / 100) * 0.2; // ADSR envelope const now = this.audioContext.currentTime; gainNode.gain.setValueAtTime(0, now); gainNode.gain.linearRampToValueAtTime(volume, now + 0.02); // Attack gainNode.gain.exponentialRampToValueAtTime(volume * 0.7, now + 0.1); // Decay gainNode.gain.setValueAtTime(volume * 0.7, now + 0.3); // Sustain gainNode.gain.exponentialRampToValueAtTime(0.01, now + 0.5); // Release // Start and stop all oscillators oscillator.start(now); harmonic1.start(now); harmonic2.start(now); oscillator.stop(now + 0.5); harmonic1.stop(now + 0.5); harmonic2.stop(now + 0.5); } catch (e) { console.log('Audio playback failed:', e); } } } // Initialize the application window.addEventListener('DOMContentLoaded', () => { const symphony = new ParticleSymphony(); // Add volume control document.getElementById('volume').addEventListener('input', (e) => { const value = e.target.value; document.getElementById('volumeValue').textContent = value; if (symphony.masterGain) { symphony.masterGain.gain.setValueAtTime(value / 100, symphony.audioContext.currentTime); } }); // Cursor trail enhancement let mouseDown = false; document.addEventListener('mousedown', () => { mouseDown = true; document.getElementById('cursorTrail').classList.add('active'); }); document.addEventListener('mouseup', () => { mouseDown = false; document.getElementById('cursorTrail').classList.remove('active'); }); // Fix for mobile cursor document.addEventListener('touchstart', (e) => { const touch = e.touches[0]; const trail = document.getElementById('cursorTrail'); trail.style.left = touch.clientX - 12 + 'px'; trail.style.top = touch.clientY - 12 + 'px'; trail.classList.add('active'); }); document.addEventListener('touchmove', (e) => { const touch = e.touches[0]; const trail = document.getElementById('cursorTrail'); trail.style.left = touch.clientX - 12 + 'px'; trail.style.top = touch.clientY - 12 + 'px'; }); document.addEventListener('touchend', () => { document.getElementById('cursorTrail').classList.remove('active'); }); });