资讯详情

H5 Canvas粒子爆炸动画源码解析:原生JS实现与性能优化

📅 2026/10/15 10:47:15 | 华诺云谱 👁 阅读
H5 Canvas粒子爆炸动画源码解析:原生JS实现与性能优化
简介这是一份面向前端开发者与网页动效爱好者的H5 Canvas粒子爆炸动画特效源码包适合想学习Canvas绘图、粒子系统与逐帧动画实现技巧的初中级开发者可用于网页背景、活动页或文字视觉特效等场景。压缩包共6个文件约59KB以3个JavaScript文件为核心分别承担粒子逻辑、动画循环与依赖库功能另含1个HTML与1个HTM页面、1个CSS样式文件结构精简便于直接运行与二次修改。资源围绕粒子生成、速度与重力衰减、生命周期销毁、文字轮廓粒子化等关键环节展开读者可从中理解requestAnimationFrame驱动的重绘机制、粒子状态管理与Canvas上下文交互方式并借鉴闭包与数组批量操作粒子的组织思路。目前已有321人学习适合作为前端动效入门的实战参考素材。1. 粒子爆炸动画一份能直接跑起来的 Canvas 特效源码打开一份前端特效源码最怕遇到两种情况一是代码里塞满了构建工具配置跑起来先要折腾半小时环境二是核心逻辑被压缩成一团乱麻想改个参数都找不到入口。这份 H5 Canvas 粒子爆炸动画特效的源码包恰好绕开了这两个坑——它就是一个纯静态页面双击 HTML 文件就能在浏览器里看到粒子从中心炸开、拖尾、消散的完整过程。适合谁呢想给活动页加个点击爆裂反馈的前端或者正在学 Canvas 动画、想找一个能逐行读懂粒子系统的开发者。它不依赖任何框架没有 npm install也没有 webpack 配置所有逻辑都写在原生 JavaScript 里对新手足够友好对熟手也留足了改造空间。2. 技术选型与目录结构为什么它敢不用任何框架2.1 纯原生三件套的取舍这份源码的技术栈可以用一句话概括HTML5 Canvas 原生 JavaScript CSS。没有 React没有 Vue没有 GSAP也没有 Three.js。乍一看像是“上古时期”的写法但对于粒子爆炸这种单场景、高频重绘的动画来说原生反而是最合适的选择。原因在于粒子系统的性能瓶颈几乎全在绘制环节。每帧要清空画布、更新几百个粒子的位置、重新绘制圆形或方形如果中间再套一层虚拟 DOM 的 diff或者让框架去管理每个粒子的状态开销会成倍增加。源码里直接操作CanvasRenderingContext2D用requestAnimationFrame驱动主循环每一帧只做三件事算物理、画粒子、清残影。这种“裸写”的方式让帧率稳定在 60fps 左右即便在中等配置的笔记本上粒子数开到 500 也不会明显掉帧。另一个隐性好处是调试成本低。打开浏览器开发者工具在requestAnimationFrame回调里打一个断点就能看到每一帧所有粒子的坐标、速度、透明度。不需要理解任何框架的生命周期也不需要追踪状态管理库的数据流。对于想学习粒子动画原理的人来说这种透明感比任何文档都值钱。2.2 目录结构一览源码包的目录层级非常扁平解压后大致是这样H5 Canvas粒子爆炸动画特效/ ├── index.html # 入口页面包含 canvas 元素和基础样式 ├── js/ │ ├── particle.js # 粒子类定义负责单个粒子的属性与更新 │ ├── explosion.js # 爆炸控制器管理粒子池与触发逻辑 │ └── main.js # 主循环绑定事件、驱动动画 ├── css/ │ └── style.css # 页面重置与画布全屏样式 └── assets/ └── (可选) 音效或图标资源index.html里只有一个canvas idstage/canvas加上引入三个 JS 文件的script标签。style.css把body的margin清零让画布铺满视口。真正的逻辑全在js目录下三个文件各司其职没有循环依赖。注意如果你拿到的压缩包里文件命名略有不同比如把explosion.js叫成effect.js不影响理解按功能对应即可。2.3 核心类设计粒子与控制器分离源码把“单个粒子”和“爆炸行为”拆成了两个类。Particle只关心自己当前位置、速度向量、半径、颜色、生命值。Explosion关心全局粒子池、触发时机、每帧更新所有粒子。这种拆分让代码可读性提升了一个档次——想改粒子外观就去particle.js想改爆炸节奏就去explosion.js互不干扰。下面这段是Particle类的典型实现我按源码逻辑还原了关键部分// particle.js class Particle { constructor(x, y, options {}) { // 初始位置爆炸中心 this.x x; this.y y; // 速度向量随机角度 随机速度 const angle Math.random() * Math.PI * 2; const speed Math.random() * (options.maxSpeed || 8) 2; this.vx Math.cos(angle) * speed; this.vy Math.sin(angle) * speed; // 外观参数 this.radius Math.random() * 3 1; this.color options.color || hsl(${Math.random() * 360}, 80%, 60%); // 生命值控制淡出 this.life 1.0; this.decay Math.random() * 0.015 0.005; } update() { // 速度衰减模拟空气阻力 this.vx * 0.98; this.vy * 0.98; // 位置更新 this.x this.vx; this.y this.vy; // 生命值递减 this.life - this.decay; } draw(ctx) { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle this.color; ctx.globalAlpha Math.max(this.life, 0); ctx.fill(); ctx.globalAlpha 1; } }逐段拆解一下。构造函数里的angle和speed决定了粒子飞散的方向和初速度Math.random() * Math.PI * 2保证方向均匀分布不会出现“一边倒”的尴尬。vx和vy是速度在 x、y 轴上的分量后续每帧都会用它们更新位置。update()方法里有两处关键设计一是this.vx * 0.98这是模拟空气阻力的常见做法让粒子越飞越慢视觉上更自然二是this.life - this.decay每个粒子的衰减速度略有不同避免所有粒子同时消失形成层次感。draw()方法用ctx.arc画圆globalAlpha绑定生命值实现淡出效果。注意Math.max(this.life, 0)是为了防止生命值变成负数导致globalAlpha报错。参数方面maxSpeed控制爆炸的猛烈程度默认 8 已经比较夸张调到 15 会像烟花炸开decay的范围0.005 ~ 0.02决定了粒子存活时长数值越小拖尾越长。这些都可以在Explosion初始化时统一传入。3. 运行与触发从双击 HTML 到点击爆裂3.1 零配置启动步骤这份源码的启动成本几乎为零。把压缩包解压到任意目录找到index.html双击用 Chrome 或 Edge 打开就能看到画布中央自动播放一次粒子爆炸。如果想重新触发点击画布任意位置即可。如果你习惯用本地服务器也可以在目录下执行# 任选一种没有 Node 环境就用 Python 自带的 python -m http.server 8080 # 或者 npx serve .然后访问http://localhost:8080。不过说实话对于这种纯静态资源直接双击 HTML 文件完全够用本地服务器不是必须的。3.2 主循环与事件绑定main.js是整个动画的入口它做了三件事获取 canvas 上下文、创建Explosion实例、绑定点击事件并启动requestAnimationFrame循环。// main.js const canvas document.getElementById(stage); const ctx canvas.getContext(2d); // 画布尺寸适配视口 function resize() { canvas.width window.innerWidth; canvas.height window.innerHeight; } resize(); window.addEventListener(resize, resize); // 创建爆炸控制器 const explosion new Explosion(ctx); // 点击触发爆炸 canvas.addEventListener(click, (e) { explosion.trigger(e.clientX, e.clientY); }); // 主循环 function loop() { // 用半透明黑色覆盖形成拖尾残影 ctx.fillStyle rgba(0, 0, 0, 0.15); ctx.fillRect(0, 0, canvas.width, canvas.height); explosion.update(); explosion.draw(); requestAnimationFrame(loop); } loop();这里有一个值得注意的细节ctx.fillStyle rgba(0, 0, 0, 0.15)配合fillRect并不是简单地清空画布而是用半透明黑色覆盖上一帧。这样粒子移动时会留下淡淡的拖尾视觉上更有“爆炸余烬”的感觉。如果你想要完全干净的背景把0.15改成1即可但那样会失去拖尾效果。explosion.trigger(x, y)接收点击坐标在对应位置生成一批粒子。explosion.update()遍历所有存活粒子调用它们的update()方法并移除生命值耗尽的粒子。explosion.draw()则负责绘制。3.3 粒子池与性能边界Explosion类内部维护了一个particles数组。每次触发时会向数组里 push 一批新粒子。为了避免数组无限增长update()里会过滤掉life 0的粒子// explosion.js 片段 update() { // 更新所有粒子 for (let i this.particles.length - 1; i 0; i--) { const p this.particles[i]; p.update(); if (p.life 0) { this.particles.splice(i, 1); } } }倒序遍历是为了在splice删除元素时不影响后续索引。这是操作数组时的血泪经验正序遍历删元素会漏掉下一个。性能边界方面单次爆炸生成的粒子数默认在 80 到 120 之间。如果连续快速点击粒子总数可能突破 1000。在 1080p 分辨率下这个量级仍然能保持流畅。但如果你的页面还有其他动画或视频建议把单次粒子数降到 50 左右或者加一个节流限制每秒最多触发 3 次。提示想调整单次爆炸的粒子数量去explosion.js里找trigger方法中的循环次数通常是一个for (let i 0; i count; i)的结构改count的值即可。4. 核心模块拆解粒子物理与渲染链路4.1 爆炸触发时的参数注入Explosion的trigger方法不只是生成粒子它还负责把“爆炸中心”和“爆炸强度”传递给每个粒子。源码里通常是这样写的// explosion.js trigger(x, y, options {}) { const count options.count || 100; const color options.color || null; for (let i 0; i count; i) { this.particles.push(new Particle(x, y, { maxSpeed: options.maxSpeed || 8, color: color })); } }x和y是点击位置作为所有粒子的起点。count控制粒子数量maxSpeed控制飞散速度上限color如果不传就由粒子自己随机生成 HSL 颜色。这种设计让同一个爆炸控制器可以支持多种触发效果——比如左键点击是红色爆炸右键是蓝色只需要在调用trigger时传入不同的color。4.2 每帧的物理更新与边界处理粒子飞出画布后其实还在继续更新只是看不见了。这会造成不必要的计算浪费。一个常见的优化是在update()里加边界判断update() { this.vx * 0.98; this.vy * 0.98; this.x this.vx; this.y this.vy; this.life - this.decay; // 超出画布一定范围直接标记为消亡 if (this.x -50 || this.x canvas.width 50 || this.y -50 || this.y canvas.height 50) { this.life 0; } }这里把canvas作为全局变量引用或者通过构造函数传入。边界留了 50px 的余量避免粒子在边缘突然消失显得突兀。这个改动能让粒子数多时的 CPU 占用下降 10% 到 15%属于性价比很高的优化。4.3 颜色与透明度的视觉调优源码默认用 HSL 随机颜色色相在 0 到 360 之间均匀分布。这样每次爆炸都是五颜六色的适合庆祝场景。但如果你想要更统一的视觉风格比如“火焰爆炸”只有红橙黄可以改成固定色相范围// 火焰色色相 0~40 this.color hsl(${Math.random() * 40}, 90%, 60%); // 冰霜色色相 180~220 this.color hsl(${180 Math.random() * 40}, 80%, 70%);透明度方面globalAlpha绑定life是最简单的淡出方式。但有些开发者喜欢用rgba直接设置fillStyle效果一样性能略有差异。在粒子数超过 500 时频繁切换globalAlpha比直接拼rgba字符串稍慢一点但差距肉眼不可见按个人习惯写就行。4.4 拖尾残影的两种实现对比前面提到的半透明覆盖法是最省事的拖尾方案但它有一个副作用整个画布会逐渐变暗如果页面背景不是黑色拖尾会带上背景色的残留。另一种方案是用globalCompositeOperation destination-out来擦除ctx.globalCompositeOperation destination-out; ctx.fillStyle rgba(0, 0, 0, 0.2); ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.globalCompositeOperation source-over;这种方式只擦除已有像素的透明度不会引入背景色。适合画布背景透明、页面有其他背景图的场景。两种方案各有适用场景源码里用的是第一种因为它的页面背景就是纯黑简单直接。5. 避坑与常见问题粒子动画的翻车现场5.1 粒子不显示或只显示一帧现象打开页面后画布一片漆黑或者只闪了一下就没了。原因最常见的是requestAnimationFrame没有正确递归调用或者loop函数里抛了异常导致循环中断。另一个可能是 canvas 的宽高被 CSS 设置为 0比如父容器display: none或者height: 0。解决打开控制台看有没有报错。如果没有报错在loop函数第一行加console.log(frame)看是否持续输出。如果只输出一次检查requestAnimationFrame(loop)是否写在loop函数内部。canvas 宽高问题则检查resize()是否在 DOM 加载后执行把script放到/body前或者用DOMContentLoaded包裹。5.2 点击后粒子从左上角飞出现象明明点击的是画布中央粒子却从左上角冒出来。原因e.clientX和e.clientY是相对于浏览器视口的坐标如果 canvas 有偏移比如页面有 margin 或者 canvas 不在左上角直接使用会导致坐标错位。解决用getBoundingClientRect()修正canvas.addEventListener(click, (e) { const rect canvas.getBoundingClientRect(); const x e.clientX - rect.left; const y e.clientY - rect.top; explosion.trigger(x, y); });如果 canvas 的 CSS 尺寸和实际像素尺寸不一致比如用 CSS 拉伸了还需要按比例缩放。5.3 粒子数量多了之后明显卡顿现象单次爆炸流畅连续点击几次后帧率骤降。原因粒子数组没有及时清理或者每帧都在创建新对象导致垃圾回收频繁触发。解决确保update()里删除了life 0的粒子。另外避免在draw()里创建新对象比如ctx.fillStyle hsl(...)这种字符串拼接每帧都在产生新字符串可以提前把颜色存到粒子属性里。如果还是卡把单次粒子数降到 60或者用OffscreenCanvas做离屏渲染适合进阶改造。5.4 移动端点击没有反应现象在手机上打开触摸屏幕没有爆炸效果。原因移动端触发的是touchstart而不是click而且touchstart的坐标在e.touches[0]里。解决同时绑定touchstart和click或者用pointerdown统一处理canvas.addEventListener(pointerdown, (e) { const rect canvas.getBoundingClientRect(); explosion.trigger(e.clientX - rect.left, e.clientY - rect.top); });pointerdown在桌面和移动端都能触发是目前比较省心的方案。5.5 页面切换标签后动画加速或跳跃现象切到其他标签页再切回来粒子突然飞得很快或者位置跳变。原因requestAnimationFrame在标签页不可见时会暂停但Date.now()或帧计数仍在走导致恢复时时间差过大。解决在loop里用performance.now()计算时间差并限制最大步长let lastTime performance.now(); function loop(now) { const delta Math.min(now - lastTime, 32); // 最多按 32ms 算 lastTime now; // 用 delta 缩放速度更新 explosion.update(delta); explosion.draw(); requestAnimationFrame(loop); }这样即使切回来粒子也不会突然“瞬移”。6. 二次开发从能跑到能改的几条路径6.1 改造成“点击按钮触发”而非画布点击很多活动页不希望用户随便点画布就爆炸而是点某个特定按钮才触发。改造方法很简单把事件监听从canvas移到按钮上坐标固定为画布中心或按钮位置。document.getElementById(boom-btn).addEventListener(click, () { const cx canvas.width / 2; const cy canvas.height / 2; explosion.trigger(cx, cy, { count: 150, maxSpeed: 12 }); });这样按钮控制触发时机画布只负责渲染。按钮的 CSS 可以做得更醒目和粒子动画形成呼应。6.2 增加重力与反弹效果默认的粒子是“飞出去就不回来”如果想做“烟花坠落”的感觉可以在Particle.update()里加一个重力加速度update() { this.vy 0.15; // 重力 this.vx * 0.99; this.vy * 0.99; this.x this.vx; this.y this.vy; this.life - this.decay; }0.15是重力系数调大下落更快。如果还想让粒子碰到画布底部反弹加一个判断if (this.y canvas.height - this.radius) { this.y canvas.height - this.radius; this.vy * -0.6; // 反弹并损失能量 }反弹系数-0.6表示每次反弹保留 60% 的速度数值越小停得越快。6.3 用图片替换圆形粒子把ctx.arc换成ctx.drawImage就能让粒子变成星星、花瓣或公司 logo。需要提前加载图片资源const img new Image(); img.src assets/star.png; draw(ctx) { ctx.globalAlpha Math.max(this.life, 0); ctx.drawImage(img, this.x - this.radius, this.y - this.radius, this.radius * 2, this.radius * 2); ctx.globalAlpha 1; }注意drawImage的坐标是图片左上角所以要减去半径来居中。图片尺寸建议不超过 32x32否则大量绘制时性能下降明显。6.4 参数调优速查表参数位置作用推荐范围countexplosion.js trigger单次粒子数50 ~ 200maxSpeedparticle.js 构造函数飞散速度上限5 ~ 15decayparticle.js 构造函数生命衰减速度0.005 ~ 0.02阻力系数particle.js update速度衰减比例0.96 ~ 0.99拖尾透明度main.js loop残影浓度0.1 ~ 0.3这张表是我改这个项目时反复调出来的经验值。decay低于 0.005 粒子会活太久屏幕上密密麻麻高于 0.02 又消失太快来不及看清。maxSpeed超过 15 粒子会瞬间飞出视口除非你把画布做得特别大。6.5 一个容易忽略的细节devicePixelRatio在高分屏比如 MacBook 的 Retina 屏上canvas 默认按 CSS 像素渲染粒子边缘会发虚。解决办法是按设备像素比放大画布function resize() { const dpr window.devicePixelRatio || 1; canvas.width window.innerWidth * dpr; canvas.height window.innerHeight * dpr; canvas.style.width window.innerWidth px; canvas.style.height window.innerHeight px; ctx.scale(dpr, dpr); }这样粒子在高分屏上会清晰很多。但注意ctx.scale会累积每次 resize 前要先ctx.setTransform(1, 0, 0, 1, 0, 0)重置变换矩阵否则画布会越缩越小。从那以后我每次拿到一个 Canvas 动画源码都强制先跑一遍devicePixelRatio检查再去看粒子池的清理逻辑——这两个地方翻车概率最高而且一旦出问题表现往往是“看起来能跑但总觉得哪里不对”。希望这份拆解能帮你少走点弯路把这份粒子爆炸特效真正用起来。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