// Pure canvas painters. No React, no DOM events. Re-painted only when peaks // change, the container resizes, or theme tokens change. See ADR-003. import { backingHeight, backingWidth, getDpr } from '../../utils/dpr'; export type PaintPeaksOptions = { color: string; background?: string; barWidth: number; barGap: number; minBarHeight?: number; }; function resizeCanvas(canvas: HTMLCanvasElement): { ctx: CanvasRenderingContext2D; cssW: number; cssH: number } | null { const cssW = canvas.clientWidth; const cssH = canvas.clientHeight; if (cssW === 0 || cssH === 0) return null; const dpr = getDpr(); const w = backingWidth(cssW, dpr); const h = backingHeight(cssH, dpr); if (canvas.width !== w) canvas.width = w; if (canvas.height !== h) canvas.height = h; const ctx = canvas.getContext('2d', { alpha: true, desynchronized: true }); if (!ctx) return null; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); return { ctx, cssW, cssH }; } export function paintPeaks( canvas: HTMLCanvasElement, peaks: Float32Array, opts: PaintPeaksOptions, ): void { const sized = resizeCanvas(canvas); if (!sized) return; const { ctx, cssW, cssH } = sized; ctx.clearRect(0, 0, cssW, cssH); if (opts.background) { ctx.fillStyle = opts.background; ctx.fillRect(0, 0, cssW, cssH); } if (peaks.length === 0) return; const step = Math.max(1, opts.barWidth + opts.barGap); const numBars = Math.max(1, Math.floor(cssW / step)); const mid = cssH / 2; const minH = opts.minBarHeight ?? 1; ctx.fillStyle = opts.color; for (let i = 0; i < numBars; i++) { const peakIdx = Math.min(peaks.length - 1, Math.floor((i / numBars) * peaks.length)); const amp = peaks[peakIdx]; const h = Math.max(minH, amp * cssH); const x = i * step; ctx.fillRect(x, mid - h / 2, opts.barWidth, h); } } export type PaintLiveOptions = { color: string; barWidth: number; barGap: number; minBarHeight?: number; }; export function paintLive( canvas: HTMLCanvasElement, levels: Float32Array, opts: PaintLiveOptions, ): void { const sized = resizeCanvas(canvas); if (!sized) return; const { ctx, cssW, cssH } = sized; ctx.clearRect(0, 0, cssW, cssH); if (levels.length === 0) return; const step = Math.max(1, opts.barWidth + opts.barGap); const numBars = Math.max(1, Math.floor(cssW / step)); const mid = cssH / 2; const minH = opts.minBarHeight ?? 1; // Concentrate visible energy in the lower frequency bands. const usable = Math.floor(levels.length * 0.7); ctx.fillStyle = opts.color; for (let i = 0; i < numBars; i++) { const idx = Math.min(usable - 1, Math.floor((i / numBars) * usable)); const v = levels[idx] ?? 0; const h = Math.max(minH, v * cssH); ctx.fillRect(i * step, mid - h / 2, opts.barWidth, h); } }