/** * WaveformVisualizer - handles canvas waveform animation for user and agent audio */ export class WaveformVisualizer { constructor(userCanvas, agentCanvas) { this.userCanvas = userCanvas; this.agentCanvas = agentCanvas; this.userCtx = userCanvas.getContext('2d'); this.agentCtx = agentCanvas.getContext('2d'); this.animationId = null; this.isUserSpeaking = false; this.isAgentSpeaking = false; this._initCanvases(); } _initCanvases() { this.userCanvas.width = 350; this.userCanvas.height = 60; this.agentCanvas.width = 350; this.agentCanvas.height = 60; this._clear(this.userCtx, this.userCanvas); this._clear(this.agentCtx, this.agentCanvas); } _clear(ctx, canvas) { ctx.fillStyle = '#000'; ctx.fillRect(0, 0, canvas.width, canvas.height); } _draw(ctx, canvas, isActive, color, waveformData) { this._clear(ctx, canvas); if (!isActive) return; ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.beginPath(); const centerY = canvas.height / 2; if (waveformData && waveformData.length > 0) { const sliceWidth = canvas.width / waveformData.length; let x = 0; for (let i = 0; i < waveformData.length; i++) { const v = waveformData[i] / 128.0; const y = (v * canvas.height) / 2; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); x += sliceWidth; } } else { const frequency = Date.now() * 0.01; for (let x = 0; x < canvas.width; x++) { const y = centerY + Math.sin((x * 0.02) + frequency) * 12 + Math.sin((x * 0.05) + frequency * 0.7) * 6 + Math.sin((x * 0.03) + frequency * 1.3) * 4 + (Math.random() - 0.5) * 3; if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } } ctx.stroke(); } /** * Start animation loop. * @param {Function} getUserWaveformData - returns Uint8Array or null * @param {Function} getAgentWaveformData - returns Uint8Array or null */ start(getUserWaveformData, getAgentWaveformData) { const animate = () => { this._draw(this.userCtx, this.userCanvas, this.isUserSpeaking, '#2196f3', getUserWaveformData()); this._draw(this.agentCtx, this.agentCanvas, this.isAgentSpeaking, '#9c27b0', getAgentWaveformData()); this.animationId = requestAnimationFrame(animate); }; animate(); } stop() { if (this.animationId) { cancelAnimationFrame(this.animationId); this.animationId = null; } } startUserSpeaking() { this.isUserSpeaking = true; } stopUserSpeaking() { this.isUserSpeaking = false; } startAgentSpeaking() { this.isAgentSpeaking = true; } stopAgentSpeaking() { this.isAgentSpeaking = false; } }