Skip to content
File

Blob: worker/src/client/exhibit/spectrum/spectrum-canvas.tsx

typescript65 lines
1import { useEffect, useRef } from 'react';
2import type { SignalBuffer } from '@/client/radio/session-state.ts';
3 
4/** Draw the board's bands, never an FFT of audio in the browser. */
5export function SpectrumCanvas({ signal }: { signal: SignalBuffer }) {
6 const ref = useRef<HTMLCanvasElement>(null);
7 useEffect(() => {
8 const canvas = ref.current;
9 const context = canvas?.getContext('2d');
10 if (!canvas || !context) return;
11 const shown = new Float32Array(32);
12 const reduced = matchMedia('(prefers-reduced-motion: reduce)');
13 let frame = 0,
14 last = 0;
15 const draw = (now: number) => {
16 frame = requestAnimationFrame(draw);
17 if (document.hidden || now - last < (reduced.matches ? 100 : 32)) return;
18 const dt = Math.min(0.1, (now - last) / 1000);
19 last = now;
20 const width = canvas.clientWidth,
21 height = canvas.clientHeight,
22 scale = Math.min(devicePixelRatio || 1, 2);
23 if (
24 canvas.width !== Math.round(width * scale) ||
25 canvas.height !== Math.round(height * scale)
26 ) {
27 canvas.width = Math.round(width * scale);
28 canvas.height = Math.round(height * scale);
29 }
30 context.setTransform(scale, 0, 0, scale, 0, 0);
31 context.clearRect(0, 0, width, height);
32 context.strokeStyle = '#d9e1da';
33 context.lineWidth = 0.7;
34 for (let row = 0; row < 4; row++) {
35 const y = Math.round((row * height) / 4) + 0.5;
36 context.beginPath();
37 context.moveTo(0, y);
38 context.lineTo(width, y);
39 context.stroke();
40 }
41 const fresh = signal.at > 0 && now - signal.at < 1200;
42 for (let i = 0; i < 32; i++) {
43 const target = fresh && signal.frame ? signal.frame.bands[i] / 255 : 0;
44 shown[i] = reduced.matches
45 ? target
46 : shown[i] + (target - shown[i]) * Math.min(1, dt * (target > shown[i] ? 22 : 8));
47 const barHeight = Math.max(2, shown[i] * (height - 5)),
48 barWidth = Math.max(1, width / 32 - 3);
49 context.fillStyle = fresh ? '#2b7463' : '#b7c9bd';
50 context.fillRect((i * width) / 32, height - barHeight, barWidth, barHeight);
51 }
52 };
53 frame = requestAnimationFrame(draw);
54 return () => cancelAnimationFrame(frame);
55 }, [signal]);
56 return (
57 <canvas
58 ref={ref}
59 className="block h-20 w-full"
60 role="img"
61 aria-label="32 frequency bands calculated on core 1 of the ESP32-S3"
62 />
63 );
64}