// 爽点节拍图:把 pace.beat_map(int 序列)映射成 ▁▃▅ 风格的 sparkline 格子(UX §6.4)。 // 纯逻辑,便于 node 环境单测;渲染时配合高度类与 prefers-reduced-motion。 // 七级火花字符(低→高),与高度档位一一对应。 export const SPARK_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇"] as const; export interface BeatCell { // 原始强度值。 value: number; // 归一化档位 0..6(映射到 SPARK_CHARS / 高度百分比)。 level: number; // 对应火花字符(▁..▇)。 char: string; // 渲染用高度百分比(12%..100%,最低也可见)。 heightPct: number; } const MIN_HEIGHT_PCT = 12; const MAX_LEVEL = SPARK_CHARS.length - 1; // 把 beat_map 映射成可渲染的格子序列。 // 归一化按「本序列 min..max」线性分桶到 0..6;全相等 → 全部居中档(level 3)。 export function toBeatCells(beatMap: readonly number[]): BeatCell[] { const values = beatMap.filter((v) => typeof v === "number" && Number.isFinite(v)); if (values.length === 0) return []; const min = Math.min(...values); const max = Math.max(...values); const span = max - min; return values.map((value) => { const level = span === 0 ? Math.round(MAX_LEVEL / 2) : Math.round(((value - min) / span) * MAX_LEVEL); const clamped = Math.min(MAX_LEVEL, Math.max(0, level)); const heightPct = MIN_HEIGHT_PCT + (clamped / MAX_LEVEL) * (100 - MIN_HEIGHT_PCT); return { value, level: clamped, char: SPARK_CHARS[clamped] ?? SPARK_CHARS[0], heightPct: Math.round(heightPct), }; }); } // 文本版 sparkline(无障碍/紧凑场景;UX §6.4 示例 ▁▃▅▂▁▇)。 export function toSparkline(beatMap: readonly number[]): string { return toBeatCells(beatMap) .map((c) => c.char) .join(""); }