Skip to content

路径与形状绘制

Canvas 的路径(Path)系统是所有复杂图形绘制的基础。通过路径 API,你可以组合直线、曲线、圆弧等基本元素,构建出任意复杂的二维图形,再通过 fill()stroke() 进行填充或描边。本节将详细介绍路径的构建方法与各种形状绘制技巧。

前置知识

阅读本节前,建议先了解:Canvas 2D 上下文

基础概念

路径是一系列点的集合,这些点通过线段或曲线连接起来。Canvas 中的路径绘制遵循以下流程:

  1. 开始路径beginPath() —— 清空子路径列表
  2. 定义路径moveTo()lineTo()arc() 等方法 —— 添加子路径
  3. 渲染路径fill()stroke() —— 将路径绘制到画布上

路径的累积性

如果不调用 beginPath(),后续的路径指令会追加到当前路径上。这会导致 stroke()fill() 重复绘制之前的路径。

路径 API 详解

beginPath()

beginPath() 清空当前路径的子路径列表,开始一条新路径。

javascript
ctx.beginPath(); // 清空之前的所有路径

closePath()

closePath() 从当前点到起始点画一条直线,形成闭合路径。

javascript
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(200, 50);
ctx.lineTo(200, 200);
ctx.closePath(); // 自动从 (200,200) 连线到 (50,50)
ctx.stroke();

moveTo() 与 lineTo()

  • moveTo(x, y) —— 移动画笔到指定坐标(不画线)
  • lineTo(x, y) —— 从当前点画一条直线到指定坐标
javascript
// 绘制三角形
ctx.beginPath();
ctx.moveTo(100, 20);   // 移动到顶部顶点
ctx.lineTo(180, 160);   // 画线到右下角
ctx.lineTo(20, 160);    // 画线到左下角
ctx.closePath();        // 闭合路径
ctx.stroke();           // 描边

矩形绘制

rect()

rect(x, y, width, height) 向当前路径添加一个矩形子路径。

javascript
ctx.beginPath();
ctx.rect(50, 50, 200, 100);
ctx.stroke();

fillRect() 与 strokeRect()

这两个是便捷方法,不需要使用路径系统:

javascript
// 填充矩形(不走路径系统)
ctx.fillStyle = '#4285f4';
ctx.fillRect(50, 50, 200, 100);

// 描边矩形
ctx.strokeStyle = '#ea4335';
ctx.lineWidth = 3;
ctx.strokeRect(270, 50, 200, 100);

// 清除矩形区域
ctx.clearRect(100, 75, 100, 50);

clearRect()

clearRect(x, y, width, height) 清除指定矩形区域内的像素,使其变为透明。

圆弧绘制

arc()

arc(x, y, radius, startAngle, endAngle, counterclockwise) 绘制圆弧。

参数说明
x, y圆心坐标
radius半径
startAngle起始角度(弧度)
endAngle结束角度(弧度)
counterclockwise是否逆时针(默认 false,顺时针)
javascript
// 绘制圆(完整圆弧)
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);  // 0 到 2π
ctx.fill();

// 绘制半圆
ctx.beginPath();
ctx.arc(250, 100, 50, 0, Math.PI);  // 0 到 π
ctx.stroke();

// 绘制四分之一圆
ctx.beginPath();
ctx.arc(400, 100, 50, 0, Math.PI / 2);  // 0 到 π/2
ctx.stroke();

arcTo()

arcTo(x1, y1, x2, y2, radius) 根据两条切线和半径绘制圆弧。常用于绘制圆角。

javascript
/**
 * arcTo 的工作原理:
 * - 当前点到 (x1, y1) 的连线是第一条切线
 * - (x1, y1) 到 (x2, y2) 的连线是第二条切线
 * - radius 是圆弧的半径
 * - 圆弧与两条切线相切
 */

// 绘制圆角矩形
function roundRect(ctx, x, y, width, height, radius) {
  ctx.beginPath();
  ctx.moveTo(x + radius, y);
  ctx.arcTo(x + width, y, x + width, y + height, radius);     // 右上角
  ctx.arcTo(x + width, y + height, x, y + height, radius);     // 右下角
  ctx.arcTo(x, y + height, x, y, radius);                       // 左下角
  ctx.arcTo(x, y, x + width, y, radius);                        // 左上角
  ctx.closePath();
}

曲线绘制

二次贝塞尔曲线:quadraticCurveTo()

