☰
原生Canvas烟花动画与新年倒计时页面开发实战
2026/10/15 5:45:29 网站建设 项目流程

简介:这是一份面向前端初学者与节日项目实践者的HTML跨年互动页面代码包,解决零点烟花庆祝场景下快速搭建轻量级动态网页的需求。资源共5个文件,包含4个HTML页面(分别实现首页、五秒倒计时、烟花主效果及使用说明)和1个TXT文档,总大小仅8KB,结构简洁、开箱即用——HTML负责页面骨架与时间逻辑触发,JavaScript实现实时倒计时检测与烟花动画控制,CSS3配合完成多层粒子绽放效果,HTML5 Audio标签集成背景音乐与爆炸音效。已有28101人学习下载,说明其在教学演示、个人博客装饰、小型活动页面等场景中具备高复用价值。读者可直接运行查看完整跨年体验,并通过修改文字、颜色、音频路径等参数快速定制专属版本,是理解时间控制、DOM操作与CSS动画协同的典型前端小项目范例。

1. 项目概述:用代码点亮新年

又到年底了,想给个人网站、博客或者给朋友发个特别的新年祝福页面吗?与其到处找千篇一律的贺卡,不如自己动手写一个。一个融合了倒计时、烟花特效的HTML新年页面,不仅心意十足,还能秀一把你的前端技能。这玩意儿听起来复杂,其实拆解开来,核心就是HTML搭骨架、CSS做美容、JavaScript(JS)搞动画和逻辑。我做过好几个类似的节日页面,发现只要思路清晰,哪怕你刚学前端不久,跟着步骤也能捣鼓出来。今天,我就把自己实现一个“春节跨年烟花页面”的完整过程、踩过的坑和优化技巧,从头到尾给你捋清楚。

这个项目适合所有对Web前端感兴趣的朋友,无论你是想找个练手小项目的前端新手,还是想给现有网站添加节日元素的开发者。最终效果会是一个全屏的页面:深色夜空中,显眼的倒计时牌滴答作响,归零瞬间,绚丽的烟花在屏幕上绽放,配合喜庆的文案和背景音乐,仪式感直接拉满。下面,我们就从最核心的设计思路开始拆解。

2. 核心思路与架构设计

2.1 效果拆解与技术选型

首先,别被“烟花”、“动画”这些词吓到。我们得把最终效果拆分成几个可以独立实现的技术模块:

  1. 倒计时模块:这是页面的逻辑核心。我们需要一个实时更新的时间显示,计算当前时间距离目标新年时刻(例如,明年春节的农历正月初一零点)的剩余天数、小时、分钟和秒数。
  2. 烟花特效模块:这是视觉核心。需要在屏幕上模拟烟花从发射、升空、爆炸、散开到消失的全过程。
  3. 页面UI与氛围模块:包括背景、文字样式、布局等,营造节日氛围。
  4. 交互与音效模块:比如点击屏幕放烟花、播放背景音乐等。

技术选型上,我们坚持最纯粹、最轻量的原生Web技术栈:

  • HTML5:作为页面结构的基础。我们会用到<canvas>这个关键元素,它是实现烟花动画的“画布”。
  • CSS3:负责所有静态样式,包括布局、颜色、字体,以及一些简单的过渡动画(比如数字变化时的微动效)。
  • JavaScript (ES6+):负责所有动态逻辑。包括倒计时的计算与更新、烟花粒子系统的模拟(物理计算、绘制)、用户交互事件的监听与处理。

为什么不直接用现成的动画库或游戏引擎(如Three.js)?对于这个特定场景,原生Canvas API足够灵活和高效,且最终代码依赖为零,只有一个HTML文件,分享和部署都极其方便。用库反而增加了不必要的复杂性和体积。

2.2 项目文件结构与初始化

我们采用单HTML文件方案,将所有代码(HTML、CSS、JS)集中在一个文件里。这样做的好处是“即开即用”,你复制一个文件就能运行,非常适合分享。当然,在开发时,我们可以在脑中或注释里保持模块化思维。

