# Canvas 2D 绘制技术详解

## 📖 概述

本文档详细分析场景编辑器中的自定义绘制函数，这些函数基于 **HTML5 Canvas 2D API** 在浏览器页面上绘制各种图形元素（点位、路线、区域、机器人）。

## 🎯 核心技术栈

### 1. HTML5 Canvas 2D API

- **技术原理**：Canvas 是 HTML5 提供的位图绘制 API
- **绘制方式**：使用 JavaScript 在 Canvas 画布上逐像素绘制
- **坐标系统**：左上角为原点 (0,0)，X轴向右，Y轴向下
- **绘制上下文**：通过 `CanvasRenderingContext2D` 对象进行所有绘制操作

### 2. Meta2D 引擎集成

- **自定义绘制**：通过 `registerCanvasDraw()` 注册自定义绘制函数
- **图形对象**：每个绘制函数接收 `MapPen` 对象，包含图形的所有属性
- **渲染时机**：引擎在每次重绘时自动调用对应的绘制函数

---

## 🎨 绘制函数详细分析

### 1. 点位绘制函数 `drawPoint()`

#### 函数签名和参数

```typescript
function drawPoint(ctx: CanvasRenderingContext2D, pen: MapPen): void;
```

#### 代码逐行分析

```typescript
// 1. 获取全局主题配置
const theme = sTheme.editor;
```

**分析**：从全局主题服务获取编辑器主题配置，用于确定颜色、样式等视觉属性。

```typescript
// 2. 从计算属性中提取绘制参数
const { active, iconSize: r = 0, fontSize = 14, lineHeight = 1.5, fontFamily } = pen.calculative ?? {};
```

**分析**：

- `active`：图形是否处于选中状态
- `iconSize`：图标大小，重命名为 `r`（半径）
- `fontSize/lineHeight/fontFamily`：文本绘制参数

```typescript
// 3. 获取世界坐标系下的矩形区域
const { x = 0, y = 0, width: w = 0, height: h = 0 } = pen.calculative?.worldRect ?? {};
```

**分析**：Meta2D 引擎会自动计算图形在世界坐标系下的实际位置和大小。

```typescript
// 4. 获取业务属性
const { type } = pen.point ?? {};
const { label = '' } = pen ?? {};
```

```typescript
// 5. 保存当前画布状态
ctx.save();
```

**分析**：`save()` 保存当前的绘制状态（变换矩阵、样式等），避免影响其他图形。

#### 小点位绘制（类型1-9）

```typescript
switch (type) {
  case MapPointType.普通点:
  case MapPointType.等待点:
  case MapPointType.避让点:
  case MapPointType.临时避让点:
    // 绘制圆角菱形
    ctx.beginPath();
    ctx.moveTo(x + w / 2 - r, y + r);
    ctx.arcTo(x + w / 2, y, x + w - r, y + h / 2 - r, r);
    ctx.arcTo(x + w, y + h / 2, x + w / 2 + r, y + h - r, r);
    ctx.arcTo(x + w / 2, y + h, x + r, y + h / 2 + r, r);
    ctx.arcTo(x, y + h / 2, x + r, y + h / 2 - r, r);
    ctx.closePath();
```

**分析**：

- `beginPath()`：开始新的绘制路径
- `moveTo()`：移动画笔到起始点
- `arcTo()`：绘制圆弧连接线，创建圆角效果
- `closePath()`：闭合路径形成完整图形

```typescript
// 填充背景色
ctx.fillStyle = get(theme, `point-s.fill-${type}`) ?? '';
ctx.fill();

// 绘制边框
ctx.strokeStyle = get(theme, active ? 'point-s.strokeActive' : 'point-s.stroke') ?? '';
```

**分析**：根据点位类型和激活状态设置不同的填充色和边框色。

#### 临时避让点特殊标记

