Skip to content

Canvas 2D 上下文

Canvas 2D 上下文(CanvasRenderingContext2D)是 HTML5 Canvas 绘图的核心接口。通过 getContext('2d') 方法获取该上下文对象后,即可使用它提供的丰富 API 进行图形绘制、文本渲染、图像处理等操作。本节将详细介绍 2D 上下文的获取方式、坐标系统与状态管理。

前置知识

阅读本节前,建议先了解:SPA 路由与历史管理

基础概念

CanvasRenderingContext2D 对象代表一个二维渲染上下文。它维护了一套完整的绘图状态机,包括当前变换矩阵、裁剪区域、样式属性等。每次绘图操作都基于当前状态进行,理解状态管理是高效使用 Canvas 的关键。

上下文的生命周期

一个 <canvas> 元素可以获取多种类型的绘图上下文,但每种类型只能获取一次。获取 2D 上下文后,如果再次调用 getContext('2d'),浏览器将返回同一个上下文对象。

javascript
// 获取 canvas 元素
const canvas = document.getElementById('myCanvas');

// 获取 2D 上下文
const ctx = canvas.getContext('2d');

// ctx 就是 CanvasRenderingContext2D 实例
console.log(ctx instanceof CanvasRenderingContext2D); // true

获取上下文

getContext() 方法

getContext() 方法接受一个上下文类型参数,返回对应的渲染上下文对象。

javascript
/**
 * 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) 可以配置上下文行为:

属性类型默认值说明
alphabooleantrue是否包含 alpha 通道。设为 false 可提升性能
colorSpacestring"srgb"颜色空间,可设为 "display-p3"
desynchronizedbooleanfalse是否减少延迟,适用于低延迟场景
willReadFrequentlybooleanfalse是否频繁调用 getImageData(),优化像素读取性能
javascript
// 不需要透明度时,关闭 alpha 提升性能
const ctx = canvas.getContext('2d', { alpha: false });

// 频繁读取像素数据时
const pixelCtx = canvas.getContext('2d', {
  willReadFrequently: true
});

// 低延迟渲染(如游戏)
const gameCtx = canvas.getContext('2d', {
  alpha: false,
  desynchronized: true
});

完整示例

html
<!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 像素绘制的,如果坐标恰好落在整数像素上,线条会跨越两个像素,导致模糊。

javascript
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)来确保画面清晰:

html
<!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 上下文维护一组绘图状态属性。这些属性在任何时候都有一个"当前值",绘图操作使用的是当前的属性值。

主要的绘图状态包括:

状态类别包含属性
变换状态translaterotatescaletransform
样式状态fillStylestrokeStylelineWidthlineCap
合成状态globalAlphaglobalCompositeOperation
文本状态fonttextAligntextBaseline
裁剪状态当前裁剪路径
阴影状态shadowColorshadowBlurshadowOffsetXshadowOffsetY

save() 和 restore()

ctx.save() 将当前所有状态压入状态栈,ctx.restore() 从栈中弹出最近保存的状态并恢复。

javascript
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);  // 蓝色矩形(因为恢复了)

状态栈的工作原理

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="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 配合变换,可以轻松实现嵌套图形的绘制:

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>嵌套图形: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

线条端点与连接样式

javascript
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';  // 尖角连接

虚线样式

javascript
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. 上下文获取不可逆

javascript
// 一旦获取了某种上下文,就不能再获取其他类型
const canvas = document.getElementById('myCanvas');
const ctx2d = canvas.getContext('2d');
const ctxWebGL = canvas.getContext('webgl'); // 返回 null!

2. 状态栈没有上限检查

虽然浏览器内部限制了状态栈深度(通常很大),但过度使用 save/restore 会消耗内存:

javascript
// 不好的做法:不必要的 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.widthcanvas.height 会清空画布并重置所有状态:

javascript
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 100, 100);

canvas.width = canvas.width; // 重置!
ctx.fillStyle = 'blue';       // 需要重新设置

最佳实践

1. 封装高清 Canvas 工具函数

javascript
/**
 * 创建高清 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. 上下文状态属性检查

javascript
/**
 * 安全地重置上下文状态
 */
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' }

下一节

继续学习:路径与形状绘制

参考链接