创建一个名为new-year-fireworks.html的文件,用任何文本编辑器(VSCode、Sublime Text甚至记事本)打开。首先搭建最基本的HTML5骨架:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>龙年大吉 · 新春倒计时烟花秀</title> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Ma+Shan+Zheng&family=ZCOOL+QingKe+HuangYou&display=swap"> <style> /* 所有CSS样式将在这里编写 */ </style> </head> <body> <!-- 页面主体结构将在这里编写 --> <script> // 所有JavaScript代码将在这里编写 </script> </body> </html>

注意几个关键点:

  1. <!DOCTYPE html>声明确保浏览器以标准模式渲染。
  2. <meta name="viewport" ...>设置对于移动端自适应至关重要,让页面在不同尺寸设备上都能正常显示。
  3. 我们通过Google Fonts引入了两款有中国风韵味的字体:“马善政”和“站酷庆科黄油体”,用于增强节日氛围。
  4. CSS和JS都以内联方式写在同一个文件中,实现“单文件部署”。

3. 页面布局与静态样式实现

3.1 构建HTML结构

在<body>标签内,我们构建页面的骨架。主要分为三个层次:最底层的Canvas画布、中间层的倒计时UI、以及顶层的控制按钮等。

<body> <!-- 画布:用于绘制烟花和背景 --> <canvas id="fireworksCanvas"></canvas> <!-- 主内容容器 --> <div class="container"> <header class="header"> <h1 class="title">🐉 恭贺新禧 · 龙年大吉 🐉</h1> <p class="subtitle">新春倒计时烟花秀</p> </header> <main class="main-content"> <!-- 倒计时显示区域 --> <div class="countdown-box"> <h2>距离甲辰龙年还有</h2> <div class="countdown-display"> <div class="time-unit"> <span id="days" class="number">00</span> <span class="label">天</span> </div> <div class="time-unit"> <span id="hours" class="number">00</span> <span class="label">时</span> </div> <div class="time-unit"> <span id="minutes" class="number">00</span> <span class="label">分</span> </div> <div class="time-unit"> <span id="seconds" class="number">00</span> <span class="label">秒</span> </div> </div> <p class="dynamic-greeting" id="greeting">辞旧迎新,万事胜意!</p> </div> <!-- 祝福语区域 --> <div class="blessings"> <p>福星高照 · 阖家欢乐 · 岁岁平安 · 恭喜发财</p> </div> </main> <footer class="footer"> <div class="controls"> <button id="musicToggle" class="btn">🎵 开启音乐</button> <button id="fireworksBtn" class="btn">🎆 手动放烟花</button> <button id="fullscreenBtn" class="btn">⛶ 全屏沉浸</button> </div> <p class="hint">点击屏幕任意位置也可触发烟花 | 页面自适应电脑与手机</p> </footer> </div> <!-- 音频元素,预加载但不自动播放 --> <audio id="bgMusic" loop preload="auto"> <source src="https://assets.codepen.io/xxxxxx/happy-new-year.mp3" type="audio/mpeg"> (此处为示例音乐链接,实际使用时需替换为可用的喜庆纯音乐或自己准备的音乐文件) </audio> <script> // JavaScript代码... </script> </body>

结构清晰明了:Canvas垫底,.container作为Flexbox或Grid容器居中管理所有内容,倒计时、祝福语、控制按钮各司其职。

3.2 编写CSS样式

接下来,在<style>标签内填充CSS,打造视觉体验。核心要点包括:全屏铺满、Canvas定位、字体应用、倒计时数字样式、按钮交互效果等。

* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'ZCOOL QingKe HuangYou', cursive, sans-serif; overflow: hidden; /* 防止滚动条出现 */ color: #FFF; background: linear-gradient(to bottom, #0c0e2a, #1a1b3a); /* 深空蓝渐变背景 */ height: 100vh; display: flex; justify-content: center; align-items: center; position: relative; } /* Canvas画布样式 */ #fireworksCanvas { position: fixed; /* 固定定位,覆盖整个视口 */ top: 0; left: 0; width: 100%; height: 100%; z-index: 1; /* 位于内容层之下 */ display: block; } /* 主内容容器,位于Canvas之上 */ .container { position: relative; z-index: 2; /* 确保内容在烟花之上 */ text-align: center; padding: 2rem; max-width: 1200px; width: 90%; background-color: rgba(20, 22, 54, 0.7); /* 半透明深色背景,提高文字可读性 */ border-radius: 20px; backdrop-filter: blur(5px); /* 毛玻璃效果 */ border: 1px solid rgba(255, 215, 0, 0.3); box-shadow: 0 15px 35px rgba(0, 0, 0, 0.5); } /* 标题样式 */ .header .title { font-family: 'Ma Shan Zheng', cursive; font-size: 3.5rem; margin-bottom: 0.5rem; color: #FFD700; /* 金色 */ text-shadow: 0 0 10px #FF4500, 0 0 20px #FF8C00; } .header .subtitle { font-size: 1.8rem; color: #87CEEB; margin-bottom: 2rem; } /* 倒计时区域 */ .countdown-box { margin: 3rem 0; } .countdown-box h2 { font-size: 2.2rem; margin-bottom: 2rem; color: #FFFAF0; } .countdown-display { display: flex; justify-content: center; flex-wrap: wrap; gap: 2rem; margin-bottom: 2rem; } .time-unit { display: flex; flex-direction: column; align-items: center; } .time-unit .number { font-size: 5.5rem; font-weight: bold; line-height: 1; color: #FF6347; /* 番茄红 */ background: rgba(0, 0, 0, 0.4); padding: 0.5rem 1.5rem; border-radius: 15px; min-width: 140px; margin-bottom: 0.5rem; text-shadow: 0 0 10px currentColor; transition: all 0.3s ease; } .time-unit .label { font-size: 1.8rem; color: #98FB98; } .dynamic-greeting { font-size: 2rem; color: #FFD700; margin-top: 1.5rem; min-height: 3rem; } /* 祝福语 */ .blessings { font-size: 1.8rem; color: #E6E6FA; margin: 2rem 0; padding: 1rem; border-top: 1px dashed #FFD700; border-bottom: 1px dashed #FFD700; } /* 页脚与控制按钮 */ .footer { margin-top: 2rem; } .controls { display: flex; justify-content: center; gap: 1.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; } .btn { font-family: inherit; font-size: 1.3rem; padding: 0.8rem 2rem; border: none; border-radius: 50px; cursor: pointer; background: linear-gradient(45deg, #FF8C00, #FF4500); color: white; transition: all 0.3s ease; box-shadow: 0 5px 15px rgba(255, 69, 0, 0.4); } .btn:hover { transform: translateY(-3px); box-shadow: 0 8px 20px rgba(255, 69, 0, 0.6); } .btn:active { transform: translateY(1px); } .hint { font-size: 1rem; color: #AAA; opacity: 0.8; } /* 响应式设计 */ @media (max-width: 768px) { .container { padding: 1.5rem; width: 95%; } .header .title { font-size: 2.5rem; } .header .subtitle { font-size: 1.4rem; } .countdown-box h2 { font-size: 1.8rem; } .time-unit .number { font-size: 3.5rem; min-width: 100px; padding: 0.5rem 1rem; } .time-unit .label { font-size: 1.4rem; } .dynamic-greeting { font-size: 1.6rem; } .blessings { font-size: 1.4rem; } .btn { font-size: 1.1rem; padding: 0.7rem 1.5rem; } .countdown-display { gap: 1rem; } }

注意:CSS中使用了backdrop-filter: blur()来实现毛玻璃效果,这是一个较新的特性,部分旧版浏览器可能不支持。如果追求最广泛的兼容性,可以移除这行,或者增加一个纯色半透明背景作为降级方案。

至此,页面的静态部分已经完成。如果你现在在浏览器中打开这个HTML文件,应该能看到一个具有节日氛围的静态页面,但倒计时不会动,烟花也没有。接下来,就是注入灵魂的JavaScript部分。

4. JavaScript核心逻辑:倒计时与烟花模拟

4.1 倒计时功能实现

倒计时的原理很简单:获取目标时间(比如2025年春节是2月10日00:00:00)和当前时间,计算差值,然后将差值分解成天、时、分、秒,并每秒更新一次。

在<script>标签内,我们首先实现倒计时:

