{T}

Canvas 高级特性

本文档介绍 Canvas 的高级特性,包括合成操作、裁剪区域、动画系统等内容。

合成操作

globalCompositeOperation 属性决定新绘制的图形如何与已有内容混合。

属性值

javascript
ctx.globalCompositeOperation = 'source-over' // 默认值

合成模式分类

基础模式

模式说明
source-over新图形绘制在已有内容之上(默认)
source-in只显示新图形与已有内容重叠的部分
source-out只显示新图形不与已有内容重叠的部分
source-atop新图形只在已有内容上显示

目标模式

模式说明
destination-over新图形绘制在已有内容之下
destination-in只显示已有内容与新图形重叠的部分
destination-out只显示已有内容不与新图形重叠的部分
destination-atop已有内容只在新图形上显示

特殊模式

模式说明
lighter颜色值相加
copy只显示新图形
xor异或操作
multiply正片叠底
screen滤色
overlay叠加
darken变暗
lighten变亮
color-dodge颜色减淡
color-burn颜色加深
hard-light强光
soft-light柔光
difference差值
exclusion排除
hue色相
saturation饱和度
color颜色
luminosity亮度

使用示例

基础示例

javascript
// 绘制基础图形
ctx.fillStyle = '#3498db'
ctx.fillRect(50, 50, 150, 150)
 
// 应用合成模式
ctx.globalCompositeOperation = 'multiply'
 
// 绘制叠加图形
ctx.fillStyle = '#e74c3c'
ctx.beginPath()
ctx.arc(175, 175, 75, 0, Math.PI * 2)
ctx.fill()
 
// 重置合成模式
ctx.globalCompositeOperation = 'source-over'

橡皮擦效果

javascript
// 绘制背景
ctx.fillStyle = '#3498db'
ctx.fillRect(0, 0, 400, 300)
 
// 擦除部分内容
ctx.globalCompositeOperation = 'destination-out'
ctx.beginPath()
ctx.arc(200, 150, 80, 0, Math.PI * 2)
ctx.fill()
 
// 重置
ctx.globalCompositeOperation = 'source-over'

光晕效果

javascript
ctx.globalCompositeOperation = 'lighter'
 
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)'
ctx.beginPath()
ctx.arc(150, 150, 80, 0, Math.PI * 2)
ctx.fill()
 
ctx.fillStyle = 'rgba(0, 255, 0, 0.5)'
ctx.beginPath()
ctx.arc(200, 150, 80, 0, Math.PI * 2)
ctx.fill()
 
ctx.fillStyle = 'rgba(0, 0, 255, 0.5)'
ctx.beginPath()
ctx.arc(175, 200, 80, 0, Math.PI * 2)
ctx.fill()
 
ctx.globalCompositeOperation = 'source-over'

裁剪区域

clip() 方法将当前路径设置为裁剪区域,之后绘制的图形只在该区域内可见。

基本用法

javascript
// 创建裁剪路径
ctx.beginPath()
ctx.arc(200, 150, 100, 0, Math.PI * 2)
ctx.clip()
 
// 绘制的图形只显示在圆形区域内
ctx.fillStyle = '#3498db'
ctx.fillRect(0, 0, 400, 300)

裁剪矩形

javascript
// 裁剪为矩形区域
ctx.beginPath()
ctx.rect(50, 50, 200, 150)
ctx.clip()
 
// 绘制渐变背景
const gradient = ctx.createLinearGradient(0, 0, 400, 300)
gradient.addColorStop(0, '#3498db')
gradient.addColorStop(1, '#2ecc71')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, 400, 300)

复杂裁剪路径

javascript
// 创建星形裁剪路径
function createStarPath(cx, cy, spikes, outerRadius, innerRadius) {
  ctx.beginPath()
  for (let i = 0; i < spikes * 2; i++) {
    const radius = i % 2 === 0 ? outerRadius : innerRadius
    const angle = (i * Math.PI) / spikes - Math.PI / 2
    const x = cx + radius * Math.cos(angle)
    const y = cy + radius * Math.sin(angle)
    
    if (i === 0) {
      ctx.moveTo(x, y)
    } else {
      ctx.lineTo(x, y)
    }
  }
  ctx.closePath()
}
 
