news 2026/9/15 1:27:31

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零实现跨年烟花特效: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 清掉,再开一晚上循环播放,如果内存曲线持续上涨,优先去查爆炸函数里有没有把粒子对象挂到全局数组或闭包之外——粒子池只解决对象复用,不解决多余引用。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 1:24:11

从无人机航拍到三维可视化:构建上帝视角系统的完整实践指南

1. 项目概述与核心需求解析1.1 “gods-eye-view”到底解决什么问题我先说结论&#xff1a;所谓“gods-eye-view”&#xff0c;在工程和内容创作领域里&#xff0c;对应的就是“上帝视角可视化”或者“全局俯瞰视图”方案。拿到这个项目名&#xff0c;第一反应不要往玄学上想&am…

作者头像 李华
网站建设 2026/9/15 1:16:52

SMB共享流量包溯源:Wireshark提取与修复载荷实战

在应急响应和攻防演练里&#xff0c;流量分析永远是绕不开的一关。很多时候&#xff0c;攻击者扫完、打进来、传完东西、擦完日志走人了&#xff0c;但网络流量是会说话的。你手上只要有一份完整的pcap&#xff0c;哪怕终端已经被还原干净&#xff0c;攻击者干了什么、传了什么…

作者头像 李华
网站建设 2026/9/15 1:16:09

PyTorch高分遥感语义分割实战:从数据到推理全流程

简介&#xff1a;基于PyTorch的高分遥感语义分割&#xff08;地物分类&#xff09;项目源码&#xff0c;面向计算机、人工智能、自动化及相关专业学生、教师或从业者&#xff0c;可作为课程设计、大作业与毕业设计的完整参考。资源源自个人毕设&#xff0c;答辩评审98分&#x…

作者头像 李华
网站建设 2026/9/15 1:15:27

从VirtualLab到Unity:光学仿真驱动的真实鱼眼镜头后处理实现

很多人第一次在Unity里做广角鱼眼模拟时&#xff0c;第一反应就是把Camera组件的Field of View拉到120甚至150&#xff0c;然后看着画面边缘被拉伸得面目全非&#xff0c;心里还安慰自己"鱼眼镜头不就是这个效果"。我最早也这么干过&#xff0c;直到我把VirtualLab里…

作者头像 李华
网站建设 2026/9/15 1:11:46

国产EDI技术突破:易连EasyLink实现全栈自主可控

1. 易连EDI-EasyLink的国产化突破与行业定位在全球化供应链数字化浪潮中&#xff0c;电子数据交换&#xff08;EDI&#xff09;技术长期被IBM Sterling、SAP等国际巨头垄断。2023年&#xff0c;北京聚信万通科技推出的易连EDI-EasyLink实现了国产软件的历史性突破——这是首款同…

作者头像 李华