// 1. 倒计时功能 document.addEventListener('DOMContentLoaded', function() { // 定义目标日期时间 (示例:2025年春节,农历正月初一,公历2月10日 00:00:00) const targetDate = new Date('2025-02-10T00:00:00').getTime(); // 获取显示时间的DOM元素 const daysEl = document.getElementById('days'); const hoursEl = document.getElementById('hours'); const minutesEl = document.getElementById('minutes'); const secondsEl = document.getElementById('seconds'); const greetingEl = document.getElementById('greeting'); // 更新倒计时的函数 function updateCountdown() { const now = new Date().getTime(); const timeLeft = targetDate - now; // 如果时间已到或过期 if (timeLeft < 0) { daysEl.textContent = '00'; hoursEl.textContent = '00'; minutesEl.textContent = '00'; secondsEl.textContent = '00'; greetingEl.textContent = '🎉 新年快乐!龙年大吉! 🎉'; // 可以在这里触发持续的烟花效果 return; } // 计算天、时、分、秒 const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24)); const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000); // 更新DOM元素,并确保两位数显示 daysEl.textContent = days.toString().padStart(2, '0'); hoursEl.textContent = hours.toString().padStart(2, '0'); minutesEl.textContent = minutes.toString().padStart(2, '0'); secondsEl.textContent = seconds.toString().padStart(2, '0'); // 动态祝福语(根据剩余时间变化) if (timeLeft < 60 * 60 * 1000) { // 最后1小时 greetingEl.textContent = '最后一小时!准备迎接新年!'; } else if (timeLeft < 24 * 60 * 60 * 1000) { // 最后1天 greetingEl.textContent = '最后一天!辞旧迎新!'; } else { const greetings = [ '辞旧迎新,万事胜意!', '瑞雪兆丰年,红梅报新春!', '福气满满,好运连连!', '阖家团圆,幸福安康!' ]; // 每小时换一句祝福语 const index = Math.floor(now / (1000 * 60 * 60)) % greetings.length; greetingEl.textContent = greetings[index]; } // 为秒数变化添加一个简单的动画类(可选) secondsEl.classList.add('pulse'); setTimeout(() => secondsEl.classList.remove('pulse'), 300); } // 立即执行一次,避免初始显示为空 updateCountdown(); // 然后每秒更新一次 const countdownInterval = setInterval(updateCountdown, 1000); // 可以在页面卸载时清除定时器(虽然不是必须) // window.addEventListener('beforeunload', () => clearInterval(countdownInterval)); });

同时,在CSS中添加一个简单的脉冲动画类:

@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } .pulse { animation: pulse 0.3s ease-in-out; }

4.2 Canvas烟花粒子系统实现

这是整个项目最有趣也最具挑战性的部分。我们将使用Canvas 2D API来模拟烟花。核心思想是“粒子系统”:每个烟花由大量微小的粒子组成,每个粒子有位置、速度、颜色、大小、生命周期等属性。通过逐帧计算和绘制所有粒子的状态,形成动画。

在倒计时代码之后,继续编写烟花系统:

