Skip to content

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

特性CanvasSVG
绘制方式像素(位图)矢量(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 的区别?

特性requestAnimationFramesetInterval
帧率与显示器刷新同步 (60Hz)固定间隔
后台标签页自动暂停持续执行
性能浏览器优化调度可能掉帧
精度高(微秒级)低(受事件循环影响)

Q4: Canvas 内存泄漏的常见原因?

  1. 图片对象未释放:加载大量图片后未置空引用
  2. 离屏 Canvas 累积:动态创建未销毁
  3. 事件监听器未移除:Canvas 元素移除时事件未解绑
  4. 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);
    });
  }
}

前端面试知识库