Skip to content

深入理解 Canvas API

2D 绑图完整 API、路径绘制、图像处理、像素操作、离屏 Canvas 与性能优化

什么是 Canvas API?

定义:Canvas 是 HTML5 引入的位图绘制接口,通过 <canvas> 元素和 JavaScript 的绘图上下文(CanvasRenderingContext2D 或 WebGL)提供像素级别的图形绑制能力。与 SVG 的声明式矢量图不同,Canvas 是命令式的即时模式渲染——每一帧需要手动重绘。

涉及场景

  • 数据可视化:ECharts、Chart.js 等图表库底层使用 Canvas 渲染
  • 游戏开发:2D 游戏引擎(Phaser、PixiJS)基于 Canvas / WebGL
  • 图片编辑:裁剪、滤镜、水印、缩放等图像处理
  • 签名 / 手写板:捕捉鼠标/触摸轨迹并绘制笔迹
  • 验证码生成:在客户端动态绘制图形验证码
  • 视频帧处理:从 <video> 捕获帧并进行实时滤镜处理
  • 截图与导出html2canvas 将 DOM 转为图片,toDataURL 导出

作用

  1. 高性能渲染:直接操作像素缓冲区,适合大量图形元素的实时渲染
  2. 像素级控制getImageData / putImageData 可逐像素读写,实现自定义滤镜
  3. OffscreenCanvas:支持在 Web Worker 中绑制,不阻塞主线程
  4. 面试考点:Canvas 与 SVG 的区别、高 DPI 适配、性能优化策略是常见考题

Canvas 基础

html
<canvas id="canvas" width="800" height="600"></canvas>
<!-- 注意:用 HTML 属性设置宽高,不要用 CSS(CSS会拉伸) -->

<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d'); // 2D 上下文

// 其他上下文
// canvas.getContext('webgl');      // WebGL 1.0
// canvas.getContext('webgl2');     // WebGL 2.0
// canvas.getContext('bitmaprenderer'); // ImageBitmap
// canvas.getContext('webgpu');     // WebGPU(新)

// 高DPI适配
const dpr = window.devicePixelRatio || 1;
canvas.width = 800 * dpr;
canvas.height = 600 * dpr;
canvas.style.width = '800px';
canvas.style.height = '600px';
ctx.scale(dpr, dpr);
</script>

绘制基本形状

矩形

javascript
// 三种矩形方法
ctx.fillRect(x, y, width, height);    // 填充矩形
ctx.strokeRect(x, y, width, height);  // 描边矩形
ctx.clearRect(x, y, width, height);   // 清除矩形区域

// 示例
ctx.fillStyle = '#4CAF50';
ctx.fillRect(10, 10, 100, 80);

ctx.strokeStyle = '#2196F3';
ctx.lineWidth = 2;
ctx.strokeRect(130, 10, 100, 80);

ctx.clearRect(30, 30, 40, 40); // 清除一部分

路径

javascript
// 路径是所有复杂图形的基础
ctx.beginPath();          // 开始新路径

// 移动和连线
ctx.moveTo(x, y);        // 移动画笔(不画线)
ctx.lineTo(x, y);        // 画直线到指定点

// 圆弧
ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise);
ctx.arcTo(x1, y1, x2, y2, radius); // 通过切线画圆弧

// 贝塞尔曲线
ctx.quadraticCurveTo(cpx, cpy, x, y);            // 二次贝塞尔
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); // 三次贝塞尔

// 矩形路径
ctx.rect(x, y, width, height);

// 圆角矩形(新API)
ctx.roundRect(x, y, width, height, radii);
// radii: number | [all] | [topLeftRight, bottomRightLeft]
//        | [topLeft, topRightBottomLeft, bottomRight]
//        | [topLeft, topRight, bottomRight, bottomLeft]

// 椭圆
ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle);

// 闭合路径
ctx.closePath();          // 从当前点到起点画直线

// 绘制
ctx.fill();               // 填充
ctx.stroke();             // 描边
ctx.clip();               // 裁剪(后续绘制只在此区域内可见)

// fill 规则
ctx.fill('nonzero');      // 默认:非零环绕规则
ctx.fill('evenodd');      // 奇偶规则