// 2. 烟花粒子系统 (function() { // 获取Canvas和上下文 const canvas = document.getElementById('fireworksCanvas'); const ctx = canvas.getContext('2d'); // 设置Canvas尺寸为窗口大小,并处理窗口缩放 function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; } window.addEventListener('resize', resizeCanvas); resizeCanvas(); // 初始化尺寸 // 粒子类 class Particle { constructor(x, y, vx, vy, color, size, life, gravity = 0.1, friction = 0.99) { this.x = x; this.y = y; this.vx = vx; // 水平速度 this.vy = vy; // 垂直速度 this.color = color; this.size = size; this.life = life; // 初始生命值 this.maxLife = life; this.gravity = gravity; this.friction = friction; this.alpha = 1.0; // 透明度 } update() { // 应用物理:速度受摩擦力和重力影响 this.vx *= this.friction; this.vy *= this.friction; this.vy += this.gravity; // 更新位置 this.x += this.vx; this.y += this.vy; // 生命值递减,透明度随之减少 this.life--; this.alpha = this.life / this.maxLife; // 返回粒子是否还“活着” return this.life > 0; } draw() { ctx.save(); ctx.globalAlpha = this.alpha; ctx.fillStyle = this.color; // 绘制圆形粒子,也可以尝试方形或星形 ctx.beginPath(); ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2); ctx.fill(); // 添加发光效果 ctx.shadowBlur = 10; ctx.shadowColor = this.color; ctx.fill(); ctx.restore(); } } // 烟花类(由多个粒子组成) class Firework { constructor(x, y, targetY, color) { this.x = x; this.y = y; this.targetY = targetY; // 爆炸高度 this.color = color; this.speed = 8 + Math.random() * 4; // 上升速度 this.velocity = { x: 0, y: -this.speed }; // 初始向上 this.gravity = 0.2; this.particles = []; // 爆炸后的粒子数组 this.exploded = false; this.trail = []; // 轨迹粒子,用于拖尾效果 } update() { if (!this.exploded) { // 上升阶段 this.velocity.y += this.gravity * 0.3; // 上升时重力影响小 this.x += this.velocity.x; this.y += this.velocity.y; // 添加轨迹粒子 if (Math.random() < 0.5) { this.trail.push(new Particle(this.x, this.y, (Math.random() - 0.5) * 0.5, Math.random() * 0.5 + 0.5, this.color, Math.random() * 2 + 1, 15, 0.05, 0.95)); } // 检查是否到达爆炸点 if (this.y <= this.targetY) { this.explode(); } } // 更新轨迹粒子 for (let i = this.trail.length - 1; i >= 0; i--) { if (!this.trail[i].update()) { this.trail.splice(i, 1); } } // 更新爆炸粒子 for (let i = this.particles.length - 1; i >= 0; i--) { if (!this.particles[i].update()) { this.particles.splice(i, 1); } } } explode() { this.exploded = true; const particleCount = 80 + Math.floor(Math.random() * 50); // 粒子数量 const colors = [this.color]; // 主色 // 可以添加一些邻近色,让爆炸色彩更丰富 if (Math.random() > 0.5) { colors.push(`hsl(${(parseInt(this.color.slice(1,3), 16) + 30) % 360}, 100%, 65%)`); } for (let i = 0; i < particleCount; i++) { const angle = Math.random() * Math.PI * 2; const speed = Math.random() * 5 + 2; // 爆炸速度 const vx = Math.cos(angle) * speed; const vy = Math.sin(angle) * speed; const color = colors[Math.floor(Math.random() * colors.length)]; const size = Math.random() * 3 + 1; const life = 50 + Math.random() * 50; // 粒子寿命 this.particles.push(new Particle( this.x, this.y, vx, vy, color, size, life )); } // 播放爆炸音效(如果有) // playSound('explosion'); } draw() { // 绘制轨迹 for (const trailParticle of this.trail) { trailParticle.draw(); } if (!this.exploded) { // 绘制上升中的烟花头 ctx.save(); ctx.fillStyle = this.color; ctx.beginPath(); ctx.arc(this.x, this.y, 3, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 15; ctx.shadowColor = this.color; ctx.fill(); ctx.restore(); } // 绘制爆炸粒子 for (const particle of this.particles) { particle.draw(); } } isDone() { // 烟花结束的条件:已爆炸且所有粒子已消失 return this.exploded && this.particles.length === 0; } } // 全局变量 const fireworks = []; const colors = ['#FF5252', '#FF4081', '#E040FB', '#7C4DFF', '#536DFE', '#448AFF', '#40C4FF', '#18FFFF', '#64FFDA', '#69F0AE', '#B2FF59', '#EEFF41', '#FFFF00', '#FFD740', '#FFAB40', '#FF6E40']; // 创建随机烟花的函数 function createRandomFirework() { const x = Math.random() * canvas.width; const startY = canvas.height; const targetY = 100 + Math.random() * (canvas.height / 2); // 爆炸高度在画布上半部分 const color = colors[Math.floor(Math.random() * colors.length)]; fireworks.push(new Firework(x, startY, targetY, color)); } // 在指定位置创建烟花(用于点击事件) function createFireworkAt(x, y) { const targetY = y - 50 - Math.random() * 100; // 在点击位置上方一点爆炸 const color = colors[Math.floor(Math.random() * colors.length)]; fireworks.push(new Firework(x, canvas.height, targetY, color)); } // 动画循环 function animate() { // 使用半透明黑色矩形覆盖上一帧,实现拖尾/淡出效果 ctx.fillStyle = 'rgba(10, 10, 30, 0.15)'; // 调整alpha值控制拖尾长度 ctx.fillRect(0, 0, canvas.width, canvas.height); // 随机生成新烟花(控制频率) if (Math.random() < 0.03) { // 每帧约3%的概率,可根据性能调整 createRandomFirework(); } // 更新并绘制所有烟花 for (let i = fireworks.length - 1; i >= 0; i--) { fireworks[i].update(); fireworks[i].draw(); // 移除已完成的烟花 if (fireworks[i].isDone()) { fireworks.splice(i, 1); } } requestAnimationFrame(animate); } // 启动动画循环 animate(); // 3. 交互事件绑定 // 点击屏幕放烟花 canvas.addEventListener('click', function(event) { const rect = canvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; // 一次点击可以生成多个烟花,效果更密集 for (let i = 0; i < 3; i++) { setTimeout(() => { createFireworkAt(x + (Math.random() - 0.5) * 50, y + (Math.random() - 0.5) * 50); }, i * 50); } }); // 手动放烟花按钮 document.getElementById('fireworksBtn').addEventListener('click', function() { for (let i = 0; i < 5; i++) { setTimeout(() => createRandomFirework(), i * 100); } }); // 全屏按钮 document.getElementById('fullscreenBtn').addEventListener('click', function() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(err => { console.log(`全屏请求失败: ${err.message}`); }); } else { if (document.exitFullscreen) { document.exitFullscreen(); } } }); // 音乐控制按钮 const bgMusic = document.getElementById('bgMusic'); const musicToggleBtn = document.getElementById('musicToggle'); let musicPlaying = false; musicToggleBtn.addEventListener('click', function() { if (musicPlaying) { bgMusic.pause(); musicToggleBtn.textContent = '🎵 开启音乐'; } else { // 注意:现代浏览器通常禁止自动播放音频,必须由用户交互触发 bgMusic.play().then(() => { musicPlaying = true; musicToggleBtn.textContent = '⏸️ 暂停音乐'; }).catch(err => { console.log('音频播放失败:', err); alert('音乐播放失败,可能是浏览器策略限制。请确保页面已与用户交互。'); }); } musicPlaying = !musicPlaying; }); // 监听全屏变化,更新按钮文字 document.addEventListener('fullscreenchange', function() { document.getElementById('fullscreenBtn').textContent = document.fullscreenElement ? '⛶ 退出全屏' : '⛶ 全屏沉浸'; }); })();

