Skip to content

Canvas 2D 入门:从像素到动画(交互式教程)

学习指南:本章节无需深厚的前端基础,通过交互式演示带你掌握 Canvas 2D 的核心原理和实践技巧。我们将从最基础的绘制开始,一直到构建复杂的交互式图形应用。

0. 引言:Canvas 是什么

Canvas(画布)是 HTML5 提供的一个通过 JavaScript 绘制 2D 图形的元素。你可以把它想象成一张数字画布,上面可以用代码"画"出任何东西:简单的形状、复杂的图表、流畅的动画,甚至是完整的游戏。

0.1 Canvas vs SVG:有什么区别?

在 Web 开发中,绘制图形主要有两种方式:Canvas 和 SVG(Scalable Vector Graphics)。它们各有优劣:

特性CanvasSVG
类型位图(光栅图形)矢量图形
DOM单个 <canvas> 元素每个图形都是 DOM 元素
交互需要手动计算碰撞天然支持事件绑定
性能适合大量对象适合少量复杂对象
缩放放大会失真无限缩放不失真
应用游戏、数据可视化图标、插画

简单总结

  • Canvas = 像素画,画完就变成像素,性能好但交互麻烦
  • SVG = 矢量图,每个图形都是对象,交互方便但对象多了会慢

0.2 Canvas 的应用场景

Canvas 的用途非常广泛,你可能在很多地方都见过它:

  1. 数据可视化:折线图、饼图、热力图(如 ECharts、Chart.js)
  2. 游戏开发:网页游戏(如 Phaser.js 引擎)
  3. 图像处理:图片裁剪、滤镜、拼图(如 Fabric.js)
  4. 创意效果:粒子特效、动画背景(如 Three.js 的 2D 渲染)
  5. 工程绘图:CAD、流程图、思维导图

1. Canvas 基础

1.1 Canvas 元素和上下文

使用 Canvas 的第一步是在 HTML 中创建一个 <canvas> 元素:

html
<canvas id="myCanvas" width="600" height="400"></canvas>

然后通过 JavaScript 获取渲染上下文(Rendering Context)

javascript
const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d') // 获取 2D 上下文

