学习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()
}
粒子动画的核心要点:
粒子系统是创造视觉特效的强大工具,通过组合不同的行为和渲染方式,可以创造出无限可能的效果。