Canvas绘图指南:从零基础到实战应用 (1)

346 阅读10分钟

Canvas 作为 HTML5 的核心绘图技术,已经成为现代 Web 开发中不可或缺的一部分。本文将带你掌握 Canvas 的核心用法,避开常见陷阱,并展示实际应用场景。

一、Canvas 快速入门

1.1 基础设置

正确设置 Canvas 尺寸是第一个关键点:

<!-- 错误做法:使用CSS设置初始尺寸会导致内容拉伸 -->
<canvas id="canvas" style="width:600px;height:400px"></canvas>

<!-- 正确做法:使用属性设置 -->
<canvas id="canvas" width="600" height="400"></canvas>

获取上下文对象时要注意兼容性处理:

const canvas = document.getElementById("canvas");
const ctx = canvas.getContext?.("2d"); // 使用可选链操作符

if (!ctx) {
  // 优雅降级方案
  canvas.innerHTML = "您的浏览器不支持Canvas,请升级浏览器";
  return;
}

1.2 坐标系理解

Canvas 使用标准的笛卡尔坐标系,但要注意:

  • 原点(0,0)在左上角
  • X 轴向右为正方向
  • Y 轴向下为正方向(与数学坐标系相反)

关键点:所有绘制操作都基于这个坐标系,理解这一点对后续的变换和动画至关重要。

二、图形绘制核心技巧

2.1 矩形绘制三剑客

方法作用使用场景参数说明
fillRect()绘制填充矩形按钮、背景色块fillRect(x, y, width, height)
strokeRect()绘制描边矩形边框、分割线strokeRect(x, y, width, height)
clearRect()清除矩形区域局部刷新、动画帧清除clearRect(x, y, width, height)

参数说明

  • x, y: 矩形左上角的坐标
  • width, height: 矩形的宽度和高度

常见错误:忘记beginPath()导致样式污染

// 错误示例:两个矩形共享样式
ctx.fillStyle = "red";
ctx.fillRect(10, 10, 50, 50);
ctx.fillStyle = "blue";
ctx.fillRect(70, 10, 50, 50); // 实际会变成红色!

// 正确做法:使用beginPath隔离
ctx.beginPath();
ctx.fillStyle = "red";
ctx.fillRect(10, 10, 50, 50);

ctx.beginPath(); // 关键!
ctx.fillStyle = "blue";
ctx.fillRect(70, 10, 50, 50);

关键点beginPath()用于开始新的绘制路径,避免样式污染。

2.2 圆形与圆弧绘制

arc()方法的角度参数容易出错:

  • 使用弧度制而非角度制
  • 0 弧度对应 3 点钟方向
  • 顺时针方向为正方向
// arc(x, y, radius, startAngle, endAngle, anticlockwise)
// 参数说明:
// x, y: 圆心的坐标位置
// radius: 圆的半径
// startAngle: 开始角度(弧度制,0表示3点钟方向)
// endAngle: 结束角度(弧度制)
// anticlockwise: 是否逆时针绘制(true=逆时针,false=顺时针,默认false)

// 绘制扇形(30度到120度)
const degreesToRadians = (deg) => (deg * Math.PI) / 180;
ctx.beginPath();
ctx.moveTo(100, 100); // 关键:从圆心开始
ctx.arc(100, 100, 50, degreesToRadians(30), degreesToRadians(120));
ctx.closePath(); // 闭合路径形成扇形
ctx.fill();

角度参考系统

  • 0 = 3 点钟方向
  • Math.PI/2 = 6 点钟方向
  • Math.PI = 9 点钟方向
  • Math.PI*2 = 完整圆

2.3 线段绘制基础

// moveTo(x, y)
// 参数说明:
// x, y: 移动到指定坐标点,作为绘制的起点

// lineTo(x, y)
// 参数说明:
// x, y: 从当前点到指定坐标点绘制一条直线

// 基本线段绘制
ctx.beginPath();
ctx.moveTo(100, 100); // 移动到起点
ctx.lineTo(200, 150); // 绘制到指定点
ctx.stroke(); // 描边
ctx.closePath(); // 关闭路径

关键点moveTo()设置起点,lineTo()绘制线段,stroke()执行描边。