关键概念

  • canvas 是 DOM 元素,控制画布的大小和位置
  • ctx 是绘图工具,所有的绘制操作都通过它完成
  • '2d' 表示使用 2D 渲染上下文(WebGL 使用 'webgl'

🕹️ 交互演示:点击下方按钮,体验 Canvas 的基本绘图操作。

Code / 代码

const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d')

ctx.fillStyle = '#3498db'
ctx.strokeStyle = '#2c3e50'
ctx.lineWidth = 2

// 绘制填充矩形
ctx.fillRect(250, 150, 100, 100)

// 绘制描边矩形
ctx.strokeRect(250, 150, 100, 100)

💡提示: Canvas 是一个位图画布,所有绘制都是像素操作。绘制后无法修改已有内容,只能覆盖或清除重绘。

1.2 坐标系统

Canvas 使用的是屏幕坐标系,这与传统数学坐标系有所不同:

  • 原点 (0, 0):在左上角(不是中心)
  • X 轴:向右为正方向
  • Y 轴:向下为正方向(注意:数学坐标系中 Y 轴向上)
  • 单位:像素(px)
javascript
// 在左上角绘制一个点
ctx.fillRect(0, 0, 10, 10)

// 在右下角绘制一个点
ctx.fillRect(canvas.width - 10, canvas.height - 10, 10, 10)

🕹️ 交互演示:拖动下方的点,感受 Canvas 的坐标系统。

Canvas Width:600px
Canvas Height:400px
Mouse Position:(0, 0)

Canvas Coordinate System / Canvas 坐标系统

  • Origin / 原点:在左上角,坐标为 (0, 0)
  • X Axis / X 轴:向右为正方向,从 0 到 canvas.width
  • Y Axis / Y 轴:向下为正方向,从 0 到 canvas.height
  • Unit / 单位:像素 (px),与 CSS 像素 1:1 对应

Example Code / 示例代码

// 绘制坐标轴
const canvas = document.getElementById('myCanvas')
const ctx = canvas.getContext('2d')

// X 轴(红色)
ctx.strokeStyle = '#e74c3c'
ctx.lineWidth = 2
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(canvas.width, 0)
ctx.stroke()

// Y 轴(蓝色)
ctx.strokeStyle = '#3498db'
ctx.beginPath()
ctx.moveTo(0, 0)
ctx.lineTo(0, canvas.height)
ctx.stroke()

// 绘制点
ctx.fillStyle = '#2ecc71'
ctx.beginPath()
ctx.arc(100, 100, 5, 0, Math.PI * 2)
ctx.fill()

💡提示: Canvas 的 Y 轴方向与传统数学坐标系相反,向下为正。这在处理图形定位时需要特别注意。

1.3 绘制基本形状

Canvas 提供了几种绘制基本形状的方法:

矩形

javascript
// 填充矩形
ctx.fillStyle = '#3498db'
ctx.fillRect(x, y, width, height)

// 描边矩形
ctx.strokeStyle = '#2c3e50'
ctx.lineWidth = 2
ctx.strokeRect(x, y, width, height)

// 清除矩形区域
ctx.clearRect(x, y, width, height)

圆形

javascript
ctx.beginPath()
ctx.arc(x, y, radius, startAngle, endAngle)
ctx.fill() // 或 ctx.stroke()

参数说明

  • x, y:圆心坐标
  • radius:半径
  • startAngle, endAngle:起始和结束角度(弧度制)
    • 0 = 3 点钟方向
    • Math.PI / 2 = 6 点钟方向
    • Math.PI = 9 点钟方向
    • Math.PI * 1.5 = 12 点钟方向
    • Math.PI * 2 = 回到 3 点钟方向

线条

javascript
ctx.beginPath()
ctx.moveTo(x1, y1) // 起点
ctx.lineTo(x2, y2) // 终点
ctx.stroke()

1.4 颜色和渐变

Canvas 支持多种颜色设置方式:

javascript
// 纯色
ctx.fillStyle = '#3498db' // 十六进制
ctx.fillStyle = 'rgb(52, 152, 219)' // RGB
ctx.fillStyle = 'rgba(52, 152, 219, 0.5)' // RGBA(带透明度)

// 线性渐变
const gradient = ctx.createLinearGradient(x1, y1, x2, y2)
gradient.addColorStop(0, '#3498db')
gradient.addColorStop(1, '#e74c3c')
ctx.fillStyle = gradient

// 径向渐变
const radialGradient = ctx.createRadialGradient(x1, y1, r1, x2, y2, r2)
radialGradient.addColorStop(0, '#3498db')
radialGradient.addColorStop(1, 'transparent')
ctx.fillStyle = radialGradient

2. 路径与形状

2.1 路径 (Path) 的概念

路径是 Canvas 中的核心概念,它是由一系列点连接成的"轨迹"。你可以把它想象成用笔画线的过程:

  1. beginPath() - 开始新路径(拿起笔)
  2. moveTo() - 移动到起点(不画线)
  3. lineTo() / arc() / curveTo() - 绘制线条或曲线
  4. closePath() - 闭合路径(可选)
  5. fill() / stroke() - 填充或描边
javascript
ctx.beginPath()
ctx.moveTo(100, 100) // 移动到起点
ctx.lineTo(200, 100) // 画横线
ctx.lineTo(150, 150) // 画斜线
ctx.closePath() // 闭合路径(回到起点)
ctx.fill() // 填充

2.2 绘制复杂形状

通过组合路径,可以绘制任意复杂的形状:

三角形

javascript
ctx.beginPath()
ctx.moveTo(100, 50)
ctx.lineTo(150, 150)
ctx.lineTo(50, 150)
ctx.closePath()
ctx.fillStyle = '#e74c3c'
ctx.fill()

星形

javascript
function drawStar(ctx, cx, cy, spikes, outerRadius, innerRadius) {
  let rot = (Math.PI / 2) * 3
  let x = cx
  let y = cy
  const step = Math.PI / spikes

  ctx.beginPath()
  ctx.moveTo(cx, cy - outerRadius)

  for (let i = 0; i < spikes; i++) {
    x = cx + Math.cos(rot) * outerRadius
    y = cy + Math.sin(rot) * outerRadius
    ctx.lineTo(x, y)
    rot += step

    x = cx + Math.cos(rot) * innerRadius
    y = cy + Math.sin(rot) * innerRadius
    ctx.lineTo(x, y)
    rot += step
  }

  ctx.lineTo(cx, cy - outerRadius)
  ctx.closePath()
  ctx.fill()
}

drawStar(ctx, 150, 150, 5, 50, 25)

3. 文本与图片

3.1 绘制文本

Canvas 也可以绘制文本:

javascript
// 填充文本
ctx.font = '30px Arial'
ctx.fillStyle = '#2c3e50'
ctx.fillText('Hello Canvas', x, y)

// 描边文本
ctx.font = 'bold 40px Arial'
ctx.strokeStyle = '#e74c3c'
ctx.lineWidth = 2
ctx.strokeText('Hello Canvas', x, y)

// 文本对齐
ctx.textAlign = 'center' // left, center, right
ctx.textBaseline = 'middle' // top, middle, bottom
ctx.fillText('Centered', canvas.width / 2, canvas.height / 2)

3.2 加载和绘制图片

javascript
const img = new Image()
img.src = 'image.png'
img.onload = () => {
  // 绘制图片
  ctx.drawImage(img, x, y)

  // 缩放图片
  ctx.drawImage(img, x, y, width, height)

  // 裁剪图片
  ctx.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
}

参数说明

  • sx, sy, sWidth, sHeight:源图像的裁剪区域
  • dx, dy, dWidth, dHeight:目标画布的绘制区域

3.3 裁剪与合成

javascript
// 裁剪区域
ctx.save()
ctx.beginPath()
ctx.arc(x, y, radius, 0, Math.PI * 2)
ctx.clip() // 之后的所有绘制都只会显示在圆形内
ctx.drawImage(img, 0, 0)
ctx.restore()

// 全局合成操作
ctx.globalCompositeOperation = 'source-over' // 默认
ctx.globalCompositeOperation = 'destination-over' // 绘制在现有内容后面
ctx.globalCompositeOperation = 'source-in' // 只保留重叠部分

4. 动画基础

4.1 requestAnimationFrame

在 Canvas 中创建动画,核心是使用 requestAnimationFrame 方法。它是浏览器专门为动画优化的 API:

javascript
function animate() {
  // 1. 清除画布(或绘制半透明背景产生拖尾效果)
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  // 2. 更新状态
  update()

  // 3. 绘制
  draw()

  // 4. 请求下一帧
  requestAnimationFrame(animate)
}

// 启动动画
animate()

为什么用 requestAnimationFrame 而不是 setInterval?

  • 自动优化,通常为 60FPS(每秒 60 帧)
  • 页面不可见时自动暂停,节省资源
  • 与浏览器刷新周期同步,避免画面撕裂

🕹️ 交互演示:点击播放,观察不同类型的动画效果。

FPS:0
Frame:0

Animation Loop Code / 动画循环代码

// 弹跳球动画
let balls = [
  { x: 100, y: 100, vx: 2, vy: 3, radius: 20 },
  // ... 更多球
]

function animate(timestamp) {
  // 清除画布
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  // 更新和绘制每个球
  balls.forEach(ball => {
    // 更新位置
    ball.x += ball.vx * 1
    ball.y += ball.vy * 1

    // 边界碰撞检测
    if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
      ball.vx = -ball.vx
    }
    if (ball.y + ball.radius > canvas.height || ball.y - ball.radius < 0) {
      ball.vy = -ball.vy
    }

    // 绘制
    ctx.beginPath()
    ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2)
    ctx.fill()
  })

  // 请求下一帧
  requestAnimationFrame(animate)
}

