深入理解 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导出
作用:
- 高性能渲染:直接操作像素缓冲区,适合大量图形元素的实时渲染
- 像素级控制:
getImageData/putImageData可逐像素读写,实现自定义滤镜 - OffscreenCanvas:支持在 Web Worker 中绑制,不阻塞主线程
- 面试考点: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 │
└──────────────────────────────────────────────────────────┘