非零环绕规则通过计算绕数决定填充区域:射线与路径交点按方向赋±1,绕数和非零则填充;开放路径会自动闭合,但手动closepath更可靠;多子路径绕数叠加,逆向内路径可实现“镂空”。

Canvas 绘制复杂矢量路径时,填充(fill())不是简单“把封闭区域涂满”,而是依赖一套明确的数学规则——核心是非零环绕规则(Non-zero Winding Rule)。它决定哪些区域被认定为“内部”,从而参与填充,尤其在自交、嵌套、逆向子路径等复杂情况下起决定性作用。
非零环绕规则怎么判断“该不该填”
Canvas 会为路径中每一段线段计算一个“绕数(winding number)”:从待判断点出发,向任意方向画一条射线,统计该射线与路径所有线段的交点,并为每个交点赋予 +1 或 −1 的方向权重:
- 路径从下往上穿过射线 → +1
- 路径从上往下穿过射线 → −1
- 所有交点绕数值之和 ≠ 0 → 填充该点所在区域
- 绕数值 = 0 → 不填充(例如两个同心反向矩形之间的环形区)
这意味着:即使视觉上看起来是“空心”的图形(如两个重叠但绘制方向相反的圆),只要绕数非零,中间区域仍可能被填充。
闭合路径不是填充的必要条件,但强烈影响结果
很多人误以为必须 closePath() 才能填充,其实不然:
- 开放路径也能调用
fill(),Canvas 会自动用直线连接首尾点形成封闭区域再计算绕数 - 但这种自动闭合可能产生意外形状(比如锯齿或翻转区域),导致填充结果不符合预期
-
closePath()的真正价值是显式控制闭合方式,避免歧义;对贝塞尔曲线、多段弧组成的图形,手动闭合更可靠
子路径与多段路径的绕数叠加
一个 beginPath() 内可包含多个子路径(比如先画外圈矩形,再 moveTo() 到内圈画小圆):
- 每个子路径独立贡献绕数,最终按点所在位置的总绕数决定是否填充
- 若内圈子路径是逆向绘制(如用
arc(..., true)逆时针画圆),其绕数为负,可“挖空”外圈区域(类似 SVG 的evenodd效果,但 Canvas 默认不用此规则) - 想实现“镂空”效果,最稳妥做法是:外路径顺时针 → 内路径逆时针 → 全部纳入同一路径 →
fill()
实际调试建议
面对填充异常,优先检查三件事:
- 是否遗漏
beginPath()?未重置路径会导致前序子路径干扰当前绕数计算 - 控制点或端点坐标是否有 NaN/Infinity?会导致路径断裂,绕数计算失效
- 是否混用
rect()和手动路径?rect()是顺时针闭合的,而手写lineTo()若顺序错乱,绕数符号就反了
不复杂但容易忽略