// 启动动画
requestAnimationFrame(animate)

Animation Principles / 动画原理

  • requestAnimationFrame: 浏览器提供的动画 API,在每次重绘前调用回调函数,通常为 60FPS
  • Clear & Redraw: 每帧先清除画布,再重新绘制所有内容
  • State Update: 更新对象位置、角度等状态
  • Performance: 使用时间差计算位置,确保不同刷新率下动画速度一致

💡提示: 动画的本质是快速连续绘制静态画面。Canvas 每秒可以绘制 60 帧(60FPS),形成流畅的动画效果。

4.2 清除与重绘

动画的本质是快速连续绘制静态画面。每帧需要:

  1. 清除旧画面ctx.clearRect() 或用半透明背景覆盖
  2. 更新状态:计算新位置、新角度等
  3. 绘制新画面:重新绘制所有对象
javascript
// 方法1:完全清除
ctx.clearRect(0, 0, canvas.width, canvas.height)

// 方法2:半透明背景(产生拖尾效果)
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'
ctx.fillRect(0, 0, canvas.width, canvas.height)

// 方法3:只清除变化区域(脏矩形优化)
objects.forEach(obj => {
  if (obj.moved) {
    ctx.clearRect(obj.oldX, obj.oldY, obj.size, obj.size)
    obj.draw(ctx)
  }
})

