在浏阳我连续看了两场烟花盛宴想实现烟花自由就来浏阳。这不仅仅是一句旅游宣传语对于开发者而言它背后隐藏着一个有趣的技术命题如何用代码模拟一场绚烂的烟花表演并实现“烟花自由”——即能够随心所欲地控制烟花的样式、颜色、轨迹和绽放效果。无论是用于前端数据可视化、游戏特效、节日庆典页面还是作为学习图形学和动画编程的实践项目用程序生成烟花都是一个极佳的切入点。本文将带你从零开始使用现代前端技术栈HTML5 Canvas JavaScript构建一个可交互的烟花模拟器。我们将从理解物理模型开始逐步实现粒子系统、重力模拟、颜色渐变、鼠标交互最终完成一个在浏览器中流畅运行的烟花秀并探讨如何优化性能以实现“连续绽放”的盛宴效果。1. 理解烟花模拟的核心粒子系统与物理模型烟花模拟的本质是一个粒子系统Particle System。每一个升空的烟花弹、每一颗炸开的火花都可以看作是一个具有特定属性的粒子。我们的程序需要管理成千上万个这样的粒子并按照物理规律更新它们的状态。1.1 粒子的基本属性一个烟花粒子至少需要包含以下属性来定义其在某一时刻的状态位置 (x, y)粒子在画布上的坐标。速度 (vx, vy)粒子在水平和垂直方向上的移动速度。vy通常会受到重力影响而持续变化。加速度 (ax, ay)影响速度变化的力最典型的就是重力加速度ay gravity。半径/大小 (radius)粒子显示的大小。颜色 (color)粒子的颜色可以是固定值也可以随时间渐变或随机。生命周期 (life, alpha)粒子从诞生到消失的时间或透明度用于实现淡出效果。摩擦力/空气阻力 (friction)一个小于1的系数用于模拟速度逐渐衰减使运动更真实。1.2 烟花的两阶段运动模型一场完整的烟花动画包含两个主要阶段发射阶段 (Launch)一个或多个粒子烟花弹以初始速度从底部向上发射。此阶段通常忽略空气阻力主要受重力影响做匀减速上升或匀加速下落运动。爆炸阶段 (Explosion)当烟花弹到达预设高度或速度接近零时触发爆炸。爆炸会瞬间生成大量新的粒子火花这些粒子从爆炸中心向四面八方以随机速度散开。火花粒子除了受重力下坠外还受到显著的空气阻力摩擦力影响速度会快速衰减同时生命周期结束最终消失。理解了这两个阶段我们就有了清晰的编码目标创建Particle类来封装粒子属性创建Firework类来管理发射弹和爆炸逻辑最后用一个主循环 (requestAnimationFrame) 来驱动所有粒子的更新与绘制。2. 环境准备与项目结构本项目完全基于浏览器原生技术无需安装任何第三方库或构建工具。你只需要一个现代浏览器如 Chrome、Edge、Firefox和一个代码编辑器如 VS Code。2.1 创建项目文件在你的工作目录下创建三个文件index.html: 主 HTML 文件包含 Canvas 画布。style.css: 简单的样式文件用于让画布全屏。script.js: 主要的 JavaScript 逻辑文件。项目结构如下fireworks-simulator/ ├── index.html ├── style.css └── script.js2.2 基础 HTML 与 CSS 设置首先在index.html中创建画布并引入资源。!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 title浏阳烟花模拟器 - 实现你的烟花自由/title link relstylesheet hrefstyle.css link relicon hrefdata:image/svgxml,svg xmlns%22http://www.w3.org/2000/svg%22 viewBox%220 0 100 100%22text y%22.9em%22 font-size%2290%22/text/svg /head body canvas idfireworksCanvas/canvas div classcontrols button idlaunchBtn发射一枚烟花/button button idautoBtn自动连续发射/button button idclearBtn清空画面/button div classslider-container label forgravitySlider重力强度/label input typerange idgravitySlider min0 max20 value10 step1 span idgravityValue0.1/span /div /div script srcscript.js/script /body /html在style.css中我们让画布充满全屏并设置一个深色背景来衬托烟花。* { margin: 0; padding: 0; box-sizing: border-box; } body { overflow: hidden; background: radial-gradient(ellipse at bottom, #0d1b2a 0%, #000000 100%); height: 100vh; font-family: sans-serif; color: #ccc; } #fireworksCanvas { display: block; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; } .controls { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(0, 0, 0, 0.7); padding: 15px 25px; border-radius: 10px; display: flex; gap: 15px; align-items: center; z-index: 2; flex-wrap: wrap; justify-content: center; } .controls button { padding: 10px 20px; border: none; border-radius: 5px; background: linear-gradient(to right, #ff7e5f, #feb47b); color: #000; font-weight: bold; cursor: pointer; transition: transform 0.1s; } .controls button:hover { transform: scale(1.05); } .controls button:active { transform: scale(0.95); } .slider-container { display: flex; align-items: center; gap: 10px; } #gravitySlider { width: 120px; }3. 核心 JavaScript 实现从粒子到烟花秀现在进入核心部分script.js。我们将按照自底向上的顺序构建粒子 - 烟花 - 系统管理。3.1 实现 Particle 粒子类Particle类是整个系统的基础。它负责存储自身状态并在每一帧更新位置、速度并绘制自己。// script.js const canvas document.getElementById(fireworksCanvas); const ctx canvas.getContext(2d); // 初始化画布尺寸 function resizeCanvas() { canvas.width window.innerWidth; canvas.height window.innerHeight; } window.addEventListener(resize, resizeCanvas); resizeCanvas(); // 初始调用 class Particle { constructor(x, y, vx, vy, color, radius, gravity, friction, life 1.0) { this.x x; this.y y; this.vx vx; // 水平速度 this.vy vy; // 垂直速度 this.color color; this.radius radius; this.gravity gravity; // 重力加速度 this.friction friction; // 摩擦力系数 (0~1) this.life life; // 初始生命值 this.alpha 1.0; // 初始透明度随life减少 this.decay 0.015 Math.random() * 0.01; // 生命衰减速率略有随机 } update() { // 应用摩擦力减缓速度 this.vx * this.friction; this.vy * this.friction; // 应用重力只影响垂直速度 this.vy this.gravity; // 更新位置 this.x this.vx; this.y this.vy; // 衰减生命 this.life - this.decay; this.alpha this.life; // 透明度与生命值绑定 } draw() { if (this.life 0) return; ctx.save(); ctx.globalAlpha this.alpha; ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle this.color; ctx.fill(); // 添加发光效果 ctx.shadowBlur 15; ctx.shadowColor this.color; ctx.fill(); ctx.restore(); } isDead() { return this.life 0; } }关键参数解释friction通常设置为0.99或0.98使粒子速度每帧减少1%-2%模拟空气阻力。值越小减速越快。gravity重力加速度典型值为0.1到0.2。这个值会直接加到垂直速度vy上。decay生命衰减速率决定了粒子存活的帧数。0.015表示大约1/0.015 ≈ 66帧后生命结束。3.2 实现 Firework 烟花类Firework类管理一个完整的烟花生命周期一个发射弹也是粒子升空然后爆炸生成多个火花粒子。class Firework { constructor(startX, startY, targetX, targetY, gravity) { // 发射弹属性 this.x startX; this.y startY; this.startY startY; this.targetX targetX; this.targetY targetY; this.gravity gravity; // 计算发射所需的速度简单的弹道计算 const dx targetX - startX; const dy targetY - startY; // 使用一个固定的“力度”和角度来计算初速度 const angle Math.atan2(dy, dx); const speed 12 Math.random() * 3; // 随机速度 this.vx Math.cos(angle) * speed; this.vy Math.sin(angle) * speed; this.color hsl(${Math.random() * 360}, 100%, 60%); // 随机鲜艳颜色 this.radius 3; this.particles []; // 存储爆炸后的火花粒子 this.exploded false; } update() { if (!this.exploded) { // 更新发射弹 this.vy this.gravity; // 重力影响 this.x this.vx; this.y this.vy; // 简单爆炸条件速度方向反转到达最高点或接近目标 if (this.vy 0) { // 开始下落时爆炸 this.explode(); } } // 更新所有爆炸后的火花粒子 for (let i this.particles.length - 1; i 0; i--) { this.particles[i].update(); if (this.particles[i].isDead()) { this.particles.splice(i, 1); } } } explode() { this.exploded true; const particleCount 80 Math.random() * 50; // 随机火花数量 for (let i 0; i particleCount; i) { // 随机角度和速度 const angle Math.random() * Math.PI * 2; const speed Math.random() * 5 2; // 2-7的随机速度 const vx Math.cos(angle) * speed; const vy Math.sin(angle) * speed; // 火花颜色可以基于烟花弹颜色做微调 const hue parseInt(this.color.substring(4, this.color.length - 1)); const sparkHue (hue Math.random() * 60 - 30) % 360; const sparkColor hsl(${sparkHue}, 100%, ${60 Math.random() * 20}%); // 创建火花粒子摩擦力更大重力更小生命周期随机 const particle new Particle( this.x, this.y, vx, vy, sparkColor, Math.random() * 2 1, // 半径 1-3 this.gravity * 0.3, // 火花重力减弱 0.95 Math.random() * 0.03, // 摩擦力 0.95-0.98 0.8 Math.random() * 0.4 // 初始生命值 ); this.particles.push(particle); } } draw() { if (!this.exploded) { // 绘制发射弹一条拖尾线 ctx.save(); ctx.globalAlpha 0.7; ctx.beginPath(); ctx.moveTo(this.x, this.y); ctx.lineTo(this.x - this.vx * 0.5, this.y - this.vy * 0.5); ctx.strokeStyle this.color; ctx.lineWidth this.radius; ctx.stroke(); ctx.restore(); } // 绘制所有火花粒子 this.particles.forEach(particle particle.draw()); } isDone() { // 烟花结束已爆炸且所有火花粒子都消失了 return this.exploded this.particles.length 0; } }关键逻辑解释发射速度计算这里使用了一个简化的模型根据起点和终点计算角度并赋予一个随机速度。更真实的模拟可以加入初始爆发力。爆炸条件if (this.vy 0)是一个简单有效的判断。当垂直速度从负向上变为非负向下或零时说明达到了运动轨迹的最高点此时触发爆炸。火花生成爆炸时在360度全方向随机生成大量粒子。通过调整speed、gravity和friction可以模拟不同“质感”的烟花如柳絮状高摩擦力、低重力或菊花状速度均匀。3.3 构建烟花系统与动画循环现在我们需要一个管理器来存放所有的烟花实例并创建动画循环来驱动整个系统。class FireworksSystem { constructor() { this.fireworks []; this.gravity 0.1; // 全局重力 this.autoLaunchInterval null; this.isAutoLaunching false; this.initControls(); } // 初始化UI控制 initControls() { const launchBtn document.getElementById(launchBtn); const autoBtn document.getElementById(autoBtn); const clearBtn document.getElementById(clearBtn); const gravitySlider document.getElementById(gravitySlider); const gravityValue document.getElementById(gravityValue); // 手动发射 launchBtn.addEventListener(click, () { this.launchRandom(); }); // 自动连续发射 autoBtn.addEventListener(click, () { if (this.isAutoLaunching) { this.stopAutoLaunch(); autoBtn.textContent 自动连续发射; } else { this.startAutoLaunch(); autoBtn.textContent 停止自动发射; } }); // 清空画面 clearBtn.addEventListener(click, () { this.fireworks []; }); // 重力调节 gravitySlider.addEventListener(input, (e) { // 将滑块值(0-20)映射到重力值(0-0.2) this.gravity e.target.value / 100; gravityValue.textContent this.gravity.toFixed(2); }); gravityValue.textContent this.gravity.toFixed(2); // 鼠标点击发射 canvas.addEventListener(click, (e) { this.launchFromBottom(e.clientX, e.clientY); }); } launchRandom() { const startX canvas.width / 2; // 从底部中央发射 const startY canvas.height; const targetX Math.random() * (canvas.width * 0.6) (canvas.width * 0.2); // 目标水平位置随机 const targetY Math.random() * (canvas.height * 0.4) 100; // 目标高度随机 this.fireworks.push(new Firework(startX, startY, targetX, targetY, this.gravity)); } launchFromBottom(targetX, targetY) { const startX targetX; // 从点击点的正下方发射 const startY canvas.height; this.fireworks.push(new Firework(startX, startY, targetX, targetY, this.gravity)); } startAutoLaunch() { this.isAutoLaunching true; // 每隔 300-800 毫秒随机发射一枚 this.autoLaunchInterval setInterval(() { this.launchRandom(); }, 300 Math.random() * 500); } stopAutoLaunch() { this.isAutoLaunching false; if (this.autoLaunchInterval) { clearInterval(this.autoLaunchInterval); this.autoLaunchInterval null; } } update() { // 更新所有烟花 for (let i this.fireworks.length - 1; i 0; i--) { this.fireworks[i].update(); // 移除已完成的烟花 if (this.fireworks[i].isDone()) { this.fireworks.splice(i, 1); } } } draw() { // 每一帧用半透明的黑色矩形覆盖上一帧形成拖尾效果 ctx.fillStyle rgba(13, 27, 42, 0.2); ctx.fillRect(0, 0, canvas.width, canvas.height); // 绘制所有烟花 this.fireworks.forEach(firework firework.draw()); } animate() { this.update(); this.draw(); requestAnimationFrame(() this.animate()); } start() { this.animate(); } } // 启动系统 const system new FireworksSystem(); system.start();系统核心机制动画循环requestAnimationFrame是浏览器提供的用于制作动画的API它会以屏幕刷新率通常60fps调用回调函数。在animate方法中我们循环调用update和draw。拖尾效果ctx.fillStyle rgba(13, 27, 42, 0.2);这一行是关键。它用带有透明度的背景色覆盖整个画布而不是完全清空。这样前一帧的粒子会留下淡淡的痕迹形成美丽的运动轨迹和光晕。性能管理在update方法中我们遍历烟花数组并移除已经爆炸且所有火花粒子都消失的烟花 (isDone())。这是防止数组无限增长、导致性能下降的重要步骤。4. 运行验证与效果调优现在双击打开index.html文件你应该能看到一个深色星空背景的页面。点击“发射一枚烟花”按钮或直接点击画布烟花就会从底部升起并绽放。点击“自动连续发射”即可模拟浏阳夜空中“连续两场烟花盛宴”的壮观景象。4.1 关键参数调优表通过修改以下参数你可以创造出风格迥异的烟花效果参数所在位置作用推荐调整范围效果影响全局重力 (gravity)FireworksSystem构造函数及滑块影响所有粒子的下落加速度。0.05 - 0.2值越小烟花升得越高火花飘散越慢值越大下落越快爆炸更急促。发射弹速度 (speed)Firework构造函数决定烟花弹升空的速度和高度。10 - 15值越大发射越高轨迹越直值小则抛物线平缓。火花数量 (particleCount)Firework.explode()方法单次爆炸产生的火花粒子数。50 - 150数量越多越绚烂但性能消耗越大。火花初始速度 (speed)Firework.explode()内层循环决定爆炸的扩散范围和力度。2 - 8值越大爆炸范围越广火花飞散越猛。火花摩擦力 (friction)Firework.explode()创建粒子时模拟空气阻力使火花速度衰减。0.92 - 0.99值越小火花减速越快爆炸形状更“团”值越大火花飞得更远更持久。火花重力 (gravity * 0.3)Firework.explode()创建粒子时火花粒子单独受到的重力通常比发射弹小。全局重力 * 0.2 ~ 0.5值越小火花飘浮感越强值越大火花下坠越快。生命衰减 (decay)Particle构造函数粒子透明度和生命值的减少速度。0.01 - 0.03值越大火花消失越快值小则拖尾更长。拖尾透明度 (rgba(... , 0.2))FireworksSystem.draw()覆盖画布的透明度形成拖尾。0.1 - 0.3值越小拖尾痕迹越长越淡值越大画面刷新越快轨迹越短。4.2 实现“浏阳烟花”特色效果浏阳烟花以花色繁多、造型独特著称。我们可以通过修改爆炸算法来模拟几种经典效果圆形菊花状上述基础代码实现的就是近似效果。心形修改explode方法中粒子的发射角度计算。// 在心形爆炸中替换角度随机生成部分 // const angle Math.random() * Math.PI * 2; // 原随机角度 const t Math.random() * Math.PI * 2; const angle t; // 角度不变 const radius 15 * (1 - Math.sin(t)); // 心形极坐标方程简化 const speed Math.random() * 3 radius * 0.1; // 速度与半径相关 const vx Math.cos(angle) * speed; const vy Math.sin(angle) * speed;环状/多重环状让粒子在特定角度区间内发射并分层。// 双层环状爆炸 const ring Math.floor(Math.random() * 2); // 0 或 1决定内环外环 const baseAngle Math.random() * Math.PI * 2; const spread Math.PI / 6; // 30度扇形 const angle baseAngle (Math.random() - 0.5) * spread; const speed ring 0 ? 3 : 6; // 内环慢外环快5. 常见问题排查与性能优化在开发或运行过程中你可能会遇到以下问题5.1 烟花不显示或立即消失现象点击按钮或画布看不到烟花弹或爆炸。排查打开浏览器开发者工具F12查看Console标签页是否有红色报错如Uncaught TypeError。检查script.js文件是否被正确引入路径是否正确。在Firework.explode()方法开始处添加console.log(‘爆炸’, this.x, this.y)查看爆炸是否被触发。检查Particle.draw()方法中的ctx.globalAlpha是否设置过低或life衰减过快 (decay值过大)。解决根据控制台错误修正代码。确保画布上下文ctx获取成功。调整decay值为0.01左右再测试。5.2 动画卡顿、掉帧现象自动发射大量烟花后动画变得不流畅。排查在FireworksSystem.update()开始处添加console.log(‘烟花数量’, this.fireworks.length, ‘粒子总数’, this.fireworks.reduce((sum, fw) sum fw.particles.length, 0))监控粒子数量。确认isDone()逻辑正确能及时清理已完成的烟花对象。解决限制粒子总数在Firework.explode()中根据性能情况减少particleCount。优化绘制Particle.draw()中的ctx.shadowBlur非常耗性能。可以改为只对部分粒子使用或完全移除。使用离屏Canvas对于大量重复绘制的粒子可以考虑使用离屏Canvas进行缓存但本项目粒子状态变化频繁收益可能不高。降低自动发射频率增加setInterval的时间间隔。5.3 烟花轨迹不自然或闪烁现象烟花弹或火花的运动轨迹生硬或有明显的闪烁感。排查检查animate循环是否稳定运行没有因为异常而中断。检查ctx.fillStyle ‘rgba(…, 0.2)’这行代码的透明度值。如果值太大如0.5拖尾太短会闪烁值太小如0.05残影过长会模糊。检查重力 (gravity) 和摩擦力 (friction) 的值是否在合理范围内。极端的值会导致运动突变。解决将拖尾透明度调整到0.15到0.25之间。确保重力值在0.05~0.2摩擦力在0.95~0.99。5.4 控制滑块不生效现象调节重力滑块烟花下坠速度没有变化。排查检查滑块input事件监听器是否绑定成功。在事件监听器内添加console.log(‘重力值’, this.gravity)查看值是否变化。检查Firework和Particle的构造函数是否正确地使用了传入的gravity参数而不是一个写死的常量。解决确保Firework和Particle实例在创建时使用的是系统当前this.gravity值。对于已存在的粒子修改全局重力不会影响其生命周期只会影响新创建的烟花。6. 生产环境最佳实践与扩展方向这个演示项目为了清晰易懂将逻辑都写在了一个文件中。在实际生产环境或更复杂的应用中可以考虑以下优化和扩展6.1 代码结构优化模块化将Particle,Firework,FireworksSystem拆分成独立的 ES6 模块文件便于维护和复用。配置化将所有可调参数颜色、数量、速度、物理常量提取到一个配置对象中方便管理和实现不同的烟花主题。使用 TypeScript为粒子属性、烟花状态等定义清晰的接口减少运行时错误。6.2 性能与体验优化粒子池创建和销毁大量Particle对象会触发垃圾回收可能导致卡顿。可以预先初始化一个粒子对象池循环使用。绘制优化当粒子数量极大时可以将多个颜色、大小相近的粒子合并使用CanvasRenderingContext2D.putImageData或 WebGL 进行批量绘制性能有数量级提升。音效添加同步的音效可以极大提升沉浸感。使用 Web Audio API 在爆炸时播放对应的声音。响应式适配确保画布在移动设备上也能正确显示和交互可能需要调整粒子大小和发射逻辑。6.3 功能扩展方向预设烟花模式实现“连珠炮”、“瀑布”、“字幕烟花”等特效本质是控制多个烟花弹的发射时序、位置和爆炸参数。粒子纹理使用图片或渐变代替纯色圆点可以绘制出星星、花朵等更复杂的火花形状。物理引擎集成引入简单的物理引擎如matter.js的轻量级部分模拟粒子间的碰撞、风力等更复杂的效果。数据驱动设计一个 JSON 格式的“烟花脚本”可以描述一场完整的烟花秀的所有事件实现无需编码即可编排复杂表演。交互升级支持鼠标拖拽绘制发射轨迹或根据音乐节奏自动生成烟花音频可视化。实现“烟花自由”的终极目标是建立一个高度可配置、性能优异、表现力丰富的粒子动画系统。本文提供的代码是一个功能完整、逻辑清晰的起点。你可以从调整参数开始感受每个变量对视觉效果的影响然后尝试实现上述扩展方向中的一项逐步打造属于你自己的浏阳级数字烟花盛宴。