// 示例:画三角形
ctx.beginPath();
ctx.moveTo(200, 10);
ctx.lineTo(150, 100);
ctx.lineTo(250, 100);
ctx.closePath();
ctx.fillStyle = '#FF5722';
ctx.fill();
ctx.stroke();

// 示例:画圆
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.fillStyle = '#9C27B0';
ctx.fill();

Path2D(可复用路径)

javascript
// 创建可复用的路径对象
const circle = new Path2D();
circle.arc(100, 100, 50, 0, Math.PI * 2);

const rect = new Path2D();
rect.rect(200, 50, 100, 100);

// 使用 SVG 路径字符串
const svgPath = new Path2D('M 10 80 Q 95 10 180 80 T 350 80');

// 绘制
ctx.fill(circle);
ctx.stroke(rect);
ctx.stroke(svgPath);

// 路径组合
const combined = new Path2D();
combined.addPath(circle);
combined.addPath(rect);

// 点是否在路径内
ctx.isPointInPath(circle, mouseX, mouseY);
ctx.isPointInStroke(rect, mouseX, mouseY);

样式与颜色

填充和描边

javascript
// 颜色
ctx.fillStyle = '#FF0000';
ctx.fillStyle = 'rgb(255, 0, 0)';
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fillStyle = 'hsl(0, 100%, 50%)';
ctx.strokeStyle = '#0000FF';

// 透明度
ctx.globalAlpha = 0.5; // 全局透明度

// 线条样式
ctx.lineWidth = 2;
ctx.lineCap = 'butt';     // butt | round | square
ctx.lineJoin = 'miter';   // miter | round | bevel
ctx.miterLimit = 10;
ctx.setLineDash([5, 3]);   // 虚线模式 [线段, 间隔]
ctx.lineDashOffset = 0;    // 虚线偏移

// 渐变
const linearGrad = ctx.createLinearGradient(0, 0, 200, 0);
linearGrad.addColorStop(0, 'red');
linearGrad.addColorStop(0.5, 'yellow');
linearGrad.addColorStop(1, 'blue');
ctx.fillStyle = linearGrad;

const radialGrad = ctx.createRadialGradient(100, 100, 20, 100, 100, 80);
radialGrad.addColorStop(0, 'white');
radialGrad.addColorStop(1, 'black');
ctx.fillStyle = radialGrad;

// 锥形渐变
const conicGrad = ctx.createConicGradient(0, 100, 100);
conicGrad.addColorStop(0, 'red');
conicGrad.addColorStop(0.25, 'yellow');
conicGrad.addColorStop(0.5, 'green');
conicGrad.addColorStop(0.75, 'blue');
conicGrad.addColorStop(1, 'red');
ctx.fillStyle = conicGrad;

// 图案
const img = new Image();
img.onload = () => {
  const pattern = ctx.createPattern(img, 'repeat');
  // repeat | repeat-x | repeat-y | no-repeat
  ctx.fillStyle = pattern;
  ctx.fillRect(0, 0, 400, 400);
};
img.src = 'texture.png';

阴影

javascript
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;

ctx.fillRect(50, 50, 100, 100); // 带阴影的矩形

混合模式

javascript
ctx.globalCompositeOperation = 'source-over';     // 默认
ctx.globalCompositeOperation = 'source-in';
ctx.globalCompositeOperation = 'source-out';
ctx.globalCompositeOperation = 'source-atop';
ctx.globalCompositeOperation = 'destination-over';
ctx.globalCompositeOperation = 'destination-in';
ctx.globalCompositeOperation = 'destination-out';
ctx.globalCompositeOperation = 'destination-atop';
ctx.globalCompositeOperation = 'lighter';
ctx.globalCompositeOperation = 'copy';
ctx.globalCompositeOperation = 'xor';
ctx.globalCompositeOperation = 'multiply';
ctx.globalCompositeOperation = 'screen';
ctx.globalCompositeOperation = 'overlay';
ctx.globalCompositeOperation = 'darken';
ctx.globalCompositeOperation = 'lighten';
// ... 等等,与 CSS mix-blend-mode 相同

文本

javascript
// 设置字体
ctx.font = '24px Arial';
ctx.font = 'bold italic 18px "Microsoft YaHei"';