三、高级路径技术

3.1 圆弧连接(arcTo)

用于绘制圆角矩形或平滑连接:

// arcTo(x1, y1, x2, y2, radius)
// 参数说明:
// x1, y1: 第一个控制点的坐标
// x2, y2: 第二个控制点的坐标
// radius: 圆弧的半径
// 从当前点到第二个控制点绘制一条圆弧,半径决定圆弧的弯曲程度

ctx.beginPath();
ctx.moveTo(300, 200);
ctx.arcTo(300, 250, 250, 250, 50); // 绘制圆角
ctx.stroke();

工作原理:arcTo 通过三个点和一个半径来绘制圆弧,常用于 UI 组件的圆角效果。

3.2 贝塞尔曲线实战

二次贝塞尔曲线适合简单弧线:

// 绘制对话气泡的尾巴
    ctx.beginPath(); // 开始新路径

    // 设置起始点
    ctx.moveTo(200, 300); // 移动到起始位置 (200, 300)

    // quadraticCurveTo(cp1x, cp1y, x, y)
    // 参数说明:
    // cp1x, cp1y: 控制点的坐标(决定曲线的弯曲程度和方向)
    // x, y: 终点的坐标
    // 从当前点到终点绘制一条二次贝塞尔曲线,控制点决定曲线的形状

    // 绘制第一条曲线:从(200,300)到(150,200),控制点为(150,300)
    ctx.quadraticCurveTo(150, 300, 150, 200);

    // 绘制第二条曲线:从(150,200)到(300,100),控制点为(150,100)
    ctx.quadraticCurveTo(150, 100, 300, 100);
    ctx.quadraticCurveTo(450, 100, 450, 200);
    ctx.quadraticCurveTo(450, 300, 250, 300);
    ctx.quadraticCurveTo(250, 350, 150, 350);
    ctx.quadraticCurveTo(200, 350, 200, 300);
    ctx.stroke();
    ctx.closePath();

d7dcb179f206d12ea4f5e9c6050d7a22.png

三次贝塞尔曲线提供更精细控制:

 // 绘制贝塞尔三次曲线 - 绘制一个复杂的曲线图形
    ctx.beginPath(); // 开始新路径

    // 设置起始点
    ctx.moveTo(300, 200); // 移动到起始位置 (300, 200)

    // bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
    // 参数说明:
    // cp1x, cp1y: 第一个控制点的坐标
    // cp2x, cp2y: 第二个控制点的坐标
    // x, y: 终点的坐标
    // 从当前点到终点绘制一条三次贝塞尔曲线,两个控制点决定曲线的形状

    // 绘制第一条曲线:从(300,200)到(300,250)
    // 第一个控制点:(350,150) - 控制曲线的开始方向
    // 第二个控制点:(400,200) - 控制曲线的结束方向
    ctx.bezierCurveTo(350, 150, 400, 200, 300, 250);

    // 绘制第二条曲线:从(300,250)到(300,200)
    // 第一个控制点:(200,200) - 控制曲线的开始方向
    // 第二个控制点:(250,150) - 控制曲线的结束方向
    ctx.bezierCurveTo(200, 200, 250, 150, 300, 200);

    ctx.stroke();
    ctx.closePath();

image.png

关键点

  • 二次贝塞尔:一个控制点,适合简单曲线
  • 三次贝塞尔:两个控制点,提供更精确控制
  • 控制点决定曲线的弯曲程度和方向
  • 第一个控制点影响曲线开始的方向
  • 第二个控制点影响曲线结束的方向

3.3 路径复用最佳实践

Path2D 对象可以显著提升性能:

// 创建可复用路径
const starPath = new Path2D();
for (let i = 0; i < 5; i++) {
  const angle = i * Math.PI * 0.4;
  const x = 50 + Math.cos(angle) * 40;
  const y = 50 + Math.sin(angle) * 40;
  if (i === 0) {
    starPath.moveTo(x, y);
  } else {
    starPath.lineTo(x, y);
  }
}
starPath.closePath();

// 多次绘制
ctx.fillStyle = "gold";
ctx.fill(starPath);

