使用 Canvas 模拟烟花效果
1. 介绍
在 Web 开发中,Canvas 提供了一种强大的方式来进行 2D 绘图。本文将介绍如何使用 HTML5 Canvas 实现高密度粒子烟花效果,包括其原理、代码规划及关键部分的详细解释。
2. 实现原理
烟花的模拟主要由以下几个部分组成:
- 烟花上升阶段:烟花从底部随机位置出发,以一定角度向目标高度上升。
- 烟花爆炸阶段:到达目标高度后,烟花爆炸并生成多个粒子向四周扩散。
- 粒子运动和衰减:粒子受空气阻力和重力影响,逐渐减速、下落并最终消失。
物理效果的主要实现方式:
- 角度计算:使用
Math.atan2计算烟花的上升角度,使其沿着正确轨迹前进。 - 爆炸扩散:粒子具有随机的扩散方向、初始速度,并受空气阻力和重力影响。
- 颜色变化:使用
HSLA颜色模式,使粒子在爆炸过程中呈现色彩渐变效果。 - 拖尾效果:使用半透明黑色背景覆盖画布,实现视觉上的动态残影。
3. 代码规划
整个程序的核心由以下部分组成:
-
Firework类:- 负责管理烟花的上升过程,包括位置计算、目标高度检测。
- 在到达目标高度后触发爆炸,生成多个粒子。
-
Particle类:- 负责管理粒子的运动,包括扩散方向、速度衰减、重力影响。
- 控制粒子的颜色变化和透明度衰减,实现视觉效果。
-
主循环
animate():- 负责更新和绘制所有烟花。
- 控制新烟花的生成频率,确保画面流畅。
- 移除已经消失的烟花,提高性能。
4. 重点代码解析
4.1 烟花上升逻辑
this.x += Math.cos(this.angle) * this.speed;
this.y += Math.sin(this.angle) * this.speed;
- 计算烟花的上升方向,使其沿着正确的轨迹前进。
Math.cos和Math.sin结合角度计算,确保烟花向目标点移动。
4.2 烟花爆炸
for (let i = 0; i < particleCount; i++) {
this.particles.push(new Particle(this.x, this.y, this.hue));
}
- 生成
particleCount个粒子,每个粒子的初始位置与烟花爆炸点相同。 - 粒子的
hue颜色基于烟花的颜色,但在爆炸过程中会有一定范围的变化。
4.3 粒子运动物理模拟
this.vx *= this.resistance;
this.vy *= this.resistance;
this.vy += this.gravity;
resistance代表空气阻力,逐渐减小粒子的速度。gravity模拟重力,使粒子向下运动。- 这些参数配合可以让粒子表现出自然的爆炸扩散和下落效果。
4.4 颜色动态变化
get currentColor() {
const currentHue = (this.baseHue + this.hueShift * (1 - this.alpha)) % 360;
return `hsla(${currentHue}, 100%, 50%, ${this.alpha})`;
}
hueShift让粒子在爆炸过程中呈现丰富的色彩变化。- 透明度
alpha逐渐减少,使粒子渐隐。 HSLA颜色模式允许粒子具有动态透明度。
5. 运行效果
代码运行后,屏幕上会不断生成烟花,每个烟花爆炸后产生高密度的粒子,粒子颜色变化且逐渐消失。
视觉特效:
- 拖尾效果:使用
rgba(0, 0, 0, 0.08)作为背景覆盖,实现动态残影。 - 动态色彩:粒子的
hue值在爆炸过程中不断变化。 - 自然物理效果:粒子扩散、减速、受重力影响逐渐下落。
6. 可能的优化点
-
增加交互性:
- 点击屏幕可以触发额外的烟花爆炸。
- 根据鼠标位置影响烟花的生成。
-
性能优化:
- 限制同时存在的烟花数量,避免过多粒子影响性能。
- 使用
requestAnimationFrame进行高效动画渲染。
-
添加音效:
- 可以结合
AudioAPI,在爆炸时播放烟花声音,提高沉浸感。
- 可以结合
7. 结论
本文介绍了如何使用 HTML5 Canvas 实现高密度粒子烟花效果,并分析了核心逻辑和关键代码。该效果可以用于网页背景动画或节日特效,进一步优化可以加入交互、音效等功能,使其更加生动。
<!DOCTYPE html>
<html>
<head>
<title>高密度粒子烟花</title>
<style>
body {
margin: 0;
background: #000;
overflow: hidden;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
// 获取Canvas上下文
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
// 设置Canvas尺寸为窗口大小
canvas.width = window.innerWidth
canvas.height = window.innerHeight
/**
* 烟花类:管理单个烟花的发射和爆炸
*/
class Firework {
constructor() {
this.reset() // 初始化烟花参数
this.particles = [] // 爆炸粒子数组
this.phase = 'launching' // 状态:发射中/爆炸中/结束
}
// 重置烟花参数(用于创建新烟花)
reset() {
this.x = Math.random() * canvas.width // 起始X坐标(底部随机位置)
this.y = canvas.height // 起始Y坐标(屏幕底部)
this.targetY = Math.random() * canvas.height * 0.4 // 目标高度(屏幕上半部)
this.speed = 8 + Math.random() * 4 // 上升速度(8-12)
this.angle = Math.atan2( // 发射角度(指向屏幕中心)
this.targetY - this.y,
canvas.width / 2 - this.x
)
this.hue = Math.random() * 360 // 基础色相(0-360)
this.phase = 'launching' // 初始状态为发射中
}
// 更新烟花状态(每帧调用)
update() {
if (this.phase === 'launching') {
// 计算上升轨迹
this.x += Math.cos(this.angle) * this.speed
this.y += Math.sin(this.angle) * this.speed
// 到达目标高度时触发爆炸
if (this.y <= this.targetY) {
this.explode()
}
}
// 更新所有粒子并移除已消失的粒子
this.particles.forEach((p, i) => {
p.update()
if (p.alpha <= 0) this.particles.splice(i, 1)
})
}
// 爆炸效果生成粒子
explode() {
this.phase = 'exploding'
const particleCount = 250 + Math.random() * 200 // 粒子数量250-400
for (let i = 0; i < particleCount; i++) {
this.particles.push(new Particle(
this.x,
this.y,
this.hue
))
}
}
// 绘制烟花(发射轨迹和粒子)
draw() {
// 绘制上升轨迹
if (this.phase === 'launching') {
ctx.beginPath()
ctx.arc(this.x, this.y, 2, 0, Math.PI * 2)
ctx.fillStyle = `hsl(${this.hue}, 100%, 50%)`
ctx.fill()
}
// 绘制所有粒子
this.particles.forEach(p => p.draw())
}
}
/**
* 粒子类:管理单个爆炸粒子的运动效果
*/
class Particle {
constructor(x, y, baseHue) {
this.x = x // 初始X坐标
this.y = y // 初始Y坐标
this.angle = Math.random() * Math.PI * 2 // 随机扩散角度
this.speed = Math.random() * 6 + 4 // 初始速度(4-12)
this.vx = Math.cos(this.angle) * this.speed // X方向速度
this.vy = Math.sin(this.angle) * this.speed // Y方向速度
this.baseHue = baseHue // 基础颜色
this.hueShift = Math.random() * 180 - 40 // 色相偏移量(-40°~+40°),爆炸后彩色
this.alpha = 1 // 初始透明度
this.decay = Math.random() * 0.01 + 0.015 // 透明度衰减速度
this.gravity = 0.2 // 重力加速度
this.resistance = .97 // 空气阻力系数,爆炸面积
}
// 更新粒子状态(每帧调用)
update() {
// 应用物理效果
this.vx *= this.resistance // X方向速度衰减
this.vy *= this.resistance // Y方向速度衰减
this.vy += this.gravity // 施加重力
// 更新位置
this.x += this.vx
this.y += this.vy
// 更新透明度
this.alpha -= this.decay
}
// 获取当前颜色(动态计算)
get currentColor() {
// 色相随透明度变化:初始色相 + 偏移量*(1-透明度)
const currentHue = (this.baseHue + this.hueShift * (1 - this.alpha)) % 360
// 使用HSLA颜色模式实现渐变透明
return `hsla(${currentHue}, 100%, 50%, ${this.alpha})`
}
// 绘制粒子
draw() {
ctx.beginPath()
ctx.arc(this.x, this.y, 1.2, 0, Math.PI * 2) // 绘制圆形粒子
ctx.fillStyle = this.currentColor // 设置动态颜色
ctx.fill()
}
}
// 烟花数组(同时存在的烟花实例)
const fireworks = []
/**
* 创建新烟花(数量控制)
*/
function createFirework() {
// 同时最多存在6个烟花(性能优化)
if (fireworks.length < 6) {
fireworks.push(new Firework())
}
}
/**
* 动画主循环
*/
function animate() {
// 用半透明黑色覆盖实现拖尾效果
ctx.fillStyle = 'rgba(0, 0, 0, 0.08)'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// 更新并绘制所有烟花
fireworks.forEach((fw, i) => {
fw.update()
fw.draw()
// 移除已完成烟花(粒子全部消失且处于爆炸状态)
if (fw.particles.length === 0 && fw.phase === 'exploding') {
fireworks.splice(i, 1)
}
})
// 随机生成新烟花(5%概率每帧)
if (Math.random() < 0.05) createFirework()
// 请求下一帧动画
requestAnimationFrame(animate)
}
// 窗口大小变化时重置Canvas尺寸
window.addEventListener('resize', () => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
})
// 启动动画
animate();
</script>
</body>
</html>