粒子动画

学习Canvas粒子动画技术,掌握粒子系统的设计、实现和优化方法。粒子动画通过大量粒子的集体运动创造壮观效果,是游戏和视觉特效中常用的技术。

什么是粒子系统

粒子系统是由大量简单对象(粒子)组成的系统,每个粒子有独立的行为,整体产生复杂效果。

粒子系统组成

组件说明
粒子单个运动单元
发射器产生粒子的源头
更新器更新粒子状态
渲染器绘制粒子

基础粒子类

class Particle {
  constructor(x, y, options = {}) {
    this.x = x
    this.y = y
    this.vx = options.vx || (Math.random() - 0.5) * 4
    this.vy = options.vy || (Math.random() - 0.5) * 4
    this.life = options.life || 1
    this.decay = options.decay || 0.02
    this.size = options.size || 5
    this.color = options.color || '#3498db'
    this.gravity = options.gravity || 0
  }
  
  update() {
    this.x += this.vx
    this.y += this.vy
    this.vy += this.gravity
    this.life -= this.decay
    this.size *= 0.99
  }
  
  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
  }
  
  isDead() {
    return this.life <= 0 || this.size < 0.5
  }
}

粒子系统类

class ParticleSystem {
  constructor(options = {}) {
    this.particles = []
    this.emitters = []
    this.maxParticles = options.maxParticles || 1000
    this.autoEmit = options.autoEmit || false
    this.emitRate = options.emitRate || 5
    this.emitX = options.emitX || 0
    this.emitY = options.emitY || 0
  }
  
  emit(x, y, count = 1, options = {}) {
    for (let i = 0; i < count && this.particles.length < this.maxParticles; i++) {
      this.particles.push(new Particle(x, y, options))
    }
  }
  
  update() {
    if (this.autoEmit) {
      this.emit(this.emitX, this.emitY, this.emitRate)
    }
    
    for (let i = this.particles.length - 1; i >= 0; i--) {
      this.particles[i].update()
      
      if (this.particles[i].isDead()) {
        this.particles.splice(i, 1)
      }
    }
  }
  
  draw(ctx) {
    this.particles.forEach(p => p.draw(ctx))
  }
  
  clear() {
    this.particles = []
  }
}

粒子动画演示

基础粒子效果(点击产生粒子)

粒子发射器

点发射器

class PointEmitter {
  constructor(x, y, options = {}) {
    this.x = x
    this.y = y
    this.rate = options.rate || 5
    this.particleOptions = options.particleOptions || {}
  }
  
  emit(particles) {
    for (let i = 0; i < this.rate; i++) {
      particles.push(new Particle(this.x, this.y, this.particleOptions))
    }
  }
}

区域发射器

class AreaEmitter {
  constructor(x, y, width, height, options = {}) {
    this.x = x
    this.y = y
    this.width = width
    this.height = height
    this.rate = options.rate || 5
    this.particleOptions = options.particleOptions || {}
  }
  
  emit(particles) {
    for (let i = 0; i < this.rate; i++) {
      const px = this.x + Math.random() * this.width
      const py = this.y + Math.random() * this.height
      particles.push(new Particle(px, py, this.particleOptions))
    }
  }
}

圆形发射器

class CircleEmitter {
  constructor(x, y, radius, options = {}) {
    this.x = x
    this.y = y
    this.radius = radius
    this.rate = options.rate || 5
    this.particleOptions = options.particleOptions || {}
  }
  
  emit(particles) {
    for (let i = 0; i < this.rate; i++) {
      const angle = Math.random() * Math.PI * 2
      const r = Math.random() * this.radius
      const px = this.x + Math.cos(angle) * r
      const py = this.y + Math.sin(angle) * r
      particles.push(new Particle(px, py, this.particleOptions))
    }
  }
}

粒子行为

重力

class GravityParticle extends Particle {
  constructor(x, y, options = {}) {
    super(x, y, options)
    this.gravity = options.gravity || 0.1
  }
  
  update() {
    super.update()
    this.vy += this.gravity
  }
}

阻力

class DragParticle extends Particle {
  constructor(x, y, options = {}) {
    super(x, y, options)
    this.drag = options.drag || 0.98
  }
  
  update() {
    this.vx *= this.drag
    this.vy *= this.drag
    super.update()
  }
}

风力

class WindParticle extends Particle {
  constructor(x, y, options = {}) {
    super(x, y, options)
    this.windX = options.windX || 0.1
    this.windY = options.windY || 0
  }
  
  update() {
    this.vx += this.windX
    this.vy += this.windY
    super.update()
  }
}

粒子效果示例

烟花效果

class FireworkParticle extends Particle {
  constructor(x, y, options = {}) {
    super(x, y, options)
    const angle = options.angle || Math.random() * Math.PI * 2
    const speed = options.speed || Math.random() * 6 + 2
    this.vx = Math.cos(angle) * speed
    this.vy = Math.sin(angle) * speed
    this.gravity = 0.1
    this.trail = []
    this.trailLength = options.trailLength || 5
  }
  
  update() {
    this.trail.unshift({ x: this.x, y: this.y })
    if (this.trail.length > this.trailLength) {
      this.trail.pop()
    }
    
    this.x += this.vx
    this.y += this.vy
    this.vy += this.gravity
    this.life -= this.decay
  }
  
  draw(ctx) {
    ctx.globalAlpha = this.life * 0.5
    ctx.strokeStyle = this.color
    ctx.lineWidth = this.size
    
    if (this.trail.length > 1) {
      ctx.beginPath()
      ctx.moveTo(this.trail[0].x, this.trail[0].y)
      this.trail.forEach(p => ctx.lineTo(p.x, p.y))
      ctx.stroke()
    }
    
    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
  }
}