quadraticCurveTo(cpx, cpy, x, y) —— 一个控制点的贝塞尔曲线。

javascript
ctx.beginPath();
ctx.moveTo(50, 200);           // 起点
ctx.quadraticCurveTo(200, 50, 350, 200);  // 控制点 (200,50),终点 (350,200)
ctx.stroke();

// 绘制控制点和辅助线
ctx.beginPath();
ctx.setLineDash([5, 5]);
ctx.moveTo(50, 200);
ctx.lineTo(200, 50);
ctx.lineTo(350, 200);
ctx.strokeStyle = '#999';
ctx.stroke();

// 控制点
ctx.setLineDash([]);
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(200, 50, 4, 0, Math.PI * 2);
ctx.fill();

三次贝塞尔曲线:bezierCurveTo()

bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) —— 两个控制点的贝塞尔曲线。

javascript
ctx.beginPath();
ctx.moveTo(50, 250);               // 起点
ctx.bezierCurveTo(
  150, 50,     // 控制点 1
  250, 350,    // 控制点 2
  350, 150     // 终点
);
ctx.lineWidth = 3;
ctx.strokeStyle = '#4285f4';
ctx.stroke();

// 辅助线
ctx.beginPath();
ctx.setLineDash([3, 3]);
ctx.strokeStyle = '#ccc';
ctx.moveTo(50, 250);
ctx.lineTo(150, 50);
ctx.lineTo(250, 350);
ctx.lineTo(350, 150);
ctx.stroke();

// 控制点
ctx.setLineDash([]);
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(150, 50, 4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(250, 350, 4, 0, Math.PI * 2);
ctx.fill();

二次与三次贝塞尔曲线对比

特性quadraticCurveTobezierCurveTo
控制点数量1 个2 个
参数(cpx, cpy, x, y)(cp1x, cp1y, cp2x, cp2y, x, y)
灵活度中等
适用场景简单曲线、圆弧近似复杂曲线、S 形曲线

椭圆绘制

ellipse()

ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise) 绘制椭圆。

javascript
// 水平椭圆
ctx.beginPath();
ctx.ellipse(150, 100, 120, 60, 0, 0, Math.PI * 2);
ctx.stroke();

// 旋转 30 度的椭圆
ctx.beginPath();
ctx.ellipse(400, 100, 120, 60, Math.PI / 6, 0, Math.PI * 2);
ctx.stroke();

// 椭圆弧
ctx.beginPath();
ctx.ellipse(150, 250, 100, 50, 0, 0, Math.PI);
ctx.fillStyle = '#34a853';
ctx.fill();

实战示例

示例 1:绘制五角星

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>五角星</title>
  <style>
    canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
  </style>
</head>
<body>
  <h1>Canvas 五角星</h1>
  <canvas id="starCanvas" width="400" height="400"></canvas>

  <script>
    const canvas = document.getElementById('starCanvas');
    const ctx = canvas.getContext('2d');

    /**
     * 绘制五角星
     * @param {CanvasRenderingContext2D} ctx
     * @param {number} cx - 中心 X
     * @param {number} cy - 中心 Y
     * @param {number} outerR - 外圆半径
     * @param {number} innerR - 内圆半径
     * @param {number} points - 角数
     */
    function drawStar(ctx, cx, cy, outerR, innerR, points) {
      ctx.beginPath();

      for (let i = 0; i < points * 2; i++) {
        // 偶数索引为外圆顶点,奇数索引为内圆顶点
        const radius = i % 2 === 0 ? outerR : innerR;
        const angle = (Math.PI / points) * i - Math.PI / 2;
        const x = cx + radius * Math.cos(angle);
        const y = cy + radius * Math.sin(angle);

        if (i === 0) {
          ctx.moveTo(x, y);
        } else {
          ctx.lineTo(x, y);
        }
      }

      ctx.closePath();
    }

    // 绘制蓝色五角星
    drawStar(ctx, 200, 200, 150, 60, 5);

    // 渐变填充
    const gradient = ctx.createRadialGradient(200, 200, 30, 200, 200, 150);
    gradient.addColorStop(0, '#fbbc05');
    gradient.addColorStop(1, '#ea4335');
    ctx.fillStyle = gradient;
    ctx.fill();

    ctx.strokeStyle = '#c5221f';
    ctx.lineWidth = 2;
    ctx.stroke();
  </script>
</body>
</html>

示例 2:绘制心形

