跳到主要内容
JavaScript 大前端 算法
基于 Canvas 和 Web Audio API 的交互式烟花动画实现 基于 Canvas 和 Web Audio API 构建交互式烟花动画,采用双层画布渲染拖尾效果与混合模式增强亮度。核心模块包含音效合成、状态管理及粒子物理系统,支持重力、空气阻力模拟。通过对象池优化性能,适配 iOS 刘海屏及触摸交互,实现全屏 PWA 体验。
CloudNative 发布于 2026/3/23 更新于 2026/5/3 4 浏览
一个基于 Canvas 和 Web Audio API 的交互式烟花动画
一、整体架构
┌─────────────────────────────────────────────────────────────┐
│ HTML 结构 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ SVG 图标 │ │ Canvas 容器 │ │ 控制面板/菜单 │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ CSS 样式 │
├─────────────────────────────────────────────────────────────┤
│ JavaScript 逻辑 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ FireworkSound│ │ Store │ │ Shell/Star/Spark │ │
│ │ 音效模块 │ │ 状态管理 │ │ 粒子系统 │ │
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
二、HTML 结构部分
1. 头部元信息
<meta charset ="UTF-8" >
<title > 2026 新年快乐!万事如意!</title >
<meta name ="viewport" content ="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" >
说明:
设置字符编码为 UTF-8
页面标题显示新年祝福
viewport-fit=cover 适配 iPhone X 系列刘海屏
禁用用户缩放,防止误操作
2. iOS PWA 支持
<meta name ="apple-mobile-web-app-capable" content ="yes" >
<meta name ="apple-mobile-web-app-status-bar-style" content = >
"black-translucent"
<meta name ="apple-mobile-web-app-title" content ="烟花盛宴" >
<link rel ="apple-touch-icon" href ="..." >
<link rel ="manifest" href ="data:application/json;base64,..." >
支持添加到 iOS 主屏幕
半透明状态栏适配刘海屏
内联 PWA manifest(Base64 编码)
3. SVG 图标定义 <svg xmlns ="http://www.w3.org/2000/svg" >
<symbol id ="icon-play" viewBox ="0 0 24 24" > <path d ="M8 5v14l11-7z" /> </symbol >
<symbol id ="icon-pause" viewBox ="0 0 24 24" > <path d ="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /> </symbol >
</svg >
使用 SVG symbol 定义可复用的图标
通过 <use href="#icon-xxx"> 引用
优点:矢量图标、可缩放、体积小
4. Canvas 画布容器 <div id ="canvas-container" >
<canvas id ="trails-canvas" > </canvas >
<canvas id ="main-canvas" > </canvas >
</div >
层级 作用 渲染方式 trails-canvas绘制烟花拖尾效果 渐变透明覆盖实现残影 main-canvas绘制当前帧的粒子和 UI 元素 每帧清空重绘
5. 控制面板 <div id ="controls" >
<div id ="pause-btn" class ="btn" > <svg fill ="white" width ="24" height ="24" > <use href ="#icon-pause" > </use > </svg > </div >
<div id ="shutter-btn" class ="btn" > <svg fill ="white" width ="24" height ="24" > <use href ="#icon-shutter-slow" > </use > </svg > </div >
<div id ="settings-btn" class ="btn" > <svg fill ="white" width ="24" height ="24" > <use href ="#icon-settings" > </use > </svg > </div >
</div >
暂停/播放 :控制动画运行
快门模式 :切换长曝光效果
设置 :打开设置菜单
6. 设置菜单 <div id ="menu" >
<div id ="menu__header" > 设置</div >
<form >
<div class ="form-option form-option--select" >
<label > 烟花类型</label > <select id ="shell-type" > </select >
</div >
<div class ="form-option form-option--checkbox" >
<label > <input id ="auto-launch" type ="checkbox" /> <span > 自动发射</span > </label >
</div >
</form >
</div >
7. 首次交互提示(iOS 适配) <div id ="audio-prompt" >
<div id ="audio-prompt__icon" > 🎆</div >
<div id ="audio-prompt__text" > 点击屏幕开始烟花盛宴</div >
<div id ="audio-prompt__hint" > Tap to start fireworks</div >
</div >
iOS Safari 要求用户交互后才能播放音频
此提示引导用户点击以解锁音频
三、CSS 样式部分
1. 全局样式与触摸优化 * {
position : relative;
box-sizing : border-box;
-webkit-tap-highlight-color : transparent;
-webkit-touch-callout: none;
-webkit-user-select : none;
user-select : none;
touch-action : manipulation;
}
html { background-color : #000 ; }
body {
overflow : hidden;
color : rgba (255 , 255 , 255 , 0.5 );
font-family : "Russo One" , arial, sans-serif;
overscroll-behavior : none;
padding : env (safe-area-inset-top) env (safe-area-inset-right) env (safe-area-inset-bottom) env (safe-area-inset-left);
}
2. 混合模式 #canvas-container canvas {
position : absolute;
mix-blend-mode : lighten;
}
lighten 混合模式使亮色粒子在黑色背景上更加突出
多个亮色叠加会产生更亮的效果
3. 动画过渡 .hide {
opacity : 0 ;
visibility : hidden;
transition : opacity 0.3s , visibility 0.3s ;
}
使用 opacity + visibility 组合实现平滑的显示/隐藏动画
visibility 延迟隐藏,避免点击穿透
4. 音频提示动画 #audio-prompt__icon {
font-size : 80px ;
animation : pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0% , 100% { transform : scale (1 ); opacity : 1 ; }
50% { transform : scale (1.1 ); opacity : 0.7 ; }
}
四、JavaScript 核心模块
1. FireworkSound 音效模块 const FireworkSound = {
ctx : null ,
enabled : true ,
soundType : 'realistic' ,
isUnlocked : false ,
init ( ) {
this .ctx = new (window .AudioContext || window .webkitAudioContext )();
},
unlock ( ) {
if (this .isUnlocked ) return true ;
if (this .ctx .state === 'suspended' ) {
this .ctx .resume ();
}
const buffer = this .ctx .createBuffer (1 , 1 , 22050 );
const source = this .ctx .createBufferSource ();
source.buffer = buffer;
source.connect (this .ctx .destination );
source.start (0 );
this .isUnlocked = true ;
},
playLaunch ( ) {},
playBurst (size ) {}
};
组件 作用 示例用途 AudioContext音频上下文,管理所有音频节点 new AudioContext()OscillatorNode振荡器,生成周期性波形 正弦波、方波、锯齿波 GainNode增益节点,控制音量 音量包络、淡入淡出 BiquadFilter双二阶滤波器 低通、高通、带通滤波 BufferSource缓冲源,播放采样数据 白噪声、爆炸声
playRealisticBurst (size = 1 ) {
const duration = 0.6 + size * 0.2 ;
const bufferSize = this .ctx .sampleRate * duration;
const buffer = this .ctx .createBuffer (1 , bufferSize, this .ctx .sampleRate );
const data = buffer.getChannelData (0 );
for (let i = 0 ; i < bufferSize; i++) {
const t = i / bufferSize;
const noise = Math .random () * 2 - 1 ;
const envelope = Math .pow (1 - t, 1.5 );
data[i] = noise * envelope;
}
const filter = this .ctx .createBiquadFilter ();
filter.type = 'lowpass' ;
filter.frequency .setValueAtTime (3000 * size, this .ctx .currentTime );
filter.frequency .exponentialRampToValueAtTime (200 , this .ctx .currentTime + duration * 0.7 );
const gain = this .ctx .createGain ();
gain.gain .setValueAtTime (0 , this .ctx .currentTime );
gain.gain .linearRampToValueAtTime (volume, this .ctx .currentTime + 0.02 );
gain.gain .exponentialRampToValueAtTime (0.01 , this .ctx .currentTime + duration);
noise.connect (filter);
filter.connect (gain);
gain.connect (this .ctx .destination );
noise.start (this .ctx .currentTime );
}
类型 中文名 特点 realistic 真实 白噪声 + 低频冲击 + 噼啪声 cinematic 电影感 深沉隆隆声 + 闪烁火花 classic 经典烟花 清脆爆破声 retro 复古 8 位 方波游戏机风格 soft 柔和轻柔 正弦波轻柔音效 synth 电子合成 多振荡器合成器 drum 鼓点贝斯 底鼓 + 军鼓 + 踩镲 whistle 哨子爆破 口哨式发射 + 爆破
2. Store 状态管理 const store = {
_listeners : new Set (),
state : {
paused : false ,
longExposure : false ,
menuOpen : false ,
config : {
shell : 'Random' ,
size : '3' ,
autoLaunch : true ,
finale : false ,
hideControls : false ,
soundEnabled : true ,
soundType : 'realistic'
}
},
setState (nextState ) {
this .state = Object .assign ({}, this .state , nextState);
this ._dispatch ();
this .persist ();
},
subscribe (listener ) {
this ._listeners .add (listener);
return () => this ._listeners .delete (listener);
},
load ( ) {},
persist ( ) {}
};
用户操作 → updateConfig () → store.setState () → _dispatch () → renderApp ()
↓ 更新 DOM
3. Shell 烟花类 class Shell {
constructor (options ) {
this .size = options.size ;
this .starCount = options.starCount ;
this .starLife = options.starLife ;
this .color = options.color ;
this .glitter = options.glitter ;
this .pistil = options.pistil ;
this .ring = options.ring ;
}
launch (position, launchHeight ) {
const launchX = position * (width - hpad * 2 ) + hpad;
const launchY = height;
const burstY = minHeight - (launchHeight * (minHeight - vpad));
const launchDistance = launchY - burstY;
const launchVelocity = Math .pow (launchDistance * 0.04 , 0.64 );
const comet = Star .add (
launchX, launchY,
this .color || COLOR .White ,
Math .PI ,
launchVelocity,
launchVelocity * 400
);
comet.heavy = true ;
comet.sparkFreq = 16 ;
comet.onDeath = () => this .burst (comet.x , comet.y );
FireworkSound .playLaunch ();
}
burst (x, y ) {
const speed = this .size / 96 ;
createParticleArc (0 , PI_2 , this .starCount , 1 , (angle ) => {
const star = Star .add (
x, y,
this .color ,
angle,
Math .pow (Math .random (), 0.45 ) * speed,
this .starLife + Math .random () * this .starLife * this .starLifeVariation
);
star.onDeath = onDeath;
});
const soundSize = Math .min (2 , this .size / 200 );
FireworkSound .playBurst (soundSize);
}
}
类型 中文名 特点 视觉效果 Crysanthemum 菊花 经典球形爆炸,密集粒子 大型圆形散开 Palm 棕榈 垂直拖尾,像棕榈树 向下垂落 Ring 圆环 环形爆炸 空心圆环 Crossette 十字 爆炸后分裂成四个小星星 二次爆炸 Floral 花簇 小型二次爆炸 多层绽放 Crackle 噼啪 金色火花,噼啪声 金色闪烁 Willow 垂柳 长时间下坠的火花 长尾下垂 Falling Leaves 落叶 飘落的金色粒子 缓慢飘落 Horse Tail 马尾 水平拖尾 横向展开
4. Star 星星粒子 const Star = {
drawWidth : 3 ,
airDrag : 0.98 ,
airDragHeavy : 0.992 ,
active : createParticleCollection (),
_pool : [],
add (x, y, color, angle, speed, life, speedOffX, speedOffY ) {
const instance = this ._pool .pop () || {};
instance.x = x;
instance.y = y;
instance.prevX = x;
instance.prevY = y;
instance.color = color;
instance.speedX = Math .sin (angle) * speed + (speedOffX || 0 );
instance.speedY = Math .cos (angle) * speed + (speedOffY || 0 );
instance.life = life;
instance.sparkFreq = 0 ;
this .active [color].push (instance);
return instance;
},
returnInstance (instance ) {
instance.onDeath && instance.onDeath (instance);
instance.onDeath = null ;
this ._pool .push (instance);
}
};
避免频繁创建/销毁对象
减少垃圾回收(GC)压力
提高大量粒子时的性能
5. Spark 火花粒子 const Spark = {
drawWidth : 0.75 ,
airDrag : 0.9 ,
active : createParticleCollection (),
_pool : [],
add (x, y, color, angle, speed, life ) {
const instance = this ._pool .pop () || {};
instance.x = x;
instance.y = y;
instance.prevX = x;
instance.prevY = y;
instance.color = color;
instance.speedX = Math .sin (angle) * speed;
instance.speedY = Math .cos (angle) * speed;
instance.life = life;
this .active [color].push (instance);
return instance;
}
};
特性 Star Spark 大小 3px 0.75px 空气阻力 0.98 0.9 用途 主要烟花粒子 闪光效果 寿命 较长 较短
6. 渲染循环 function update (frameTime, lag ) {
if (!canInteract ()) return ;
const timeStep = frameTime * simSpeed;
const speed = simSpeed * lag;
updateGlobals (timeStep, lag);
const starDrag = 1 - (1 - Star .airDrag ) * speed;
const sparkDrag = 1 - (1 - Spark .airDrag ) * speed;
const gAcc = timeStep / 1000 * GRAVITY ;
COLOR_CODES_W_INVIS .forEach (color => {
Star .active [color].forEach ((star, i, stars ) => {
star.life -= timeStep;
if (star.life <= 0 ) {
stars.splice (i, 1 );
Star .returnInstance (star);
} else {
star.prevX = star.x ;
star.prevY = star.y ;
star.x += star.speedX * speed;
star.y += star.speedY * speed;
star.speedX *= starDrag;
star.speedY *= starDrag;
star.speedY += gAcc;
if (star.spinRadius ) {
star.spinAngle += star.spinSpeed * speed;
star.x += Math .sin (star.spinAngle ) * star.spinRadius * speed;
star.y += Math .cos (star.spinAngle ) * star.spinRadius * speed;
}
if (star.sparkFreq ) {
star.sparkTimer -= timeStep;
while (star.sparkTimer < 0 ) {
star.sparkTimer += star.sparkFreq ;
Spark .add (star.x , star.y , star.sparkColor , ...);
}
}
}
});
Spark .active [color].forEach ((spark, i, sparks ) => {
});
});
render (speed);
}
7. 渲染函数 function render (speed ) {
const { dpr, width, height } = mainStage;
const trailsCtx = trailsStage.ctx ;
const mainCtx = mainStage.ctx ;
colorSky (speed);
trailsCtx.scale (dpr, dpr);
mainCtx.scale (dpr, dpr);
trailsCtx.globalCompositeOperation = 'source-over' ;
trailsCtx.fillStyle = `rgba(0, 0, 0, ${longExposure ? 0.0025 : 0.1 * speed} )` ;
trailsCtx.fillRect (0 , 0 , width, height);
trailsCtx.globalCompositeOperation = 'lighten' ;
mainCtx.clearRect (0 , 0 , width, height);
while (BurstFlash .active .length ) {
const bf = BurstFlash .active .pop ();
const gradient = trailsCtx.createRadialGradient (bf.x , bf.y , 0 , bf.x , bf.y , bf.radius );
gradient.addColorStop (0.05 , 'white' );
gradient.addColorStop (0.25 , 'rgba(255, 160, 20, 0.2)' );
gradient.addColorStop (1 , 'rgba(255, 160, 20, 0)' );
trailsCtx.fillStyle = gradient;
trailsCtx.fillRect (bf.x - bf.radius , bf.y - bf.radius , bf.radius * 2 , bf.radius * 2 );
}
trailsCtx.lineWidth = Star .drawWidth ;
trailsCtx.lineCap = 'round' ;
COLOR_CODES .forEach (color => {
const stars = Star .active [color];
trailsCtx.strokeStyle = color;
trailsCtx.beginPath ();
stars.forEach (star => {
trailsCtx.moveTo (star.x , star.y );
trailsCtx.lineTo (star.prevX , star.prevY );
});
trailsCtx.stroke ();
});
trailsCtx.lineWidth = Spark .drawWidth ;
trailsCtx.lineCap = 'butt' ;
if (speedBarOpacity) {
mainCtx.globalAlpha = speedBarOpacity;
mainCtx.fillStyle = COLOR .Blue ;
mainCtx.fillRect (0 , height - 6 , width * simSpeed, 6 );
mainCtx.globalAlpha = 1 ;
}
trailsCtx.resetTransform ();
mainCtx.resetTransform ();
}
8. 天空颜色计算 const currentSkyColor = { r : 0 , g : 0 , b : 0 };
const targetSkyColor = { r : 0 , g : 0 , b : 0 };
function colorSky (speed ) {
const maxSkySaturation = 30 ;
const maxStarCount = 500 ;
let totalStarCount = 0 ;
targetSkyColor.r = 0 ;
targetSkyColor.g = 0 ;
targetSkyColor.b = 0 ;
COLOR_CODES .forEach (color => {
const tuple = COLOR_TUPLES [color];
const count = Star .active [color].length ;
totalStarCount += count;
targetSkyColor.r += tuple.r * count;
targetSkyColor.g += tuple.g * count;
targetSkyColor.b += tuple.b * count;
});
const intensity = Math .pow (Math .min (1 , totalStarCount / maxStarCount), 0.3 );
const maxColorComponent = Math .max (1 , targetSkyColor.r , targetSkyColor.g , targetSkyColor.b );
targetSkyColor.r = targetSkyColor.r / maxColorComponent * maxSkySaturation * intensity;
targetSkyColor.g = targetSkyColor.g / maxColorComponent * maxSkySaturation * intensity;
targetSkyColor.b = targetSkyColor.b / maxColorComponent * maxSkySaturation * intensity;
const colorChange = 10 ;
currentSkyColor.r += (targetSkyColor.r - currentSkyColor.r ) / colorChange * speed;
currentSkyColor.g += (targetSkyColor.g - currentSkyColor.g ) / colorChange * speed;
currentSkyColor.b += (targetSkyColor.b - currentSkyColor.b ) / colorChange * speed;
appNodes.canvasContainer .style .backgroundColor = `rgb(${currentSkyColor.r | 0 } , ${currentSkyColor.g | 0 } , ${currentSkyColor.b | 0 } )` ;
}
五、用户交互
1. 点击发射 function handlePointerStart (event ) {
activePointerCount++;
FireworkSound .unlock ();
FireworkSound .resume ();
const btnSize = 44 ;
if (event.y < btnSize) {
if (event.x < btnSize) { togglePause (); return ; }
if (event.x > mainStage.width / 2 - btnSize / 2 && event.x < mainStage.width / 2 + btnSize / 2 ) { toggleLongExposure (); return ; }
if (event.x > mainStage.width - btnSize) { toggleMenu (); return ; }
}
if (!canInteract ()) return ;
if (updateSpeedFromEvent (event)) { isUpdatingSpeed = true ; }
else if (event.onCanvas ) { launchShellFromConfig (event); }
}
2. 键盘快捷键 function handleKeydown (event ) {
if (event.keyCode === 80 ) togglePause ();
if (event.keyCode === 79 ) toggleMenu ();
if (event.keyCode === 27 ) toggleMenu (false );
}
3. 自动发射序列 function startSequence ( ) {
if (isFirstSeq) {
isFirstSeq = false ;
const shell = new Shell (crysanthemumShell (shellSizeSelector ()));
shell.launch (0.5 , 0.5 );
return 2400 ;
}
if (finaleSelector ()) {
seqRandomShell ();
if (currentFinaleCount < finaleCount) {
currentFinaleCount++;
return 170 ;
} else {
currentFinaleCount = 0 ;
return 6000 ;
}
}
const rand = Math .random ();
if (rand < 0.2 ) return seqSmallBarrage ();
if (rand < 0.6 ) return seqRandomShell ();
if (rand < 0.8 ) return seqTwoRandom ();
return seqTriple ();
}
六、性能优化要点 优化技术 说明 效果 对象池 复用 Star/Spark 对象 减少 GC 压力 颜色分组 按颜色分组批量渲染 减少 Canvas 状态切换 双缓冲 trails-canvas 保留历史轨迹 实现拖尾效果 requestAnimationFrame 与屏幕刷新同步 流畅动画 空气阻力模拟 粒子速度衰减 自然消散效果 设备像素比适配 scale(dpr, dpr)高清显示
七、iOS 适配
1. Meta 标签优化 <meta name ="viewport" content ="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover" >
<meta name ="apple-mobile-web-app-capable" content ="yes" >
<meta name ="apple-mobile-web-app-status-bar-style" content ="black-translucent" >
<meta name ="apple-mobile-web-app-title" content ="烟花盛宴" >
<meta name ="format-detection" content ="telephone=no" >
<link rel ="manifest" href ="data:application/json;base64,..." >
2. CSS 触摸优化 * {
-webkit-tap-highlight-color : transparent;
-webkit-touch-callout: none;
-webkit-user-select : none;
touch-action : manipulation;
}
body {
overscroll-behavior : none;
padding : env (safe-area-inset-*);
}
3. Web Audio 解锁 unlock ( ) {
if (this .isUnlocked ) return true ;
if (this .ctx .state === 'suspended' ) {
this .ctx .resume ();
}
const buffer = this .ctx .createBuffer (1 , 1 , 22050 );
const source = this .ctx .createBufferSource ();
source.buffer = buffer;
source.connect (this .ctx .destination );
source.start (0 );
this .isUnlocked = true ;
}
4. 手势防护 document .addEventListener ('gesturestart' , (e ) => e.preventDefault ());
document .addEventListener ('gesturechange' , (e ) => e.preventDefault ());
document .addEventListener ('gestureend' , (e ) => e.preventDefault ());
let lastTouchEnd = 0 ;
document .addEventListener ('touchend' , (e ) => {
const now = Date .now ();
if (now - lastTouchEnd <= 300 ) {
e.preventDefault ();
}
lastTouchEnd = now;
});
5. iOS 支持功能表 功能 状态 说明 添加到主屏幕 ✅ 支持全屏运行 声音播放 ✅ 用户交互后解锁 刘海屏适配 ✅ 安全区域 padding 禁用缩放 ✅ 手势防护 禁用橡皮筋效果 ✅ overscroll-behavior 触摸响应 ✅ Pointer Events
八、文件依赖
外部库 ├── fscreen@1 .0.1 .js - 全屏 API 兼容库
├── Stage@0 .1.4 .js - Canvas 舞台管理
└── MyMath.js - 数学工具函数
本地文件
总结
视觉效果 :双层 Canvas 实现拖尾效果,混合模式增强亮度
音效系统 :8 种不同风格的音效,Web Audio API 实时生成
物理模拟 :重力、空气阻力、粒子生命周期
状态管理 :发布 - 订阅模式,localStorage 持久化
性能优化 :对象池、颜色分组、requestAnimationFrame
跨平台 :完整的 iOS/移动端适配
相关免费在线工具 加密/解密文本 使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
Gemini 图片去水印 基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online