4.3 动画循环

一个完整的动画循环示例:

javascript
let ball = {
  x: 300,
  y: 200,
  vx: 2,
  vy: 3,
  radius: 20
}

function update() {
  // 更新位置
  ball.x += ball.vx
  ball.y += ball.vy

  // 边界碰撞
  if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
    ball.vx = -ball.vx
  }
  if (ball.y + ball.radius > canvas.height || ball.y - ball.radius < 0) {
    ball.vy = -ball.vy
  }
}

function draw() {
  // 清除画布
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  // 绘制球
  ctx.beginPath()
  ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2)
  ctx.fillStyle = '#3498db'
  ctx.fill()
}

function animate() {
  update()
  draw()
  requestAnimationFrame(animate)
}

animate()

5. 事件处理

Canvas 只是一个 DOM 元素,不像 SVG 那样每个图形都是独立的 DOM 元素。因此,我们需要手动处理交互事件

5.1 鼠标事件

javascript
canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top

  console.log(`Clicked at (${x}, ${y})`)
})

canvas.addEventListener('mousemove', (e) => {
  const rect = canvas.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top

  // 检测是否悬停在某个对象上
  objects.forEach(obj => {
    const dist = Math.sqrt((x - obj.x) ** 2 + (y - obj.y) ** 2)
    if (dist < obj.radius) {
      canvas.style.cursor = 'pointer'
      obj.hovered = true
    }
  })
})

5.2 拖拽实现

javascript
let isDragging = false
let selectedObject = null

canvas.addEventListener('mousedown', (e) => {
  const { x, y } = getMousePos(e)

  objects.forEach(obj => {
    const dist = Math.sqrt((x - obj.x) ** 2 + (y - obj.y) ** 2)
    if (dist < obj.radius) {
      isDragging = true
      selectedObject = obj
    }
  })
})

canvas.addEventListener('mousemove', (e) => {
  if (isDragging && selectedObject) {
    const { x, y } = getMousePos(e)
    selectedObject.x = x
    selectedObject.y = y
    draw() // 重绘
  }
})

canvas.addEventListener('mouseup', () => {
  isDragging = false
  selectedObject = null
})

5.3 键盘事件

javascript
canvas.tabIndex = 0 // 使 canvas 可以获取焦点
canvas.focus()

canvas.addEventListener('keydown', (e) => {
  const step = 10

  switch(e.key) {
    case 'ArrowUp':
      selectedObject.y -= step
      break
    case 'ArrowDown':
      selectedObject.y += step
      break
    case 'ArrowLeft':
      selectedObject.x -= step
      break
    case 'ArrowRight':
      selectedObject.x += step
      break
    case 'Delete':
      objects = objects.filter(obj => obj !== selectedObject)
      break
  }

  draw()
})