ctx.translate(100, 0);
ctx.fillStyle = "silver";
ctx.fill(starPath);

SVG 路径字符串支持

// 使用SVG路径字符串创建Path2D
const path = new Path2D("M10 10 h 80 v 80 h -80 z");
ctx.stroke(path);

SVG 路径命令

  • M x y: 移动到(x,y)
  • h dx: 水平移动 dx 像素
  • v dy: 垂直移动 dy 像素
  • z: 闭合路径

四、状态管理与变换

4.1 状态保存与恢复

ctx.save(); // 保存当前状态
// 修改样式、变换等
ctx.restore(); // 恢复之前的状态

保存的状态包括

  • 填充样式(fillStyle)
  • 描边样式(strokeStyle)
  • 线宽(lineWidth)
  • 变换矩阵
  • 裁剪路径

4.2 坐标变换

// translate(dx, dy)
// 参数说明:
// dx: 水平移动的距离(正值向右,负值向左)
// dy: 垂直移动的距离(正值向下,负值向上)

// rotate(angle)
// 参数说明:
// angle: 旋转角度(弧度制,正值顺时针,负值逆时针)

// scale(sx, sy)
// 参数说明:
// sx: X轴缩放比例(1.0为原始大小,>1放大,<1缩小)
// sy: Y轴缩放比例(1.0为原始大小,>1放大,<1缩小)

// 平移变换
ctx.translate(100, 50); // 移动坐标原点

// 旋转变换
ctx.rotate(Math.PI / 4); // 旋转45度

// 缩放变换
ctx.scale(2, 0.5); // X轴放大2倍,Y轴缩小一半

关键点:变换是累积的,使用save()restore()可以避免变换污染。

五、性能优化关键点

5.1 减少绘制调用

// 错误做法:逐个绘制
shapes.forEach((shape) => {
  ctx.beginPath();
  // ...绘制逻辑
  ctx.fill();
});

// 正确做法:批量绘制
ctx.beginPath();
shapes.forEach((shape) => {
  // 累积路径
});
ctx.fill(); // 单次绘制调用

5.2 路径复用优化

// 创建可复用的路径对象
const heartPath = new Path2D();
heartPath.moveTo(300, 200);
heartPath.bezierCurveTo(350, 150, 400, 200, 300, 250);
heartPath.bezierCurveTo(200, 200, 250, 150, 300, 200);

// 多次使用同一路径
ctx.fillStyle = "red";
ctx.fill(heartPath);

ctx.translate(100, 0);
ctx.fillStyle = "pink";
ctx.fill(heartPath);

5.3 错误处理最佳实践

function safeCanvasOperation(operation) {
  try {
    const ctx = canvas.getContext("2d");
    if (!ctx) {
      throw new Error("Canvas 2D context not supported");
    }
    return operation(ctx);
  } catch (error) {
    console.error("Canvas operation failed:", error);
    // 降级处理
  }
}

六、实战应用案例

6.1 仪表盘绘制

// drawGauge(ctx, value, max)
// 参数说明:
// ctx: Canvas 2D上下文对象
// value: 当前值(用于计算进度百分比)
// max: 最大值(用于计算进度百分比)

function drawGauge(ctx, value, max) {
  const centerX = 150,
    centerY = 150;
  const radius = 120;
  const startAngle = -Math.PI * 0.8;
  const endAngle = Math.PI * 0.8;

  // 背景圆弧
  ctx.beginPath();
  ctx.lineWidth = 20;
  ctx.strokeStyle = "#eee";
  ctx.arc(centerX, centerY, radius, startAngle, endAngle);
  ctx.stroke();

  // 进度圆弧
  const progressAngle = startAngle + (endAngle - startAngle) * (value / max);
  ctx.beginPath();
  ctx.lineWidth = 20;
  ctx.strokeStyle = "#4CAF50";
  ctx.lineCap = "round"; // 圆角端点
  ctx.arc(centerX, centerY, radius, startAngle, progressAngle);
  ctx.stroke();

  // 指针
  const pointerAngle = startAngle + (endAngle - startAngle) * (value / max);
  const pointerLength = radius * 0.7;
  ctx.beginPath();
  ctx.lineWidth = 4;
  ctx.strokeStyle = "#333";
  ctx.moveTo(centerX, centerY);
  ctx.lineTo(
    centerX + Math.cos(pointerAngle) * pointerLength,
    centerY + Math.sin(pointerAngle) * pointerLength
  );
  ctx.stroke();
}