4.3 代码整合与优化

将以上所有JavaScript代码按顺序放入同一个<script>标签内。注意,由于烟花动画使用了requestAnimationFrame循环,它是一个持续运行的函数,确保放在最后并正确初始化。

关键点解析与优化技巧:

  1. Canvas尺寸与高清屏适配:resizeCanvas函数确保画布始终覆盖整个视口。在高DPI(Retina)屏幕上,Canvas可能会模糊。一个常见的优化是使用window.devicePixelRatio来缩放Canvas的绘图上下文,这里为了代码简洁未采用,但如果你追求极致清晰度,可以研究“Canvas HiDPI”适配方案。
  2. 粒子物理模拟:Particle类中的friction(摩擦力)和gravity(重力)参数是调节烟花“感觉”的关键。摩擦力小于1会使粒子速度逐渐衰减,重力为正数会使粒子下落。通过调整这些值,你可以模拟出轻盈的星光或沉重的火花。
  3. 性能优化:
    • 粒子回收:在Firework.update()中,我们遍历粒子数组并移除life <= 0的粒子(splice(i, 1)),这是防止数组无限增长、导致内存泄漏和性能下降的关键。
    • 绘制优化:在animate循环中,我们使用rgba(10,10,30,0.15)半透明填充来清屏,这创造了漂亮的拖尾淡出效果。Alpha值越小,拖尾越长,烟花轨迹越明显。
    • 控制烟花生成频率:if (Math.random() < 0.03)这行代码控制了自动烟花的生成率。数值越大,烟花越密集,对性能要求也越高。在性能较弱的设备上,可以调低此值。
  4. 用户体验:
    • 点击交互:点击事件不仅触发一个烟花,而是通过循环和setTimeout触发一小簇,体验更好。
    • 音频播放策略:由于浏览器自动播放策略,背景音乐必须在用户与页面交互(如点击按钮)后才能播放。我们通过“开启音乐”按钮来触发,并妥善处理了播放失败的情况。