function createFirework(x, y, particles) {
  const colors = ['#e74c3c', '#f39c12', '#2ecc71', '#3498db', '#9b59b6', '#1abc9c']
  const color = colors[Math.floor(Math.random() * colors.length)]
  
  for (let i = 0; i < 50; i++) {
    particles.push(new FireworkParticle(x, y, {
      color: color,
      speed: Math.random() * 8 + 4,
      decay: 0.015,
      size: 3
    }))
  }
}

雪花效果

class SnowParticle extends Particle {
  constructor(x, y, options = {}) {
    super(x, y, options)
    this.vx = (Math.random() - 0.5) * 1
    this.vy = Math.random() * 1 + 0.5
    this.size = Math.random() * 3 + 1
    this.wobble = Math.random() * Math.PI * 2
    this.wobbleSpeed = Math.random() * 0.05 + 0.02
  }
  
  update() {
    this.wobble += this.wobbleSpeed
    this.x += this.vx + Math.sin(this.wobble) * 0.5
    this.y += this.vy
  }
  
  draw(ctx) {
    ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2)
    ctx.fill()
  }
  
  isDead() {
    return this.y > canvas.height
  }
}

火焰效果

class FlameParticle extends Particle {
  constructor(x, y, options = {}) {
    super(x, y, options)
    this.vx = (Math.random() - 0.5) * 2
    this.vy = -Math.random() * 3 - 2
    this.size = Math.random() * 10 + 5
    this.life = 1
    this.decay = Math.random() * 0.03 + 0.02
  }
  
  update() {
    this.x += this.vx
    this.y += this.vy
    this.vx *= 0.98
    this.life -= this.decay
    this.size *= 0.97
  }
  
  draw(ctx) {
    const gradient = ctx.createRadialGradient(
      this.x, this.y, 0,
      this.x, this.y, this.size
    )
    
    const alpha = this.life
    gradient.addColorStop(0, `rgba(255, 255, 200, ${alpha})`)
    gradient.addColorStop(0.4, `rgba(255, 150, 50, ${alpha * 0.8})`)
    gradient.addColorStop(1, `rgba(255, 50, 0, 0)`)
    
    ctx.fillStyle = gradient
    ctx.beginPath()
    ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2)
    ctx.fill()
  }
}

粒子效果演示

火焰粒子效果

粒子连接效果

class ConnectedParticles {
  constructor(count, width, height) {
    this.particles = []
    this.width = width
    this.height = height
    this.maxDistance = 100
    
    for (let i = 0; i < count; i++) {
      this.particles.push({
        x: Math.random() * width,
        y: Math.random() * height,
        vx: (Math.random() - 0.5) * 2,
        vy: (Math.random() - 0.5) * 2,
        radius: 3
      })
    }
  }
  
  update() {
    this.particles.forEach(p => {
      p.x += p.vx
      p.y += p.vy
      
      if (p.x < 0 || p.x > this.width) p.vx *= -1
      if (p.y < 0 || p.y > this.height) p.vy *= -1
    })
  }
  
  draw(ctx) {
    ctx.fillStyle = '#3498db'
    this.particles.forEach(p => {
      ctx.beginPath()
      ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2)
      ctx.fill()
    })
    
    ctx.strokeStyle = 'rgba(52, 152, 219, 0.2)'
    ctx.lineWidth = 1
    
    for (let i = 0; i < this.particles.length; i++) {
      for (let j = i + 1; j < this.particles.length; j++) {
        const dx = this.particles[i].x - this.particles[j].x
        const dy = this.particles[i].y - this.particles[j].y
        const distance = Math.sqrt(dx * dx + dy * dy)
        
        if (distance < this.maxDistance) {
          ctx.globalAlpha = 1 - distance / this.maxDistance
          ctx.beginPath()
          ctx.moveTo(this.particles[i].x, this.particles[i].y)
          ctx.lineTo(this.particles[j].x, this.particles[j].y)
          ctx.stroke()
        }
      }
    }
    ctx.globalAlpha = 1
  }
}

粒子连接演示

粒子连接效果

性能优化

对象池

class ParticlePool {
  constructor(factory, size = 100) {
    this.factory = factory
    this.pool = []
    this.active = []
    
    for (let i = 0; i < size; i++) {
      this.pool.push(this.factory())
    }
  }
  
  get() {
    let particle = this.pool.pop()
    if (!particle) {
      particle = this.factory()
    }
    this.active.push(particle)
    return particle
  }
  
  release(particle) {
    const index = this.active.indexOf(particle)
    if (index !== -1) {
      this.active.splice(index, 1)
      this.pool.push(particle)
    }
  }
  
  releaseDead() {
    for (let i = this.active.length - 1; i >= 0; i--) {
      if (this.active[i].isDead()) {
        this.pool.push(this.active[i])
        this.active.splice(i, 1)
      }
    }
  }
}

批量渲染

function renderParticles(ctx, particles) {
  ctx.beginPath()
  
  particles.forEach(p => {
    ctx.moveTo(p.x + p.size, p.y)
    ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2)
  })
  
  ctx.fillStyle = '#3498db'
  ctx.fill()
}

小结

粒子动画的核心要点:

  1. 粒子系统:由粒子、发射器、更新器、渲染器组成
  2. 粒子行为:重力、阻力、风力等物理效果
  3. 发射器类型:点发射、区域发射、圆形发射
  4. 常见效果:烟花、雪花、火焰、连接效果
  5. 性能优化:对象池、批量渲染、限制数量

粒子系统是创造视觉特效的强大工具,通过组合不同的行为和渲染方式,可以创造出无限可能的效果。