关键点

  • 使用lineCap = 'round'实现圆角端点
  • 通过角度计算实现进度显示
  • 指针位置与进度同步

6.2 笑脸绘制

// 绘制圆形脸
ctx.beginPath();
ctx.arc(300, 200, 80, 0, Math.PI * 2);
ctx.stroke();

// 绘制眼睛
ctx.beginPath();
ctx.arc(280, 180, 8, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(320, 180, 8, 0, Math.PI * 2);
ctx.fill();

// 绘制嘴巴
ctx.beginPath();
ctx.arc(300, 220, 30, 0, Math.PI);
ctx.stroke();

七、代码组织最佳实践

7.1 类封装

class CanvasDrawer {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext("2d");
    this.paths = new Map();
  }

  // 创建可复用的路径
  createPath(name, pathFunction) {
    const path = new Path2D();
    pathFunction(path);
    this.paths.set(name, path);
  }

  // 绘制路径
  drawPath(name, style = {}) {
    const path = this.paths.get(name);
    if (!path) return;

    this.ctx.save();
    Object.assign(this.ctx, style);
    this.ctx.stroke(path);
    this.ctx.restore();
  }
}

7.2 工具函数

// degreesToRadians(deg)
// 参数说明:
// deg: 角度值(度数)
// 返回值: 对应的弧度值

// radiansToDegrees(rad)
// 参数说明:
// rad: 弧度值
// 返回值: 对应的角度值(度数)

// drawRoundedRect(ctx, x, y, width, height, radius)
// 参数说明:
// ctx: Canvas 2D上下文对象
// x, y: 矩形左上角坐标
// width, height: 矩形的宽度和高度
// radius: 圆角半径

// 角度转弧度
const degreesToRadians = (deg) => (deg * Math.PI) / 180;

// 弧度转角度
const radiansToDegrees = (rad) => (rad * 180) / Math.PI;

// 绘制圆角矩形
function drawRoundedRect(ctx, x, y, width, height, radius) {
  ctx.beginPath();
  ctx.moveTo(x + radius, y);
  ctx.lineTo(x + width - radius, y);
  ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
  ctx.lineTo(x + width, y + height - radius);
  ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
  ctx.lineTo(x + radius, y + height);
  ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
  ctx.lineTo(x, y + radius);
  ctx.quadraticCurveTo(x, y, x + radius, y);
  ctx.closePath();
}

八、常见陷阱与解决方案

8.1 样式污染问题

问题:忘记beginPath()导致样式影响后续绘制

解决方案

// 每次绘制前都调用beginPath()
ctx.beginPath();
ctx.fillStyle = "red";
ctx.fillRect(10, 10, 50, 50);

ctx.beginPath(); // 关键!
ctx.fillStyle = "blue";
ctx.fillRect(70, 10, 50, 50);

8.2 坐标变换累积问题

问题:多次变换导致坐标混乱

解决方案

ctx.save(); // 保存当前状态
ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);
// 绘制操作
ctx.restore(); // 恢复状态

8.3 性能问题

问题:频繁的绘制调用影响性能

解决方案

// 使用Path2D对象复用路径
const path = new Path2D();
// 定义路径
// 多次使用同一路径对象

九、总结

Canvas 绘图技术是现代 Web 开发的重要技能,掌握以下关键点:

  1. 基础概念:正确设置 Canvas 尺寸,理解坐标系
  2. 绘制技巧:掌握基本图形绘制,避免样式污染
  3. 高级特性:熟练使用贝塞尔曲线、Path2D 对象
  4. 性能优化:减少绘制调用,复用路径对象
  5. 最佳实践:合理使用状态管理,组织代码结构

通过系统学习和实践,你可以创建丰富的图形应用,从简单的 UI 组件到复杂的游戏界面。记住性能优化和代码组织的重要性,这将使你的 Canvas 应用更加高效和可维护。


参考资料