🕹️ 交互演示:尝试在下方的 Canvas 中点击、拖拽、悬停,体验不同的事件处理方式。

Instructions / 操作说明

  • Click Mode:点击画布创建圆形,按住 Shift 可创建不同颜色

Event Log / 事件日志

Event Handling Code / 事件处理代码

// 点击创建圆形
canvas.addEventListener('click', (e) => {
  const rect = canvas.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top

  const circle = {
    x: x,
    y: y,
    radius: 30,
    color: '#3498db'
  }

  circles.push(circle)
  draw()
})

Event Handling Tips / 事件处理要点

  • 坐标转换: 使用 getBoundingClientRect() 获取 Canvas 在页面中的位置,计算相对坐标
  • 碰撞检测: 对于圆形,计算鼠标位置到圆心的距离;对于矩形,判断点是否在范围内
  • 事件委托: Canvas 只有一个元素,需要手动判断事件发生在哪个对象上
  • 性能优化: 使用 requestAnimationFrame 优化重绘,避免频繁操作

6. 实战案例

6.1 绘制折线图

javascript
const data = [10, 50, 30, 80, 60, 90, 40]

function drawLineChart(ctx, data) {
  const padding = 50
  const chartWidth = canvas.width - padding * 2
  const chartHeight = canvas.height - padding * 2
  const maxValue = Math.max(...data)

  // 绘制坐标轴
  ctx.beginPath()
  ctx.moveTo(padding, padding)
  ctx.lineTo(padding, canvas.height - padding)
  ctx.lineTo(canvas.width - padding, canvas.height - padding)
  ctx.strokeStyle = '#2c3e50'
  ctx.stroke()

  // 绘制折线
  ctx.beginPath()
  data.forEach((value, index) => {
    const x = padding + (index / (data.length - 1)) * chartWidth
    const y = canvas.height - padding - (value / maxValue) * chartHeight

    if (index === 0) {
      ctx.moveTo(x, y)
    } else {
      ctx.lineTo(x, y)
    }
  })
  ctx.strokeStyle = '#3498db'
  ctx.lineWidth = 2
  ctx.stroke()

  // 绘制数据点
  data.forEach((value, index) => {
    const x = padding + (index / (data.length - 1)) * chartWidth
    const y = canvas.height - padding - (value / maxValue) * chartHeight

    ctx.beginPath()
    ctx.arc(x, y, 5, 0, Math.PI * 2)
    ctx.fillStyle = '#e74c3c'
    ctx.fill()
  })
}

6.2 简单粒子系统

粒子系统是游戏和特效中常见的技术,它由大量小粒子组成,每个粒子有独立的位置、速度、生命周期等属性。

javascript
class Particle {
  constructor(x, y) {
    this.x = x
    this.y = y
    this.vx = (Math.random() - 0.5) * 4
    this.vy = (Math.random() - 0.5) * 4
    this.life = 1.0
    this.color = `hsl(${Math.random() * 360}, 70%, 50%)`
  }

  update() {
    this.x += this.vx
    this.y += this.vy
    this.life -= 0.02
  }

  draw(ctx) {
    ctx.globalAlpha = this.life
    ctx.fillStyle = this.color
    ctx.beginPath()
    ctx.arc(this.x, this.y, 3, 0, Math.PI * 2)
    ctx.fill()
    ctx.globalAlpha = 1.0
  }

  isDead() {
    return this.life <= 0
  }
}

// 动画循环
let particles = []

function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  // 更新和绘制粒子
  particles = particles.filter(p => !p.isDead())
  particles.forEach(p => {
    p.update()
    p.draw(ctx)
  })

  requestAnimationFrame(animate)
}

