简介:一份基于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) | 500ms | 400 ~ 1000ms |
| 爆炸粒子数量 | count = ... | 60 ~ 100 | 40 ~ 150 |
| 爆炸初速度 | speed = ... | 2 ~ 5 | 1 ~ 8 |
| 透明度衰减 | decay = ... | 0.02 | 0.008 ~ 0.05 |
| 重力加速度 | p.vy += ... | 0.05 | 0.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 清掉,再开一晚上循环播放,如果内存曲线持续上涨,优先去查爆炸函数里有没有把粒子对象挂到全局数组或闭包之外——粒子池只解决对象复用,不解决多余引用。
本文还有配套的精品资源,点击获取