应使用 canvas + 原生 javascript 实现粒子散开效果,因其比 css 伪元素更可控、可响应点击坐标、避免层级/事件穿透;伪元素无法动态生成带物理行为的粒子,不支持定位、衰减、旋转、碰撞及生命周期管理,且易导致内存泄漏和移动端适配问题。

直接用 canvas + 原生 JavaScript 实现粒子散开效果,比纯 CSS 动画更可控、更易响应点击位置,也避免了伪元素叠加导致的层级/事件穿透问题。
为什么不用 CSS 伪元素模拟粒子
CSS 伪元素(如 ::before/::after)无法动态生成任意数量、带物理行为的粒子;它只能预设有限图层,且无法按点击坐标定位、无法做衰减/旋转/碰撞等真实运动。实际项目中,一旦按钮要支持多次点击、不同位置触发、或与其它交互(如 disabled 状态)共存,伪元素方案会迅速失控。
常见错误现象:button::after 粒子始终从按钮中心炸开、点击偏移时粒子不跟随、连续点击后粒子残留或卡顿。
- 伪元素粒子是静态背景图或固定 radial-gradient,本质是“贴图”,不是“粒子”
- 无法控制生命周期(比如 2s 后自动销毁),容易内存泄漏
- 移动端 touch 事件坐标获取不准,伪元素无法适配
clientX/clientY
canvas 粒子系统必须做的三件事
核心逻辑就这三步:监听点击 → 创建粒子 → 每帧更新绘制。漏掉任一环节,粒子就会不动、不消、或只在左上角闪一下。
-
监听位置必须用
e.clientX和e.clientY,不是button.getBoundingClientRect()的中心点 —— 否则所有爆炸都从按钮正中开始 -
粒子类必须含
update(dt)方法,其中dt是帧间隔(毫秒),用于匀速衰减和时间驱动位移,否则粒子飞出去就停住 -
canvas 尺寸必须随窗口 resize 重设,否则缩放页面后粒子坐标错乱,
canvas.width/canvas.height不等于视口尺寸
示例关键片段:
button.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
for (let i = 0; i <h3>Particle 类里最容易写错的参数</h3><p>粒子运动是否自然,全看这几个值怎么设。调得太猛像喷射,太弱像慢放雪花。</p>
-
this.vx和this.vy:别用固定值,要用Math.cos(angle) * speed随机方向,否则所有粒子朝同一方向飞 -
this.life和this.elapsed:必须用毫秒计时(如3000),不是帧数 —— 否则高刷屏粒子消失太快,低刷屏拖尾严重 -
ctx.globalAlpha:必须在save()/restore()内设置,否则影响后续所有绘制;别写成ctx.fillStyle = 'rgba(255,255,255,' + alpha + ')'—— 这样无法叠加透明度
错误写法:this.alpha = 1; this.alpha *= 0.99; → 会指数衰减过快,20 帧就看不见了。正确做法是用 alpha = Math.max(1 - this.elapsed / this.life, 0)。
性能卡点:requestAnimationFrame 里别做 DOM 查询
动画循环里每帧都调 document.getElementById('particle-canvas') 或 canvas.getContext('2d'),CPU 占用会飙升。这些操作只做一次,存变量里复用。
另一个隐形坑:particles.splice(i, 1) 在 for 循环里删除数组项时,必须 i--,否则会跳过下一个粒子。更稳妥写法是倒序遍历:
for (let i = particles.length - 1; i >= 0; i--) {
particles[i].update(delta);
if (particles[i].elapsed > particles[i].life) {
particles.splice(i, 1);
}
}
粒子数超过 200 个时,建议加个上限(如 if (particles.length > 300) particles.shift()),不然低端设备会掉帧。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











