用HTML+Canvas打造2026马年动态烟花祝福页(纯前端零依赖)
2026/9/16 17:56:45 网站建设 项目流程

还有差不多一个月就到2026年春节了,每年这时候微信里全是复制粘贴的祝福文案,看多了真的分不清是谁发的。去年我换了个方式拜年——用HTML写一个动态烟花新年祝福页面,单文件发过去,点开就是满天烟花,家里长辈都觉得挺新鲜。今年赶上丙午马年,我把整个页面重做了一版,烟花配色、祝福语、视觉细节都按马年调过,顺便把完整实现思路和源码整理出来。

这篇文章适合三类人:一是想学Canvas动画但一直没找到合适练手项目的初学者,二是想给家人、客户、公司群做点不一样拜年页面的运营或开发者,三是单纯想拿现成源码改一改就用的朋友。文章里不会出现任何框架和依赖,全部是浏览器原生HTML、CSS、JavaScript,保存成一个.html文件就能跑。

1. 为什么我坚持用单文件HTML做祝福页:四类方案横向对比

1.1 短视频模板、小程序、PPT祝福页的问题

先说说我为什么不用更省事的方案。每逢春节,短视频平台都会上线一堆新年模板,选个背景音乐、套个模版就能生成视频。问题是这种东西太容易撞车,你发出去,亲戚群里已经有三个同款了,完全没有"这是专门给我做的"那种感觉。

小程序祝福页也一样,开发门槛高是一方面,更重要的是分享链路很别扭——点开小程序会有一个加载过程,在微信里还会被各种"请使用完整服务"的提示打断。长辈们对小程序的第一反应往往不是"好漂亮",而是"这是什么?怎么又要授权?"

PPT转成视频或者图片长图也做过,但那种方式是静态的,烟花不会动,少了最关键的"活"的感觉。而且长图一旦压缩,文字可能糊掉,字号小了在手机上根本看不清。

1.2 单文件HTML到底赢在哪里

HTML页面方案最核心的优势,是零依赖、零门槛、平台无关

  • 不需要安装任何软件,一个记事本就能改代码;
  • 不需要服务器,双击本地文件就能打开;
  • 不需要上传App Store或过审核,压缩包发过去、链接甩过去都能看;
  • 视觉效果的上限极高,Canvas动画能做成什么样,完全取决于你的想象力。

实际使用中,我把这个页面压缩包通过微信文件传助手发到家庭群,点开即是全屏烟花,没有加载条、没有授权弹窗,连手机自带浏览器都能直接播放。对长辈来说,这就是"打开就有"的体验,比任何花哨的载体都可靠。

1.3 一个祝福页面的三块拼图

这类页面技术上其实就三件事:

  1. HTML结构:负责承载页面上的文字、按钮、Canvas画布;
  2. CSS样式:负责夜空渐变背景、艺术字、印章等视觉元素;
  3. JavaScript逻辑:负责烟花的生成、运动、爆炸、消散,以及文案交互。

把这三点理清楚,再往里填代码就不乱了。下面要讲的烟花效果,全部集中在JavaScript和Canvas画布上。

2. 烟花动效的三个核心机制:粒子、火箭与半透明拖尾

2.1 一个烟花的完整生命周期

很多人一想到"烟花效果"就开始搜花哨的库,其实拆开看,它就是一个再典型不过的物理模拟问题。现实里烟花升空到炸开的过程,拆成代码大概是这样的:

  1. 一枚火箭从屏幕底部往上飞,拖着一条亮线;
  2. 火箭飞到指定高度后触发爆炸;
  3. 爆炸瞬间在爆点周围生成几十上百个粒子,每个粒子拥有随机的方向和速度;
  4. 粒子飞出后,受到重力影响,速度逐渐衰减,同时自身慢慢变淡;
  5. 粒子完全透明后从数组移除。

只要把这五个环节用代码表达出来,烟花效果就已经有了九成。剩下的"氛围感"全靠拖尾和颜色调得对不对。

2.2 粒子:整个效果的最小原子单位

