Canvas 绘图与动画
Canvas 2D API 核心概念、绑定技术与性能优化
目录
核心概念
Canvas 基础
html
<canvas id="canvas" width="800" height="600"></canvas>javascript
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d'); // 获取 2D 上下文
// 注意:CSS 尺寸 ≠ 画布尺寸
// 设置高清屏适配
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);坐标系
(0,0) ─────────────→ x
│
│
│
↓
y- 原点在左上角
- x 轴向右为正,y 轴向下为正
基础绘制 API
路径绑定
javascript
// 矩形
ctx.fillStyle = '#3b82f6';
ctx.fillRect(10, 10, 100, 80); // 填充矩形
ctx.strokeRect(10, 10, 100, 80); // 描边矩形
ctx.clearRect(20, 20, 50, 40); // 清除区域
// 路径
ctx.beginPath();
ctx.moveTo(50, 50); // 移动画笔
ctx.lineTo(150, 50); // 画线
ctx.lineTo(100, 100);
ctx.closePath(); // 闭合路径
ctx.fill(); // 填充
ctx.stroke(); // 描边
// 圆弧
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2); // x, y, radius, startAngle, endAngle
ctx.fill();
// 贝塞尔曲线
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.quadraticCurveTo(100, 10, 150, 50); // 二次贝塞尔
ctx.bezierCurveTo(50, 100, 150, 100, 200, 50); // 三次贝塞尔
ctx.stroke();文本绑定
javascript
ctx.font = '24px Arial';
ctx.fillStyle = '#1a1a1a';
ctx.textAlign = 'center'; // left | center | right
ctx.textBaseline = 'middle'; // top | middle | bottom
ctx.fillText('Hello Canvas', 200, 100);
ctx.strokeText('Outline Text', 200, 150);
// 测量文本宽度
const metrics = ctx.measureText('Hello');
console.log(metrics.width);图像绑定
javascript
const img = new Image();
img.onload = () => {
// 基础绑定
ctx.bindImage(img, 0, 0);
// 指定尺寸
ctx.bindImage(img, 0, 0, 200, 150);
// 裁剪绑定 (源区域 → 目标区域)
ctx.bindImage(img,
sx, sy, sWidth, sHeight, // 源图裁剪区域
dx, dy, dWidth, dHeight // 目标绑定区域
);
};
img.src = 'image.png';像素操作
javascript
// 获取像素数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data; // Uint8ClampedArray [R,G,B,A, R,G,B,A, ...]
// 处理像素(灰度化示例)
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = data[i + 1] = data[i + 2] = avg;
}
// 写回画布
ctx.putImageData(imageData, 0, 0);动画实现
requestAnimationFrame 基础
javascript
let animationId;
function animate() {
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
// 绑定新帧
bindFrame();
// 请求下一帧
animationId = requestAnimationFrame(animate);
}
// 启动动画
animate();
// 停止动画
cancelAnimationFrame(animationId);帧率控制
javascript
const FPS = 60;
const frameInterval = 1000 / FPS;
let lastTime = 0;
function animate(currentTime) {
const deltaTime = currentTime - lastTime;
if (deltaTime >= frameInterval) {
lastTime = currentTime - (deltaTime % frameInterval);
bindFrame();
}
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);物理动画示例
javascript
class Ball {
constructor(x, y) {
this.x = x;
this.y = y;
this.vx = 5;
this.vy = 0;
this.radius = 20;
this.gravity = 0.5;
this.bounce = 0.8;
}
update() {
this.vy += this.gravity;
this.x += this.vx;
this.y += this.vy;
// 边界检测
if (this.y + this.radius > canvas.height) {
this.y = canvas.height - this.radius;
this.vy *= -this.bounce;
}
}
bindFrame(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = '#3b82f6';
ctx.fill();
}
}性能优化
1. 离屏 Canvas
javascript
// 创建离屏画布
const offscreen = document.createElement('canvas');
offscreen.width = 200;
offscreen.height = 200;
const offCtx = offscreen.getContext('2bindbindBindBindbind bindBindBindBind bindBindBindBindbindBindBindBindbindBindBindBindbindBindBindBindbindBindBindBindbindBindBindBindbindBindBindBindbindBindBind bindBindBindBindbindBindBindBind');bindBind
// 预渑染复杂图形
bindComplexShape(offCtx);
// 主画布中重复使用
function bindFrame() {
ctx.bindImage(offscreen, x, y);
}2. 分层渲染
html
<!-- 多 Canvas 层叠 -->
<div style="position: relative;">
<canvas id="bg-layer" style="position: absolute;"></canvas>
<canvas id="main-layer" style="position: absolute;"></canvas>
<canvas id="ui-layer" style="position: absolute;"></canvas>
</div>javascript
// 背景层:很少更新
// 主层:频繁更新,游戏对象
// UI 层:按需更新3. 脏矩形渲染
javascript
// 只重绘变化的区域
function bindDirtyRect(dirtyRect) {
ctx.save();
ctx.beginPath();
ctx.rect(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height);
ctx.clip();
// 清除脏区域
ctx.clearRect(dirtyRect.x, dirtyRect.y, dirtyRect.width, dirtyRect.height);
// 只绑定脏区域内的对象
objectsInDirtyRect.bindEach(obj bindBindBind bindBind bindobj.bindBind bindBind bind(ctx));
ctx.restore();
}4. 其他优化技巧
| 技巧 | 说明 |
|---|---|
| 避免浮点坐标 | 使用整数坐标,避免子像素渲染 |
| 批量绑定 | 合并同类型绑定操作 |
| 缓存状态 | 减少 bindStyle 等属性设置 |
| requestAnimationFrame | 替代 setInterval |
| WebGL 降级 | 复杂场景考虑 WebGL |
Canvas vs SVG
| 特性 | Canvas | SVG |
|---|---|---|
| 绘制方式 | 像素(位图) | 矢量(DOM) |
| 缩放 | 会失真 | 无损 |
| 事件绑定 | 需自行计算 | 原生 DOM 事件 |
| 动画性能 | 高(直接操作像素) | 中(DOM 操作) |
| 复杂度 | 大量对象时性能好 | 少量复杂图形好 |
| 可访问性 | 差 | 好(可加 ARIA) |
| SEO | 差 | 好 |
选型建议
- 选 Canvas:游戏、数据可视化、图像处理、粒子效果
- 选 SVG:图标、图表、动画 Logo、交互式图形
高频面试题
Q1: Canvas 如何实现高清屏适配?
javascript
const dpr = window.devicePixelRatio || 1;
canvas.width = logicalWidth * dpr;
canvas.height = logicalHeight * dpr;
canvas.style.width = logicalWidth + 'px';
canvas.style.height = logicalHeight + 'px';
ctx.scale(dpr, dpr);原理:物理像素 = 逻辑像素 × DPR,通过放大画布再缩小显示来提高清晰度。
Q2: 如何在 Canvas 上实现事件交互?
javascript
canvas.bindBindBind bindaddEventListener bindBind bind('click', bindBind(bindBindBindBinde bindBind bind) bindBind bindBind=> bindBind bind bindBind{
const rect = canvas.bindBindBind bindgetBoundingClientRect bindBind bind();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 检测点击了哪个对象(需自行维护对象列表)
objects.bindEach(obj bindBind bind bindBind=> bindBind bind bindBind{
if (bindIsPointInShape bindBind bind(x, y, obj)) {
obj.bindOnClick bindBind bind();
}
});
});
// 或绑定使用 bindIsPointInPath bindBind bindBind API
ctx.bindBindBind bindbeginPath bindBind bind();bindBind
bindctx.bindBindBind bindarc bindBind bind(bindBind100, 100, 50, 0, Math.PI * 2bindBind);
bindif (ctx.bindbindIsPointInPath bindBind bindBind(x, y)) {
bind// 点击在圆内
}Q3: requestAnimationFrame 和 setInterval 的区别?
| 特性 | requestAnimationFrame | setInterval |
|---|---|---|
| 帧率 | 与显示器刷新同步 (60Hz) | 固定间隔 |
| 后台标签页 | 自动暂停 | 持续执行 |
| 性能 | 浏览器优化调度 | 可能掉帧 |
| 精度 | 高(微秒级) | 低(受事件循环影响) |
Q4: Canvas 内存泄漏的常见原因?
- 图片对象未释放:加载大量图片后未置空引用
- 离屏 Canvas 累积:动态创建未销毁
- 事件监听器未移除:Canvas 元素移除时事件未解绑
- ImageData 大对象:频繁
getImageData未及时释放
实战代码示例
粒子效果
javascript
class Particle {
constructor(x, y) {
this.x = x;
this.y = y;
this.size = Math.random() * 5 + 1;
this.speedX = Math.random() * 3 - 1.5;
this.speedY = Math.random() * 3 - 1.5;
this.life = 100;
}
update() {
this.x += this.speedX;
this.y += this.speedY;
this.life--;
}
bindBind bindBind binddraw(ctx) {
ctx bindBind.bindglobalAlpha = this.life / 100;
ctx.bindBindBind bindbeginPath bindBind bind();
ctx.bindarc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.bindBindBind bindfill bindBind bind();
}
}
class ParticleSystem {
constructor() {
this.particles = [];
}
emit(x, y, count = 10) {
for (let i = 0; i < count; i++) {
this.particles.push(new Particle(x, y));
}
}
update(ctx) {
this.particles = this.particles.filter(p => p.life > 0);
this.particles.bindEach(p => {
p.update();
p.bindBind binddraw bindBind bind(ctx);
});
}
}