```typescript
if (type === MapPointType.临时避让点) {
  ctx.lineCap = 'round'; // 设置线条端点为圆形
  ctx.beginPath();
  // 绘制8个短线标记，形成放射状效果
  ctx.moveTo(x + 0.66 * r, y + h / 2 - 0.66 * r);
  ctx.lineTo(x + r, y + h / 2 - r);
  // ... 其他7个方向的短线
}
```

**分析**：在菱形的8个方向绘制短线，形成特殊的视觉标识。

#### 大点位绘制（类型11+）

```typescript
case MapPointType.电梯点:
case MapPointType.自动门点:
case MapPointType.充电点:
case MapPointType.停靠点:
case MapPointType.动作点:
case MapPointType.禁行点:
  ctx.roundRect(x, y, w, h, r);  // 绘制圆角矩形
  ctx.strokeStyle = get(theme, active ? 'point-l.strokeActive' : 'point-l.stroke') ?? '';
  ctx.stroke();
```

**分析**：大点位使用圆角矩形，通过 `roundRect()` API 一次性绘制。

#### 文本标签绘制

```typescript
// 设置文本样式
ctx.fillStyle = get(theme, 'color') ?? '';
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
ctx.textAlign = 'center'; // 水平居中
ctx.textBaseline = 'top'; // 垂直顶部对齐

// 在点位上方绘制标签
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
```

```typescript
// 恢复画布状态
ctx.restore();
```

---

### 2. 路线绘制函数 `drawLine()`

#### 核心绘制逻辑

```typescript
// 1. 获取路线的两个端点坐标
const [p1, p2] = pen.calculative?.worldAnchors ?? [];
const { x: x1 = 0, y: y1 = 0 } = p1 ?? {};
const { x: x2 = 0, y: y2 = 0 } = p2 ?? {};
```

```typescript
// 2. 获取路线属性
const { type, direction = 1, pass = 0, c1, c2 } = pen.route ?? {};
const { x: dx1 = 0, y: dy1 = 0 } = c1 ?? {}; // 控制点1偏移
const { x: dx2 = 0, y: dy2 = 0 } = c2 ?? {}; // 控制点2偏移
```

#### 路线类型绘制

```typescript
ctx.moveTo(x1, y1); // 移动到起点
switch (type) {
  case MapRouteType.直线:
    ctx.lineTo(x2, y2); // 直接连线到终点
    break;

  case MapRouteType.二阶贝塞尔曲线:
    // 使用一个控制点绘制曲线
    ctx.quadraticCurveTo(x1 + dx1 * s, y1 + dy1 * s, x2, y2);
    break;

  case MapRouteType.三阶贝塞尔曲线:
    // 使用两个控制点绘制更复杂的曲线
    ctx.bezierCurveTo(x1 + dx1 * s, y1 + dy1 * s, x2 + dx2 * s, y2 + dy2 * s, x2, y2);
    break;
}
```

**贝塞尔曲线原理**：

- **二阶贝塞尔曲线**：由起点、一个控制点、终点定义的曲线
- **三阶贝塞尔曲线**：由起点、两个控制点、终点定义的更灵活曲线
- **数学公式**：基于参数方程计算曲线上的每个点

#### 禁行路线绘制

```typescript
if (pass === MapRoutePassType.禁行) {
  ctx.setLineDash([s * 5]); // 设置虚线样式
}
ctx.stroke(); // 绘制路线
```

#### 方向箭头绘制

```typescript
// 1. 计算箭头角度
let r = (() => {
  switch (type) {
    case MapRouteType.直线:
      return Math.atan2(y2 - y1, x2 - x1); // 直线的角度
    case MapRouteType.二阶贝塞尔曲线:
      // 根据控制点计算切线角度
      return direction < 0 ? Math.atan2(dy1 * s, dx1 * s) : Math.atan2(y2 - y1 - dy1 * s, x2 - x1 - dx1 * s);
    // ...
  }
})();
```

