# 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 实现了丰富的图形绘制功能,每个绘制函数都经过精心设计,既保证了视觉效果,又兼顾了性能表现。理解这些绘制原理对于进一步扩展和优化编辑器功能具有重要意义。