Spaces:
Sleeping
Sleeping
| class OrbVisualizer { | |
| constructor(canvas, audio) { | |
| this.canvas = canvas; | |
| this.ctx = canvas.getContext('2d'); | |
| this.audio = audio; | |
| this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); | |
| this.analyser = this.audioCtx.createAnalyser(); | |
| this.analyser.fftSize = 256; | |
| try { | |
| this.source = this.audioCtx.createMediaElementSource(this.audio); | |
| this.source.connect(this.analyser); | |
| this.analyser.connect(this.audioCtx.destination); | |
| } catch (e) { | |
| console.warn("Audio source already connected"); | |
| } | |
| this.bufferLength = this.analyser.frequencyBinCount; | |
| this.dataArray = new Uint8Array(this.bufferLength); | |
| this.isPlaying = false; | |
| this.animationId = null; | |
| this.particles = []; | |
| this.resize(); | |
| window.addEventListener('resize', () => this.resize()); | |
| } | |
| resize() { | |
| const rect = this.canvas.parentElement.getBoundingClientRect(); | |
| this.canvas.width = rect.width * 1.5; // Higher res | |
| this.canvas.height = rect.height * 1.5; | |
| this.canvas.style.width = `${rect.width}px`; | |
| this.canvas.style.height = `${rect.height}px`; | |
| } | |
| play() { | |
| if (this.audioCtx.state === 'suspended') { | |
| this.audioCtx.resume(); | |
| } | |
| this.isPlaying = true; | |
| this.draw(); | |
| } | |
| pause() { | |
| this.isPlaying = false; | |
| cancelAnimationFrame(this.animationId); | |
| this.drawIdle(); | |
| } | |
| destroy() { | |
| this.pause(); | |
| } | |
| drawIdle() { | |
| this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); | |
| const cx = this.canvas.width / 2; | |
| const cy = this.canvas.height / 2; | |
| const radius = Math.min(cx, cy) * 0.4; | |
| // Inner core | |
| this.ctx.beginPath(); | |
| this.ctx.arc(cx, cy, radius, 0, 2 * Math.PI); | |
| const gradient = this.ctx.createRadialGradient(cx, cy, 0, cx, cy, radius); | |
| gradient.addColorStop(0, '#7c3aed'); | |
| gradient.addColorStop(1, '#2563eb'); | |
| this.ctx.fillStyle = gradient; | |
| this.ctx.fill(); | |
| // Idle glow | |
| this.ctx.shadowBlur = 40; | |
| this.ctx.shadowColor = 'rgba(124, 58, 237, 0.4)'; | |
| this.ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; | |
| this.ctx.lineWidth = 2; | |
| this.ctx.stroke(); | |
| this.ctx.shadowBlur = 0; | |
| } | |
| draw() { | |
| if (!this.isPlaying) return; | |
| this.animationId = requestAnimationFrame(() => this.draw()); | |
| this.analyser.getByteFrequencyData(this.dataArray); | |
| this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); | |
| const cx = this.canvas.width / 2; | |
| const cy = this.canvas.height / 2; | |
| // Split frequencies into bass, mid, treble | |
| let bass = 0, mid = 0, treble = 0; | |
| for(let i=0; i<40; i++) bass += this.dataArray[i]; | |
| for(let i=40; i<80; i++) mid += this.dataArray[i]; | |
| for(let i=80; i<120; i++) treble += this.dataArray[i]; | |
| bass /= 40; mid /= 40; treble /= 40; | |
| const baseRadius = Math.min(cx, cy) * 0.35; | |
| // Draw Outer Ring (Treble) | |
| this.ctx.beginPath(); | |
| this.ctx.arc(cx, cy, baseRadius + (treble * 0.5) + 30, 0, 2 * Math.PI); | |
| this.ctx.strokeStyle = `rgba(37, 99, 235, ${treble/255})`; | |
| this.ctx.lineWidth = 4; | |
| this.ctx.stroke(); | |
| // Draw Mid Ring | |
| this.ctx.beginPath(); | |
| this.ctx.arc(cx, cy, baseRadius + (mid * 0.4) + 15, 0, 2 * Math.PI); | |
| this.ctx.strokeStyle = `rgba(244, 63, 94, ${mid/255})`; | |
| this.ctx.lineWidth = 6; | |
| this.ctx.stroke(); | |
| // Draw Core (Bass) | |
| const coreRadius = baseRadius + (bass * 0.3); | |
| this.ctx.beginPath(); | |
| this.ctx.arc(cx, cy, coreRadius, 0, 2 * Math.PI); | |
| const gradient = this.ctx.createRadialGradient(cx, cy, 0, cx, cy, coreRadius); | |
| gradient.addColorStop(0, '#7c3aed'); | |
| gradient.addColorStop(0.7, '#2563eb'); | |
| gradient.addColorStop(1, 'transparent'); | |
| this.ctx.fillStyle = gradient; | |
| this.ctx.shadowBlur = bass; | |
| this.ctx.shadowColor = '#7c3aed'; | |
| this.ctx.fill(); | |
| this.ctx.shadowBlur = 0; // reset | |
| // Particles emission on heavy bass | |
| if (bass > 180 && Math.random() > 0.5) { | |
| this.particles.push({ | |
| x: cx, y: cy, | |
| vx: (Math.random() - 0.5) * 10, | |
| vy: (Math.random() - 0.5) * 10, | |
| life: 1.0, | |
| color: Math.random() > 0.5 ? '#7c3aed' : '#2563eb' | |
| }); | |
| } | |
| // Draw and update particles | |
| for (let i = this.particles.length - 1; i >= 0; i--) { | |
| let p = this.particles[i]; | |
| p.x += p.vx; | |
| p.y += p.vy; | |
| p.life -= 0.05; | |
| if (p.life <= 0) { | |
| this.particles.splice(i, 1); | |
| continue; | |
| } | |
| this.ctx.beginPath(); | |
| this.ctx.arc(p.x, p.y, 3 * p.life, 0, 2 * Math.PI); | |
| this.ctx.fillStyle = p.color; | |
| this.ctx.globalAlpha = p.life; | |
| this.ctx.fill(); | |
| this.ctx.globalAlpha = 1.0; | |
| } | |
| } | |
| } | |