粒子系统是这个页面最核心的部分。我不想把概念讲得太玄,你只要知道一个粒子就是一个"会移动的小圆点",它身上带着位置、速度、颜色、透明度和生命周期。

function Particle(x, y, color, vx, vy) { this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.color = color; this.alpha = 1; // 不透明度,从1开始逐渐变淡 this.decay = 0.008 + Math.random() * 0.01; // 每次更新衰减多少 this.gravity = 0.045; // 重力加速度 } Particle.prototype.update = function () { this.vx *= 0.985; // 空气阻力 this.vy *= 0.985; this.vy += this.gravity; // 重力让粒子向下拐弯 this.x += this.vx; this.y += this.vy; this.alpha -= this.decay; return this.alpha > 0.02; };

这里有几个数值是我反复调出来的,说一下心得:

  • 速度衰减0.985代表空气阻力。如果不加,粒子会一直匀速直线飞出去,炸开后像"爆炸的瓜子壳",完全不像烟花。乘上0.985之后粒子会慢慢减速,停在半空再下坠,才有一朵烟花缓缓绽放的感觉。
  • 重力0.045决定了烟花的形态。重力太大,粒子刚炸开就被拽向地面,形成一束"垂柳";重力太小,粒子飘在半空半天不落,节奏拖沓。0.045是我在1920x1080屏幕上测试比较舒服的数值,你可以按自己屏幕尺寸微调。
  • 衰减0.008~0.018之间随机,目的是让粒子寿命长短不一。如果所有粒子同时消失,爆炸后会出现一个明显的"断崖",很假。随机衰减能让粒子陆陆续续灭掉,更接近真实烟火。

理解了这个最小单元,你就能看懂后面所有效果是怎么堆出来的。

2.3 火箭:先上升,再绽放

粒子负责爆炸后的花样,火箭负责把粒子送到空中。火箭的逻辑不复杂,但要提前想清楚:它什么时候爆炸?以什么角度飞?

我的做法是在创建火箭时就算好一个目标高度,然后让它带一点随机的水平偏转往上飞。这个水平偏转非常重要,如果所有火箭都竖直向上,烟花放出来全在屏幕中轴线上,画面会很死板;加一个Math.random() - 0.5的偏转之后,火箭会像真正的烟花一样在空中形成高低错落的爆点。

function Rocket() { this.x = Math.random() * window.innerWidth; this.y = window.innerHeight + 10; var targetY = window.innerHeight * (0.15 + Math.random() * 0.35); var angle = -Math.PI / 2 + (Math.random() - 0.5) * 0.25; var speed = 6 + Math.random() * 4; this.vx = Math.cos(angle) * speed; this.vy = Math.sin(angle) * speed; this.dist = Math.abs(window.innerHeight - targetY); this.travel = 0; this.trail = []; }

天空高度为什么要限定在屏幕的15%~50%这个区间?因为太低了,爆炸会被中央的祝福文字挡住;太高了,粒子炸开后有三分之一会飘出屏幕顶部,浪费性能。把爆点控制在画面中上部,是最稳妥的视觉黄金区。

2.4 拖尾:用"半透明覆盖"骗过眼睛

这是最容易被忽略、但对效果影响最大的地方。

如果你用clearRect()把画布每帧清空,你会看到烟花是"一颗一颗"寂寞地闪,没有任何拖尾。因为烟花要在空中留下轨迹,就需要让上一帧的画面"残留"一点点。

实现方式非常取巧:每帧绘制前,不用clearRect清空画布,而是用一层半透明深蓝色蒙版盖上去。这样旧的画面会变暗一点点但不会立即消失,新画的火星则保持明亮,连续十几帧之后,烟花身后就会留下一道从亮到暗的渐隐尾巴。

ctx.fillStyle = 'rgba(8, 12, 35, 0.16)'; ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);

这个0.16就是拖尾长度的"开关"。数值越大,蒙版越不透明,旧画面消失越快,拖尾越短;数值越小,旧画面残留越久,拖尾越长。我试过0.08,拖尾拉得很长,但烟花炸开后轨迹太多,画面显得脏;试过0.3,拖尾几乎看不见,又回到了"孤单闪光弹"的效果。0.16是我在深蓝色夜空中最满意的平衡点。