```typescript
// 2. 移动坐标系到箭头位置
if (direction < 0) {
  ctx.translate(x1, y1); // 反向箭头在起点
} else {
  ctx.translate(x2, y2); // 正向箭头在终点
  r += Math.PI; // 旋转180度
}

// 3. 绘制箭头（两条线段形成尖角）
ctx.moveTo(Math.cos(r + Math.PI / 5) * s * 10, Math.sin(r + Math.PI / 5) * s * 10);
ctx.lineTo(0, 0);
ctx.lineTo(Math.cos(r - Math.PI / 5) * s * 10, Math.sin(r - Math.PI / 5) * s * 10);
```

**箭头绘制原理**：

- 使用三角函数计算箭头两条边的端点
- `Math.PI / 5` (36度) 是箭头的张开角度
- 通过坐标变换将箭头定位到正确位置和角度

---

### 3. 区域绘制函数 `drawArea()`

#### 矩形区域绘制

```typescript
// 1. 绘制填充矩形
ctx.rect(x, y, w, h); // 定义矩形路径
ctx.fillStyle = get(theme, `area.fill-${type}`) ?? ''; // 设置填充色
ctx.fill(); // 填充矩形

// 2. 绘制边框
ctx.strokeStyle = get(theme, active ? 'area.strokeActive' : `area.stroke-${type}`) ?? '';
ctx.stroke(); // 绘制边框
```

#### 区域标签