// 对齐
ctx.textAlign = 'start';     // start | end | left | right | center
ctx.textBaseline = 'alphabetic'; // top | hanging | middle | alphabetic | ideographic | bottom
ctx.direction = 'ltr';       // ltr | rtl | inherit

// 绘制文本
ctx.fillText('Hello Canvas', 100, 100);
ctx.strokeText('Hello Canvas', 100, 150);

// 限制最大宽度
ctx.fillText('Very long text...', 100, 200, 200); // 最大200px

// 测量文本
const metrics = ctx.measureText('Hello');
console.log(metrics.width);                    // 文本宽度
console.log(metrics.actualBoundingBoxAscent);  // 上方边界
console.log(metrics.actualBoundingBoxDescent); // 下方边界
console.log(metrics.fontBoundingBoxAscent);    // 字体上方边界

变换

javascript
// 平移
ctx.translate(100, 50);

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

// 缩放
ctx.scale(2, 2);

// 变换矩阵
ctx.transform(a, b, c, d, e, f);    // 叠加变换
ctx.setTransform(a, b, c, d, e, f); // 重置并设置变换
ctx.resetTransform();                // 重置为单位矩阵

// DOMMatrix(更现代的方式)
const matrix = ctx.getTransform();   // 获取当前变换矩阵
ctx.setTransform(new DOMMatrix().rotate(45).translate(100, 0));

// 保存和恢复状态(状态栈)
ctx.save();    // 压栈(保存当前所有状态)
// ... 修改样式、变换等
ctx.restore(); // 出栈(恢复到 save 时的状态)

// save/restore 保存的状态包括:
// fillStyle, strokeStyle, globalAlpha, lineWidth, lineCap, lineJoin,
// miterLimit, shadowColor/Blur/Offset, globalCompositeOperation,
// font, textAlign, textBaseline, transform, clip region,
// lineDash, lineDashOffset, filter, imageSmoothingEnabled

图像操作

javascript
// drawImage - 三种重载
ctx.drawImage(image, dx, dy);                                    // 原始尺寸
ctx.drawImage(image, dx, dy, dWidth, dHeight);                   // 缩放
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); // 裁剪+缩放

// image 可以是:
// HTMLImageElement (<img>)
// HTMLVideoElement (<video>)
// HTMLCanvasElement (<canvas>)
// ImageBitmap
// OffscreenCanvas
// VideoFrame

// 示例:视频帧捕获
const video = document.querySelector('video');
function captureFrame() {
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  requestAnimationFrame(captureFrame);
}

// 导出图片
canvas.toDataURL('image/png');           // Base64 PNG
canvas.toDataURL('image/jpeg', 0.8);     // JPEG 质量 0.8
canvas.toBlob((blob) => {
  // Blob 对象,可用于上传
  const url = URL.createObjectURL(blob);
}, 'image/png');

像素操作

javascript
// 获取像素数据
const imageData = ctx.getImageData(x, y, width, height);
// imageData.data: Uint8ClampedArray [R,G,B,A, R,G,B,A, ...]
// imageData.width, imageData.height

// 创建空的 ImageData
const newData = ctx.createImageData(width, height);
const newData2 = new ImageData(width, height);

// 写回像素数据
ctx.putImageData(imageData, dx, dy);
ctx.putImageData(imageData, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight);

// 实用函数:获取/设置单个像素
function getPixel(imageData, x, y) {
  const i = (y * imageData.width + x) * 4;
  return {
    r: imageData.data[i],
    g: imageData.data[i + 1],
    b: imageData.data[i + 2],
    a: imageData.data[i + 3]
  };
}

function setPixel(imageData, x, y, r, g, b, a = 255) {
  const i = (y * imageData.width + x) * 4;
  imageData.data[i] = r;
  imageData.data[i + 1] = g;
  imageData.data[i + 2] = b;
  imageData.data[i + 3] = a;
}

// 滤镜示例:灰度化
function grayscale(ctx, x, y, w, h) {
  const imageData = ctx.getImageData(x, y, w, h);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    const gray = data[i] * 0.299 + data[i+1] * 0.587 + data[i+2] * 0.114;
    data[i] = data[i+1] = data[i+2] = gray;
  }
  ctx.putImageData(imageData, x, y);
}

// CSS filter 也可以直接用在 Canvas 上
ctx.filter = 'blur(5px)';
ctx.filter = 'brightness(150%) contrast(120%)';
ctx.filter = 'grayscale(100%)';
ctx.filter = 'none'; // 重置