createStarPath(200, 150, 5, 100, 50)
ctx.clip()
 
// 绘制内容
const gradient = ctx.createLinearGradient(0, 0, 400, 300)
gradient.addColorStop(0, '#f39c12')
gradient.addColorStop(1, '#e74c3c')
ctx.fillStyle = gradient
ctx.fillRect(0, 0, 400, 300)

取消裁剪

javascript
ctx.save()
 
// 创建裁剪区域
ctx.beginPath()
ctx.arc(200, 150, 100, 0, Math.PI * 2)
ctx.clip()
 
// 绘制裁剪内容
ctx.fillRect(0, 0, 400, 300)
 
// 取消裁剪
ctx.restore()
 
// 现在可以正常绘制
ctx.fillStyle = 'red'
ctx.fillRect(300, 200, 50, 50)

动画系统

requestAnimationFrame

requestAnimationFrame 是浏览器提供的动画 API,会在下次重绘之前调用指定函数。

javascript
function animate() {
  // 清除画布
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  
  // 更新状态
  update()
  
  // 绘制
  draw()
  
  // 继续动画循环
  requestAnimationFrame(animate)
}
 
animate()

基础动画示例

javascript
let x = 0
const speed = 2
 
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  
  // 更新位置
  x += speed
  if (x > canvas.width) {
    x = -50
  }
  
  // 绘制
  ctx.fillStyle = '#3498db'
  ctx.fillRect(x, 100, 50, 50)
  
  requestAnimationFrame(animate)
}
 
animate()

旋转动画

javascript
let angle = 0
 
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  
  ctx.save()
  ctx.translate(200, 150)
  ctx.rotate(angle)
  
  ctx.fillStyle = '#e74c3c'
  ctx.fillRect(-40, -40, 80, 80)
  
  ctx.restore()
  
  angle += 0.02
  requestAnimationFrame(animate)
}
 
animate()

弹跳动画

javascript
let y = 0
let velocity = 0
const gravity = 0.5
const bounce = -0.8
const ground = 250
 
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  
  // 应用重力
  velocity += gravity
  y += velocity
  
  // 碰撞检测
  if (y > ground) {
    y = ground
    velocity *= bounce
  }
  
  // 绘制球
  ctx.fillStyle = '#3498db'
  ctx.beginPath()
  ctx.arc(200, y, 20, 0, Math.PI * 2)
  ctx.fill()
  
  requestAnimationFrame(animate)
}
 
animate()

粒子系统

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
    this.decay = Math.random() * 0.02 + 0.01
    this.color = `hsl(${Math.random() * 360}, 70%, 60%)`
  }
  
  update() {
    this.x += this.vx
    this.y += this.vy
    this.life -= this.decay
  }
  
  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
  }
}
 
const particles = []
 
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  
  // 添加新粒子
  if (Math.random() > 0.5) {
    particles.push(new Particle(200, 150))
  }
  
  // 更新和绘制粒子
  for (let i = particles.length - 1; i >= 0; i--) {
    particles[i].update()
    particles[i].draw(ctx)
    
    // 移除死亡粒子
    if (particles[i].life <= 0) {
      particles.splice(i, 1)
    }
  }
  
  requestAnimationFrame(animate)
}
 
animate()

缓动函数

缓动函数可以让动画更自然流畅。

常用缓动函数

