粒子动画效果在现代网页设计中越来越受欢迎,它能为页面增添动态感和视觉吸引力。本文将分享一个基于 Vue 和 Canvas 实现的粒子动画组件,该组件具有高度可定制性,可轻松集成到各种 Web 项目中。
我们实现的粒子动画具有以下特点:
- 粒子从底部向上飘动,模拟轻盈上升的效果
- 粒子带有呼吸式发光效果,增强视觉层次感
- 每个粒子都有随机的大小、速度和颜色
- 支持响应式布局,自动适应容器大小变化
- 所有参数均可通过 props 灵活配置
技术选择
为什么选择 Canvas 而非 DOM 元素来实现粒子效果?
-
性能优势:Canvas 在处理大量粒子时性能远优于 DOM 操作
-
绘制灵活性:Canvas 提供丰富的绘图 API,便于实现复杂的视觉效果
-
资源占用低:相比创建大量 DOM 节点,Canvas 渲染更高效
核心实现步骤
- 初始化 Canvas 并设置合适的尺寸
- 创建粒子类,定义粒子的属性和行为
- 实现粒子的绘制逻辑,包括发光效果
- 构建动画循环,更新粒子状态
- 添加响应式处理和组件生命周期管理
组件结构
<template>
<div class="particle-container">
<canvas ref="particleCanvas" class="particle-canvas"></canvas>
</div>
</template>
模板部分非常简洁,只包含一个容器和 canvas 元素,canvas 将作为我们绘制粒子的画布。
可配置参数
props: {
// 粒子数量
particleCount: {
type: Number,
default: 50,
validator: (value) => value >= 0
},
// 粒子颜色数组
particleColors: {
type: Array,
default: () => [
'rgba(255, 255, 255,', // 白色
'rgba(153, 204, 255,', // 淡蓝
'rgba(255, 204, 255,', // 淡粉
'rgba(204, 255, 255,' // 淡青
]
},
// 发光强度
glowIntensity: {
type: Number,
default: 1.5
},
// 粒子大小控制参数
minParticleSize: {
type: Number,
default: 0.5 // 最小粒子半径
},
maxParticleSize: {
type: Number,
default: 1.5 // 最大粒子半径
}
}
这些参数允许开发者根据需求调整粒子效果的密度、颜色、大小和发光强度。
粒子创建与初始化
createParticle() {
// 根据传入的范围计算粒子半径
const radius = this.minParticleSize + Math.random() * (this.maxParticleSize - this.minParticleSize)
return {
x: Math.random() * this.canvasWidth,
y: this.canvasHeight + Math.random() * 50,
radius, // 使用新的半径范围
color: this.getRandomColor(),
speedY: Math.random() * 1.5 + 0.5, // 垂直速度
speedX: (Math.random() - 0.5) * 0.3, // 水平漂移
alpha: Math.random() * 0.5 + 0.5,
life: Math.random() * 150 + 150, // 生命周期
glow: Math.random() * 0.8 + 0.2,
glowSpeed: (Math.random() - 0.5) * 0.02,
shadowBlur: radius * 3 + 1 // 阴影模糊与粒子大小成比例
}
}
每个粒子都有随机的初始位置(从底部进入)、大小、速度和发光属性,这确保了动画效果的自然和丰富性。
动画循环
动画的核心是animate方法,它使用requestAnimationFrame创建流畅的动画循环:
animate() {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
this.particles.forEach((particle, index) => {
// 更新粒子位置
particle.y -= particle.speedY
particle.x += particle.speedX
particle.life--
// 处理发光动画
particle.glow += particle.glowSpeed
if (particle.glow > 1.2) {
particle.glow = 1.2
particle.glowSpeed = -particle.glowSpeed
} else if (particle.glow < 0.2) {
particle.glow = 0.2
particle.glowSpeed = -particle.glowSpeed
}
// 粒子生命周期结束,重新创建
if (particle.y < -particle.radius || particle.life <= 0) {
this.particles[index] = this.createParticle()
}
// 绘制粒子(包括发光效果、核心和高光)
// ...绘制代码省略
})
this.animationId = requestAnimationFrame(this.animate)
}
在每次动画帧中,我们更新所有粒子的位置和状态,当粒子超出画布或生命周期结束时,会创建新的粒子替换它,从而实现循环不断的动画效果。
响应式处理
为了使粒子动画适应不同屏幕尺寸,我们添加了窗口大小变化的监听:
handleResize() {
this.initCanvas()
this.particles = this.particles.map(() => this.createParticle())
}
当窗口大小改变时,我们重新初始化 Canvas 尺寸并重新创建所有粒子,确保动画始终充满整个容器
完整代码
<template>
<div class="particle-container">
<canvas ref="particleCanvas" class="particle-canvas"></canvas>
</div>
</template>
<script>
export default {
name: 'ParticleAnimation',
props: {
// 粒子数量
particleCount: {
type: Number,
default: 50,
validator: (value) => value >= 0
},
// 粒子颜色数组
particleColors: {
type: Array,
default: () => [
'rgba(255, 255, 255,', // 白色
'rgba(153, 204, 255,', // 淡蓝
'rgba(255, 204, 255,', // 淡粉
'rgba(204, 255, 255,' // 淡青
]
},
// 发光强度
glowIntensity: {
type: Number,
default: 1.5
},
// 粒子大小控制参数
minParticleSize: {
type: Number,
default: 0.5 // 最小粒子半径
},
maxParticleSize: {
type: Number,
default: 1.5 // 最大粒子半径
}
},
data() {
return {
canvas: null,
ctx: null,
particles: [],
animationId: null,
canvasWidth: 0,
canvasHeight: 0
}
},
watch: {
particleCount(newVal) {
this.particles = []
this.initParticles(newVal)
},
particleColors: {
deep: true,
handler() {
this.particles.forEach((particle, index) => {
this.particles[index].color = this.getRandomColor()
})
}
},
// 监听粒子大小变化
minParticleSize() {
this.resetParticles()
},
maxParticleSize() {
this.resetParticles()
}
},
methods: {
initCanvas() {
this.canvas = this.$refs.particleCanvas
this.ctx = this.canvas.getContext('2d')
const container = this.canvas.parentElement
this.canvasWidth = container.clientWidth
this.canvasHeight = container.clientHeight
this.canvas.width = this.canvasWidth
this.canvas.height = this.canvasHeight
},
initParticles(count) {
for (let i = 0; i < count; i++) {
this.particles.push(this.createParticle())
}
},
createParticle() {
// 根据传入的范围计算粒子半径
const radius = this.minParticleSize + Math.random() * (this.maxParticleSize - this.minParticleSize)
return {
x: Math.random() * this.canvasWidth,
y: this.canvasHeight + Math.random() * 50,
radius, // 使用新的半径范围
color: this.getRandomColor(),
speedY: Math.random() * 1.5 + 0.5, // 降低速度,配合小粒子
speedX: (Math.random() - 0.5) * 0.3, // 减少漂移
alpha: Math.random() * 0.5 + 0.5,
life: Math.random() * 150 + 150, // 延长生命周期,让小粒子存在更久
glow: Math.random() * 0.8 + 0.2,
glowSpeed: (Math.random() - 0.5) * 0.02,
shadowBlur: radius * 3 + 1 // 阴影模糊与粒子大小成比例
}
},
getRandomColor() {
if (this.particleColors.length === 0) {
return 'rgba(255, 255, 255,'
}
return this.particleColors[Math.floor(Math.random() * this.particleColors.length)]
},
animate() {
this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
this.particles.forEach((particle, index) => {
particle.y -= particle.speedY
particle.x += particle.speedX
particle.life--
// 闪亮动画
particle.glow += particle.glowSpeed
if (particle.glow > 1.2) {
particle.glow = 1.2
particle.glowSpeed = -particle.glowSpeed
} else if (particle.glow < 0.2) {
particle.glow = 0.2
particle.glowSpeed = -particle.glowSpeed
}
if (particle.y < -particle.radius || particle.life <= 0) {
this.particles[index] = this.createParticle()
}
// 绘制粒子(适配小粒子的比例)
this.ctx.save()
// 阴影效果
this.ctx.shadowColor = `${particle.color}${particle.glow * this.glowIntensity})`
this.ctx.shadowBlur = particle.shadowBlur * particle.glow
this.ctx.shadowOffsetX = 0
this.ctx.shadowOffsetY = 0
// 外发光圈(按粒子大小比例缩放)
this.ctx.beginPath()
this.ctx.arc(particle.x, particle.y, particle.radius * (1 + particle.glow * 0.8), 0, Math.PI * 2)
this.ctx.fillStyle = `${particle.color}${0.2 * particle.glow})`
this.ctx.fill()
// 粒子核心
this.ctx.beginPath()
this.ctx.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2)
this.ctx.fillStyle = `${particle.color}${particle.alpha + (particle.glow * 0.3)})`
this.ctx.fill()
// 高光点(适配小粒子)
if (particle.glow > 0.8) {
this.ctx.beginPath()
const highlightSize = particle.radius * 0.3 * particle.glow
this.ctx.arc(
particle.x - particle.radius * 0.2,
particle.y - particle.radius * 0.2,
highlightSize,
0,
Math.PI * 2
)
this.ctx.fillStyle = `rgba(255, 255, 255, ${0.6 * particle.glow})`
this.ctx.fill()
}
this.ctx.restore()
})
this.animationId = requestAnimationFrame(this.animate)
},
handleResize() {
this.initCanvas()
this.particles = this.particles.map(() => this.createParticle())
},
// 重置粒子大小
resetParticles() {
this.particles = this.particles.map(() => this.createParticle())
}
},
mounted() {
this.initCanvas()
this.initParticles(this.particleCount)
this.animate()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
cancelAnimationFrame(this.animationId)
window.removeEventListener('resize', this.handleResize)
}
}
</script>
<style scoped>
.particle-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.particle-canvas {
display: block;
width: 100%;
height: 100%;
}
</style>
使用方法
<template>
<div class="page-container">
<ParticleAnimation
:particle-count="80"
:glow-intensity="2"
:min-particle-size="0.8"
:max-particle-size="2"
/>
<!-- 其他内容 -->
</div>
</template>
<script>
import ParticleAnimation from '@/components/ParticleAnimation.vue'
export default {
components: {
ParticleAnimation
}
}
</script>
<style>
.page-container {
width: 100vw;
height: 100vh;
}
</style>