OffscreenCanvas(离屏 Canvas)

javascript
// 主线程中使用
const offscreen = new OffscreenCanvas(800, 600);
const offCtx = offscreen.getContext('2d');
offCtx.fillRect(0, 0, 100, 100);

// 将离屏内容绘制到可见 Canvas
ctx.drawImage(offscreen, 0, 0);

// 在 Web Worker 中使用(不阻塞主线程!)
// main.js
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);

// worker.js
onmessage = (e) => {
  const canvas = e.data.canvas;
  const ctx = canvas.getContext('2d');
  // 在 Worker 线程中绑制,不阻塞主线程
  function draw() {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    // 复杂绑制逻辑...
    requestAnimationFrame(draw);
  }
  draw();
};

// 转换为 Blob
const blob = await offscreen.convertToBlob({ type: 'image/png' });

// 转换为 ImageBitmap
const bitmap = offscreen.transferToImageBitmap();

动画与性能

javascript
// requestAnimationFrame 动画循环
let lastTime = 0;

function animate(timestamp) {
  const deltaTime = timestamp - lastTime;
  lastTime = timestamp;

  // 清除画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 更新和绘制
  update(deltaTime);
  draw();

  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

// 性能优化技巧
// 1. 分层 Canvas:静态层 + 动态层
const bgCanvas = document.createElement('canvas');   // 静态背景
const fgCanvas = document.getElementById('canvas');   // 动态前景

// 2. 脏矩形:只重绘变化的区域
ctx.clearRect(dirtyX, dirtyY, dirtyW, dirtyH);
// 只在脏矩形区域内绘制

// 3. 使用 OffscreenCanvas 在 Worker 中绑制

// 4. 批量绘制:减少状态切换
// ❌ 频繁切换样式
for (const item of items) {
  ctx.fillStyle = item.color;
  ctx.fillRect(item.x, item.y, item.w, item.h);
}
// ✅ 按颜色分组绘制
const groups = groupBy(items, 'color');
for (const [color, group] of groups) {
  ctx.fillStyle = color;
  for (const item of group) {
    ctx.fillRect(item.x, item.y, item.w, item.h);
  }
}

// 5. 使用 ImageBitmap 替代 Image
const bitmap = await createImageBitmap(imgElement);
ctx.drawImage(bitmap, 0, 0); // 比直接用 Image 更快

// 6. 避免使用 ctx.getImageData(很慢)
// 7. 开启硬件加速
canvas.getContext('2d', {
  willReadFrequently: false, // 如果不常读像素,设为 false
  alpha: false,              // 不需要透明度时设为 false(性能提升)
  desynchronized: true       // 低延迟模式(可能跳帧)
});

总结

Canvas API 核心知识点:
┌──────────────────────────────────────────────────────────┐
│ 基本绘制                                                  │
│ • 矩形:fillRect / strokeRect / clearRect                │
│ • 路径:beginPath → moveTo/lineTo/arc/curve → fill/stroke│
│ • Path2D:可复用路径对象                                    │
│ • 文本:fillText / strokeText / measureText               │
├──────────────────────────────────────────────────────────┤
│ 样式                                                      │
│ • 颜色/渐变/图案:fillStyle / strokeStyle                  │
│ • 线条:lineWidth / lineCap / lineJoin / setLineDash      │
│ • 阴影、混合模式、CSS filter                               │
├──────────────────────────────────────────────────────────┤
│ 变换                                                      │
│ • translate / rotate / scale / transform                  │
│ • save() / restore() 状态栈管理                            │
├──────────────────────────────────────────────────────────┤
│ 图像与像素                                                │
│ • drawImage 三种重载(原始/缩放/裁剪)                      │
│ • getImageData / putImageData 像素级操作                   │
│ • toDataURL / toBlob 导出图片                              │
├──────────────────────────────────────────────────────────┤
│ 性能优化                                                  │
│ • 分层 Canvas / 脏矩形重绘                                 │
│ • OffscreenCanvas + Web Worker                            │
│ • 批量绘制减少状态切换                                      │
│ • ImageBitmap / willReadFrequently / alpha: false          │
└──────────────────────────────────────────────────────────┘