Canvas 路径与形状:从直线、曲线到复合路径

在 Canvas 里,真正灵活的绘制能力几乎都来自「路径」:从简单的折线到圆弧、贝塞尔曲线,再到由多个子路径构成的复杂形状。
这一篇聚焦路径相关 API,尝试把几件事讲清楚:路径是怎么被构建和复用的、常用的直线/曲线/圆弧怎么组合,以及在工程里如何把复杂图形封装成可维护的形状函数。

在 Canvas 2D API 中,路径的使用大致遵循一个流程:

  1. beginPath():开始一条新路径;
  2. 用一系列命令(moveTolineToarc 等)构建路径;
  3. 通过 fill()stroke() 输出;
  4. 可以选择再次使用同一条路径(例如多次描边/填充),也可以开始下一条路径。

几个要点:

  • beginPath() 会清空当前路径,但不会影响样式和变换等状态;
  • closePath() 会自动把当前点和起始点连接起来,形成闭合路径;
  • 同一条路径既可以被描边又可以被填充,顺序会影响视觉效果(通常先 fillstroke)。

理解这一点后,后面再看复杂路径时,只需要关注「怎么构造」,输出方式可以灵活选择。

构造路径最基础的两块是:

  • moveTo(x, y):把当前绘制点移动到某位置,不画线;
  • lineTo(x, y):从当前点画一条直线到新位置。

一个典型的折线路径:

1ctx.beginPath();
2ctx.moveTo(x0, y0);
3ctx.lineTo(x1, y1);
4ctx.lineTo(x2, y2);
5ctx.lineTo(x3, y3);
6ctx.stroke();

工程实践中的小技巧:

  • 可以把一系列点放在数组里,用循环构造路径;
  • 为重复使用的折线封装函数,例如绘制折线图的线条部分。

arc(x, y, radius, startAngle, endAngle, anticlockwise) 的参数含义是:

  • (x, y):圆心坐标;
  • radius:半径;
  • startAngleendAngle:起止角度(弧度制,0 代表 x 轴正方向);
  • anticlockwise:是否逆时针绘制(默认顺时针)。

常见用法:

  • 画完整圆:
1ctx.beginPath();
2ctx.arc(cx, cy, r, 0, Math.PI * 2);
3ctx.fill();
  • 画扇形(配合 moveToclosePath):
1ctx.beginPath();
2ctx.moveTo(cx, cy);
3ctx.arc(cx, cy, r, start, end);
4ctx.closePath();
5ctx.fill();

arcTo(x1, y1, x2, y2, radius) 会根据当前点和两个目标点定义一个圆弧,常用于:

  • 在两个线段之间连接一个圆角;
  • 封装圆角矩形等形状。

虽然直接用 arcTo 略难直观想象,但它可以避免手算圆角切点坐标,在封装常用形状时非常有用。

quadraticCurveTo(cpX, cpY, x, y) 定义了一条从当前点到 (x, y) 的二次贝塞尔曲线,(cpX, cpY) 是控制点:

  • 控制点位置影响曲线的「拉伸方向」和弯曲程度;
  • 适合简单平滑过渡(如对折线做平滑处理)。

bezierCurveTo(cp1X, cp1Y, cp2X, cp2Y, x, y) 使用两个控制点:

  • 从当前点到 (x, y),中间通过 (cp1X, cp1Y)(cp2X, cp2Y) 控制形状;
  • 适合更复杂的曲线,比如图标轮廓、手写风曲线等。

在工程实践中:

  • 手写控制点坐标会比较枯燥,通常会通过设计工具导出路径数据,再在代码中使用;
  • 也可以封装一些「平滑曲线」函数,用简单规则生成控制点。

Canvas 支持在一条路径中包含多个子路径,常见场景包括:

  • 绘制带洞的形状(例如甜甜圈、环形图块);
  • 同一图形有多个不连续区域。

构造方式大致是:

  • 在一次 beginPath() 之后:
    • 使用多组 moveTo + 绘制命令构造多个子路径;
    • 再统一调用 fill()stroke()

在填充时,Canvas 使用「非零环绕规则」判断内部区域。
简单理解是:

  • 子路径方向(顺时针/逆时针)会影响哪些区域被视为「内部」;
  • 在需要带洞效果时,可以合理调整子路径方向或使用 clip 配合。

在真实项目中,很少直接在业务逻辑里堆一大段路径命令,更好的做法是:

  • 为常用形状封装函数,例如:
    • 圆角矩形:drawRoundedRect(ctx, x, y, w, h, radius)
    • 环形扇区:drawDonutSlice(ctx, cx, cy, innerR, outerR, start, end)
    • 自定义图标:drawCustomIcon(ctx, x, y, size);
  • 在函数内部:
    • 使用路径 API 构造形状;
    • 不直接调用 fillstroke,而是交给调用方决定如何输出。

这样可以:

  • 复用图形构造逻辑;
  • 在不同场景下用不同样式渲染同一种形状。

可以把这一篇的核心点概括为:

  • 路径是 Canvas 绘制的基础单位,通过 beginPath + 一系列构造命令 + fill/stroke 组合完成;
  • 直线、圆弧、贝塞尔曲线可以灵活组合成复杂形状;
  • 多个子路径可以共享一次填充/描边,构成带洞或多部分的图形;
  • 在工程实践中,通过封装常用形状函数,让路径构造逻辑集中管理,既提高可读性,也便于调整与复用。

在掌握路径之后,Canvas 就不再只是「几个矩形和圆」,而是一套可以表达任意 2D 形状的绘制语言。