5. 功能扩展与高级技巧

基础功能完成后,我们可以考虑添加一些“锦上添花”的特性,让页面更具个性化和趣味性。

5.1 动态背景与星空效果

在烟花之下,我们可以绘制一个简单的动态星空背景,增加深邃感。在animate函数开始绘制烟花之前,先绘制星空。

// 在animate函数开头,清屏代码之后,绘制星空 // 星星数组初始化 let stars = []; const STAR_COUNT = 200; function initStars() { stars = []; for (let i = 0; i < STAR_COUNT; i++) { stars.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, size: Math.random() * 1.5 + 0.5, brightness: Math.random() * 0.5 + 0.5, speed: Math.random() * 0.05 + 0.02 }); } } initStars(); window.addEventListener('resize', initStars); // 窗口变化时重置星星 function drawStars() { ctx.fillStyle = 'white'; for (const star of stars) { ctx.globalAlpha = star.brightness; ctx.beginPath(); ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2); ctx.fill(); // 让星星缓慢向下移动(模拟视觉滚动) star.y += star.speed; if (star.y > canvas.height) { star.y = 0; star.x = Math.random() * canvas.width; } } ctx.globalAlpha = 1.0; } // 然后在animate循环中,在清屏后立即调用 drawStars(); // 将原来的 ctx.fillRect(...) 行替换为: ctx.fillStyle = 'rgba(10, 10, 30, 0.15)'; ctx.fillRect(0, 0, canvas.width, canvas.height); drawStars(); // 新增:绘制星空

5.2 新年钟声与倒计时归零特效

我们可以在倒计时归零的瞬间,触发一个特别的“庆典模式”。

首先,修改updateCountdown函数中时间到期的部分:

