1.2 路径绘制

2 阅读7分钟

一、路径(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) 时,就会创建一个新的子路径,它就像一个“提笔移动”的动作,开始一段新的、不连续的线条。

产生新子路径的情况:

  1. beginPath()
  2. moveTo()
  3. 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. 二次贝塞尔(1 个控制点)
  2. 三次贝塞尔(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个控制点,复杂曲线

六、一句话记住路径规则

  1. 必须 beginPath(),否则会连着之前的画
  2. moveTo 产生子路径
  3. arc 画圆/圆弧,角度从右顺时针
  4. arcTo 画圆角
  5. 二次曲线简单,三次曲线自由

如果你需要,我可以给你写一段完整可运行的演示代码,把圆、圆弧、圆角、贝塞尔全部画在同一块 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>