// 鼠标移动产生粒子
canvas.addEventListener('mousemove', (e) => {
  const { x, y } = getMousePos(e)
  for (let i = 0; i < 3; i++) {
    particles.push(new Particle(x, y))
  }
})

animate()

🕹️ 交互演示:在下方 Canvas 中移动鼠标,体验不同的粒子效果。

Active Particles:0
FPS:0

Particle System Code / 粒子系统代码

// 粒子系统核心代码
class Particle {
  constructor(x, y) {
    this.x = x
    this.y = y
    this.vx = (Math.random() - 0.5) * [object Object]
    this.vy = (Math.random() - 0.5) * [object Object]
    this.life = 1.0
    this.decay = 0.01 + Math.random() * 0.02
    this.size = [object Object]
    this.color = this.randomColor()
  }

  update() {
    this.x += this.vx
    this.y += this.vy
    this.vy += [object Object]  // 重力
    this.life -= this.decay
  }

  draw(ctx) {
    ctx.globalAlpha = this.life
    ctx.fillStyle = this.color
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2)
    ctx.fill()
    ctx.globalAlpha = 1.0
  }

  isDead() {
    return this.life <= 0
  }
}

// 动画循环
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  // 更新和绘制粒子
  particles = particles.filter(p => !p.isDead())
  particles.forEach(p => {
    p.update()
    p.draw(ctx)
  })

  requestAnimationFrame(animate)
}

Particle System Tips / 粒子系统要点

  • 粒子类: 每个粒子是一个对象,包含位置、速度、加速度、生命周期等属性
  • 更新循环: 每帧更新所有粒子的位置和状态,移除死亡的粒子
  • 性能优化: 限制粒子数量,使用对象池复用粒子对象
  • 视觉效果: 使用透明度、混合模式、渐变等增强视觉效果

💡提示: 移动鼠标或点击画布来产生粒子!不同的效果有不同的交互方式。


7. 性能优化

随着绘制的对象增多,Canvas 性能会下降。以下是一些常用的优化技巧:

7.1 离屏 Canvas (Offscreen Canvas)

预渲染静态内容到离屏 Canvas,减少每帧的绘制操作:

javascript
// 创建离屏 Canvas
const offscreenCanvas = document.createElement('canvas')
const offscreenCtx = offscreenCanvas.getContext('2d')
offscreenCanvas.width = 600
offscreenCanvas.height = 400

// 预渲染背景
function drawBackground(ctx) {
  ctx.fillStyle = '#f0f0f0'
  ctx.fillRect(0, 0, 600, 400)
  // 绘制网格...
}
drawBackground(offscreenCtx)

// 主渲染循环
function draw() {
  // 直接复制预渲染的背景
  ctx.drawImage(offscreenCanvas, 0, 0)

  // 只绘制动态对象
  objects.forEach(obj => obj.draw(ctx))
}

7.2 图层管理

将静态背景和动态对象分层渲染:

javascript
// 背景层(只绘制一次)
const backgroundLayer = document.createElement('canvas')
// ... 绘制静态背景

// 动态层(每帧重绘)
const dynamicLayer = canvas

function draw() {
  // 清除动态层
  ctx.clearRect(0, 0, canvas.width, canvas.height)

  // 绘制背景层
  ctx.drawImage(backgroundLayer, 0, 0)

  // 绘制动态对象
  objects.forEach(obj => obj.draw(ctx))
}

7.3 减少重绘

只重绘变化的部分(脏矩形优化):

javascript
function draw() {
  objects.forEach(obj => {
    if (obj.moved) {
      // 清除旧位置
      ctx.clearRect(obj.oldX - obj.size, obj.oldY - obj.size, obj.size * 2, obj.size * 2)

      // 绘制新位置
      obj.draw(ctx)

      obj.moved = false
    }
  })
}

7.4 批量渲染

减少状态切换(fillStyle、strokeStyle 等):

javascript
// 按颜色分组
const batches = {}
objects.forEach(obj => {
  if (!batches[obj.color]) {
    batches[obj.color] = []
  }
  batches[obj.color].push(obj)
})

