Waveform.tsx
1,797 bytes
| 1 | import { useEffect, useRef } from 'react' |
|---|---|
| 2 | |
| 3 | export type WaveformMode = 'idle' | 'live' | 'simulated' |
| 4 | |
| 5 | interface WaveformProps { |
| 6 | mode: WaveformMode |
| 7 | levelRef?: React.RefObject<number> |
| 8 | } |
| 9 | |
| 10 | const BAR_COUNT = 36 |
| 11 | |
| 12 | export function Waveform({ mode, levelRef }: WaveformProps) { |
| 13 | const barRefs = useRef<(HTMLSpanElement | null)[]>([]) |
| 14 | const historyRef = useRef<number[]>(Array<number>(BAR_COUNT).fill(0)) |
| 15 | |
| 16 | useEffect(() => { |
| 17 | if (mode !== 'live' || !levelRef) { |
| 18 | historyRef.current = Array<number>(BAR_COUNT).fill(0) |
| 19 | for (const bar of barRefs.current) { |
| 20 | if (bar) bar.style.transform = '' |
| 21 | } |
| 22 | return |
| 23 | } |
| 24 | let rafId = 0 |
| 25 | let lastPush = 0 |
| 26 | const tick = (time: number) => { |
| 27 | if (time - lastPush > 40) { |
| 28 | lastPush = time |
| 29 | historyRef.current.push(levelRef.current ?? 0) |
| 30 | historyRef.current.shift() |
| 31 | } |
| 32 | const history = historyRef.current |
| 33 | for (let i = 0; i < BAR_COUNT; i++) { |
| 34 | const bar = barRefs.current[i] |
| 35 | if (bar) bar.style.transform = `scaleY(${0.08 + (history[i] ?? 0) * 0.92})` |
| 36 | } |
| 37 | rafId = requestAnimationFrame(tick) |
| 38 | } |
| 39 | rafId = requestAnimationFrame(tick) |
| 40 | return () => cancelAnimationFrame(rafId) |
| 41 | }, [mode, levelRef]) |
| 42 | |
| 43 | return ( |
| 44 | <div className={`waveform waveform-${mode}`} aria-hidden="true"> |
| 45 | {Array.from({ length: BAR_COUNT }, (_, i) => ( |
| 46 | <span |
| 47 | key={i} |
| 48 | ref={(el) => { |
| 49 | barRefs.current[i] = el |
| 50 | }} |
| 51 | className="waveform-bar" |
| 52 | style={ |
| 53 | mode === 'simulated' |
| 54 | ? { |
| 55 | animationDelay: `${(i % 7) * 0.09}s`, |
| 56 | animationDuration: `${0.7 + (i % 5) * 0.13}s`, |
| 57 | } |
| 58 | : undefined |
| 59 | } |
| 60 | /> |
| 61 | ))} |
| 62 | </div> |
| 63 | ) |
| 64 | } |
| 65 | |