import { useEffect, useRef } from 'react' /** * The live-acceleration waveform. Reads the magnitude history from a ref and * redraws via requestAnimationFrame — independent of React renders, so it stays * smooth. A ResizeObserver keeps the drawing buffer exactly matched to the * element's pixel size (so the trace is always centered and peaks never clip), * and the line turns to the spike colour during an impact event. */ export function WaveformCanvas({ wave, impact, }: { wave: React.MutableRefObject impact: boolean }) { const ref = useRef(null) const impactRef = useRef(impact) impactRef.current = impact useEffect(() => { const cv = ref.current if (!cv) return const ctx = cv.getContext('2d') if (!ctx) return const dpr = Math.max(1, window.devicePixelRatio || 1) // Keep the drawing buffer matched to the element's actual size. Setting // width/height clears the canvas, so only assign when it actually changed. const sync = () => { const w = Math.round(cv.clientWidth * dpr) const h = Math.round(cv.clientHeight * dpr) if (w && cv.width !== w) cv.width = w if (h && cv.height !== h) cv.height = h } sync() const ro = new ResizeObserver(sync) ro.observe(cv) let raf = 0 const draw = () => { sync() const w = cv.width const h = cv.height ctx.clearRect(0, 0, w, h) // center baseline ctx.strokeStyle = 'rgba(255,255,255,0.06)' ctx.lineWidth = 1 ctx.beginPath() ctx.moveTo(0, h / 2) ctx.lineTo(w, h / 2) ctx.stroke() // waveform — centered, clamped to ±45% of height so peaks never clip const buf = wave.current const n = buf.length if (n > 1) { ctx.strokeStyle = impactRef.current ? '#ff8a5c' : '#7fa8ff' ctx.lineWidth = 1.6 * dpr ctx.lineJoin = 'round' ctx.beginPath() for (let i = 0; i < n; i++) { const x = (i / (n - 1)) * w const v = Math.max(-1, Math.min(1, buf[i])) const y = h / 2 - v * (h * 0.45) i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y) } ctx.stroke() } raf = requestAnimationFrame(draw) } raf = requestAnimationFrame(draw) return () => { cancelAnimationFrame(raf) ro.disconnect() } }, [wave]) return }