javascript
const easing = {
  // 线性
  linear: t => t,
  
  // 缓入(加速)
  easeInQuad: t => t * t,
  easeInCubic: t => t * t * t,
  easeInQuart: t => t * t * t * t,
  easeInExpo: t => t === 0 ? 0 : Math.pow(2, 10 * (t - 1)),
  easeInCirc: t => 1 - Math.sqrt(1 - t * t),
  easeInBack: t => {
    const c = 1.70158
    return t * t * ((c + 1) * t - c)
  },
  
  // 缓出(减速)
  easeOutQuad: t => t * (2 - t),
  easeOutCubic: t => (--t) * t * t + 1,
  easeOutQuart: t => 1 - (--t) * t * t * t,
  easeOutExpo: t => t === 1 ? 1 : 1 - Math.pow(2, -10 * t),
  easeOutCirc: t => Math.sqrt(1 - (--t) * t),
  easeOutBack: t => {
    const c = 1.70158
    return (--t) * t * ((c + 1) * t + c) + 1
  },
  
  // 缓入缓出
  easeInOutQuad: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
  easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1,
  easeInOutQuart: t => t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t,
  easeInOutExpo: t => {
    if (t === 0 || t === 1) return t
    return t < 0.5
      ? Math.pow(2, 20 * t - 10) / 2
      : (2 - Math.pow(2, -20 * t + 10)) / 2
  },
  easeInOutCirc: t => t < 0.5
    ? (1 - Math.sqrt(1 - 4 * t * t)) / 2
    : (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2,
  
  // 弹性
  easeOutElastic: t => {
    const c4 = (2 * Math.PI) / 3
    return t === 0 ? 0 : t === 1 ? 1
      : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1
  },
  
  // 弹跳
  easeOutBounce: t => {
    const n1 = 7.5625
    const d1 = 2.75
    if (t < 1 / d1) {
      return n1 * t * t
    } else if (t < 2 / d1) {
      return n1 * (t -= 1.5 / d1) * t + 0.75
    } else if (t < 2.5 / d1) {
      return n1 * (t -= 2.25 / d1) * t + 0.9375
    } else {
      return n1 * (t -= 2.625 / d1) * t + 0.984375
    }
  }
}

缓动函数可视化

plaintext
线性 (linear)
  │╱
  │  ╱
  │    ╱
  │      ╱
  └────────
 
缓入 (easeIn)
  │          ╱
  │        ╱
  │      ╱
  │    ╱
  │  ╱
  │╱
  └────────
 
缓出 (easeOut)
  │╲
  │  ╲
  │    ╲
  │      ╲
  │        ╲
  │          ╲
  └────────
 
缓入缓出 (easeInOut)
  │      ╱
  │    ╱
  │  ╱
  │╱
  │╲
  │  ╲
  └────────

使用缓动动画

javascript
function animateWithEasing(start, end, duration, easingFn, callback) {
  const startTime = performance.now()
  
  function update(currentTime) {
    const elapsed = currentTime - startTime
    const progress = Math.min(elapsed / duration, 1)
    const easedProgress = easingFn(progress)
    
    const currentValue = start + (end - start) * easedProgress
    
    callback(currentValue, progress)
    
    if (progress < 1) {
      requestAnimationFrame(update)
    }
  }
  
  requestAnimationFrame(update)
}
 
// 使用示例
animateWithEasing(0, 300, 2000, easing.easeOutElastic, (value, progress) => {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  ctx.fillStyle = '#3498db'
  ctx.fillRect(value, 100, 50, 50)
})

动画队列

javascript
class AnimationQueue {
  constructor() {
    this.queue = []
    this.running = false
  }
  
  add(animation) {
    this.queue.push(animation)
    if (!this.running) {
      this.runNext()
    }
  }
  
  runNext() {
    if (this.queue.length === 0) {
      this.running = false
      return
    }
    
    this.running = true
    const animation = this.queue.shift()
    animation(() => this.runNext())
  }
}
 
// 使用示例
const queue = new AnimationQueue()
 
queue.add(next => {
  animateWithEasing(0, 100, 1000, easing.easeOutQuad, (value, progress) => {
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    ctx.fillRect(value, 100, 50, 50)
    if (progress >= 1) next()
  })
})
 
queue.add(next => {
  animateWithEasing(100, 200, 1000, easing.easeInQuad, (value, progress) => {
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    ctx.fillRect(value, 100, 50, 50)
    if (progress >= 1) next()
  })
})

综合示例

游戏循环

一个完整的游戏循环需要处理时间、更新状态和渲染:

javascript
class GameLoop {
  constructor(update, render) {
    this.update = update      // 更新函数
    this.render = render      // 渲染函数
    this.running = false
    this.lastTime = 0
    this.accumulator = 0
    this.fixedDeltaTime = 1 / 60  // 固定时间步长
  }
  
  start() {
    if (this.running) return
    this.running = true
    this.lastTime = performance.now()
    this.loop()
  }
  
  stop() {
    this.running = false
  }
  
  loop() {
    if (!this.running) return
    
    const currentTime = performance.now()
    const deltaTime = (currentTime - this.lastTime) / 1000
    this.lastTime = currentTime
    
    // 累积时间
    this.accumulator += deltaTime
    
    // 固定时间步长更新(保证物理模拟稳定)
    while (this.accumulator >= this.fixedDeltaTime) {
      this.update(this.fixedDeltaTime)
      this.accumulator -= this.fixedDeltaTime
    }
    
    // 渲染(可传入插值因子实现平滑渲染)
    const alpha = this.accumulator / this.fixedDeltaTime
    this.render(alpha)
    
    requestAnimationFrame(() => this.loop())
  }
}
 
// 使用示例
const gameLoop = new GameLoop(
  // 更新函数
  (dt) => {
    // 更新游戏对象
    player.update(dt)
    enemies.forEach(enemy => enemy.update(dt))
  },
  // 渲染函数
  (alpha) => {
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    
    // 使用插值进行平滑渲染
    const interpolatedX = player.previousX + (player.x - player.previousX) * alpha
    const interpolatedY = player.previousY + (player.y - player.previousY) * alpha
    
    ctx.fillStyle = '#3498db'
    ctx.fillRect(interpolatedX, interpolatedY, 50, 50)
  }
)
 
gameLoop.start()

时间管理器

javascript
class TimeManager {
  constructor() {
    this.timeScale = 1        // 时间缩放(慢动作/快进)
    this.paused = false
    this.lastTime = 0
    this.deltaTime = 0
    this.elapsedTime = 0
    this.frameCount = 0
    this.fps = 0
    this.fpsUpdateInterval = 1000
    this.lastFpsUpdate = 0
    this.framesSinceLastUpdate = 0
  }
  
  update(currentTime) {
    if (this.paused) {
      this.deltaTime = 0
      return
    }
    
    // 计算增量时间
    this.deltaTime = ((currentTime - this.lastTime) / 1000) * this.timeScale
    this.lastTime = currentTime
    
    // 累计时间
    this.elapsedTime += this.deltaTime
    this.frameCount++
    
    // 计算 FPS
    this.framesSinceLastUpdate++
    if (currentTime - this.lastFpsUpdate >= this.fpsUpdateInterval) {
      this.fps = this.framesSinceLastUpdate
      this.framesSinceLastUpdate = 0
      this.lastFpsUpdate = currentTime
    }
  }
  
  pause() {
    this.paused = true
  }
  
  resume() {
    this.paused = false
    this.lastTime = performance.now()
  }
  
  setSlowMotion(factor) {
    this.timeScale = factor  // 0.5 = 半速,2 = 双倍速
  }
  
  resetTimeScale() {
    this.timeScale = 1
  }
}
 
// 使用示例
const time = new TimeManager()
 
function gameLoop() {
  const currentTime = performance.now()
  time.update(currentTime)
  
  // 使用 deltaTime 更新
  updateGame(time.deltaTime)
  
  // 显示 FPS
  ctx.fillStyle = '#000'
  ctx.font = '14px Arial'
  ctx.fillText(`FPS: ${time.fps}`, 10, 20)
  ctx.fillText(`Time: ${time.elapsedTime.toFixed(1)}s`, 10, 40)
  
  requestAnimationFrame(gameLoop)
}
 
// 慢动作效果
function triggerSlowMotion() {
  time.setSlowMotion(0.2)
  setTimeout(() => time.resetTimeScale(), 1000)
}

定时器管理

javascript
class TimerManager {
  constructor() {
    this.timers = []
  }
  
  // 延迟执行
  setTimeout(callback, delay) {
    const timer = {
      callback,
      delay,
      elapsed: 0,
      repeat: false,
      active: true
    }
    this.timers.push(timer)
    return timer
  }
  
  // 重复执行
  setInterval(callback, interval) {
    const timer = {
      callback,
      delay: interval,
      elapsed: 0,
      repeat: true,
      active: true
    }
    this.timers.push(timer)
    return timer
  }
  
  // 更新所有定时器
  update(deltaTime) {
    for (let i = this.timers.length - 1; i >= 0; i--) {
      const timer = this.timers[i]
      
      if (!timer.active) {
        this.timers.splice(i, 1)
        continue
      }
      
      timer.elapsed += deltaTime * 1000
      
      if (timer.elapsed >= timer.delay) {
        timer.callback()
        
        if (timer.repeat) {
          timer.elapsed -= timer.delay
        } else {
          this.timers.splice(i, 1)
        }
      }
    }
  }
  
  // 清除定时器
  clear(timer) {
    timer.active = false
  }
  
  // 清除所有定时器
  clearAll() {
    this.timers = []
  }
}
 
// 使用示例
const timers = new TimerManager()
 
// 延迟执行
timers.setTimeout(() => {
  console.log('2秒后执行')
}, 2000)
 
// 重复执行
const spawnTimer = timers.setInterval(() => {
  spawnEnemy()
}, 1000)
 
// 停止重复执行
timers.clear(spawnTimer)
 
// 在游戏循环中更新
function gameLoop() {
  timers.update(time.deltaTime)
  // ...
}

太阳系动画

javascript
const sun = { x: 300, y: 250, radius: 30, color: '#f39c12' }
const planets = [
  { distance: 60, radius: 8, speed: 0.02, color: '#95a5a6', angle: 0 },
  { distance: 100, radius: 12, speed: 0.015, color: '#3498db', angle: Math.PI },
  { distance: 150, radius: 10, speed: 0.01, color: '#e74c3c', angle: Math.PI / 2 },
  { distance: 200, radius: 18, speed: 0.008, color: '#f39c12', angle: Math.PI * 1.5 }
]
 
function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height)
  
  // 绘制太阳
  ctx.fillStyle = sun.color
  ctx.beginPath()
  ctx.arc(sun.x, sun.y, sun.radius, 0, Math.PI * 2)
  ctx.fill()
  
  // 绘制行星
  planets.forEach(planet => {
    planet.angle += planet.speed
    
    const x = sun.x + Math.cos(planet.angle) * planet.distance
    const y = sun.y + Math.sin(planet.angle) * planet.distance
    
    // 轨道
    ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'
    ctx.beginPath()
    ctx.arc(sun.x, sun.y, planet.distance, 0, Math.PI * 2)
    ctx.stroke()
    
    // 行星
    ctx.fillStyle = planet.color
    ctx.beginPath()
    ctx.arc(x, y, planet.radius, 0, Math.PI * 2)
    ctx.fill()
  })
  
  requestAnimationFrame(animate)
}
 
animate()

常见问题

1. 动画卡顿

问题: 动画不够流畅。

解决方案:

  • 使用 requestAnimationFrame 而非 setInterval
  • 减少 clearRect 的范围
  • 使用离屏 Canvas 缓存静态内容

2. 裁剪性能问题

问题: 复杂裁剪路径影响性能。

解决方案: 使用简单的裁剪形状或缓存裁剪结果。

3. 合成模式不生效

问题: globalCompositeOperation 设置无效。

解决方案: 确保已有内容存在,且在绘制新内容前设置。


下一步学习


返回Canvas 教程目录 | 上一篇变换操作