import { useEffect, useRef, useState } from "react"; import { useT } from "../i18n"; interface Props { scores: number[]; threshold: number; onsetIndex?: number; stepSeconds?: number; width?: number; height?: number; } const NARROW_TIMELINE_WIDTH = 420; const NARROW_TIMELINE_PADDING = 22; const DESKTOP_TIMELINE_PADDING = 28; export default function ScoreTimeline({ scores, threshold, onsetIndex, stepSeconds = 10, width, height, }: Props) { const t = useT(); const ref = useRef(null); const [box, setBox] = useState({ w: 600, h: 180 }); useEffect(() => { if (width != null && height != null) return; if (!ref.current) return; const node = ref.current; const observer = new ResizeObserver((entries) => { for (const entry of entries) { const { width: w, height: h } = entry.contentRect; if (w <= 0 || h <= 0) continue; setBox({ w: Math.max(280, w), h: Math.max(140, h) }); } }); observer.observe(node); return () => observer.disconnect(); }, [width, height]); const w = width ?? box.w; const h = height ?? box.h; const pad = w < NARROW_TIMELINE_WIDTH ? NARROW_TIMELINE_PADDING : DESKTOP_TIMELINE_PADDING; const count = scores.length; const maxScore = Math.max(threshold, ...scores) * 1.15 || 1; const x = (i: number) => pad + (count > 1 ? (i / (count - 1)) * (w - 2 * pad) : 0); const y = (s: number) => h - pad - (s / maxScore) * (h - 2 * pad); const line = scores .map((s, i) => `${i === 0 ? "M" : "L"}${x(i).toFixed(1)},${y(s).toFixed(1)}`) .join(" "); const thresholdY = y(threshold); return (
{t.timeline.threshold} {onsetIndex != null && ( )} {scores.map((s, i) => s >= threshold ? : null, )} 0s {(count - 1) * stepSeconds}s
); }