// vita-waveform.jsx — animated voice visualizer (the "Vita orb") // variants: 'barras' | 'espejo' | 'linea' // states: 'idle' | 'listening' | 'thinking' | 'speaking' function Waveform({ state = 'idle', accent = '#3DD9D6', accentDeep = '#16A8A5', variant = 'barras', size = 200, level = 0 }) { const canvasRef = React.useRef(null); const timerRef = React.useRef(0); const tRef = React.useRef(0); // smoothed target amplitude const ampRef = React.useRef(0.12); const stateRef = React.useRef(state); const levelRef = React.useRef(level); stateRef.current = state; levelRef.current = level; React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = size * dpr; canvas.height = size * dpr; ctx.scale(dpr, dpr); const targetFor = (s) => { if (s === 'speaking') return 0.92; if (s === 'listening') return 0.55; if (s === 'thinking') return 0.30; return 0.14; // idle }; const hx = (hex, a) => { const h = hex.replace('#', ''); return `rgba(${parseInt(h.slice(0,2),16)},${parseInt(h.slice(2,4),16)},${parseInt(h.slice(4,6),16)},${a})`; }; const draw = () => { tRef.current += 0.045; const t = tRef.current; const s = stateRef.current; // approach target amplitude smoothly let target = targetFor(s); if (s === 'listening' || s === 'speaking') { target = Math.max(target * 0.5, Math.min(1, target + levelRef.current * 0.6)); } ampRef.current += (target - ampRef.current) * 0.12; const amp = ampRef.current; const c = size / 2; ctx.clearRect(0, 0, size, size); // soft halo const halo = ctx.createRadialGradient(c, c, size * 0.15, c, c, size * 0.5); halo.addColorStop(0, hx(accent, 0.22 + amp * 0.22)); halo.addColorStop(1, hx(accent, 0)); ctx.fillStyle = halo; ctx.beginPath(); ctx.arc(c, c, size * 0.5, 0, Math.PI * 2); ctx.fill(); const grad = ctx.createLinearGradient(0, c - size * 0.3, 0, c + size * 0.3); grad.addColorStop(0, accent); grad.addColorStop(1, accentDeep); if (variant === 'barras') { const N = 5; const barW = size * 0.052; const gap = size * 0.046; const totalW = N * barW + (N - 1) * gap; const startX = c - totalW / 2; ctx.fillStyle = grad; for (let i = 0; i < N; i++) { const phase = t * (s === 'thinking' ? 3 : 2.4) + i * 0.7; const mid = (N - 1) / 2; const weight = 1 - Math.abs(i - mid) / (mid + 1.2); const h = size * (0.10 + (0.5 * amp) * weight * (0.55 + 0.45 * Math.sin(phase))); const x = startX + i * (barW + gap); const y = c - h / 2; roundRect(ctx, x, y, barW, h, barW / 2); ctx.fill(); } } else if (variant === 'espejo') { // centered mirrored line spectrum const N = 48; const spread = size * 0.74; const startX = c - spread / 2; ctx.lineWidth = size * 0.018; ctx.lineCap = 'round'; ctx.strokeStyle = grad; for (let i = 0; i < N; i++) { const x = startX + (i / (N - 1)) * spread; const d = Math.abs(i - (N - 1) / 2) / ((N - 1) / 2); // 0 center → 1 edge const env = Math.cos(d * Math.PI / 2); // taper edges const wob = 0.5 + 0.5 * Math.sin(t * 2.2 + i * 0.55) * Math.sin(t * 1.3 + i * 0.2); const h = size * (0.012 + 0.42 * amp * env * wob); ctx.beginPath(); ctx.moveTo(x, c - h); ctx.lineTo(x, c + h); ctx.stroke(); } } else { // 'linea' — flowing dual sine const spread = size * 0.78; const startX = c - spread / 2; const drawWave = (alpha, phase, ampMul, lw) => { ctx.beginPath(); for (let x = 0; x <= spread; x += 3) { const px = startX + x; const u = x / spread; const env = Math.sin(u * Math.PI); // 0 at ends const y = c + Math.sin(u * 9 + t * 3 + phase) * size * 0.30 * amp * env + Math.sin(u * 4 - t * 1.7 + phase) * size * 0.12 * amp * env; if (x === 0) ctx.moveTo(px, y); else ctx.lineTo(px, y); } ctx.strokeStyle = hx(accent, alpha); ctx.lineWidth = lw; ctx.lineCap = 'round'; ctx.stroke(); }; drawWave(0.35, 1.6, 0.7, size * 0.012); drawWave(0.6, 0.8, 0.85, size * 0.016); drawWave(1, 0, 1, size * 0.02); } }; timerRef.current = setInterval(draw, 33); draw(); return () => clearInterval(timerRef.current); }, [size, variant, accent, accentDeep]); return ( ); } function roundRect(ctx, x, y, w, h, r) { r = Math.min(r, w / 2, h / 2); ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); } Object.assign(window, { Waveform });