2.5 动画循环与发射节奏

所有粒子运动都在requestAnimationFrame()的回调里完成,这是浏览器提供的动画专用API,会跟随屏幕刷新率执行,比setInterval准时得多,也不会在标签页切到后台时继续消耗资源。

发射节奏也要控制一下。如果每帧都发射,画面上会同时存在无数枚火箭,反而看不清每一朵烟花的细节。我用时间戳做了节流:

if (time - lastLaunch > 480 && rockets.length < 4) { lastLaunch = time; rockets.push(new Rocket()); }

意思是每480毫秒最多发射一枚火箭,且同时在场火箭不超过4枚。这样烟花会错开节奏,一朵刚散尽、另一朵正好炸开,视觉上节奏感最好。

3. 马年元素怎么融进页面:丙午配色、文案与印章设计

3.1 先把2026年这个细节搞对

做马年专属页面,第一件事不是写代码,是先确认年份信息。2026年是农历丙午年,生肖属马,春节是2月17日。所以在页面里我写了"丙午马年"而不是笼统的"2026马年",这种细节虽然不会被人刻意注意,但懂的人一眼就能感觉出页面是用心做的。

想加倒计时的话,可以用new Date('2026-02-17T00:00:00')减去当前时间,在页面底部显示"距离丙午马年还有多少天"。今年我把这个倒计时放在了落地页加载前的黑屏阶段,体验上更有仪式感。

3.2 三色定调:夜空、鎏金与中国红

烟花页面的背景我用了放射状渐变的深蓝色,而不是纯黑。

