在React项目里加入春节烟花动画,既能提升节日页面的互动感,也能展示Canvas在复杂视觉效果上的优势。相比用大量DOM元素堆叠爆炸碎片,Canvas通过一张画布逐帧重绘,可以用更少资源实现上千颗粒子的自由运动。点击特效则让用户主动参与,每次按下鼠标就在对应位置升腾一束烟花,这种反馈比自动播放更亲切。

粒子系统的底层设计与数据结构
烟花动画的本质是一个粒子系统。每一颗烟花在Canvas中都被抽象成一个粒子对象,包含位置、速度、加速度、颜色、半径与生命周期等属性。上升阶段的烟花通常作为“发射体”,到达顶点后拆分为几十个“爆炸体”,这些爆炸体继承发射体位置并以随机角度散开。我们用一个数组统一管理所有活跃粒子,每帧遍历更新并绘制,消亡的粒子从数组中移除,避免无限增长。
在React中,推荐把粒子数组保存在useRef里而不是useState,因为动画循环每秒执行六十次,若用状态更新会触发大量重复渲染,拖垮性能。useRef的.current属性持有数组引用,循环直接读写即可。下面的代码展示了粒子类的简化定义,包含更新与绘制两个方法,绘制时使用全局透明度表现淡出。
class Particle {
constructor(x, y, color, isRocket) {
this.x = x;
this.y = y;
this.color = color;
// 火箭向上,爆炸体随机散开
if (isRocket) {
this.vx = (Math.random() - 0.5) * 2;
this.vy = -Math.random() * 6 - 6;
} else {
const angle = Math.random() * Math.PI * 2;
const speed = Math.random() * 4 + 1;
this.vx = Math.cos(angle) * speed;
this.vy = Math.sin(angle) * speed;
}
this.life = 1;
this.decay = Math.random() * 0.015 + 0.005;
this.isRocket = isRocket;
}
update() {
this.x += this.vx;
this.y += this.vy;
if (!this.isRocket) {
this.vy += 0.05; // 重力
}
this.life -= this.decay;
}
draw(ctx) {
ctx.globalAlpha = Math.max(this.life, 0);
ctx.fillStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, 2, 0, Math.PI * 2);
ctx.fill();
ctx.globalAlpha = 1;
}
}
上述结构把上升与爆炸统一在一个类里,通过isRocket区分行为。实际项目中可进一步拆分,让爆炸体支持拖尾与闪烁。粒子数量控制也很重要,单束烟花爆炸产生四十到八十颗粒子较为合适,过多会让低端机掉帧,过少则缺乏层次。结合对象池复用粒子能再降一点GC压力,但新手阶段先用数组增删更直观。
在React组件中驱动渲染循环与交互
动画循环依靠requestAnimationFrame实现,它会在浏览器每次重绘前调用回调函数,通常每秒六十次。我们在useEffect里启动循环,并在回调中清空画布、更新粒子、绘制粒子,然后再次注册自身。清空画布不能用CSS背景叠加,而要用ctx.clearRect或填充半透明黑制造余晖,后者能让烟花留下短暂光迹,更有真实感。
点击特效需要监听容器的鼠标事件,将clientX与clientY换算为Canvas内部坐标后,向粒子数组推入一个上升火箭。注意React合成事件挂在容器div上即可,不必挂到window。下面代码演示了循环与点击处理的骨架,其中dpr处理放在后文,此处先忽略缩放。
function FireworksCanvas() {
const canvasRef = useRef(null);
const particlesRef = useRef([]);
useEffect(() => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
let rafId;
const loop = () => {
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const ps = particlesRef.current;
for (let i = ps.length - 1; i >= 0; i--) {
const p = ps[i];
p.update();
p.draw(ctx);
if (p.life <= 0 || p.y > canvas.height) {
// 火箭到顶则爆炸
if (p.isRocket && p.life <= 0) {
explode(ps, p.x, p.y, p.color);
}
ps.splice(i, 1);
}
}
rafId = requestAnimationFrame(loop);
};
rafId = requestAnimationFrame(loop);
const onClick = (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
particlesRef.current.push(new Particle(x, canvas.height, randomColor(), true));
};
canvas.addEventListener('click', onClick);
return () => {
cancelAnimationFrame(rafId);
canvas.removeEventListener('click', onClick);
};
}, []);
return <canvas ref={canvasRef} width={800} height={400}></canvas>;
}
代码中explode函数负责生成一圈爆炸体,randomColor返回随机暖色调以契合春节氛围。组件卸载时必须cancelAnimationFrame并移除监听,否则离开页面后循环仍在后台跑,属于典型内存泄漏。若页面有多个烟花组件,建议提升粒子管理到父层或用单例控制器,减少上下文切换。
交互还可扩展为触摸事件,移动端用户点按同样能放烟花。为避免快速连点导致粒子暴涨,可加节流或在点击时判断当前火箭数,超过上限就忽略。配合音效组件,爆炸瞬间播放轻微砰声,沉浸感会更强,但要注意默认静音以免吓到用户。
高清屏适配与性能优化策略
现代设备像素比(dpr)常大于一,若Canvas的width与CSS尺寸相同,绘制内容会被拉伸模糊。正确做法是将画布后备分辨率乘以dpr,再用ctx.scale(dpr, dpr)让坐标系统保持逻辑像素。这样烟花边缘在视网膜屏上依旧锐利,尤其配合小半径粒子时差异明显。
性能方面,除了用useRef存粒子,还可以限制总粒子数、降低爆炸体数量,或在页面不可见时暂停循环。下面代码展示带dpr的初始设置,以及用document.hidden判断可见性来跳帧,从而省电。
const dpr = window.devicePixelRatio || 1;
canvas.width = 800 * dpr;
canvas.height = 400 * dpr;
canvas.style.width = '800px';
canvas.style.height = '400px';
ctx.scale(dpr, dpr);
const loop = () => {
if (!document.hidden) {
// 正常绘制
}
rafId = requestAnimationFrame(loop);
};
另一个常见优化是离屏Canvas缓存爆炸贴图,但烟花颜色随机,缓存收益有限。更实在的是合并绘制调用,比如同色粒子用一次fillStyle批量画,不过现有粒子数下浏览器已足够快。最后提醒,春节活动页常嵌入第三方统计脚本,若发现烟花卡顿,先用Performance面板确认是否长任务抢占主线程,再决定是否降低帧率到三十帧以腾出资源。
总结而言,React结合Canvas做烟花动画并不复杂,核心是把视觉拆成粒子、用引用存数据、用帧循环驱动,并处理好生命周期与分辨率。点击特效只需坐标映射与事件绑定,就能让用户成为烟花导演。掌握这些,你也能在任意节日页轻松加上属于自己的绚烂特效。
ReactCanvasfireworks_animation修改时间:2026-08-18 22:50:37