// 批量绘制相同颜色的对象
Object.keys(batches).forEach(color => {
  ctx.fillStyle = color // 只设置一次颜色
  batches[color].forEach(obj => {
    ctx.beginPath()
    ctx.arc(obj.x, obj.y, obj.size, 0, Math.PI * 2)
    ctx.fill()
  })
})

🕹️ 交互演示:对比不同优化技术的性能差异。

FPS:0
Frame Time:0ms
Objects:1000

Optimization Code / 优化代码

// 脏矩形优化 - 只重绘变化的部分
function draw() {
  // 不清除整个画布,只清除变化的区域
  if (useDirtyRect) {
    objects.forEach(obj => {
      if (obj.moved) {
        // 清除旧位置
        ctx.clearRect(
          obj.oldX - obj.size,
          obj.oldY - obj.size,
          obj.size * 2,
          obj.size * 2
        )
        // 绘制新位置
        obj.draw(ctx)
        obj.moved = false
      }
    })
  } else {
    // 传统方式:清除整个画布
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    objects.forEach(obj => obj.draw(ctx))
  }
}

Performance Tips / 性能优化要点

  • 减少重绘: 只重绘变化的部分(脏矩形技术),避免不必要的 clearRect
  • 离屏 Canvas: 预渲染静态内容到离屏 Canvas,减少每帧的绘制操作
  • 批量渲染: 减少状态切换(fillStyle、strokeStyle 等),批量处理相同类型的绘制
  • 对象池: 复用对象,减少垃圾回收压力
  • requestAnimationFrame: 使用浏览器提供的动画 API,优化渲染时机

8. 常见库与框架

虽然原生 Canvas 已经很强大,但在实际项目中,使用成熟的库可以大大提高开发效率。

8.1 Fabric.js

特点:对象模型,支持交互

javascript
const canvas = new fabric.Canvas('c')

// 创建圆形
const circle = new fabric.Circle({
  radius: 20,
  fill: '#3498db',
  left: 100,
  top: 100
})

canvas.add(circle)

// 自动处理事件
circle.on('click', () => {
  circle.set('fill', '#e74c3c')
  canvas.renderAll()
})

适用场景:图片编辑器、白板工具、图形设计工具

8.2 Konva.js

特点:高性能,支持动画和滤镜

javascript
const stage = new Konva.Stage({
  container: 'container',
  width: 600,
  height: 400
})

const layer = new Konva.Layer()
stage.add(layer)

const circle = new Konva.Circle({
  x: 300,
  y: 200,
  radius: 50,
  fill: '#3498db',
  draggable: true
})

layer.add(circle)

适用场景:复杂的图形应用、动画演示

8.3 PixiJS (WebGL)

特点:WebGL 加速,超高性能

javascript
const app = new PIXI.Application({
  width: 600,
  height: 400,
  backgroundColor: 0x1099bb
})
document.body.appendChild(app.view)

const graphics = new PIXI.Graphics()
graphics.beginFill(0x3498db)
graphics.drawCircle(300, 200, 50)
graphics.endFill()
app.stage.addChild(graphics)

适用场景:大型游戏、粒子系统、大量对象的场景

8.4 Three.js (3D)

虽然 Three.js 主要用于 3D,但也支持 2D 渲染:

javascript
const scene = new THREE.Scene()
const camera = new THREE.OrthographicCamera(0, 600, 400, 0, 1, 1000)
const renderer = new THREE.WebGLRenderer()
renderer.setSize(600, 400)
document.body.appendChild(renderer.domElement)

const geometry = new THREE.CircleGeometry(50, 32)
const material = new THREE.MeshBasicMaterial({ color: 0x3498db })
const circle = new THREE.Mesh(geometry, material)
circle.position.set(300, 200, 0)
scene.add(circle)

function animate() {
  requestAnimationFrame(animate)
  renderer.render(scene, camera)
}
animate()

适用场景:2.5D 游戏、混合 2D/3D 应用

8.5 选择建议

