Spaces:
Running
Running
| "use client"; | |
| import { useEffect, useRef } from "react"; | |
| interface WaveformVisualizerProps { | |
| analyserNode: AnalyserNode | null; | |
| isActive: boolean; | |
| height?: number; | |
| color?: string; | |
| } | |
| export default function WaveformVisualizer({ | |
| analyserNode, | |
| isActive, | |
| height = 80, | |
| color = "#8B5CF6", | |
| }: WaveformVisualizerProps) { | |
| const canvasRef = useRef<HTMLCanvasElement>(null); | |
| const animFrameRef = useRef<number>(0); | |
| useEffect(() => { | |
| const canvas = canvasRef.current; | |
| if (!canvas) return; | |
| const ctx = canvas.getContext("2d"); | |
| if (!ctx) return; | |
| const draw = () => { | |
| animFrameRef.current = requestAnimationFrame(draw); | |
| const W = canvas.width; | |
| const H = canvas.height; | |
| ctx.clearRect(0, 0, W, H); | |
| if (!analyserNode || !isActive) { | |
| // Idle animation — gentle sine wave | |
| ctx.beginPath(); | |
| const t = Date.now() / 800; | |
| for (let x = 0; x < W; x++) { | |
| const y = H / 2 + Math.sin(x * 0.03 + t) * 8 * Math.sin(x * 0.008 + t * 0.5); | |
| x === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); | |
| } | |
| ctx.strokeStyle = color + "60"; | |
| ctx.lineWidth = 2; | |
| ctx.stroke(); | |
| return; | |
| } | |
| const bufLen = analyserNode.frequencyBinCount; | |
| const dataArray = new Uint8Array(bufLen); | |
| analyserNode.getByteTimeDomainData(dataArray); | |
| // Draw gradient waveform | |
| const gradient = ctx.createLinearGradient(0, 0, W, 0); | |
| gradient.addColorStop(0, "#8B5CF6"); | |
| gradient.addColorStop(0.5, "#14b8a6"); | |
| gradient.addColorStop(1, "#8B5CF6"); | |
| ctx.beginPath(); | |
| const sliceW = W / bufLen; | |
| let x = 0; | |
| for (let i = 0; i < bufLen; i++) { | |
| const v = dataArray[i] / 128.0; | |
| const y = (v * H) / 2; | |
| i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y); | |
| x += sliceW; | |
| } | |
| ctx.lineTo(W, H / 2); | |
| ctx.strokeStyle = gradient; | |
| ctx.lineWidth = 2.5; | |
| ctx.shadowBlur = 12; | |
| ctx.shadowColor = color; | |
| ctx.stroke(); | |
| ctx.shadowBlur = 0; | |
| }; | |
| draw(); | |
| return () => cancelAnimationFrame(animFrameRef.current); | |
| }, [analyserNode, isActive, color]); | |
| return ( | |
| <canvas | |
| ref={canvasRef} | |
| width={600} | |
| height={height} | |
| className="w-full rounded-xl" | |
| style={{ height }} | |
| /> | |
| ); | |
| } | |