Spaces:
Running
Running
| // visualizer.js — Canvas frequency visualizer | |
| // | |
| // Duo uses Three.js for 3D robot visualization + MusicNoteSystem particles. | |
| // We use a 2D canvas with frequency bars + waveform + beat flash, | |
| // since Autonomous OS robots don't have a standard 3D model to render. | |
| export class Visualizer { | |
| constructor(canvas) { | |
| this.canvas = canvas; | |
| this.ctx = canvas.getContext('2d'); | |
| this._resize(); | |
| // Smooth values for animation | |
| this._smoothBars = null; | |
| this._beatFlash = 0; | |
| // Resize on window change | |
| this._resizeHandler = () => this._resize(); | |
| window.addEventListener('resize', this._resizeHandler); | |
| } | |
| destroy() { | |
| window.removeEventListener('resize', this._resizeHandler); | |
| } | |
| _resize() { | |
| const rect = this.canvas.parentElement.getBoundingClientRect(); | |
| this.canvas.width = rect.width * window.devicePixelRatio; | |
| this.canvas.height = rect.height * window.devicePixelRatio; | |
| this.ctx.scale(window.devicePixelRatio, window.devicePixelRatio); | |
| this.w = rect.width; | |
| this.h = rect.height; | |
| } | |
| // Called every frame from dance engine tick | |
| draw(freqData, timeData, analysis) { | |
| const { w, h, ctx } = this; | |
| const { bass, mid, high, energy, isBeat } = analysis; | |
| // Beat flash decay | |
| if (isBeat) this._beatFlash = 1.0; | |
| else this._beatFlash *= 0.9; | |
| // --- Background fade (creates trail effect) --- | |
| ctx.fillStyle = `rgba(10, 10, 15, 0.3)`; | |
| ctx.fillRect(0, 0, w, h); | |
| // Beat flash overlay | |
| if (this._beatFlash > 0.05) { | |
| ctx.fillStyle = `rgba(108, 92, 231, ${this._beatFlash * 0.12})`; | |
| ctx.fillRect(0, 0, w, h); | |
| } | |
| // --- Frequency bars --- | |
| if (freqData) { | |
| this._drawBars(freqData); | |
| } | |
| // --- Waveform --- | |
| if (timeData) { | |
| this._drawWaveform(timeData); | |
| } | |
| // --- Center energy circle --- | |
| this._drawEnergyCircle(energy, isBeat); | |
| } | |
| _drawBars(freqData) { | |
| const { w, h, ctx } = this; | |
| const barCount = Math.min(freqData.length, 96); | |
| // Initialize smooth bars | |
| if (!this._smoothBars || this._smoothBars.length !== barCount) { | |
| this._smoothBars = new Float32Array(barCount); | |
| } | |
| const barW = (w / barCount) * 0.8; | |
| const gap = (w / barCount) * 0.2; | |
| for (let i = 0; i < barCount; i++) { | |
| const raw = freqData[i] / 255; | |
| // Smooth falloff (bars fall slower than they rise) | |
| if (raw > this._smoothBars[i]) { | |
| this._smoothBars[i] = raw; | |
| } else { | |
| this._smoothBars[i] *= 0.92; // Decay | |
| } | |
| const val = this._smoothBars[i]; | |
| const barH = val * h * 0.75; | |
| const x = i * (barW + gap); | |
| // Color gradient based on frequency position | |
| const ratio = i / barCount; | |
| const r = ratio < 0.1 ? 160 : ratio < 0.4 ? 60 : Math.floor(180 * ratio); | |
| const g = ratio < 0.1 ? 50 : ratio < 0.4 ? Math.floor(160 + 95 * ratio) : 230; | |
| const b = ratio < 0.1 ? 255 : ratio < 0.4 ? 255 : Math.floor(80 * (1 - ratio)); | |
| const alpha = 0.3 + val * 0.7; | |
| ctx.fillStyle = `rgba(${r},${g},${b},${alpha})`; | |
| // Mirror bars (top and bottom) | |
| const halfH = barH / 2; | |
| const centerY = h / 2; | |
| ctx.fillRect(x, centerY - halfH, barW, halfH); | |
| ctx.fillRect(x, centerY, barW, halfH * 0.6); | |
| } | |
| } | |
| _drawWaveform(timeData) { | |
| const { w, h, ctx } = this; | |
| ctx.beginPath(); | |
| ctx.strokeStyle = `rgba(108, 92, 231, ${0.3 + this._beatFlash * 0.4})`; | |
| ctx.lineWidth = 1.5; | |
| const sliceW = w / timeData.length; | |
| for (let i = 0; i < timeData.length; i++) { | |
| const v = timeData[i] / 128.0; | |
| const y = (v * h) / 2; | |
| if (i === 0) ctx.moveTo(0, y); | |
| else ctx.lineTo(i * sliceW, y); | |
| } | |
| ctx.stroke(); | |
| } | |
| _drawEnergyCircle(energy, isBeat) { | |
| const { w, h, ctx } = this; | |
| const cx = w / 2; | |
| const cy = h / 2; | |
| const norm = Math.min(energy / 200, 1); | |
| const radius = 20 + norm * 40 + (isBeat ? 15 : 0); | |
| ctx.beginPath(); | |
| ctx.arc(cx, cy, radius, 0, Math.PI * 2); | |
| ctx.fillStyle = `rgba(108, 92, 231, ${0.05 + norm * 0.1})`; | |
| ctx.fill(); | |
| ctx.strokeStyle = `rgba(108, 92, 231, ${0.2 + norm * 0.3 + this._beatFlash * 0.3})`; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| } | |
| } | |