Canvas 2D 上下文
Canvas 2D 上下文(CanvasRenderingContext2D)是 HTML5 Canvas 绘图的核心接口。通过 getContext('2d') 方法获取该上下文对象后,即可使用它提供的丰富 API 进行图形绘制、文本渲染、图像处理等操作。本节将详细介绍 2D 上下文的获取方式、坐标系统与状态管理。
前置知识
阅读本节前,建议先了解:SPA 路由与历史管理
基础概念
CanvasRenderingContext2D 对象代表一个二维渲染上下文。它维护了一套完整的绘图状态机,包括当前变换矩阵、裁剪区域、样式属性等。每次绘图操作都基于当前状态进行,理解状态管理是高效使用 Canvas 的关键。
上下文的生命周期
一个 <canvas> 元素可以获取多种类型的绘图上下文,但每种类型只能获取一次。获取 2D 上下文后,如果再次调用 getContext('2d'),浏览器将返回同一个上下文对象。
// 获取 canvas 元素
const canvas = document.getElementById('myCanvas');
// 获取 2D 上下文
const ctx = canvas.getContext('2d');
// ctx 就是 CanvasRenderingContext2D 实例
console.log(ctx instanceof CanvasRenderingContext2D); // true获取上下文
getContext() 方法
getContext() 方法接受一个上下文类型参数,返回对应的渲染上下文对象。
/**
* getContext(contextType, contextAttributes)
* @param {string} contextType - 上下文类型
* @param {object} contextAttributes - 可选的上下文配置
* @returns {RenderingContext|null}
*/
// 基本用法
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');2D 上下文配置选项
通过 getContext('2d', options) 可以配置上下文行为:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
alpha | boolean | true | 是否包含 alpha 通道。设为 false 可提升性能 |
colorSpace | string | "srgb" | 颜色空间,可设为 "display-p3" |
desynchronized | boolean | false | 是否减少延迟,适用于低延迟场景 |
willReadFrequently | boolean | false | 是否频繁调用 getImageData(),优化像素读取性能 |
// 不需要透明度时,关闭 alpha 提升性能
const ctx = canvas.getContext('2d', { alpha: false });
// 频繁读取像素数据时
const pixelCtx = canvas.getContext('2d', {
willReadFrequently: true
});
// 低延迟渲染(如游戏)
const gameCtx = canvas.getContext('2d', {
alpha: false,
desynchronized: true
});完整示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Canvas 2D 上下文</title>
<style>
body { font-family: sans-serif; padding: 20px; }
canvas {
border: 1px solid #ccc;
margin: 10px 0;
display: block;
}
.info { margin: 10px 0; padding: 10px; background: #f5f5f5; border-radius: 4px; }
</style>
</head>
<body>
<h1>Canvas 2D 上下文</h1>
<canvas id="ctxDemo" width="400" height="200"></canvas>
<div id="info" class="info"></div>
<script>
const canvas = document.getElementById('ctxDemo');
const info = document.getElementById('info');
// 获取 2D 上下文
const ctx = canvas.getContext('2d');
// 显示上下文信息
info.innerHTML = `
<p><strong>canvas 尺寸:</strong>${canvas.width} x ${canvas.height}</p>
<p><strong>上下文类型:</strong>${ctx.constructor.name}</p>
<p><strong>设备像素比:</strong>${window.devicePixelRatio}</p>
`;
// 使用上下文绘制
ctx.fillStyle = '#4285f4';
ctx.fillRect(20, 20, 150, 100);
ctx.fillStyle = '#34a853';
ctx.fillRect(200, 50, 150, 100);
// 添加文本
ctx.fillStyle = '#333';
ctx.font = '16px sans-serif';
ctx.fillText('Canvas 2D 上下文演示', 80, 170);
</script>
</body>
</html>坐标系统
标准坐标
Canvas 使用标准的笛卡尔坐标系,但 Y 轴方向与数学坐标系相反:
- 原点 (0, 0) 位于画布左上角
- X 轴向右为正方向
- Y 轴向下为正方向
(0,0) ────────────────► X
│
│ Canvas 坐标系统
│ (X 增大 → 向右)
│ (Y 增大 → 向下)
│
▼
Y像素对齐
当绘制 1 像素宽的线条时,需要注意像素对齐问题。Canvas 的线条是沿坐标轴两侧各延伸 0.5 像素绘制的,如果坐标恰好落在整数像素上,线条会跨越两个像素,导致模糊。
const canvas = document.getElementById('pixelDemo');
const ctx = canvas.getContext('2d');
// 模糊的线条(坐标在整数位置)
ctx.beginPath();
ctx.moveTo(10, 10); // 不推荐
ctx.lineTo(200, 10);
ctx.strokeStyle = '#ea4335';
ctx.lineWidth = 1;
ctx.stroke();
// 清晰的线条(坐标偏移 0.5 像素)
ctx.beginPath();
ctx.moveTo(10, 30.5); // 推荐
ctx.lineTo(200, 30.5);
ctx.strokeStyle = '#4285f4';
ctx.lineWidth = 1;
ctx.stroke();设备像素比处理
高 DPI 屏幕上,需要考虑设备像素比(devicePixelRatio)来确保画面清晰:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>高清 Canvas</title>
<style>
canvas { border: 1px solid #ccc; }
</style>
</head>
<body>
<h1>高清 Canvas 渲染</h1>
<canvas id="hdCanvas" style="width: 400px; height: 300px;"></canvas>
<script>
function setupHDCanvas(canvas, width, height) {
const dpr = window.devicePixelRatio || 1;
// 设置 canvas 缓冲区大小(实际像素)
canvas.width = width * dpr;
canvas.height = height * dpr;
// CSS 尺寸保持不变
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
// 获取上下文并缩放
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
return ctx;
}
const canvas = document.getElementById('hdCanvas');
const ctx = setupHDCanvas(canvas, 400, 300);
// 后续绘图代码无需关心 DPR,使用逻辑坐标即可
ctx.fillStyle = '#4285f4';
ctx.fillRect(20, 20, 160, 100);
ctx.fillStyle = '#34a853';
ctx.fillRect(200, 20, 160, 100);
ctx.font = '18px sans-serif';
ctx.fillStyle = '#333';
ctx.fillText('高清渲染文字', 130, 160);
// 显示信息
const dpr = window.devicePixelRatio || 1;
ctx.fillStyle = '#666';
ctx.font = '14px sans-serif';
ctx.fillText(`缓冲区: ${canvas.width}x${canvas.height} | DPR: ${dpr}`, 80, 200);
</script>
</body>
</html>状态管理
理解绘图状态
Canvas 上下文维护一组绘图状态属性。这些属性在任何时候都有一个"当前值",绘图操作使用的是当前的属性值。
主要的绘图状态包括:
| 状态类别 | 包含属性 |
|---|---|
| 变换状态 | translate、rotate、scale、transform |
| 样式状态 | fillStyle、strokeStyle、lineWidth、lineCap 等 |
| 合成状态 | globalAlpha、globalCompositeOperation |
| 文本状态 | font、textAlign、textBaseline |
| 裁剪状态 | 当前裁剪路径 |
| 阴影状态 | shadowColor、shadowBlur、shadowOffsetX、shadowOffsetY |
save() 和 restore()
ctx.save() 将当前所有状态压入状态栈,ctx.restore() 从栈中弹出最近保存的状态并恢复。
const ctx = canvas.getContext('2d');
// 状态 1(初始状态)
ctx.fillStyle = '#4285f4';
ctx.save(); // 保存状态 1
ctx.fillStyle = '#ea4335'; // 修改状态
ctx.fillRect(10, 10, 100, 50); // 红色矩形
ctx.restore(); // 恢复到状态 1
ctx.fillRect(10, 70, 100, 50); // 蓝色矩形(因为恢复了)状态栈的工作原理
<!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="stateDemo" width="500" height="400"></canvas>
<script>
const canvas = document.getElementById('stateDemo');
const ctx = canvas.getContext('2d');
// === 第 1 层:初始状态 ===
ctx.fillStyle = '#4285f4';
ctx.font = 'bold 20px sans-serif';
ctx.fillText('第 1 层:蓝色 (#4285f4)', 20, 30);
ctx.save(); // 保存第 1 层状态
// ─── 进入第 2 层 ───
ctx.fillStyle = '#ea4335';
ctx.font = 'bold 20px sans-serif';
ctx.fillText('第 2 层:红色 (#ea4335)', 20, 70);
ctx.save(); // 保存第 2 层状态
// ─── 进入第 3 层 ───
ctx.fillStyle = '#34a853';
ctx.font = 'bold 20px sans-serif';
ctx.fillText('第 3 层:绿色 (#34a853)', 20, 110);
// 绘制矩形验证当前颜色
ctx.fillRect(20, 130, 120, 40);
ctx.restore(); // 恢复到第 2 层
// ─── 回到第 2 层 ───
ctx.fillText('恢复到第 2 层:红色', 20, 200);
ctx.fillRect(20, 220, 120, 40); // 红色矩形
ctx.save(); // 在第 2 层基础上保存
// ─── 再次进入新层 ───
ctx.fillStyle = '#fbbc05';
ctx.font = 'bold 20px sans-serif';
ctx.fillText('新层:黄色 (#fbbc05)', 20, 290);
ctx.fillRect(20, 310, 120, 40); // 黄色矩形
ctx.restore(); // 恢复到第 2 层
ctx.restore(); // 恢复到第 1 层
ctx.fillText('恢复到第 1 层:蓝色', 20, 380);
</script>
</body>
</html>实战示例:嵌套图形绘制
利用 save/restore 配合变换,可以轻松实现嵌套图形的绘制:
<!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>嵌套图形:save/restore 实战</h1>
<canvas id="nestedDemo" width="500" height="500"></canvas>
<script>
const canvas = document.getElementById('nestedDemo');
const ctx = canvas.getContext('2d');
/**
* 绘制一个带旋转的矩形框
* @param {number} x - 中心 X 坐标
* @param {number} y - 中心 Y 坐标
* @param {number} size - 矩形尺寸
* @param {string} color - 颜色
* @param {number} rotation - 旋转角度(弧度)
* @param {number} depth - 递归深度
*/
function drawNestedRect(x, y, size, color, rotation, depth) {
if (depth <= 0 || size < 5) return;
// 保存当前状态
ctx.save();
// 移动到指定位置并旋转
ctx.translate(x, y);
ctx.rotate(rotation);
// 绘制矩形
ctx.strokeStyle = color;
ctx.lineWidth = Math.max(1, depth * 0.5);
ctx.strokeRect(-size / 2, -size / 2, size, size);
// 递归绘制嵌套矩形
const nextSize = size * 0.75;
const nextRotation = rotation + 0.15;
const colors = ['#4285f4', '#ea4335', '#34a853', '#fbbc05', '#9c27b0'];
drawNestedRect(
0, 0, nextSize,
colors[depth % colors.length],
nextRotation,
depth - 1
);
// 恢复状态
ctx.restore();
}
// 绘制 10 层嵌套矩形
drawNestedRect(250, 250, 400, '#4285f4', 0, 10);
</script>
</body>
</html>常用属性一览
样式属性
| 属性 | 说明 | 默认值 |
|---|---|---|
fillStyle | 填充颜色/渐变/图案 | #000000 |
strokeStyle | 描边颜色/渐变/图案 | #000000 |
lineWidth | 线条宽度 | 1.0 |
lineCap | 线条端点样式 | butt |
lineJoin | 线条连接样式 | miter |
miterLimit | 斜接长度比例 | 10 |
线条端点与连接样式
const ctx = canvas.getContext('2d');
// lineCap 可选值:butt(默认)、round、square
ctx.lineCap = 'round'; // 圆角端点
ctx.lineCap = 'square'; // 方角端点
ctx.lineCap = 'butt'; // 平头端点
// lineJoin 可选值:miter(默认)、round、bevel
ctx.lineJoin = 'round'; // 圆角连接
ctx.lineJoin = 'bevel'; // 斜角连接
ctx.lineJoin = 'miter'; // 尖角连接虚线样式
const ctx = canvas.getContext('2d');
// 设置虚线模式
ctx.setLineDash([10, 5]); // 10px 线段,5px 间隔
ctx.setLineDash([5, 3, 2]); // 多段虚线模式
ctx.setLineDash([]); // 清除虚线(实线)
// 获取虚线偏移量
ctx.lineDashOffset = 0; // 无偏移
ctx.lineDashOffset = 10; // 向前偏移 10px
// 蚂蚁线动画示例
function marchingAnts(ctx, offset) {
ctx.setLineDash([4, 4]);
ctx.lineDashOffset = -offset;
ctx.strokeRect(50, 50, 200, 100);
}注意事项
1. 上下文获取不可逆
// 一旦获取了某种上下文,就不能再获取其他类型
const canvas = document.getElementById('myCanvas');
const ctx2d = canvas.getContext('2d');
const ctxWebGL = canvas.getContext('webgl'); // 返回 null!2. 状态栈没有上限检查
虽然浏览器内部限制了状态栈深度(通常很大),但过度使用 save/restore 会消耗内存:
// 不好的做法:不必要的 save/restore
for (let i = 0; i < 1000; i++) {
ctx.save();
// ... 简单操作 ...
ctx.restore();
}
// 好的做法:只在需要时使用
ctx.save();
ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);
ctx.fillRect(-50, -50, 100, 100);
ctx.restore();3. Canvas 尺寸变更会重置状态
修改 canvas.width 或 canvas.height 会清空画布并重置所有状态:
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);
canvas.width = canvas.width; // 重置!
ctx.fillStyle = 'blue'; // 需要重新设置最佳实践
1. 封装高清 Canvas 工具函数
/**
* 创建高清 Canvas
* @param {number} logicalWidth - 逻辑宽度
* @param {number} logicalHeight - 逻辑高度
* @returns {{ canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D }}
*/
function createHDCanvas(logicalWidth, logicalHeight) {
const canvas = document.createElement('canvas');
const dpr = window.devicePixelRatio || 1;
canvas.width = logicalWidth * dpr;
canvas.height = logicalHeight * dpr;
canvas.style.width = logicalWidth + 'px';
canvas.style.height = logicalHeight + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
return { canvas, ctx };
}2. 上下文状态属性检查
/**
* 安全地重置上下文状态
*/
function resetContext(ctx) {
ctx.setTransform(1, 0, 0, 1, 0, 0); // 重置变换矩阵
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
ctx.shadowColor = 'rgba(0, 0, 0, 0)';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
ctx.fillStyle = '#000000';
ctx.strokeStyle = '#000000';
ctx.lineWidth = 1;
ctx.lineCap = 'butt';
ctx.lineJoin = 'miter';
ctx.setLineDash([]);
ctx.font = '10px sans-serif';
ctx.textAlign = 'start';
ctx.textBaseline = 'alphabetic';
}3. 配置项选择指南
| 场景 | 推荐配置 |
|---|---|
| 一般 2D 绘图 | 默认配置即可 |
| 游戏渲染 | { alpha: false, desynchronized: true } |
| 图像编辑/滤镜 | { willReadFrequently: true } |
| 数据可视化 | { alpha: true } |
| 打印输出 | { colorSpace: 'display-p3' } |
下一节
继续学习:路径与形状绘制