javascript
/**
 * 使用贝塞尔曲线绘制心形
 */
function drawHeart(ctx, cx, cy, size) {
  ctx.beginPath();
  ctx.moveTo(cx, cy + size / 4);

  // 左半部分
  ctx.bezierCurveTo(
    cx, cy,
    cx - size / 2, cy,
    cx - size / 2, cy + size / 4
  );
  ctx.bezierCurveTo(
    cx - size / 2, cy + size / 2,
    cx, cy + size * 0.6,
    cx, cy + size * 0.75
  );

  // 右半部分
  ctx.bezierCurveTo(
    cx, cy + size * 0.6,
    cx + size / 2, cy + size / 2,
    cx + size / 2, cy + size / 4
  );
  ctx.bezierCurveTo(
    cx + size / 2, cy,
    cx, cy,
    cx, cy + size / 4
  );

  ctx.closePath();
}

// 使用
drawHeart(ctx, 200, 100, 200);
ctx.fillStyle = '#ea4335';
ctx.fill();

示例 3:绘制箭头

javascript
/**
 * 绘制带箭头的线段
 */
function drawArrow(ctx, fromX, fromY, toX, toY, headLength) {
  const angle = Math.atan2(toY - fromY, toX - fromX);

  ctx.beginPath();
  ctx.moveTo(fromX, fromY);
  ctx.lineTo(toX, toY);
  ctx.stroke();

  // 绘制箭头
  ctx.beginPath();
  ctx.moveTo(toX, toY);
  ctx.lineTo(
    toX - headLength * Math.cos(angle - Math.PI / 6),
    toY - headLength * Math.sin(angle - Math.PI / 6)
  );
  ctx.lineTo(
    toX - headLength * Math.cos(angle + Math.PI / 6),
    toY - headLength * Math.sin(angle + Math.PI / 6)
  );
  ctx.closePath();
  ctx.fill();
}

// 使用
ctx.strokeStyle = '#4285f4';
ctx.fillStyle = '#4285f4';
ctx.lineWidth = 2;

drawArrow(ctx, 50, 200, 350, 150, 20);

示例 4:多种形状综合展示

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>形状绘制综合</title>
  <style>
    canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
  </style>
</head>
<body>
  <h1>Canvas 形状绘制综合</h1>
  <canvas id="shapesCanvas" width="600" height="500"></canvas>

  <script>
    const canvas = document.getElementById('shapesCanvas');
    const ctx = canvas.getContext('2d');

    // === 矩形 ===
    ctx.fillStyle = '#4285f4';
    ctx.fillRect(20, 20, 120, 80);

    ctx.strokeStyle = '#ea4335';
    ctx.lineWidth = 3;
    ctx.strokeRect(20, 120, 120, 80);

    // === 圆形 ===
    ctx.beginPath();
    ctx.arc(250, 60, 50, 0, Math.PI * 2);
    ctx.fillStyle = '#34a853';
    ctx.fill();

    ctx.beginPath();
    ctx.arc(250, 180, 50, 0, Math.PI * 2);
    ctx.strokeStyle = '#fbbc05';
    ctx.lineWidth = 3;
    ctx.stroke();

    // === 椭圆 ===
    ctx.beginPath();
    ctx.ellipse(450, 60, 80, 40, 0, 0, Math.PI * 2);
    ctx.fillStyle = '#9c27b0';
    ctx.fill();

    // === 三角形 ===
    ctx.beginPath();
    ctx.moveTo(400, 120);
    ctx.lineTo(500, 220);
    ctx.lineTo(350, 220);
    ctx.closePath();
    ctx.fillStyle = '#ff5722';
    ctx.fill();

    // === 贝塞尔曲线花朵 ===
    ctx.save();
    ctx.translate(300, 380);
    for (let i = 0; i < 6; i++) {
      ctx.rotate(Math.PI / 3);
      ctx.beginPath();
      ctx.moveTo(0, 0);
      ctx.quadraticCurveTo(60, -40, 0, -90);
      ctx.quadraticCurveTo(-60, -40, 0, 0);
      ctx.fillStyle = 'rgba(234, 67, 53, 0.3)';
      ctx.fill();
      ctx.strokeStyle = '#ea4335';
      ctx.lineWidth = 1;
      ctx.stroke();
    }
    // 花心
    ctx.beginPath();
    ctx.arc(0, 0, 15, 0, Math.PI * 2);
    ctx.fillStyle = '#fbbc05';
    ctx.fill();
    ctx.restore();

    // === 标签 ===
    ctx.fillStyle = '#333';
    ctx.font = '12px sans-serif';
    ctx.fillText('矩形', 55, 220);
    ctx.fillText('圆形', 230, 260);
    ctx.fillText('椭圆', 435, 110);
    ctx.fillText('三角形', 395, 240);
    ctx.fillText('贝塞尔花朵', 255, 490);
  </script>
</body>
</html>

注意事项

1. beginPath() 的时机

javascript
// 常见错误:忘记 beginPath()
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 50, 50);

// 忘记 beginPath() —— 会重复之前的路径
ctx.beginPath(); // 必须调用!
ctx.arc(200, 100, 30, 0, Math.PI * 2);
ctx.fillStyle = 'blue';
ctx.fill();

2. fill() 会自动闭合路径

fill() 在填充前会自动闭合路径(相当于自动调用 closePath()),但 stroke() 不会。

javascript
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(200, 50);
ctx.lineTo(125, 150);
// fill() 会自动闭合,stroke() 不会
ctx.fill();   // 三角形被填充
ctx.stroke(); // 只描了两条线,缺少第三条

3. 弧度与角度的转换

Canvas 中的角度使用弧度制:

javascript
// 角度转弧度
function degToRad(degrees) {
  return degrees * (Math.PI / 180);
}

// 弧度转角度
function radToDeg(radians) {
  return radians * (180 / Math.PI);
}

// 常用弧度值
const FULL_CIRCLE = Math.PI * 2;  // 360 度
const HALF_CIRCLE = Math.PI;       // 180 度
const RIGHT_ANGLE = Math.PI / 2;  // 90 度

最佳实践

1. 封装形状绘制工具

javascript
const Shapes = {
  // 圆角矩形
  roundRect(ctx, x, y, w, h, r) {
    if (typeof r === 'number') r = [r, r, r, r];
    const [tl, tr, br, bl] = r;
    ctx.beginPath();
    ctx.moveTo(x + tl, y);
    ctx.arcTo(x + w, y, x + w, y + h, tr);
    ctx.arcTo(x + w, y + h, x, y + h, br);
    ctx.arcTo(x, y + h, x, y, bl);
    ctx.arcTo(x, y, x + w, y, tl);
    ctx.closePath();
  },

  // 正多边形
  polygon(ctx, cx, cy, radius, sides, rotation) {
    ctx.beginPath();
    for (let i = 0; i < sides; i++) {
      const angle = (Math.PI * 2 / sides) * i + (rotation || 0);
      const x = cx + radius * Math.cos(angle);
      const y = cy + radius * Math.sin(angle);
      if (i === 0) ctx.moveTo(x, y);
      else ctx.lineTo(x, y);
    }
    ctx.closePath();
  },

  // 箭头
  arrow(ctx, fx, fy, tx, ty, size) {
    const angle = Math.atan2(ty - fy, tx - fx);
    ctx.beginPath();
    ctx.moveTo(fx, fy);
    ctx.lineTo(tx, ty);
    ctx.moveTo(tx, ty);
    ctx.lineTo(
      tx - size * Math.cos(angle - Math.PI / 6),
      ty - size * Math.sin(angle - Math.PI / 6)
    );
    ctx.moveTo(tx, ty);
    ctx.lineTo(
      tx - size * Math.cos(angle + Math.PI / 6),
      ty - size * Math.sin(angle + Math.PI / 6)
    );
  }
};

2. 路径复用模式

javascript
// 定义一次路径,多次使用
function createPath(ctx) {
  ctx.beginPath();
  ctx.moveTo(50, 50);
  ctx.bezierCurveTo(100, 10, 150, 90, 200, 50);
}

// 先描边
createPath(ctx);
ctx.strokeStyle = '#4285f4';
ctx.lineWidth = 3;
ctx.stroke();

// 再填充
createPath(ctx);
ctx.fillStyle = 'rgba(66, 133, 244, 0.3)';
ctx.fill();

3. 使用 isPointInPath 检测点击

javascript
// 检测点是否在路径内
ctx.beginPath();
ctx.arc(200, 200, 50, 0, Math.PI * 2);
ctx.fill();

canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const y = e.clientY - rect.top;

  if (ctx.isPointInPath(x, y)) {
    console.log('点击在圆形内');
  }
});

下一节

继续学习:颜色、渐变与阴影

参考链接