一、路径(Path)与子路径(Subpath)
1. 什么是路径?
- 是当前所有子路径的集合。
- 在任意时刻,Canvas 上下文中只存在一个“当前路径”。
- Canvas 里所有非矩形的图形,都必须用路径绘制。
- 路径就像一支笔,你先规划路线,最后再
stroke()或fill()。
2. 核心路径 API
-
beginPath()**开始一条新路径,清空之前所有子路径。**不写这个,新绘制会和旧路径连在一起。 -
moveTo(x, y)提笔移动到 (x,y),不画线。 这会创建一条新的子路径。 -
lineTo(x, y)从当前点画线到 (x,y)。 -
closePath()从终点连回起点,形成闭合图形。 不是结束路径,是闭合形状。
3. 子路径是什么?
一条路径里可以有多段互不相连的线,每一段就是子路径。当你使用 moveTo(x, y) 时,就会创建一个新的子路径,它就像一个“提笔移动”的动作,开始一段新的、不连续的线条。
产生新子路径的情况:
beginPath()后moveTo()时arc()、rect()等图形函数内部也会产生子路径
一句话: 一条路径 = 多个子路径,一起 stroke / fill
二、绘制圆、圆弧 arc()
1. arc() 方法用于绘制圆弧或完整的圆形。
语法:
ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise);
| 参数 | 说明 |
|---|---|
x, y | 圆心的坐标。 |
radius | 圆的半径。 |
startAngle | 起始角度,以弧度为单位。0 弧度代表 X 轴正方向(3点钟方向)。 |
endAngle | 结束角度,以弧度为单位。 |
counterclockwise | 可选。true 为逆时针,false(默认)为顺时针。 |
2. 角度规则(重要)
0 弧度 → 向右(3 点钟方向) 顺时针增加。
常用:
- 0 → 右
- Math.PI / 2 → 下
- Math.PI → 左
- Math.PI * 1.5 → 上
- Math.PI * 2 → 一圈
3. 画圆
ctx.arc(100, 100, 50, 0, Math.PI * 2)
ctx.stroke()
4. 画圆弧(半圆、1/4圆)
ctx.arc(100,100,50, 0, Math.PI/2) // 右下 1/4 圆
ctx.arc(100,100,50, 0, Math.PI) // 下半圆
三、切线圆弧 arcTo()
arcTo() 用于在两条直线之间绘制一段平滑的圆弧,常用于制作圆角矩形。它会自动计算与两条线都相切的圆弧。
语法:
ctx.arcTo(x1, y1, x2, y2, radius);
| 参数 | 说明 |
|---|---|
x1, y1 | 第一个控制点(两条假想线的交点)。 |
x2, y2 | 第二个控制点(圆弧的终点方向)。 |
radius | 圆弧的半径。 |
工作原理:
想象你要从当前点画一条线到 (x1, y1),然后再画一条线到 (x2, y2)。arcTo() 会在拐角处,用一段半径为 radius 的圆弧来替代尖锐的拐角。
- 当前点 → (x1,y1) 一条线
- (x1,y1) → (x2,y2) 一条线
- 在拐角处画一个指定半径的圆角,与两条线相切
ctx.beginPath();
ctx.moveTo(20, 20); // 1. 设置起点
ctx.lineTo(100, 20); // 2. 画第一条线
ctx.arcTo(150, 20, 150, 75, 50); // 3. 在(100,20)到(150,20)再到(150,75)的拐角处画一个半径为50的圆弧
ctx.lineTo(150, 130); // 4. 继续画线
ctx.stroke();
四、贝塞尔曲线(重点)
Canvas 有两种:
- 二次贝塞尔(1 个控制点)
- 三次贝塞尔(2 个控制点)
1. 二次贝塞尔 quadraticCurveTo
它有一个起点、一个控制点和一个终点。控制点像一块磁铁,将直线“吸”成曲线。
语法:
ctx.quadraticCurveTo(cpx, cpy, x, y);
| 参数 | 说明 |
|---|---|
cpx, cpy | 控制点的坐标。 |
x, y | 终点的坐标。 |
适合:
- 平滑弧线
- 气泡、圆角、简单曲线
ctx.beginPath();
ctx.moveTo(500, 100);
ctx.quadraticCurveTo(600, 100, 600, 200);
ctx.strokeStyle = '#000';
ctx.stroke();
2. 三次贝塞尔 bezierCurveTo
它有一个起点、两个控制点和一个终点。两个控制点分别影响曲线的起点和终点部分的曲率,提供了更精细的控制。
语法:
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
| 参数 | 说明 |
|---|---|
cp1x, cp1y | 第一个控制点的坐标。 |
cp2x, cp2y | 第二个控制点的坐标。 |
x, y | 终点的坐标。 |
- 两个控制点
- 可以画波浪、S 形、复杂曲线
ctx.beginPath();
ctx.moveTo(700,100)
ctx.bezierCurveTo(800, 50, 900, 150, 1000, 100);
ctx.strokeStyle = '#000';
ctx.stroke();
五、快速对比总结
| API | 作用 | 特点 |
|---|---|---|
arc | 圆、圆弧 | 圆心+角度,最常用 |
arcTo | 切线圆弧 | 画圆角神器 |
quadraticCurveTo | 二次贝塞尔 | 1个控制点,简单平滑 |
bezierCurveTo | 三次贝塞尔 | 2个控制点,复杂曲线 |
六、一句话记住路径规则
- 必须 beginPath(),否则会连着之前的画
- moveTo 产生子路径
- arc 画圆/圆弧,角度从右顺时针
- arcTo 画圆角
- 二次曲线简单,三次曲线自由
如果你需要,我可以给你写一段完整可运行的演示代码,把圆、圆弧、圆角、贝塞尔全部画在同一块 canvas 上直观对比。
我直接给你写两段可运行对比代码,一眼看懂: 写 beginPath() 和不写的区别。
情况1:不写 beginPath() → 所有旧路径会被重绘、连在一起
<canvas id="c1" width="300" height="200"></canvas>
<script>
const ctx = document.getElementById('c1').getContext('2d');
// 第一个矩形
ctx.strokeStyle = 'red';
ctx.strokeRect(50, 50, 50, 50);
// 第二个矩形,没有 beginPath
ctx.strokeStyle = 'blue';
ctx.strokeRect(150, 50, 50, 50);
</script>
结果:
两个矩形都变成 蓝色。
原因:
- 第一次
strokeRect生成了路径 - 第二次没有
beginPath(),所以旧路径还在 - 第二次
strokeRect会把 第一条路径 + 第二条路径一起描边 - 颜色用最新的
blue
情况2:写了 beginPath() → 新旧路径完全分开
<canvas id="c2" width="300" height="200"></canvas>
<script>
const ctx = document.getElementById('c2').getContext('2d');
// 第一个矩形
ctx.strokeStyle = 'red';
ctx.strokeRect(50, 50, 50, 50);
// ✅ 关键:开始新路径,清空之前的
ctx.beginPath();
// 第二个矩形
ctx.strokeStyle = 'blue';
ctx.strokeRect(150, 50, 50, 50);
</script>
结果:
- 第一个矩形 红色
- 第二个矩形 蓝色 互不干扰。
更明显的例子:两条线连在一起
不写 beginPath()
ctx.moveTo(50, 50);
ctx.lineTo(100, 50);
ctx.strokeStyle = 'red';
ctx.stroke();
// 没有 beginPath
ctx.moveTo(50, 100);
ctx.lineTo(100, 100);
ctx.strokeStyle = 'blue';
ctx.stroke();
结果:
- 第一条红线也会被重绘成蓝色
- 因为路径还在,第二次 stroke 会画所有已存在的路径
写 beginPath()
ctx.moveTo(50, 50);
ctx.lineTo(100, 50);
ctx.strokeStyle = 'red';
ctx.stroke();
// ✅ 新路径
ctx.beginPath();
ctx.moveTo(50, 100);
ctx.lineTo(100, 100);
ctx.strokeStyle = 'blue';
ctx.stroke();
结果:
- 红线是红线
- 蓝线是蓝线 互不影响。
最核心一句话总结
不 beginPath() → 路径会累积,每次 stroke/fill 都会画全部旧路径 beginPath() → 清空之前所有子路径,重新开始画
这就是为什么画多个独立图形时,必须每次都 beginPath()。
案例
<canvas id="myCanvas"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
canvas.width = 1000;
canvas.height = 800;
const ctx = canvas.getContext('2d');
// 1.绘制一个弧线
// ctx.fillStyle = '#ADD8E6';
// ctx.arc(100,100,50, 0, Math.PI/2) 从圆心右侧 (150,100) 开始画弧 弧线终点在圆心下方 (100,150) 如果不连回圆心,fill() 会直接连接起点和终点,形成一个"弓形"而非扇形
// ctx.fill();
// 2.绘制一个扇形
ctx.beginPath(); // 1. 开始路径
ctx.moveTo(100, 100); // 2. 移动到圆心
ctx.arc(100, 100, 50, 0, Math.PI/2); // 3. 画弧线
ctx.lineTo(100, 100); // 4. 连回圆心(或用 closePath)
ctx.closePath(); // 封闭路径
ctx.fillStyle = '#ADD8E6';
ctx.fill(); // 5. 填充
// 为什么需要 moveTo 和 closePath
// ctx.arc(100,100,50, 0, Math.PI/2) 从圆心右侧 (150,100) 开始画弧
// 弧线终点在圆心下方 (100,150)
// 如果不连回圆心,fill() 会直接连接起点和终点,形成一个"弓形"而非扇形
// moveTo 确保从圆心开始,closePath 确保路径封闭回圆心
// 3.绘制切线圆弧
ctx.beginPath();
ctx.moveTo(300, 100);
ctx.arcTo(400, 100, 400, 200, 50);
ctx.lineTo(400, 200);
ctx.strokeStyle = '#000';
ctx.stroke();
// 1.从当前点 (300, 100) 向控制点 (400, 100) 画一条切线
// 2.然后画一个半径为 50 的圆弧
// 3.圆弧结束在从 (400, 100) 到 (400, 200) 的切线上
// 4.关键点:arcTo 画完后,当前点不在 (400, 200),而是在圆弧的终点(圆弧与第二条切线的切点附近)。
// 5.所以 lineTo(400, 200) 是为了:
// 6.补全剩余的直线段 —— 从圆弧终点画到角点 (400, 200)
// 7.让路径完整 —— 形成一个可见的"圆角"效果(一条横线 → 圆弧 → 一条竖线)
// 4.贝塞尔曲线
ctx.beginPath();
ctx.moveTo(500, 100);
// 二次贝塞尔曲线
ctx.quadraticCurveTo(600, 100, 600, 200);
ctx.strokeStyle = '#000';
ctx.stroke();
ctx.beginPath();
ctx.moveTo(700,100)
// 三次次贝塞尔曲线
ctx.bezierCurveTo(800, 50, 900, 150, 1000, 100);
ctx.strokeStyle = '#000';
ctx.stroke();
</script>
<canvas id="myCanvas"></canvas>
<script>
const canvas = document.getElementById('myCanvas');
canvas.width = 1000;
canvas.height = 800;
const ctx = canvas.getContext('2d');
ctx.moveTo(50, 50);
ctx.lineTo(100, 50);
ctx.strokeStyle = 'red';
ctx.stroke();
// 没有 beginPath
// ctx.beginPath();
ctx.moveTo(50, 100);
ctx.lineTo(100, 100);
ctx.strokeStyle = 'blue';
ctx.stroke();
</script>