优势劣势适用场景
原生 Canvas轻量、无依赖开发效率低学习、简单图形
Fabric.js对象模型、交互友好性能一般图片编辑器、白板
Konva.js高性能、API 简洁体积较大复杂图形应用
PixiJS超高性能、WebGL学习曲线陡大型游戏
Three.js3D 能力强过于重量级2.5D 游戏

9. 总结与最佳实践

9.1 核心要点回顾

  1. Canvas 是位图画布:绘制后就是像素,无法直接修改已有内容
  2. 坐标系统:原点在左上角,Y 轴向下为正
  3. 路径系统:beginPath → moveTo → lineTo → fill/stroke
  4. 动画原理:清除 → 更新 → 绘制 → requestAnimationFrame
  5. 事件处理:需要手动计算碰撞检测
  6. 性能优化:离屏 Canvas、脏矩形、批量渲染

9.2 最佳实践

代码组织

javascript
// 使用类封装对象
class GameObject {
  constructor(x, y) {
    this.x = x
    this.y = y
  }

  update() {
    // 更新状态
  }

  draw(ctx) {
    // 绘制
  }

  isHit(x, y) {
    // 碰撞检测
    const dist = Math.sqrt((x - this.x) ** 2 + (y - this.y) ** 2)
    return dist < this.radius
  }
}

性能优化清单

  • ✅ 使用 requestAnimationFrame 而不是 setInterval
  • ✅ 减少状态切换(按颜色分组绘制)
  • ✅ 使用离屏 Canvas 预渲染静态内容
  • ✅ 只重绘变化的部分(脏矩形)
  • ✅ 限制对象数量,使用对象池
  • ✅ 避免 save()restore() 的频繁调用

调试技巧

javascript
// 绘制边界框(用于调试)
function drawBoundingBox(ctx, obj) {
  ctx.strokeStyle = '#e74c3c'
  ctx.lineWidth = 1
  ctx.strokeRect(
    obj.x - obj.radius,
    obj.y - obj.radius,
    obj.radius * 2,
    obj.radius * 2
  )
}

// 显示 FPS
let lastTime = performance.now()
let frameCount = 0

function showFPS() {
  const now = performance.now()
  frameCount++

  if (now >= lastTime + 1000) {
    console.log(`FPS: ${frameCount}`)
    frameCount = 0
    lastTime = now
  }

  requestAnimationFrame(showFPS)
}
showFPS()

9.3 学习路线

  1. 入门:掌握基本形状绘制和颜色
  2. 进阶:学习动画原理和事件处理
  3. 实战:制作小游戏(贪吃蛇、打砖块)
  4. 优化:学习性能优化技巧
  5. 扩展:尝试成熟的 Canvas 库(Fabric.js、Konva)

10. 名词速查表 (Glossary)

名词解释
Context / 上下文Canvas 的渲染环境,通过 getContext('2d') 获取,所有绘制操作都通过它完成
Path / 路径由一系列点连接成的轨迹,是 Canvas 绘图的基础
Stroke / 描边绘制路径的轮廓线
Fill / 填充用颜色填充路径内部
requestAnimationFrame浏览器提供的动画 API,在每次重绘前调用回调函数
Offscreen Canvas离屏 Canvas,用于预渲染静态内容以提高性能
Dirty Rect脏矩形优化,只重绘变化的部分
Particle System粒子系统,由大量小粒子组成的特效系统
Collision Detection碰撞检测,判断鼠标或对象是否点击了某个图形
Raster vs Vector位图 vs 矢量图,Canvas 是位图,SVG 是矢量图

下一步建议

  • 如果你想深入学习 Canvas 动画,可以尝试制作一个贪吃蛇游戏打砖块游戏
  • 如果你对数据可视化感兴趣,可以学习 EChartsD3.js
  • 如果你想做游戏开发,可以尝试 Phaser.js 游戏引擎
  • 如果你对 WebGL 感兴趣,可以学习 Three.jsPixiJS

祝你学习愉快!🎨