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();
三次贝塞尔曲线提供更精细控制:
// 绘制贝塞尔三次曲线 - 绘制一个复杂的曲线图形
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();
关键点:
- 二次贝塞尔:一个控制点,适合简单曲线
- 三次贝塞尔:两个控制点,提供更精确控制
- 控制点决定曲线的弯曲程度和方向
- 第一个控制点影响曲线开始的方向
- 第二个控制点影响曲线结束的方向
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 开发的重要技能,掌握以下关键点:
- 基础概念:正确设置 Canvas 尺寸,理解坐标系
- 绘制技巧:掌握基本图形绘制,避免样式污染
- 高级特性:熟练使用贝塞尔曲线、Path2D 对象
- 性能优化:减少绘制调用,复用路径对象
- 最佳实践:合理使用状态管理,组织代码结构
通过系统学习和实践,你可以创建丰富的图形应用,从简单的 UI 组件到复杂的游戏界面。记住性能优化和代码组织的重要性,这将使你的 Canvas 应用更加高效和可维护。
参考资料: