Canvas绘制不规则图形的关键在于路径定义清晰、闭合准确及fillRule可控;fill()配合fillRule("nonzero"或"evenodd")和closePath()可实现镂空、环形等效果,stroke()不受fillRule影响且不自动闭合。

Canvas 绘制不规则图形,关键不在“画得多复杂”,而在于路径是否定义清晰、闭合是否准确、填充逻辑是否可控。fill() 看似简单,但配合 fillRule 和 closePath(),能实现裁剪、镂空、环形、自相交区域选择等高级效果。
fillRule 决定“哪里算里面”
默认 fill() 使用 "nonzero"(非零环绕规则),它通过路径方向叠加计数判断填充区域:从任意点引一条射线,统计与路径的交点方向(顺时针 +1,逆时针 −1),总和 ≠ 0 就填充。这个规则天然支持“圆环”——外圆逆时针、内圆顺时针,中间区域计数为 0,自动留白。
换成 "evenodd" 则更直观:射线穿过路径奇数次 → 填充;偶数次 → 不填。适合做反向遮罩,比如在大矩形里挖多个小洞。
用法很简单:
- 设置规则:
ctx.fillRule = "evenodd";或"nonzero" - 必须在
fill()前设置,对后续所有 fill() 生效 - 描边
stroke()不受 fillRule 影响
closePath 是“连回起点”,不是“自动画线”
closePath() 只做一件事:从当前终点画一条直线回到路径起点(即最后一次 moveTo() 的位置)。它本身不渲染,也不改变颜色或线宽。
常见误区:
- 没调
stroke()或fill()→ 什么也看不到 - 中间误用了
moveTo()→ 起点被重设,closePath()连错位置 - 只用
lineTo()没moveTo()→ 起点默认是 (0, 0),闭合线可能飞到画布左上角
正确三角形示例:
ctx.beginPath(); ctx.moveTo(50, 50); ctx.lineTo(150, 50); ctx.lineTo(100, 150); ctx.closePath(); // ← 连回 (50, 50) ctx.fill(); // ← 必须调用才显示
fill() 隐式闭合 vs stroke() 严格按路径走
这是最容易踩坑的区别:
-
fill()在执行前会自动补一条线闭合路径(等效于先 closePath() 再 fill),所以即使漏写closePath(),填充看起来仍是闭合的 -
stroke()完全不补线,终点和起点之间就是断开的,视觉上是“开口”的 - 若先
fill()再stroke(),描边仍按原始未闭合路径绘制,不会因 fill 的隐式闭合而改变
验证技巧:画一个三边 lineTo() 后只 fill() —— 区域闭合;只 stroke() —— 第三边缺失。
组合路径实现复杂不规则图形
多个子路径可共存于一次 beginPath() 中,closePath() 只关闭最近一次未闭合的路径段。这让你能轻松画出带孔的形状、多部件图标或布尔运算效果:
- 外轮廓逆时针 → 填充主体
- 内孔顺时针 → 在 nonzero 规则下抵消填充区域
- 用
moveTo()跳转到新起点,开启第二段路径
例如圆环:
ctx.beginPath(); ctx.arc(100, 100, 60, 0, Math.PI * 2, false); // 外圆,逆时针 ctx.arc(100, 100, 30, 0, Math.PI * 2, true); // 内圆,顺时针 ctx.fill(); // 自动按 nonzero 规则处理,中间留空


