html, body { background: radial-gradient(ellipse at 50% 20%, #1a2350 0%, #0a0e25 65%, #05081a 100%); }

这个渐变模拟了真实夜空中"城市上方微微泛光、高空接近纯黑"的效果,比平铺的纯#000更有层次。而且烟花在深蓝色背景上爆炸时,暖色粒子会显得更透亮。

主文字选了鎏金色,用CSS渐变文字实现:

.year { background: linear-gradient(180deg, #fff2c0 0%, #f9c74f 45%, #d9890f 100%); -webkit-background-clip: text; background-clip: text; color: transparent; }

金色带一点从上到下的明暗变化,会让人联想到金属的反光。副标题和祝福语用偏亮的杏黄或浅金色,整体氛围就是中国节日里最经典的"红金配",但我把大面积红色留给了远处的印章,避免整个页面变成一个大红灯笼,反而没那么高级。

3.3 版式层级:年份 > 主标题 > 标语 > 印章

页面文字做了四层视觉层级,从上到下依次是:

  • 年份"2026":超大字号,是一眼扫过去最先被看到的信息;
  • 主标题"新春快乐":比年份稍小,但字形更粗、更厚重;
  • 副标题"丙午马年 · 辞旧迎新":小字,拉出间距,补充年份信息;
  • 祝福语胶囊:一个圆角边框内的短句,带一点交互感。

这种大中小三级排布在视觉上非常稳。你要避免的是所有文字都一个字号、一个颜色,那样页面会显得像一张摊开的报纸,没有重心。

3.4 马年祝福文案库与点击切换

马年能用的祝福语其实很多,我挑了六个朗朗上口、不落俗套的:马到成功、龙马精神、一马当先、骏业日新、万马奔腾、策马扬鞭。

做成点击切换的交互很简单:

var mottos = ['马到成功', '龙马精神', '一马当先', '骏业日新', '万马奔腾', '策马扬鞭']; var mottoIndex = 0; mottoEl.addEventListener('click', function () { mottoIndex = (mottoIndex + 1) % mottos.length; mottoEl.textContent = mottos[mottoIndex]; });

彩蛋感就在这里——对方点一下,祝福语就换一个,像在拆小盲盒。我把这个交互提示直接写在了页面底部:"点击祝福语切换内容"。

3.5 印章:在角落盖一个"马"字

要让页面更有马年记忆点,除了文字,我加了一个红色圆形印章,里面写一个大大的"马"字。用CSS就能做,不需要图片:

.stamp { position: fixed; z-index: 2; top: 30px; right: 30px; width: 74px; height: 74px; border-radius: 50%; background: rgba(190, 30, 40, 0.85); color: #fff5e0; display: flex; align-items: center; justify-content: center; font-size: 40px; transform: rotate(12deg); }

旋转12度是关键,印章如果端端正正摆在角落会显得很死板,稍微歪一点才有"啪"一声盖上去的鲜活感。这里顺便说一句,如果你想要一个更复杂的马形剪影,不建议在Canvas里手绘路径,直接去免费图标站找一张CC0授权的马剪影SVG,内联进HTML就行,效果远比自己画的高好几个档次。

4. 完整源码:一个可运行的2026马年烟花祝福页

4.1 文件结构与使用方法

整个祝福页只有一个index.html文件,CSS写进<style>,JavaScript写进<script>,没有任何外部文件。这意味着你可以把它存到U盘、发到微信群、传到任何静态托管平台上,双击就能运行。

使用步骤:

  1. 新建一个文本文件,重命名为index.html
  2. 把下面完整代码粘贴进去并保存;
  3. 双击打开,即可看到全屏烟花和祝福文字。

4.2 完整源码

<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>2026丙午马年 · 新春快乐</title> <meta name="description" content="2026马年专属动态烟花祝福页,纯HTML/CSS/JavaScript实现,打开即看,可分享给家人朋友。"> <style> * { margin: 0; padding: 0; box-sizing: border-box; } html, body { width: 100%; height: 100%; overflow: hidden; background: radial-gradient(ellipse at 50% 20%, #1a2350 0%, #0a0e25 65%, #05081a 100%); font-family: "STKaiti", "KaiTi", "Microsoft YaHei", serif; } canvas { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 1; pointer-events: none; } #greeting { position: fixed; z-index: 2; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center; color: #f7e3af; width: 100%; padding: 0 16px; user-select: none; text-shadow: 0 0 20px rgba(255, 200, 80, 0.35); } .year { font-size: clamp(46px, 12vw, 130px); font-weight: bold; letter-spacing: 0.12em; background: linear-gradient(180deg, #fff2c0 0%, #f9c74f 45%, #d9890f 100%); -webkit-background-clip: text; background-clip: text; color: transparent; -webkit-text-fill-color: transparent; filter: drop-shadow(0 4px 18px rgba(249, 199, 79, 0.35)); line-height: 1.15; } .main-title { margin-top: 8px; font-size: clamp(38px, 8vw, 96px); font-weight: bold; letter-spacing: 0.15em; color: #ffd97a; text-shadow: 0 0 30px rgba(255, 160, 40, 0.45), 0 0 60px rgba(255, 120, 30, 0.2); line-height: 1.25; } .sub-title { margin-top: 14px; font-size: clamp(14px, 3.2vw, 26px); letter-spacing: 0.3em; color: rgba(255, 241, 214, 0.85); } .motto { margin-top: 20px; display: inline-block; padding: 8px 24px; border: 1px solid rgba(249, 199, 79, 0.55); border-radius: 999px; color: #ffd97a; font-size: clamp(16px, 3.4vw, 28px); letter-spacing: 0.35em; cursor: pointer; transition: all 0.3s ease; background: rgba(20, 24, 50, 0.25); } .motto:hover { background: rgba(249, 199, 79, 0.12); box-shadow: 0 0 24px rgba(249, 199, 79, 0.25); } .tip { position: fixed; z-index: 2; bottom: 22px; left: 0; right: 0; text-align: center; color: rgba(255, 255, 255, 0.45); font-size: 13px; letter-spacing: 0.1em; } .stamp { position: fixed; z-index: 2; top: 30px; right: 30px; width: 74px; height: 74px; border-radius: 50%; background: rgba(190, 30, 40, 0.85); color: #fff5e0; display: flex; align-items: center; justify-content: center; font-size: 40px; font-family: "STKaiti", "KaiTi", serif; box-shadow: 0 0 30px rgba(190, 30, 40, 0.5); transform: rotate(12deg); } </style> </head> <body> <canvas id="fireworksCanvas"></canvas> <div id="greeting"> <div class="year">2026</div> <div class="main-title">新春快乐</div> <div class="sub-title">丙午马年 · 辞旧迎新</div> <div class="motto" id="motto">马到成功</div> </div> <div class="stamp">马</div> <div class="tip">点击祝福语切换内容 · 建议手机/电脑浏览器打开</div> <script> (function () { var canvas = document.getElementById('fireworksCanvas'); var ctx = canvas.getContext('2d'); var rockets = []; var particles = []; var maxParticles = 400; var lastLaunch = 0; function resize() { var dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = window.innerWidth * dpr; canvas.height = window.innerHeight * dpr; canvas.style.width = window.innerWidth + 'px'; canvas.style.height = window.innerHeight + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } window.addEventListener('resize', resize); resize(); function randomColor() { var hues = [0, 20, 45, 50, 320, 340, 360]; var h = hues[Math.floor(Math.random() * hues.length)]; return 'hsl(' + h + ', 90%, 68%)'; } function Rocket() { this.x = Math.random() * window.innerWidth; this.y = window.innerHeight + 10; var targetY = window.innerHeight * (0.15 + Math.random() * 0.35); var angle = -Math.PI / 2 + (Math.random() - 0.5) * 0.25; var speed = 6 + Math.random() * 4; this.vx = Math.cos(angle) * speed; this.vy = Math.sin(angle) * speed; this.dist = Math.abs(window.innerHeight - targetY); this.travel = 0; this.trail = []; } Rocket.prototype.update = function () { this.trail.push({ x: this.x, y: this.y, life: 1 }); if (this.trail.length > 6) this.trail.shift(); for (var i = 0; i < this.trail.length; i++) { this.trail[i].life -= 0.16; } this.x += this.vx; this.y += this.vy; this.travel += Math.abs(this.vy); if (this.travel >= this.dist || this.y <= window.innerHeight * 0.1) { explode(this.x, this.y); return false; } return true; }; Rocket.prototype.draw = function () { for (var i = 0; i < this.trail.length; i++) { var t = this.trail[i]; ctx.globalAlpha = t.life * 0.8; ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(t.x, t.y, ((i + 1) / this.trail.length) * 2.5, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(this.x, this.y, 2.5, 0, Math.PI * 2); ctx.fill(); }; function explode(x, y) { var color = randomColor(); var count = 50 + Math.floor(Math.random() * 30); for (var i = 0; i < count; i++) { var angle = (Math.PI * 2 * i) / count + Math.random() * 0.2; var speed = 1.5 + Math.random() * 3.5; particles.push(new Particle(x, y, color, Math.cos(angle) * speed, Math.sin(angle) * speed)); } } function Particle(x, y, color, vx, vy) { this.x = x; this.y = y; this.vx = vx; this.vy = vy; this.color = color; this.alpha = 1; this.decay = 0.008 + Math.random() * 0.01; this.gravity = 0.045; } Particle.prototype.update = function () { this.vx *= 0.985; this.vy *= 0.985; this.vy += this.gravity; this.x += this.vx; this.y += this.vy; this.alpha -= this.decay; return this.alpha > 0.02; }; Particle.prototype.draw = function () { ctx.globalAlpha = this.alpha; ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(this.x, this.y, 1.8, 0, Math.PI * 2); ctx.fill(); }; function tick(time) { requestAnimationFrame(tick); ctx.fillStyle = 'rgba(8, 12, 35, 0.16)'; ctx.fillRect(0, 0, window.innerWidth, window.innerHeight); if (time - lastLaunch > 480 && rockets.length < 4) { lastLaunch = time; rockets.push(new Rocket()); } for (var i = rockets.length - 1; i >= 0; i--) { if (rockets[i].update()) { rockets[i].draw(); } else { rockets.splice(i, 1); } } if (particles.length > maxParticles) { particles.splice(0, particles.length - maxParticles); } for (var j = particles.length - 1; j >= 0; j--) { if (particles[j].update()) { particles[j].draw(); } else { particles.splice(j, 1); } } } var mottos = ['马到成功', '龙马精神', '一马当先', '骏业日新', '万马奔腾', '策马扬鞭']; var mottoIndex = 0; var mottoEl = document.getElementById('motto'); mottoEl.addEventListener('click', function () { mottoIndex = (mottoIndex + 1) % mottos.length; mottoEl.textContent = mottos[mottoIndex]; }); requestAnimationFrame(tick); })(); </script> </body> </html>

4.3 三处最值得细读的代码

第一处是resize()里的DPR处理:

var dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = window.innerWidth * dpr; canvas.height = window.innerHeight * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

这里解决的是"高DPI屏幕模糊"问题。iPhone和很多安卓旗舰机的物理分辨率是CSS像素的两倍甚至三倍,如果不把Canvas画布按倍率放大,画出来的烟花边缘会发糊。Math.min(..., 2)是为了限制放大倍数,因为3倍DPI下Canvas像素数会暴涨到9倍,普通手机渲染起来压力很大,画质提升却几乎看不出区别。

第二处是tick()里的半透明蒙版,前面已经详细讲过。你运行完代码后,可以试着把0.16改成0.050.3各看一遍,立刻就能理解拖尾是怎么来的。

第三处是particles.splice(0, particles.length - maxParticles)这个剪枝操作。当粒子数量超过400时,直接从数组头部删掉多出来的部分——这相当于淘汰"最老"的粒子。它保证了无论烟花频率多高、屏幕多大,页面都只会为最多400个粒子计算坐标和绘制,稳稳控制住性能上限。

4.4 改成你的配色和文案

想二次创作的朋友,重点改这几处就行:

  • randomColor()函数里的hues数组决定了烟花颜色。想改成"国旗红+金色"主题,就保留0、20、45、50这几个色相;想改成马年更典雅的金银配色,可以把色相集中在40~50之间,饱和度降低;
  • .year.main-title里的background: linear-gradient(...)color决定文字颜色;
  • mottos数组直接换成你想要的祝福语,比如送给客户可以写"骏业日新""马不停蹄";
  • 副标题丙午马年 · 辞旧迎新也可以改成"一路驰骋·万事顺遂"之类的风格。

5. 实测定调:高DPI、移动端掉帧与微信内置浏览器的处理

5.1 高DPI模糊问题

第一次写完这个页面时,我在一台Windows电脑上用Chrome打开,一切正常。但发到iPhone上一看,烟花的边缘明显发虚,像是蒙了一层雾。原因就是前面说的devicePixelRatio。

这个问题在桌面端不常见,因为大多数桌面显示器DPR是1,但在手机上几乎是必现的。所以resize()里那段DPR处理不是锦上添花,而是移动端可用的前提。我把DPR上限限制在2,也是为了让中低端安卓机不至于因为画布太大而明显掉帧。

5.2 移动端掉帧与粒子剪枝

在两千元档安卓机的微信内置浏览器里打开时,如果同时有3~4枚火箭爆炸,粒子数量一度能到800以上,帧率会明显下降。画面表现就是烟花"一顿一顿"地往外蹦。

两个手段解决:

  1. 粒子上限maxParticles = 400,超过就直接剪枝;
  2. 火箭发射节流间隔从400ms调大到480ms,降低同屏火箭数量。

如果还是觉得卡,可以再把粒子单次爆炸数量从50 + Math.random() * 30下调到35 + Math.random() * 20。牺牲一点"炸得密"的爽感,换来的整晚稳定运行是值得的。

5.3 微信内置浏览器的兼容策略

分享到微信的场景绕不开内置浏览器。以前老版本微信的X5内核确实对ES6支持不好,所以我在这份源码里刻意采用了ES5语法——var、构造函数、prototype、普通function,没有用const/let、箭头函数和class

这样做的好处是,哪怕对方是台很老的低端手机,只要浏览器不是古董,页面都能正常跑。如果你打算把代码升级成更现代的写法(比如用ES6的class),建议给目标用户加一层Babel转译,或者明确知道接收方设备比较新。

5.4 参数调优参考表

参数位置我的取值效果
重力gravityParticle.update0.045决定烟花炸开后下坠速度
阻力系数Particle.update0.985决定粒子减速快慢与飞行距离
衰减decayParticle构造0.008~0.018随机决定粒子可见时间与渐隐速度
蒙版透明度tick填充色0.16决定拖尾长度,越大越短
发射间隔tick条件480ms决定烟花出现的密度
同屏火箭上限tick条件4防止舞台过于拥挤
粒子总数上限tick剪枝400移动端保帧率的红线
单次爆炸粒子数explode50~80决定单朵烟花的饱满度

这张表是我调参过程中沉淀下来的,数值都不是拍脑袋定的,每一档都对应一个肉眼可见的视觉差异。建议大家改的时候一次只改一个参数,观察完再改下一个,这样你才能知道哪个值起什么作用。

6. 把祝福送出去:本地发送、免费托管与分享彩蛋

6.1 直接发送文件的姿势

最省事的方式是把index.html放进一个文件夹,压缩成zip,通过微信文件传助手或邮件发给对方。对方解压后双击就能打开。 有一点要注意:文件名别带特殊符号和空格,尽量用2026-mahun.html新年祝福.html这种简单名字,否则在部分手机上解压后可能无法直接打开。

如果你是发给不太会用电脑的长辈,压缩包可能有点门槛。更稳妥的方式是发一个在线链接,见下一节。

6.2 免费托管与分享链接

把HTML页面变成网址,主要用两种方式。

GitHub Pages:注册登录后新建一个仓库,把index.html拖进去,然后在仓库设置里开启Pages功能,等两分钟就会得到一个形如https://你的用户名.github.io/仓库名/的链接。这个链接对接收方非常友好,点开就是完整页面。

Vercel:如果不熟悉Git,直接把文件夹拖入Vercel的Web面板也能部署,流程更图形化。免费版足够个人使用。

拿到链接后,再用任意二维码工具生成二维码,放到贺卡、邮件签名或者拜年海报里,对方扫码就能打开。

6.3 分享预览优化

链接在微信里的预览效果,取决于页面<head>里的几个标签。我这份源码里已经写好了:

<title>2026丙午马年 · 新春快乐</title> <meta name="description" content="2026马年专属动态烟花祝福页,纯HTML/CSS/JavaScript实现,打开即看,可分享给家人朋友。">

再想精益求精,可以加一个1200x630左右尺寸的预览图片,并补充Open Graph标签:

<meta property="og:title" content="2026丙午马年 · 新春快乐"> <meta property="og:description" content="点开看2026马年专属烟花祝福"> <meta property="og:image" content="https://你的域名/preview.jpg">

这样链接在微信和多数社交平台里会显示成带封面图的卡片,比光秃秃的文字链更有质感。

6.4 再加一点互动的小脑洞

页面跑通之后,还可以继续往里面加东西:

  • 群名定制版:把mottos数组换成朋友群里的"黑话",或者每个人的名字和一句祝福,点击轮播时像是在点名;
  • 倒计时彩蛋:通过new Date('2026-02-17T00:00:00') - Date.now()算出距离春节还有多少天,显示在页面底部;
  • 音效加持:用AudioContext在烟花爆炸时合成一段白噪音短音效,不用引入任何音频文件,代码量不大,效果却很喜庆;
  • 生日/教师节通用版:把mottos换成"生日快乐""感谢恩师",年份位置改成对方年龄或"9.10",这套模板就能复用到其他节日。

有一点要提醒:如果加音乐,建议用无版权音乐资源或者自己的音频文件,不要直接外链其他网站的版权音源,否则分享出去很容易被处理。

做完这个页面之后我最大的感受是:调参比写代码更花时间。写逻辑也许一个晚上就够了,但为了让烟花节奏、拖尾长短、文字发光程度都刚刚好,我前前后后改了十几版数值。如果你也想做一个类似的页面,别指望一次写完就完美,先让一个粒子动起来,再加一个爆炸,再加火箭、加文字、加交互,每一步都能肉眼看到变化,最后自然就越改越有感觉。这份源码我特意保留了最稳定的参数配置,你可以直接拿去用,也可以像我一样,一点点改出属于自己的版本。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询