尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从零实现跨年烟花特效:HTML+Canvas粒子系统与性能优化

从零实现跨年烟花特效:HTML+Canvas粒子系统与性能优化 简介一份基于HTML与jQuery的跨年烟花特效网页源码面向前端入门与中级开发者适合在除夕、跨年晚会或个人博客中打造炫酷的烟花背景也可作为学习Canvas动画、DOM事件与粒子系统的练手项目。压缩包约162KB解压后可直接在浏览器中运行无需构建工具或后端环境分发和部署都很方便。目前已有10071人学习下载是节日类前端特效中人气较高的素材包。代码围绕烟花从发射到绽放再到消散的完整过程展开读者可逐步拆解其中的坐标计算、颜色渐变、粒子运动与透明度变化逻辑同时提供了便于调整的参数区域可自由修改烟花数量、爆炸范围、背景样式等快速适配不同跨年场景适合在此基础上二次开发和创意改造。1. 为什么一个 HTML 文件就能实现跨年烟花特效跨年倒计时最不缺的就是气氛缺的是一个能打开就自动炸开、还不用装任何客户端的烟花页面。很多人一听说“烟花代码”就想到 Python 或 Three.js其实手头这份《跨新年烟花代码编程html》只用了 HTML CSS JavaScript配合 jQuery就把 2022 跨年烟花特效做出来了效果演示地址 www.lmtaolu.cn/biaobai/yanhua 在原项目说明页里可以直接打开。没有后端没有构建工具拿个 html 文件丢进浏览器就是完整页面。适合三类人前端初学者想摸清粒子系统怎么落地活动运营要在年会上放一个自带氛围的页面开发者想拿现成代码改年份、改颜色、改成自己想要的版本。下面不按“复制粘贴”路线走我会把粒子生命周期、发射编排、参数调优和性能排查拆开讲让你既能直接跑起来也能把它改成 2025 甚至 2026 的跨年烟花。2. 烟花粒子的核心Canvas 画布与粒子生命周期2.1 为什么用 Canvas 而不是 CSS 动画烟花特效最难的并不是“画一个点”而是要同时维持几百个点的状态不断变化。CSS 动画适合数量少、路径固定的元素比如几个光点循环移动一旦粒子数量过百浏览器要同时管理几百个 DOM 节点每一帧都要触发重排和重绘很容易掉到 20 FPS 以下。Canvas 是位图式绘制粒子本质上是内存里的坐标、速度和颜色数据浏览器只关心最终像素输出粒子数量 200 个以内时压力非常小。jQuery 在整套代码里不负责具体动画计算它只做 DOM 选择、事件绑定和控制逻辑这正好符合 jQuery 的定位也让这份跨年烟花代码保持了很高的可改性。2.2 粒子的状态字段与生命周期一个烟花从点火到消失一般经历两个阶段上升阶段一个“火箭”带着亮度从底部向上飞爆炸阶段火箭到达指定高度后生成几十到上百个粒子向四周散开并逐渐变淡。要实现这个效果每个粒子对象至少需要保存下面这些字段字段含义取值范围示例调整后视觉影响x / y粒子当前位置画布坐标决定烟花出现在屏幕上哪里vx / vy粒子速度通常 -4 ~ 4速度越大爆炸半径越大alpha透明度0 ~ 1从 1 衰减到 0 就是尾迹长度color颜色hsl 字符串控制烟花颜色层次decay透明度衰减率0.015 ~ 0.03越大消失越快区域越小gravity重力加速度0.02 ~ 0.1越大越像柳树型越小越像球形这份跨年代码里的粒子对象也大致如此有的版本会把 gravity 直接写进 vy 的增量里有的版本用 life 字段倒计时。字段多没有关系关键是要让所有粒子的更新逻辑都集中在一个 update() 方法里避免在处理帧循环时到处改数据。2.3 用 requestAnimationFrame 驱动渲染循环演示版用的不是 setTimeout 而是 requestAnimationFrame。两者的区别在后台标签页上特别明显setInterval 即使页面被切走也会继续跑白白消耗 CPU而 requestAnimationFrame 会在页面不可见时自动暂停回到页面后自动恢复。另一个区别是执行频率setInterval 写 16ms 并不一定真的 16ms浏览器有最小间隔限制而 requestAnimationFrame 会跟随屏幕刷新率常见 60Hz 屏幕上就是每帧约 16.7ms在 120Hz 屏幕上跑得更顺滑。所以现代前端动画里requestAnimationFrame 已经成了默认选择。2.4 绘制粒子的最小可运行代码var canvas document.getElementById(fireworks); var ctx canvas.getContext(2d); var particles []; function Particle(x, y, color, vx, vy) { this.x x; this.y y; this.color color; this.vx vx; this.vy vy; this.alpha 1; this.decay 0.02 Math.random() * 0.015; } Particle.prototype.draw function () { ctx.globalAlpha this.alpha 0 ? this.alpha : 0; ctx.fillStyle this.color; ctx.fillRect(this.x, this.y, 2, 2); }; function inFrame() { ctx.clearRect(0, 0, canvas.width, canvas.height); for (var i 0; i particles.length; i) { var p particles[i]; p.vy 0.05; // 每帧叠加重力 p.x p.vx; p.y p.vy; p.alpha - p.decay; // 透明度递减 p.draw(); } particles particles.filter(function (p) { return p.alpha 0; }); requestAnimationFrame(inFrame); } requestAnimationFrame(inFrame);这段代码抽掉了颜色生成和爆炸逻辑只保留粒子运动模型跑起来就是一堆小方块向上飞然后落下来。重点看三个参数p.vy 0.05 是重力加速度数值越大粒子落地越快适合做瀑布型烟花p.decay 是透明度衰减率每次绘制都减一次值越大尾迹越短fillRect(2, 2) 用 2 像素方块代替圆点性能更高代价是屏幕放大后能看到锯齿。实际跨年版本里会把方块改成 arc 圆形用 beginPath 和 fill 绘制视觉效果更柔和但粒子多的时候需要注意性能。如果发现烟花爆炸后是一团圆点而不是炸开的散点大概率是 vx/vy 初始化时被写成了 0后面没有给速度粒子就会静止在原处只做透明度变化。3. jQuery 控制发射逻辑定时器、随机参数与 DOM 交互3.1 用定时器编排火箭发射粒子系统只解决“怎么画”真正让页面像跨年烟花的是发射编排。演示版的做法很直接页面加载后用 setInterval 每隔几百毫秒从画布底部随机位置发射一枚火箭火箭飞到一定高度后爆炸。这里每一步都是随机参数所以你不会看到两朵一模一样的烟花。var launchTimer null; var rocket null; function fireRocket() { var fromX canvas.width * (0.2 Math.random() * 0.6); var fromY canvas.height; var targetY canvas.height * (0.2 Math.random() * 0.35); rocket new Rocket({ x: fromX, y: fromY, targetY: targetY, speed: 6 Math.random() * 3 }); } $(function () { launchTimer setInterval(fireRocket, 400 Math.random() * 500); });fromX 限制在画布宽度 20% 到 80% 之间避免烟花贴边炸开导致视觉重心偏移。targetY 是爆炸高度取画布高度的 20% 到 55%太低会挡住后面文字太高又显得开局即巅峰。speed 是上升速度6 到 9 像素每帧对应整体节奏太快会看不清上升尾迹太慢会让人着急。setInterval 的间隔用了 400 随机 500也就是 400ms 到 900ms 一次平均每秒约 1.5 发这个密度适合作为跨年倒计时页面不会让画面太满。3.2 随机颜色与爆炸半径爆炸瞬间会生成 60 到 100 个粒子每个粒子从爆炸中心获得一个方向和速度。方向用角度均匀分布速度加随机扰动这样画出来才不会是一条完美直线。颜色这里推荐用 HSL 而非预设颜色数组因为 HSL 可以只改色相就让整组粒子产生渐变层次。烟花颜色色相范围建议场景金色40 ~ 60跨年主色调配“2025”字样最好看红色0 ~ 20接近真实节庆烟花适合打底蓝紫220 ~ 280夜间反差强适合单独放一朵粉色300 ~ 340适合室内或屏幕背景较亮的场景function explode(x, y, baseHue) { var count 60 Math.floor(Math.random() * 40); for (var i 0; i count; i) { var angle (Math.PI * 2 / count) * i; var speed 2 Math.random() * 3; var vx Math.cos(angle) * speed; var vy Math.sin(angle) * speed; var h baseHue (Math.random() * 20 - 10); var color hsl( h , 100%, 65%); particles.push(new Particle(x, y, color, vx, vy)); } }count 控制在 60 到 100少于 40 显得稀疏多于 150 在低端手机上会掉帧。speed 是爆炸初速度2 到 5 对应屏幕上几十像素的爆炸半径如果想做“大菊花”效果可以提到 6 以上。h 在 baseHue 基础上加 -10 到 10 的随机偏移目的是避免整个烟花只有一种纯色而是有高光和暗部层次。hsl 第三个参数 65% 的亮度比较适中可以适当调整。3.3 点击发射与背景音频开关跨年页面通常还有一个交互彩蛋点击任意位置发射烟花。jQuery 绑定 click 事件很简单但有两个坑要提醒一是 click 的坐标是页面坐标画布可能被 CSS 拉伸需要先 getBoundingClientRect 换算二是移动端点击事件有 300ms 延迟更好的做法是同时监听 touchstart。$(document).on(click touchstart, function (e) { var rect canvas.getBoundingClientRect(); var x (e.clientX - rect.left) * (canvas.width / rect.width); var y (e.clientY - rect.top) * (canvas.height / rect.height); launchRocketAt(x, y); // 自行实现把火箭起点放在点击位置 }); $(#musicBtn).on(click, function () { var audio document.getElementById(bgm); if (audio.paused) { audio.play().catch(function () { console.log(浏览器拦截了自动播放等待用户手势后再试); }); } else { audio.pause(); } });这段代码把页面坐标转换成 canvas 内部坐标scaleX/scaleY 处理了 canvas 被 CSS 放大或缩小的情况。音频播放必须放在用户手势事件里因为现代浏览器会拦截没有用户操作的 audio.play()所以不要尝试页面加载后立刻自动播放音乐。被拦截时 catch 里打一行日志比直接静默失败更利于排查。3.4 清理定时器与事件这个资源如果用于年会大屏可能会连续跑几个小时。setInterval 和 requestAnimationFrame 都需要在卸载页面时清理否则切换路由或关闭弹窗后动画还在后台跑。常见做法是给定时器和 rAF 句柄保存全局变量并在 beforeunload 或页面隐藏事件里清理。function stopFireworks() { if (launchTimer) { clearInterval(launchTimer); launchTimer null; } cancelAnimationFrame(rafId); document.body.style.overflow ; } window.addEventListener(beforeunload, stopFireworks);beforeunload 只在刷新/关闭时触发如果你想在页面上加“停止”按钮也可以在按钮点击里调用 stopFireworks。注意 cancelAnimationFrame 需要在 rAF 的请求句柄更新后保存每次 requestAnimationFrame 会返回一个递增 ID保存最后一次的 ID 即可。4. 把烟花代码跑起来并调成自己的跨年版本4.1 运行环境与启动命令先别急着改代码。下载资源后解压确认里面包含 index.html、jquery.min.js 和可能的 bgm.mp3、img 目录。用浏览器直接双击 index.html 一般就能运行但如果你发现音乐文件加载不出来或者字体异常多半是 file:// 协议对本地资源的限制建议用本地 HTTP 服务打开。在项目目录下执行python -m http.server 8080然后浏览器访问 http://localhost:8080。file:// 和 http:// 在 Canvas 里的差别不大但在音频、字体和后续你加的 Worker 场景下有区别。用本地服务器还可以避免手机和电脑同局域网测试时的路径问题手机直接访问 http://本机IP:8080 就能看真实效果。4.2 核心参数调整对照表参数所在位置默认效果推荐范围发射间隔setInterval(…, ms)500ms400 ~ 1000ms爆炸粒子数量count ...60 ~ 10040 ~ 150爆炸初速度speed ...2 ~ 51 ~ 8透明度衰减decay ...0.020.008 ~ 0.05重力加速度p.vy ...0.050.02 ~ 0.1画布分辨率canvas.width/height窗口大小按 devicePixelRatio 缩放发射间隔越短画面越密适合副歌高潮段爆炸速度越大烟花铺得越开衰减率越小尾迹越长看起来越飘逸重力越大烟花越像垂柳。改参数时建议一次只改一个把效果图截下来对比而不是一把梭全改掉。4.3 常见失败现象与排查顺序页面白屏打开 DevTools Console看报错是找不到 canvas 还是 jQuery 未定义。如果用的是 CDN 引入 jQuery断网时整个脚本会挂掉建议把 jquery.min.js 下载到本地。只有火星没有炸开explode 函数里新生成的粒子没有 push 到全局 particles检查是否在函数里重新定义了 particles。烟花往天上飞重力写成负数查找 p.vy -0.05。文字被烟花盖住如果先绘制烟花再绘制文字烟花会盖住文字调整顺序为先处理粒子更新再绘制烟花最后绘制跨年文字。大屏上模糊canvas 的 CSS 尺寸和属性尺寸不一致需要按下一节的方式设置。4.4 修改年份文字和画布比例跨年烟花代码里通常有一句 fillText 输出年份不同资源写法不同有的直接写在 html 的 div 上有的用 canvas 绘制。从改造成本上来讲保留 Canvas 绘制方式更方便后续叠加动态效果用下面这段可以同时处理年份和副标题function drawTitle() { // 在帧循环中先 clearRect再绘制粒子最后调用这里 ctx.font 48px Microsoft YaHei, sans-serif; ctx.fillStyle rgba(255, 215, 0, 0.85); ctx.textAlign center; ctx.textBaseline middle; ctx.fillText(2025, canvas.width / 2, canvas.height * 0.6); ctx.font 16px sans-serif; ctx.fillStyle rgba(255, 255, 255, 0.6); ctx.fillText(跨年烟花, canvas.width / 2, canvas.height * 0.6 32); }textAlign center 配合 fillText 的第二个参数传入中心点 x 坐标就不用自己算字符串宽度了。canvas.width / 2 是画布的逻辑中心如果你之前按 dpr 缩放过 canvas.width这一句也需要保持一致。修改年份只需改 fillText 里的字符串前面的 48px 数字越大文字越粗壮。5. 进阶粒子池复用、帧率验证与移动端适配5.1 用粒子池减少 GC 卡顿当烟花数量增多频繁 new Particle 又丢弃浏览器会反复触发垃圾回收表现为每过几秒就掉一次帧。常见做法是维护一个数组作为粒子池粒子死亡后放回池里下次爆炸再从池里取。只要把 pool 定义为全局数组然后在过滤时调用 release 函数就能省掉大部分对象创建。var particlePool []; function getParticle(x, y, color, vx, vy) { if (particlePool.length 0) { var p particlePool.pop(); p.x x; p.y y; p.color color; p.vx vx; p.vy vy; p.alpha 1; p.decay 0.02 Math.random() * 0.015; return p; } return new Particle(x, y, color, vx, vy); }第二处要改的是更新循环里的 filter不要用 filter 丢弃对象而是 for 循环倒序遍历遇到 alpha 0 就把对象 push 回 particlePool。这样避免了 filter 每次生成新数组的额外开销。注意从池里取出时要把 vx/vy/alpha/decay 全部重置否则上一轮的参数会残留。5.2 用帧率日志验证性能把帧率打印到控制台是最直观的验证方式。在动画函数里维护一个计数器每满一秒输出一次 FPS然后你在手机上打开同一页面对比两种发射密度下的数值差异。稳定在 50 以上就算健康低于 30 就要考虑减少粒子数或使用粒子池。var frameCount 0, fpsTime performance.now(); function countFPS() { frameCount; if (performance.now() - fpsTime 1000) { console.log(FPS:, frameCount); frameCount 0; fpsTime performance.now(); } }性能面板录制 3 秒和这个每秒日志得到的结果基本一致。如果看到 FPS 定期掉到 20 左右优先怀疑是 GC 或者 Canvas 绘制次数过多而不是 jQuery 选择器慢。5.3 devicePixelRatio 适配手机屏幕和电脑屏幕的物理像素不同如果 canvas.width 直接等于 innerWidth在部分手机上会发虚。常见做法是把画布分辨率放大 dpr 倍再用 CSS 把画布显示尺寸撑满屏幕。var dpr window.devicePixelRatio || 1; canvas.width window.innerWidth * dpr; canvas.height window.innerHeight * dpr; canvas.style.width 100vw; canvas.style.height 100vh; ctx.scale(dpr, dpr);ctx.scale(dpr, dpr) 之后所有逻辑坐标不变仍是 CSS 像素尺寸但渲染像素密度提升了。如果代码里已经用 innerWidth 作为爆炸区域画布宽度会自动变化不需要额外修改。最后在 head 里加上 viewport meta移动端才不会出现缩放错位。调试这种纯前端动画时我习惯把 getParticle、countFPS 和 explode 挂到 window 上直接在控制台执行爆炸函数这样不用反复刷新页面就能验证参数效果。等你调好配色把 console 清掉再开一晚上循环播放如果内存曲线持续上涨优先去查爆炸函数里有没有把粒子对象挂到全局数组或闭包之外——粒子池只解决对象复用不解决多余引用。本文还有配套的精品资源点击获取
返回列表