if (timeLeft < 0) { daysEl.textContent = '00'; hoursEl.textContent = '00'; minutesEl.textContent = '00'; secondsEl.textContent = '00'; greetingEl.textContent = '🎉 新年快乐!龙年大吉! 🎉'; // 触发庆典模式 if (!window.celebrationStarted) { window.celebrationStarted = true; startCelebration(); } return; }

然后,添加startCelebration函数:

function startCelebration() { // 1. 改变背景色或添加喜庆背景 document.body.style.background = 'linear-gradient(to bottom, #8B0000, #FF4500)'; // 2. 密集生成烟花 const celebrationInterval = setInterval(() => { for(let i = 0; i < 3; i++) { createRandomFirework(); } }, 200); // 每200毫秒生成3个 // 3. 播放钟声或欢呼声(需要准备音频文件) // const audio = new Audio('new-year-bell.mp3'); // audio.play().catch(e => console.log('钟声播放失败:', e)); // 4. 10秒后停止密集烟花,恢复自动节奏 setTimeout(() => { clearInterval(celebrationInterval); document.body.style.background = ''; // 恢复原背景 }, 10000); }

5.3 响应式与兼容性打磨

我们已经通过CSS媒体查询做了基础响应式。但还有一些细节可以优化:

  1. 移动端触摸事件:将canvas.addEventListener('click', ...)改为同时监听click和touchstart事件,以兼容移动设备。
    function handleInteraction(event) { event.preventDefault(); // 防止触摸时滚动 const clientX = event.clientX || event.touches[0].clientX; const clientY = event.clientY || event.touches[0].clientY; const rect = canvas.getBoundingClientRect(); const x = clientX - rect.left; const y = clientY - rect.top; // ... 原有的创建烟花代码 } canvas.addEventListener('click', handleInteraction); canvas.addEventListener('touchstart', handleInteraction);
  2. 性能节流:在resize和scroll事件中,可以使用requestAnimationFrame或setTimeout进行节流,避免频繁重绘导致卡顿。
  3. 字体加载优化:我们使用了网络字体,如果加载慢,会导致页面初始渲染时字体闪烁。可以考虑使用font-display: swap;或在本地托管字体文件。

6. 常见问题与调试实录

在实际编写和运行过程中,你可能会遇到以下问题。这里是我踩过坑后的解决方案:

6.1 Canvas绘制问题

  • 问题:烟花绘制不出来,或者一闪而过。
  • 排查:
    1. 检查Canvas尺寸:确保canvas.width和canvas.height被正确设置为窗口大小。在resizeCanvas函数中打印这两个值,确认不为0。
    2. 检查动画循环:确认requestAnimationFrame(animate)被调用,且animate函数内部没有因为错误而中断。打开浏览器开发者工具的“控制台”(Console)查看是否有JS报错。
    3. 检查粒子生命周期:如果Particle的life值设置过小,粒子会瞬间消失。尝试增大life的初始值(如从50改为150)。
    4. 检查清屏逻辑:ctx.fillStyle = 'rgba(10,10,30,0.15)'中的Alpha值如果太大(比如1),会完全覆盖上一帧,看不到拖尾效果;如果太小,拖尾会过长导致画面混乱。0.1到0.2是比较好的范围。

6.2 倒计时不准或不动

  • 问题:倒计时数字不更新,或者更新频率不对。
  • 排查:
    1. 检查定时器:setInterval(updateCountdown, 1000)确保是1000毫秒(1秒)。
    2. 检查目标时间:new Date('2025-02-10T00:00:00')注意时区。这个构造方式使用的是本地时区。如果你想使用UTC时间,可以加上Z:new Date('2025-02-10T00:00:00Z')。最好用Date.UTC()构造一个明确的UTC时间戳,再与Date.now()(总是UTC)进行比较,可以避免时区问题。
    3. 检查DOM元素ID:确保getElementById获取的元素ID与HTML中的id属性完全一致,大小写敏感。

6.3 音频无法播放

  • 问题:点击“开启音乐”按钮后,音乐没有声音。
  • 排查:
    1. 浏览器策略:这是最常见的原因。现代浏览器(Chrome, Safari, Firefox等)禁止音频在用户与页面交互之前自动播放。我们的代码已经将播放逻辑放在按钮点击事件中,符合要求。如果仍不行,检查浏览器是否静音,或者控制台是否有Autoplay策略相关的警告。
    2. 网络问题:我们示例中使用的是外链(CodePen的资产)。这个链接可能失效或很慢。最佳实践是将音乐文件放在你自己的服务器或项目目录下,使用相对路径(如src="./assets/happy-new-year.mp3")。
    3. 格式兼容性:确保音频文件是MP3等广泛支持的格式,并在<audio>标签中提供多个<source>以兼容不同浏览器。

6.4 移动端体验不佳

  • 问题:在手机上查看,布局错乱,或者点击没反应。
  • 排查:
    1. Viewport:确认<meta name="viewport">标签存在且正确。
    2. CSS单位:尽量使用rem、vw、vh或百分比等相对单位,避免固定像素(px)导致在小屏幕上过大。
    3. 触摸事件:如前所述,确保绑定了touchstart事件,并调用event.preventDefault()防止页面滚动。
    4. 性能:移动设备性能有限。如果感觉卡顿,可以尝试减少同时存在的最大粒子数(降低particleCount)、减少自动烟花生成频率(降低Math.random() < 0.03中的数值)、或减小Canvas的绘制尺寸(虽然会模糊)。

6.5 代码调试技巧

  • 多用console.log:在关键函数入口、变量变化处添加console.log,可以快速跟踪程序流和数据状态。例如,在createRandomFirework里打印坐标,看烟花是否在预期位置生成。
  • 利用浏览器开发者工具:
    • Elements面板:检查DOM结构、CSS样式是否正确应用。
    • Console面板:查看JS错误和日志,是调试的第一站。
    • Sources面板:可以设置断点,单步调试JavaScript代码,查看调用栈和变量值,这是解决复杂逻辑问题的利器。
    • Performance面板:如果页面卡顿,可以录制一段时间内的性能,分析是哪部分代码(通常是animate函数或复杂的绘制操作)占用了过多时间。

最后,分享一个我个人的心得:这类视觉项目,参数调优占了大半时间。烟花的速度、重力、摩擦力、颜色、数量,没有一个“标准答案”。最好的方法是大胆修改代码中的各种常数,比如gravity、friction、particleCount、颜色数组,然后立即刷新页面看效果。这个过程本身就像在调配一场视觉盛宴,非常有乐趣。你可以尝试把重力调大,让烟花迅速坠落;或者把摩擦力调小,让粒子飘得更久,创造出完全不同的氛围。

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

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

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

立即咨询