# 实时波形图复现指南

以下是 AI 可执行的复现步骤。严格按顺序完成，每步验证后再继续。

## ⚠️ 铁律

**复制全部 5 个辅助函数，缺一不可。** 只复制绘制函数会因缺少 `chartArea` / `yForValue` / `fillTextInGutter` 而报错。这 5 个函数是绘制函数的硬依赖，不存在"只复制部分"的选项。

## 工作流清单

```
- [ ] 1. 创建 HTML 骨架 ⛔ 前置条件
  - [ ] 1.1 放置 <canvas>，CSS: width:100%; height:260px; border-radius:10px; background:#fff
  - [ ] 1.2 确认 CSS 尺寸与 JS devicePixelRatio 适配不冲突
- [ ] 2. 复制辅助函数层 ⚠️ 必须全部复制（5 个，约 30 行）
  - [ ] 2.1 resizeCanvas(canvas)
  - [ ] 2.2 clamp(value, min, max)
  - [ ] 2.3 chartArea(canvas)
  - [ ] 2.4 yForValue(value, min, max, area)
  - [ ] 2.5 fillTextInGutter(ctx, area, text, y)
- [ ] 3. 复制绘制函数
  - [ ] 3.1 drawGrid + drawTimelineTicks + drawTimelineLine + drawHorizontalLine（约 60 行）
  - [ ] 3.2 drawTimelineWaveform（约 70 行）
  - [ ] 3.3 检查所有引用的辅助函数是否已复制
- [ ] 4. 接入数据源
  - [ ] 4.1 纯数值数组 → drawTimelineWaveform(canvas, samples, {strokeStyle, ...})
  - [ ] 4.2 带质量标记 → drawTimelineWaveform(canvas, samples, {segmentOk, ticks, overlays, ...})
  - [ ] 4.3 新数据 push，超出上限 shift，每次更新调用绘制函数
- [ ] 5. 验证 ⚠️ 必须逐项检查
  - [ ] 5.1 白底显示正常（Canvas 区域为纯白 #fff）
  - [ ] 5.2 浅色网格线可见（4 条水平线，颜色 #e2e8f0）
  - [ ] 5.3 右侧刻度数字可见（颜色 #64748b，字体 11px Arial）
  - [ ] 5.4 Retina 屏幕文字清晰不模糊
  - [ ] 5.5 折线颜色与 strokeStyle 一致
```

## 函数清单

### 辅助函数（5 个，约 30 行，必须全部复制）

| 函数 | 职责 |
|------|------|
| `resizeCanvas(canvas)` | 按 devicePixelRatio 设置实际像素尺寸 |
| `clamp(v, lo, hi)` | 钳位 |
| `chartArea(canvas)` | 计算右侧留 34px gutter 的绘图区 |
| `yForValue(v, min, max, area)` | 数值 → Canvas Y 坐标 |
| `fillTextInGutter(ctx, area, text, y)` | 在右侧 gutter 居中绘制刻度文字 |

### 绘制函数

| 函数 | 适用场景 | 关键参数 |
|------|------|------|
| `drawTimelineWaveform(canvas, samples, opts)` | 通用波形图 | `opts.strokeStyle`, `opts.sampleValue`, `opts.fixedRange`, `opts.ticks`, `opts.areaFill`, `opts.segmentOk`, `opts.showLatest`, `opts.overlays` |

### 数据格式

```
纯数值数组：       samples = [number, number, ...]
带质量标记的趋势： samples = [{bpm, quality, ts}, ...] + opts.sampleValue
```

### 关键 opts 参数速查

| 参数 | 类型 | 作用 |
|------|------|------|
| `strokeStyle` | string | 折线颜色 |
| `mutedStrokeStyle` | string | 无效段折线颜色 |
| `areaFill` | string | 面积渐变起色 |
| `mutedAreaFill` | string | 无效段面积起色 |
| `fixedRange` | `[min, max]` | 固定 Y 轴范围，不传则自适应 +12% padding |
| `ticks` | `[number, ...]` | 固定刻度线数组，每档带右侧标注 |
| `sampleValue` | `fn(sample) => number` | 从结构化数据中提取数值 |
| `segmentOk` | `fn(next, prev) => boolean` | 逐段判断是否有效 |
| `showLatest` | boolean | 是否绘制最新点高亮（白底圆点） |
| `latestStrokeStyle` | `fn(sample) => string` | 最新点外圈颜色 |
| `overlays` | `[{type:"horizontal", value, strokeStyle}, ...]` | 叠加图层（中位虚线等） |

## 常见错误

- ❌ **只复制绘制函数，不复制辅助函数** → `chartArea is not defined`。解决：复制全部 5 个辅助函数。
- ❌ **忘记 devicePixelRatio 适配** → Retina 屏幕模糊。解决：布局参数、线宽、字体、圆点半径全部 `× devicePixelRatio`。用 `window.devicePixelRatio || 1` 兜底。
- ❌ **max === min 时没兜底** → 分母为零，产生 NaN。解决：`if (max === min) max = min + 1`。
- ❌ **Canvas CSS 尺寸和实际像素混淆** → CSS `width:100%` ≠ JS `canvas.width`。解决：始终先调 `resizeCanvas`。
- ❌ **首帧只有 1 个数据点** → `stroke()` 需要至少一次 `lineTo`，单一 `moveTo` 不可见。解决：初始化时预填 2 个点。
- ❌ **fillTextInGutter 传入数字** → `ctx.measureText(number)` 可能异常。解决：用 `.toFixed(0)` 或 `String()` 转换。
- ❌ **setInterval 里的函数引用错误** → `setInterval(drawChart, 200)` 中 `drawChart` 必须在闭包内可见。解决：IIFE 包裹或确保函数在正确作用域。

## 完成检查

复制代码后，逐项自检：

```
- [ ] 无报错：浏览器控制台无红色错误
- [ ] 白底：Canvas 区域为纯白 #fff
- [ ] 网格：4 条水平线均匀分布，颜色 #e2e8f0
- [ ] 刻度：右侧显示 Y 轴范围数字，颜色 #64748b
- [ ] 折线：颜色与 strokeStyle 一致，线宽约 1.2 CSS px
- [ ] 面积：从折线向下渐变填充到 Canvas 底部
- [ ] 最新点：右侧白底圆点，外圈与折线同色
- [ ] 高分屏：2x/3x 屏幕上文字清晰
- [ ] 分段着色：segmentOk=true 段红色，false 段灰色
- [ ] 中位线：虚线水平线，颜色 rgba(255,85,95,.38)
```
