profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM

Commit

Add waveform visualization component

commit a6ec55c

1 changed file with +64 and −0

added client/src/components/Waveform.tsx +64 −0
@@ -0,0 +1,64 @@
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 +}