profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
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