Download script.js from scottrx11/deepsite-project-lr0up: direct link, hf CLI and curl.
- Browser
- Download file 23 kB
-
https://huggingface.co/spaces/scottrx11/deepsite-project-lr0up/resolve/main/script.js
- Command line
-
hf download hf://spaces/scottrx11/deepsite-project-lr0up/script.js
-
curl -L -o script.js https://huggingface.co/spaces/scottrx11/deepsite-project-lr0up/resolve/main/script.js
23 kB
| // Audio Context and State | |
| let audioContext = null; | |
| let isPlaying = false; | |
| let currentMode = 'spatial'; // 'spatial' or 'monaural' | |
| let rampMode = 'none'; // 'none', 'up', 'down' | |
| let rampDuration = 60; | |
| let activeNoiseType = null; | |
| // Nodes | |
| let binauralNodes = { | |
| leftOsc: null, | |
| rightOsc: null, | |
| leftGain: null, | |
| rightGain: null, | |
| merger: null, | |
| masterGain: null, | |
| modulator: null // For monaural | |
| }; | |
| let noiseNodes = { | |
| leftBuffer: null, | |
| rightBuffer: null, | |
| leftSource: null, | |
| rightSource: null, | |
| leftGain: null, | |
| rightGain: null, | |
| leftPanner: null, | |
| rightPanner: null, | |
| masterGain: null, | |
| filter: null | |
| }; | |
| let visualizer = { | |
| analyser: null, | |
| canvas: null, | |
| ctx: null, | |
| animationId: null | |
| }; | |
| // Initialize Audio Context | |
| function initAudioContext() { | |
| if (!audioContext) { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| setupVisualizer(); | |
| } | |
| if (audioContext.state === 'suspended') { | |
| audioContext.resume(); | |
| } | |
| } | |
| // Setup Visualizer | |
| function setupVisualizer() { | |
| const canvas = document.getElementById('audioVisualizer'); | |
| const ctx = canvas.getContext('2d'); | |
| // Handle high DPI displays | |
| const dpr = window.devicePixelRatio || 1; | |
| const rect = canvas.getBoundingClientRect(); | |
| canvas.width = rect.width * dpr; | |
| canvas.height = rect.height * dpr; | |
| ctx.scale(dpr, dpr); | |
| visualizer.canvas = canvas; | |
| visualizer.ctx = ctx; | |
| visualizer.analyser = audioContext.createAnalyser(); | |
| visualizer.analyser.fftSize = 2048; | |
| visualizer.analyser.smoothingTimeConstant = 0.8; | |
| drawVisualizer(); | |
| } | |
| function drawVisualizer() { | |
| if (!visualizer.ctx) return; | |
| const { ctx, canvas, analyser } = visualizer; | |
| const width = canvas.width / (window.devicePixelRatio || 1); | |
| const height = canvas.height / (window.devicePixelRatio || 1); | |
| ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; | |
| ctx.fillRect(0, 0, width, height); | |
| if (isPlaying && analyser) { | |
| const bufferLength = analyser.frequencyBinCount; | |
| const dataArray = new Uint8Array(bufferLength); | |
| analyser.getByteFrequencyData(dataArray); | |
| const barWidth = (width / bufferLength) * 2.5; | |
| let barHeight; | |
| let x = 0; | |
| for (let i = 0; i < bufferLength; i++) { | |
| barHeight = (dataArray[i] / 255) * height * 0.8; | |
| // Create gradient for bars | |
| const gradient = ctx.createLinearGradient(0, height, 0, height - barHeight); | |
| gradient.addColorStop(0, 'rgba(6, 182, 212, 0.8)'); // Cyan | |
| gradient.addColorStop(0.5, 'rgba(168, 85, 247, 0.8)'); // Purple | |
| gradient.addColorStop(1, 'rgba(236, 72, 153, 0.8)'); // Pink | |
| ctx.fillStyle = gradient; | |
| ctx.fillRect(x, height - barHeight, barWidth, barHeight); | |
| // Add glow effect | |
| ctx.shadowBlur = 10; | |
| ctx.shadowColor = 'rgba(236, 72, 153, 0.5)'; | |
| x += barWidth + 1; | |
| } | |
| ctx.shadowBlur = 0; | |
| } else { | |
| // Idle animation | |
| const time = Date.now() * 0.001; | |
| ctx.strokeStyle = 'rgba(6, 182, 212, 0.3)'; | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| for (let x = 0; x < width; x++) { | |
| const y = height / 2 + Math.sin(x * 0.01 + time) * 20 * Math.sin(time * 0.5); | |
| if (x === 0) ctx.moveTo(x, y); | |
| else ctx.lineTo(x, y); | |
| } | |
| ctx.stroke(); | |
| } | |
| visualizer.animationId = requestAnimationFrame(drawVisualizer); | |
| } | |
| // Binaural Beat Generation | |
| function createBinauralBeats() { | |
| const carrier = parseFloat(document.getElementById('carrierSlider').value); | |
| const beat = parseFloat(document.getElementById('beatSlider').value); | |
| // Cleanup existing | |
| stopBinaural(); | |
| // Create nodes | |
| binauralNodes.masterGain = audioContext.createGain(); | |
| binauralNodes.masterGain.connect(audioContext.destination); | |
| binauralNodes.masterGain.connect(visualizer.analyser); | |
| if (currentMode === 'spatial') { | |
| // Spatial/Binaural mode: Different frequencies per ear | |
| binauralNodes.leftOsc = audioContext.createOscillator(); | |
| binauralNodes.rightOsc = audioContext.createOscillator(); | |
| binauralNodes.leftGain = audioContext.createGain(); | |
| binauralNodes.rightGain = audioContext.createGain(); | |
| binauralNodes.merger = audioContext.createChannelMerger(2); | |
| // Left ear: carrier | |
| binauralNodes.leftOsc.frequency.value = carrier; | |
| binauralNodes.leftOsc.connect(binauralNodes.leftGain); | |
| binauralNodes.leftGain.connect(binauralNodes.merger, 0, 0); | |
| // Right ear: carrier + beat | |
| binauralNodes.rightOsc.frequency.value = carrier + beat; | |
| binauralNodes.rightOsc.connect(binauralNodes.rightGain); | |
| binauralNodes.rightGain.connect(binauralNodes.merger, 0, 1); | |
| binauralNodes.merger.connect(binauralNodes.masterGain); | |
| binauralNodes.leftOsc.start(); | |
| binauralNodes.rightOsc.start(); | |
| } else { | |
| // Monaural mode: Amplitude modulation at beat frequency | |
| binauralNodes.leftOsc = audioContext.createOscillator(); | |
| binauralNodes.modulator = audioContext.createOscillator(); | |
| binauralNodes.modulatorGain = audioContext.createGain(); | |
| // Carrier tone | |
| binauralNodes.leftOsc.frequency.value = carrier; | |
| // Modulator for amplitude | |
| binauralNodes.modulator.frequency.value = beat; | |
| binauralNodes.modulatorGain.gain.value = 0.5; // Modulation depth | |
| // Connect modulator to gain | |
| binauralNodes.modulator.connect(binauralNodes.modulatorGain.gain); | |
| // Create constant offset for gain | |
| const constantOffset = audioContext.createGain(); | |
| constantOffset.gain.value = 0.5; | |
| binauralNodes.leftOsc.connect(constantOffset); | |
| constantOffset.connect(binauralNodes.modulatorGain); | |
| // Connect to both channels equally (monaural) | |
| binauralNodes.merger = audioContext.createChannelMerger(2); | |
| binauralNodes.modulatorGain.connect(binauralNodes.merger, 0, 0); | |
| binauralNodes.modulatorGain.connect(binauralNodes.merger, 0, 1); | |
| binauralNodes.merger.connect(binauralNodes.masterGain); | |
| binauralNodes.leftOsc.start(); | |
| binauralNodes.modulator.start(); | |
| } | |
| // Apply ramping | |
| applyRamp(binauralNodes.masterGain.gain, 0.3); | |
| } | |
| function stopBinaural() { | |
| if (binauralNodes.leftOsc) { | |
| try { binauralNodes.leftOsc.stop(); } catch(e) {} | |
| binauralNodes.leftOsc = null; | |
| } | |
| if (binauralNodes.rightOsc) { | |
| try { binauralNodes.rightOsc.stop(); } catch(e) {} | |
| binauralNodes.rightOsc = null; | |
| } | |
| if (binauralNodes.modulator) { | |
| try { binauralNodes.modulator.stop(); } catch(e) {} | |
| binauralNodes.modulator = null; | |
| } | |
| if (binauralNodes.masterGain) { | |
| binauralNodes.masterGain.disconnect(); | |
| } | |
| } | |
| // Noise Generation (Always Spatial) | |
| function createNoise(type) { | |
| stopNoise(); | |
| activeNoiseType = type; | |
| const bufferSize = 2 * audioContext.sampleRate; // 2 seconds buffer | |
| const numChannels = 2; // Stereo | |
| const buffer = audioContext.createBuffer(numChannels, bufferSize, audioContext.sampleRate); | |
| // Generate noise data | |
| for (let channel = 0; channel < numChannels; channel++) { | |
| const data = buffer.getChannelData(channel); | |
| if (type === 'white') { | |
| // White noise: random -1 to 1 | |
| for (let i = 0; i < bufferSize; i++) { | |
| data[i] = Math.random() * 2 - 1; | |
| } | |
| } else if (type === 'pink') { | |
| // Pink noise: 1/f using Paul Kellet's method | |
| let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0; | |
| for (let i = 0; i < bufferSize; i++) { | |
| const white = Math.random() * 2 - 1; | |
| b0 = 0.99886 * b0 + white * 0.0555179; | |
| b1 = 0.99332 * b1 + white * 0.0750759; | |
| b2 = 0.96900 * b2 + white * 0.1538520; | |
| b3 = 0.86650 * b3 + white * 0.3104856; | |
| b4 = 0.55000 * b4 + white * 0.5329522; | |
| b5 = -0.7616 * b5 - white * 0.0168980; | |
| data[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362; | |
| data[i] *= 0.11; // Normalize | |
| b6 = white * 0.115926; | |
| } | |
| } else if (type === 'brown') { | |
| // Brown noise: 1/f² (integration of white noise) | |
| let lastOut = 0; | |
| for (let i = 0; i < bufferSize; i++) { | |
| const white = Math.random() * 2 - 1; | |
| data[i] = (lastOut + (0.02 * white)) / 1.02; | |
| lastOut = data[i]; | |
| data[i] *= 3.5; // Normalize | |
| } | |
| } | |
| } | |
| // Create spatial setup | |
| noiseNodes.masterGain = audioContext.createGain(); | |
| noiseNodes.leftSource = audioContext.createBufferSource(); | |
| noiseNodes.rightSource = audioContext.createBufferSource(); | |
| noiseNodes.leftGain = audioContext.createGain(); | |
| noiseNodes.rightGain = audioContext.createGain(); | |
| noiseNodes.leftPanner = audioContext.createStereoPanner(); | |
| noiseNodes.rightPanner = audioContext.createStereoPanner(); | |
| // Set up looping | |
| noiseNodes.leftSource.buffer = buffer; | |
| noiseNodes.rightSource.buffer = buffer; | |
| noiseNodes.leftSource.loop = true; | |
| noiseNodes.rightSource.loop = true; | |
| // Get spatial width | |
| const spatialWidth = parseInt(document.getElementById('spatialWidthSlider').value) / 100; | |
| const panValue = spatialWidth; // 0 = center, 1 = full side | |
| // Left channel fully left | |
| noiseNodes.leftPanner.pan.value = -panValue; | |
| noiseNodes.leftSource.connect(noiseNodes.leftGain); | |
| noiseNodes.leftGain.connect(noiseNodes.leftPanner); | |
| noiseNodes.leftPanner.connect(noiseNodes.masterGain); | |
| // Right channel fully right (with slight offset for true spatial feel) | |
| noiseNodes.rightPanner.pan.value = panValue; | |
| noiseNodes.rightSource.connect(noiseNodes.rightGain); | |
| noiseNodes.rightGain.connect(noiseNodes.rightPanner); | |
| noiseNodes.rightPanner.connect(noiseNodes.masterGain); | |
| // Add slight delay to right channel for enhanced spatialization | |
| if (spatialWidth > 0.5) { | |
| const delay = audioContext.createDelay(); | |
| delay.delayTime.value = 0.01; // 10ms delay | |
| noiseNodes.rightPanner.disconnect(); | |
| noiseNodes.rightPanner.connect(delay); | |
| delay.connect(noiseNodes.masterGain); | |
| } | |
| noiseNodes.masterGain.connect(audioContext.destination); | |
| noiseNodes.masterGain.connect(visualizer.analyser); | |
| // Set volume | |
| const volume = parseInt(document.getElementById('noiseVolumeSlider').value) / 100; | |
| noiseNodes.masterGain.gain.value = volume; | |
| noiseNodes.leftSource.start(); | |
| noiseNodes.rightSource.start(); | |
| // Add slight variation between channels for true spatial effect | |
| // By starting at slightly different offsets | |
| noiseNodes.rightSource.playbackRate.value = 0.999; // Slight pitch difference creates beating | |
| updateNoiseButtonStates(); | |
| } | |
| function stopNoise() { | |
| if (noiseNodes.leftSource) { | |
| try { noiseNodes.leftSource.stop(); } catch(e) {} | |
| noiseNodes.leftSource = null; | |
| } | |
| if (noiseNodes.rightSource) { | |
| try { noiseNodes.rightSource.stop(); } catch(e) {} | |
| noiseNodes.rightSource = null; | |
| } | |
| if (noiseNodes.masterGain) { | |
| noiseNodes.masterGain.disconnect(); | |
| } | |
| activeNoiseType = null; | |
| updateNoiseButtonStates(); | |
| } | |
| function updateNoiseButtonStates() { | |
| const buttons = ['pinkNoiseBtn', 'brownNoiseBtn', 'whiteNoiseBtn']; | |
| const types = ['pink', 'brown', 'white']; | |
| buttons.forEach((id, index) => { | |
| const btn = document.getElementById(id); | |
| if (activeNoiseType === types[index]) { | |
| btn.classList.add('playing'); | |
| btn.querySelector('i[data-lucide="play-circle"]').setAttribute('data-lucide', 'stop-circle'); | |
| } else { | |
| btn.classList.remove('playing'); | |
| btn.querySelector('i[data-lucide="stop-circle"]')?.setAttribute('data-lucide', 'play-circle'); | |
| } | |
| }); | |
| lucide.createIcons(); | |
| } | |
| // Ramping Functionality | |
| function applyRamp(gainNode, targetValue) { | |
| const now = audioContext.currentTime; | |
| if (rampMode === 'none') { | |
| gainNode.setValueAtTime(targetValue, now); | |
| } else if (rampMode === 'up') { | |
| gainNode.setValueAtTime(0.001, now); | |
| gainNode.exponentialRampToValueAtTime(targetValue, now + rampDuration); | |
| } else if (rampMode === 'down') { | |
| gainNode.setValueAtTime(targetValue, now); | |
| gainNode.exponentialRampToValueAtTime(0.001, now + rampDuration); | |
| // Stop after ramp down | |
| setTimeout(() => { | |
| stopAll(); | |
| }, rampDuration * 1000); | |
| } | |
| } | |
| function stopAll() { | |
| stopBinaural(); | |
| stopNoise(); | |
| isPlaying = false; | |
| updatePlayButton(); | |
| } | |
| function updatePlayButton() { | |
| const btn = document.getElementById('playBinauralBtn'); | |
| const stopBtn = document.getElementById('stopAllBtn'); | |
| if (isPlaying) { | |
| btn.innerHTML = '<i data-lucide="pause" class="w-6 h-6"></i><span>Stop Binaural</span>'; | |
| btn.classList.remove('from-cyan-400', 'via-purple-500', 'to-pink-500'); | |
| btn.classList.add('from-red-400', 'via-red-500', 'to-rose-600'); | |
| stopBtn.disabled = false; | |
| stopBtn.classList.remove('opacity-50', 'cursor-not-allowed'); | |
| } else { | |
| btn.innerHTML = '<i data-lucide="play" class="w-6 h-6"></i><span>Generate Binaural Beat</span>'; | |
| btn.classList.add('from-cyan-400', 'via-purple-500', 'to-pink-500'); | |
| btn.classList.remove('from-red-400', 'via-red-500', 'to-rose-600'); | |
| stopBtn.disabled = true; | |
| stopBtn.classList.add('opacity-50', 'cursor-not-allowed'); | |
| } | |
| lucide.createIcons(); | |
| } | |
| // Event Listeners | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Mode selection | |
| document.getElementById('spatialBtn').addEventListener('click', () => { | |
| currentMode = 'spatial'; | |
| document.getElementById('spatialBtn').classList.add('active', 'from-cyan-500', 'to-blue-500', 'text-white'); | |
| document.getElementById('spatialBtn').classList.remove('bg-white/10', 'text-purple-200'); | |
| document.getElementById('monauralBtn').classList.remove('active', 'from-purple-500', 'to-pink-500', 'text-white'); | |
| document.getElementById('monauralBtn').classList.add('bg-white/10', 'text-purple-200'); | |
| document.getElementById('modeDescription').textContent = 'Different frequencies in each ear create the beat perception'; | |
| }); | |
| document.getElementById('monauralBtn').addEventListener('click', () => { | |
| currentMode = 'monaural'; | |
| document.getElementById('monauralBtn').classList.add('active', 'from-purple-500', 'to-pink-500', 'text-white'); | |
| document.getElementById('monauralBtn').classList.remove('bg-white/10', 'text-purple-200'); | |
| document.getElementById('spatialBtn').classList.remove('active', 'from-cyan-500', 'to-blue-500', 'text-white'); | |
| document.getElementById('spatialBtn').classList.add('bg-white/10', 'text-purple-200'); | |
| document.getElementById('modeDescription').textContent = 'Same frequency in both ears with amplitude modulation'; | |
| }); | |
| // Ramp selection | |
| const rampButtons = { | |
| 'rampNone': 'none', | |
| 'rampUp': 'up', | |
| 'rampDown': 'down' | |
| }; | |
| Object.keys(rampButtons).forEach(id => { | |
| document.getElementById(id).addEventListener('click', () => { | |
| rampMode = rampButtons[id]; | |
| Object.keys(rampButtons).forEach(btnId => { | |
| const btn = document.getElementById(btnId); | |
| if (btnId === id) { | |
| btn.classList.add('active', 'from-purple-500', 'to-pink-500', 'text-white'); | |
| btn.classList.remove('bg-white/10', 'text-purple-200'); | |
| } else { | |
| btn.classList.remove('active', 'from-purple-500', 'to-pink-500', 'text-white'); | |
| btn.classList.add('bg-white/10', 'text-purple-200'); | |
| } | |
| }); | |
| }); | |
| }); | |
| // Sliders | |
| document.getElementById('carrierSlider').addEventListener('input', (e) => { | |
| document.getElementById('carrierValue').textContent = e.target.value + ' Hz'; | |
| if (isPlaying && currentMode === 'spatial') { | |
| const beat = parseFloat(document.getElementById('beatSlider').value); | |
| binauralNodes.leftOsc.frequency.setValueAtTime(parseFloat(e.target.value), audioContext.currentTime); | |
| binauralNodes.rightOsc.frequency.setValueAtTime(parseFloat(e.target.value) + beat, audioContext.currentTime); | |
| } else if (isPlaying) { | |
| binauralNodes.leftOsc.frequency.setValueAtTime(parseFloat(e.target.value), audioContext.currentTime); | |
| } | |
| }); | |
| document.getElementById('beatSlider').addEventListener('input', (e) => { | |
| document.getElementById('beatValue').textContent = e.target.value + ' Hz'; | |
| if (isPlaying) { | |
| const carrier = parseFloat(document.getElementById('carrierSlider').value); | |
| const beat = parseFloat(e.target.value); | |
| if (currentMode === 'spatial') { | |
| binauralNodes.rightOsc.frequency.setValueAtTime(carrier + beat, audioContext.currentTime); | |
| } else { | |
| binauralNodes.modulator.frequency.setValueAtTime(beat, audioContext.currentTime); | |
| } | |
| } | |
| }); | |
| document.getElementById('rampDuration').addEventListener('input', (e) => { | |
| rampDuration = parseInt(e.target.value); | |
| document.getElementById('rampDurationValue').textContent = rampDuration + 's'; | |
| }); | |
| document.getElementById('noiseVolumeSlider').addEventListener('input', (e) => { | |
| const value = e.target.value; | |
| document.getElementById('noiseVolumeValue').textContent = value + '%'; | |
| if (noiseNodes.masterGain) { | |
| noiseNodes.masterGain.gain.setValueAtTime(value / 100, audioContext.currentTime); | |
| } | |
| }); | |
| document.getElementById('spatialWidthSlider').addEventListener('input', (e) => { | |
| const value = parseInt(e.target.value); | |
| const label = value < 30 ? 'Narrow' : value < 70 ? 'Medium' : 'Wide'; | |
| document.getElementById('spatialWidthValue').textContent = label; | |
| if (activeNoiseType) { | |
| // Recreate noise with new spatial settings | |
| createNoise(activeNoiseType); | |
| } | |
| }); | |
| // Brainwave presets | |
| document.querySelectorAll('.brainwave-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const carrier = btn.dataset.carrier; | |
| const beat = btn.dataset.beat; | |
| document.getElementById('carrierSlider').value = carrier; | |
| document.getElementById('beatSlider').value = beat; | |
| document.getElementById('carrierValue').textContent = carrier + ' Hz'; | |
| document.getElementById('beatValue').textContent = beat + ' Hz'; | |
| // Visual feedback | |
| btn.style.transform = 'scale(0.95)'; | |
| setTimeout(() => btn.style.transform = '', 100); | |
| }); | |
| }); | |
| // Play buttons | |
| document.getElementById('playBinauralBtn').addEventListener('click', () => { | |
| initAudioContext(); | |
| if (isPlaying) { | |
| stopBinaural(); | |
| isPlaying = false; | |
| } else { | |
| createBinauralBeats(); | |
| isPlaying = true; | |
| } | |
| updatePlayButton(); | |
| }); | |
| // Noise buttons (toggle behavior) | |
| document.getElementById('pinkNoiseBtn').addEventListener('click', () => { | |
| initAudioContext(); | |
| if (activeNoiseType === 'pink') { | |
| stopNoise(); | |
| } else { | |
| createNoise('pink'); | |
| } | |
| }); | |
| document.getElementById('brownNoiseBtn').addEventListener('click', () => { | |
| initAudioContext(); | |
| if (activeNoiseType === 'brown') { | |
| stopNoise(); | |
| } else { | |
| createNoise('brown'); | |
| } | |
| }); | |
| document.getElementById('whiteNoiseBtn').addEventListener('click', () => { | |
| initAudioContext(); | |
| if (activeNoiseType === 'white') { | |
| stopNoise(); | |
| } else { | |
| createNoise('white'); | |
| } | |
| }); | |
| document.getElementById('stopAllBtn').addEventListener('click', stopAll); | |
| // Particle background animation | |
| initParticles(); | |
| }); | |
| // Particle System | |
| function initParticles() { | |
| const canvas = document.getElementById('particleCanvas'); | |
| const ctx = canvas.getContext('2d'); | |
| function resize() { | |
| canvas.width = window.innerWidth; | |
| canvas.height = window.innerHeight; | |
| } | |
| resize(); | |
| window.addEventListener('resize', resize); | |
| const particles = []; | |
| const particleCount = 50; | |
| for (let i = 0; i < particleCount; i++) { | |
| particles.push({ | |
| x: Math.random() * canvas.width, | |
| y: Math.random() * canvas.height, | |
| radius: Math.random() * 3 + 1, | |
| vx: (Math.random() - 0.5) * 0.5, | |
| vy: (Math.random() - 0.5) * 0.5, | |
| color: `hsla(${Math.random() * 60 + 240}, 70%, 60%, ${Math.random() * 0.3})` | |
| }); | |
| } | |
| function animate() { | |
| ctx.clearRect(0, 0, canvas.width, canvas.height); | |
| particles.forEach(p => { | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| if (p.x < 0 || p.x > canvas.width) p.vx *= -1; | |
| if (p.y < 0 || p.y > canvas.height) p.vy *= -1; | |
| ctx.beginPath(); | |
| ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2); | |
| ctx.fillStyle = p.color; | |
| ctx.fill(); | |
| }); | |
| // Connect nearby particles | |
| particles.forEach((p1, i) => { | |
| particles.slice(i + 1).forEach(p2 => { | |
| const dx = p1.x - p2.x; | |
| const dy = p1.y - p2.y; | |
| const distance = Math.sqrt(dx * dx + dy * dy); | |
| if (distance < 100) { | |
| ctx.beginPath(); | |
| ctx.moveTo(p1.x, p1.y); | |
| ctx.lineTo(p2.x, p2.y); | |
| ctx.strokeStyle = `rgba(147, 51, 234, ${0.1 * (1 - distance / 100)})`; | |
| ctx.stroke(); | |
| } | |
| }); | |
| }); | |
| requestAnimationFrame(animate); | |
| } | |
| animate(); | |
| } | |
| // Cleanup on page unload | |
| window.addEventListener('beforeunload', () => { | |
| stopAll(); | |
| if (audioContext) { | |
| audioContext.close(); | |
| } | |
| }); |