```typescript
// 在区域上方居中显示标签
ctx.fillStyle = get(theme, 'color') ?? '';
ctx.font = `${fontSize}px/${lineHeight} ${fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(label, x + w / 2, y - fontSize * lineHeight);
```

---

### 4. 机器人绘制函数 `drawRobot()`

#### 机器人本体绘制

```typescript
const ox = x + w / 2; // 机器人中心X坐标
const oy = y + h / 2; // 机器人中心Y坐标

// 绘制椭圆形机器人
ctx.ellipse(ox, oy, w / 2, h / 2, 0, 0, Math.PI * 2);
ctx.fillStyle = get(theme, 'robot.fill') ?? '';
ctx.fill();
ctx.strokeStyle = get(theme, 'robot.stroke') ?? '';
ctx.stroke();
```

#### 路径轨迹绘制

```typescript
if (path?.length) {
  // 设置路径样式
  ctx.strokeStyle = get(theme, 'robot.line') ?? '';
  ctx.lineCap = 'round'; // 圆形线帽
  ctx.lineWidth = s * 4; // 粗线条
  ctx.setLineDash([s * 5, s * 10]); // 虚线样式

  // 坐标变换：移动到机器人中心并旋转
  ctx.translate(ox, oy);
  ctx.rotate((-deg * Math.PI) / 180);

  // 绘制路径线条
  ctx.beginPath();
  ctx.moveTo(0, 0);
  path.forEach((d) => ctx.lineTo(d.x * s, d.y * s));
  ctx.stroke();
}
```

#### 路径终点箭头

```typescript
// 计算路径最后两个点的方向
const { x: ex1 = 0, y: ey1 = 0 } = nth(path, -1) ?? {}; // 最后一个点
const { x: ex2 = 0, y: ey2 = 0 } = nth(path, -2) ?? {}; // 倒数第二个点
const r = Math.atan2(ey1 - ey2, ex1 - ex2) + Math.PI;

// 在路径终点绘制箭头
ctx.translate(ex1 * s, ey1 * s);
ctx.beginPath();
ctx.moveTo(Math.cos(r + Math.PI / 4) * s * 10, Math.sin(r + Math.PI / 4) * s * 10);
ctx.lineTo(0, 0);
ctx.lineTo(Math.cos(r - Math.PI / 4) * s * 10, Math.sin(r - Math.PI / 4) * s * 10);
ctx.stroke();
```

---

## 🔧 Canvas 2D API 核心方法说明

### 路径绘制方法

| 方法                                          | 功能               | 示例                 |
| --------------------------------------------- | ------------------ | -------------------- |
| `beginPath()`                                 | 开始新的绘制路径   | 每次绘制新图形前调用 |
| `moveTo(x, y)`                                | 移动画笔到指定位置 | 设置绘制起点         |
| `lineTo(x, y)`                                | 画直线到指定位置   | 绘制线段             |
| `arcTo(x1, y1, x2, y2, r)`                    | 绘制圆弧连接       | 创建圆角效果         |
| `quadraticCurveTo(cpx, cpy, x, y)`            | 二阶贝塞尔曲线     | 简单曲线             |
| `bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)` | 三阶贝塞尔曲线     | 复杂曲线             |
| `rect(x, y, w, h)`                            | 矩形路径           | 绘制矩形             |
| `ellipse(x, y, rx, ry, rotation, start, end)` | 椭圆路径           | 绘制椭圆/圆形        |
| `closePath()`                                 | 闭合当前路径       | 连接起点和终点       |

### 样式设置方法

| 属性/方法            | 功能             | 示例                            |
| -------------------- | ---------------- | ------------------------------- |
| `fillStyle`          | 设置填充颜色     | `ctx.fillStyle = '#ff0000'`     |
| `strokeStyle`        | 设置描边颜色     | `ctx.strokeStyle = '#0000ff'`   |
| `lineWidth`          | 设置线条宽度     | `ctx.lineWidth = 2`             |
| `lineCap`            | 设置线条端点样式 | `'round'`, `'square'`, `'butt'` |
| `setLineDash([...])` | 设置虚线样式     | `ctx.setLineDash([5, 5])`       |

### 变换方法

| 方法                             | 功能         | 说明           |
| -------------------------------- | ------------ | -------------- |
| `translate(x, y)`                | 平移坐标系   | 移动原点位置   |
| `rotate(angle)`                  | 旋转坐标系   | 按弧度旋转     |
| `setTransform(a, b, c, d, e, f)` | 重置变换矩阵 | 恢复标准坐标系 |

### 状态管理方法

| 方法        | 功能         | 说明       |
| ----------- | ------------ | ---------- |
| `save()`    | 保存当前状态 | 压入状态栈 |
| `restore()` | 恢复之前状态 | 弹出状态栈 |

---

## 🎯 绘制流程总结

### 1. 标准绘制流程

```typescript
function customDraw(ctx: CanvasRenderingContext2D, pen: MapPen): void {
  // 1. 保存画布状态
  ctx.save();

  // 2. 提取绘制参数
  const { x, y, width, height } = pen.calculative?.worldRect ?? {};

  // 3. 设置样式属性
  ctx.fillStyle = '填充色';
  ctx.strokeStyle = '边框色';

  // 4. 创建绘制路径
  ctx.beginPath();
  // ... 具体绘制操作

  // 5. 执行绘制
  ctx.fill(); // 填充
  ctx.stroke(); // 描边

  // 6. 恢复画布状态
  ctx.restore();
}
```

### 2. 性能优化要点

- **状态管理**：及时调用 `save()` 和 `restore()` 避免状态污染
- **路径复用**：合理使用 `beginPath()` 清除之前的路径
- **批量绘制**：同类型图形可以合并绘制操作
- **避免重复计算**：缓存复杂的数学计算结果

---

## 📊 技术优势

### 1. Canvas 2D 的优势

- **高性能**：直接操作像素，渲染速度快
- **灵活性**：可以绘制任意复杂的图形
- **交互性**：支持鼠标事件检测和处理
- **兼容性**：现代浏览器完全支持

### 2. 自定义绘制的优势

- **个性化**：完全定制化的视觉效果
- **主题支持**：动态切换颜色主题
- **状态反馈**：不同状态显示不同样式
- **扩展性**：易于添加新的图形类型

---

## 🔚 结语

本场景编辑器通过 Canvas 2D API 实现了丰富的图形绘制功能，每个绘制函数都经过精心设计，既保证了视觉效果，又兼顾了性能表现。理解这些绘制原理对于进一步扩展和优化编辑器功能具有重要意义。
