<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<title>超级烟花模拟器(优化版)</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box
}
body {
font-family: Arial, sans-serif;
overflow: hidden;
user-select: none;
background: linear-gradient(135deg, #0a0a0a, #1a1a2e, #0f0f23);
}
canvas {
display: block;
cursor: crosshair;
background: radial-gradient(ellipse at center, #000428 0%, #004e92 100%);
transform: translateZ(0);
/* 提示浏览器走合成层,减少抖动 */
}
/* ===== UI 容器 ===== */
.ui-overlay {
position: fixed;
top: 20px;
left: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
transition: transform .25s ease, opacity .25s ease;
transform: translateX(0);
opacity: 1;
pointer-events: auto;
}
.ui-panel {
background: rgba(0, 0, 0, 0.82);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 12px;
padding: 15px;
color: #fff;
min-width: 220px;
box-shadow: 0 10px 30px rgba(0, 0, 0, .25);
}
.control-group {
margin-bottom: 15px
}
.control-group label {
display: block;
margin-bottom: 6px;
font-size: 12px;
color: #ccc;
text-transform: uppercase;
letter-spacing: 1px
}
.control-row {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px
}
input[type="range"] {
flex: 1;
height: 4px;
border-radius: 2px;
background: #333;
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
cursor: pointer;
box-shadow: 0 0 10px rgba(255, 107, 107, .5)
}
.value-display {
min-width: 38px;
font-size: 11px;
color: #4ecdc4;
text-align: right
}
button {
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
border: none;
border-radius: 8px;
color: #fff;
padding: 8px 12px;
cursor: pointer;
font-size: 12px;
transition: all .2s ease;
text-transform: uppercase;
letter-spacing: 1px;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 10px 18px rgba(102, 126, 234, .35)
}
button.active {
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
box-shadow: 0 0 20px rgba(255, 107, 107, .55)
}
.firework-type-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-top: 8px
}
.firework-type-btn {
padding: 7px 8px;
font-size: 10px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 8px;
}
.firework-type-btn.active {
background: linear-gradient(45deg, #4ecdc4, #44a08d);
box-shadow: 0 0 18px rgba(78, 205, 196, .25)
}
.preset-buttons {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-top: 10px
}
.preset-btn {
flex: 1;
min-width: 72px;
font-size: 10px;
padding: 6px;
border-radius: 8px
}
.auto-mode-controls {
display: flex;
gap: 6px;
margin-top: 8px;
flex-wrap: wrap
}
.color-palette {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-top: 10px
}
.color-btn {
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: all .25s ease;
}
.color-btn.active {
border-color: #fff;
box-shadow: 0 0 15px rgba(255, 255, 255, .65)
}
.performance-indicator {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
background: rgba(0, 0, 0, 0.82);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 10px;
padding: 10px 12px;
color: #fff;
font-size: 11px;
min-width: 150px;
}
.fps-counter {
color: #4ecdc4;
font-weight: bold
}
.particle-count {
color: #ff6b6b
}
.mem {
color: #ffeaa7
}
.text-display {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 999;
pointer-events: none;
font-size: 60px;
font-weight: 800;
text-align: center;
text-shadow: 0 0 20px rgba(255, 255, 255, .8);
opacity: 0;
}
@keyframes textPulse {
0% {
opacity: 0;
transform: translate(-50%, -50%) scale(.6)
}
45% {
opacity: 1;
transform: translate(-50%, -50%) scale(1.15)
}
100% {
opacity: 0;
transform: translate(-50%, -50%) scale(1)
}
}
.text-display.show {
animation: textPulse 2.8s ease-in-out
}
/* ===== 设置面板弹出/关闭 ===== */
.ui-backdrop {
position: fixed;
inset: 0;
z-index: 900;
background: rgba(0, 0, 0, .46);
backdrop-filter: blur(2px);
opacity: 0;
pointer-events: none;
transition: opacity .25s ease;
}
.ui-toggle {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 1200;
border-radius: 999px;
padding: 10px 14px;
font-size: 12px;
background: linear-gradient(45deg, #4ecdc4, #44a08d);
box-shadow: 0 12px 30px rgba(0, 0, 0, .35);
}
.ui-toggle small {
opacity: .85;
font-size: 10px;
margin-left: 6px
}
body.ui-closed .ui-overlay {
transform: translateX(-16px);
opacity: 0;
pointer-events: none;
}
body.ui-open .ui-backdrop {
opacity: 1;
pointer-events: auto;
}
/* 小分割线 */
.hr {
height: 1px;
background: rgba(255, 255, 255, 0.12);
margin: 10px 0;
}
.hint {
font-size: 10px;
color: #bfbfbf;
line-height: 1.35
}
/* 下拉背景 */
select {
width: 100%;
padding: 8px 10px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.08);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.18);
outline: none;
font-size: 12px;
}
option {
color: #000
}
</style>
</head>
<body class="ui-open">
<canvas id="fireworksCanvas"></canvas>
<!-- 设置面板 -->
<div class="ui-overlay" id="uiOverlay">
<div class="ui-panel">
<h3 style="margin-bottom:12px;color:#4ecdc4;font-size:14px;">🎆 烟花控制台</h3>
<div class="control-group">
<label>烟花类型</label>
<div class="firework-type-grid">
<button class="firework-type-btn active" data-type="burst">爆裂</button>
<button class="firework-type-btn" data-type="ring">环形</button>
<button class="firework-type-btn" data-type="heart">心形</button>
<button class="firework-type-btn" data-type="star">星形</button>
<button class="firework-type-btn" data-type="spiral">螺旋</button>
<button class="firework-type-btn" data-type="fountain">喷泉</button>
</div>
</div>
<div class="control-group">
<label>强度设置</label>
<div class="control-row">
<span style="font-size:10px;">粒子数</span>
<input type="range" id="particleCount" min="20" max="240" value="80" />
<span class="value-display" id="particleCountValue">80</span>
</div>
<div class="control-row">
<span style="font-size:10px;">爆炸力</span>
<input type="range" id="explosionForce" min="1" max="12" value="5" step="0.1" />
<span class="value-display" id="explosionForceValue">5.0</span>
</div>
<div class="control-row">
<span style="font-size:10px;">重力</span>
<input type="range" id="gravity" min="0" max="1" value="0.3" step="0.01" />
<span class="value-display" id="gravityValue">0.30</span>
</div>
</div>
<div class="control-group">
<label>视觉效果</label>
<div class="control-row">
<span style="font-size:10px;">拖尾长度</span>
<input type="range" id="trailLength" min="0.1" max="1" value="0.95" step="0.01" />
<span class="value-display" id="trailLengthValue">0.95</span>
</div>
<div class="control-row">
<span style="font-size:10px;">发光强度</span>
<input type="range" id="glowIntensity" min="0" max="50" value="20" />
<span class="value-display" id="glowIntensityValue">20</span>
</div>
</div>
<div class="control-group">
<label>颜色主题</label>
<div class="color-palette">
<div class="color-btn active" style="background:linear-gradient(45deg,#ff6b6b,#feca57)" data-colors="#ff6b6b,#feca57,#48dbfb,#ff9ff3"></div>
<div class="color-btn" style="background:linear-gradient(45deg,#00d2ff,#3a7bd5)" data-colors="#00d2ff,#3a7bd5,#74b9ff,#0984e3"></div>
<div class="color-btn" style="background:linear-gradient(45deg,#a8edea,#fed6e3)" data-colors="#a8edea,#fed6e3,#ffeaa7,#fab1a0"></div>
<div class="color-btn" style="background:linear-gradient(45deg,#ff9a9e,#fecfef)" data-colors="#ff9a9e,#fecfef,#ffecd2,#fcb69f"></div>
<div class="color-btn" style="background:linear-gradient(45deg,#667eea,#764ba2)" data-colors="#667eea,#764ba2,#f093fb,#f5576c"></div>
<div class="color-btn" style="background:linear-gradient(45deg,#4ecdc4,#44a08d)" data-colors="#4ecdc4,#44a08d,#96fbc4,#f9ca24"></div>
<div class="color-btn" style="background:linear-gradient(45deg,#ffeaa7,#fab1a0)" data-colors="#ffeaa7,#fab1a0,#ff7675,#fd79a8"></div>
<div class="color-btn" style="background:linear-gradient(45deg,#a29bfe,#6c5ce7)" data-colors="#a29bfe,#6c5ce7,#fd79a8,#fdcb6e"></div>
</div>
</div>
<div class="hr"></div>
<div class="control-group">
<label>背景(可切换)</label>
<select id="bgSelect">
<option value="default" selected>默认背景</option>
<option value="deepSpace">深空紫</option>
<option value="aurora">极光绿</option>
<option value="sunset">日落橙</option>
<option value="midnight">午夜蓝</option>
<option value="pureBlack">纯黑舞台</option>
</select>
<div class="hint" style="margin-top:8px;">
默认背景=原先背景;其它背景只改变画布底色,不影响烟花。
</div>
</div>
</div>
<div class="ui-panel">
<h3 style="margin-bottom:12px;color:#4ecdc4;font-size:14px;">🎮 播放模式</h3>
<div class="control-group">
<button id="autoMode" style="width:100%;margin-bottom:10px;">自动模式</button>
<div class="auto-mode-controls">
<button class="preset-btn" data-text="新年快乐">新年快乐</button>
<button class="preset-btn" data-text="生日快乐">生日快乐</button>
<button class="preset-btn" data-text="恭喜发财">恭喜发财</button>
<button class="preset-btn" data-text="I ❤️ U">I ❤️ U</button>
<button class="preset-btn" data-text="HAPPY">HAPPY</button>
<button class="preset-btn" data-text="🎉">🎉</button>
</div>
</div>
<div class="preset-buttons">
<button class="preset-btn" data-preset="romantic">浪漫</button>
<button class="preset-btn" data-preset="celebration">庆典</button>
<button class="preset-btn" data-preset="gentle">温柔</button>
<button class="preset-btn" data-preset="intense">激烈</button>
</div>
<div class="hr"></div>
<div class="control-group">
<label>可用功能(已接通)</label>
<button id="musicModeBtn" style="width:100%;margin-bottom:8px;background:linear-gradient(45deg,#4ecdc4,#667eea);">
音乐模式(模拟节拍) M
</button>
<button id="shakeBtn" style="width:100%;margin-bottom:8px;background:linear-gradient(45deg,#fdcb6e,#e17055);">
震动特效(演示)
</button>
<button id="screenshotBtn" style="width:100%;margin-bottom:8px;background:linear-gradient(45deg,#55efc4,#00b894);">
截图 PNG
</button>
<button id="exportBtn" style="width:100%;margin-bottom:8px;background:linear-gradient(45deg,#a29bfe,#6c5ce7);">
导出设置 JSON
</button>
<label for="importFile" class="hint" style="margin-top:6px;">导入设置(选择 JSON 文件)</label>
<input id="importFile" type="file" accept="application/json" style="width:100%;margin-top:6px;color:#fff;font-size:12px;" />
</div>
<button id="clearBtn" style="width:100%;margin-top:6px;background:linear-gradient(45deg,#ff7675,#d63031);">
清空画面 C
</button>
<div class="hint" style="margin-top:10px;">
快捷键:空格(自动)|C(清屏)|M(音乐)|Esc(关闭设置)|1-6(类型)
</div>
</div>
</div>
<!-- 遮罩 + 开关按钮 -->
<div class="ui-backdrop" id="uiBackdrop"></div>
<button class="ui-toggle" id="uiToggleBtn">⚙ 设置 <small>(Esc)</small></button>
<!-- 性能面板 -->
<div class="performance-indicator">
<div class="fps-counter">FPS: <span id="fpsCounter">60</span></div>
<div class="particle-count">粒子: <span id="particleCounter">0</span></div>
<div class="mem">内存: <span id="memoryUsage">—</span></div>
</div>
<div class="text-display" id="textDisplay"></div>
<script>
/* ==========================
背景管理(默认=原先背景)
========================== */
const BACKGROUNDS = {
default: 'radial-gradient(ellipse at center, #000428 0%, #004e92 100%)',
deepSpace: 'radial-gradient(ellipse at center, #11002b 0%, #020024 55%, #000000 100%)',
aurora: 'radial-gradient(ellipse at top, #003b36 0%, #001a33 55%, #00040f 100%)',
sunset: 'radial-gradient(ellipse at center, #2b1055 0%, #ff512f 55%, #000428 100%)',
midnight: 'radial-gradient(ellipse at center, #00111f 0%, #000428 60%, #000000 100%)',
pureBlack: 'radial-gradient(ellipse at center, #000000 0%, #000000 100%)'
};
function applyBackground(key) {
const canvas = document.getElementById('fireworksCanvas');
const bg = BACKGROUNDS[key] || BACKGROUNDS.default;
canvas.style.background = bg;
localStorage.setItem('fireworks_bg', key);
}
/* ==========================
主模拟器(性能优化版)
========================== */
class FireworkSimulator {
constructor() {
this.canvas = document.getElementById('fireworksCanvas');
this.ctx = this.canvas.getContext('2d', {
alpha: false
});
this.particles = [];
this.settings = {
particleCount: 80,
explosionForce: 5,
gravity: 0.3,
trailLength: 0.95,
glowIntensity: 20,
currentType: 'burst',
colors: ['#ff6b6b', '#feca57', '#48dbfb', '#ff9ff3']
};
this.performance = {
fps: 60,
frameSamples: [],
particlePool: [],
maxParticles: 5500
};
this.autoMode = false;
this.autoInterval = null;
this._lastT = 0;
this.init();
this.setupEventListeners();
this.startLoops();
}
init() {
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
// 预创建粒子池(减少 GC)
for (let i = 0; i < this.performance.maxParticles; i++) {
this.performance.particlePool.push(this.createParticle(0, 0, 0, 0, '#ffffff'));
}
}
resizeCanvas() {
const dpr = Math.min(2, window.devicePixelRatio || 1);
this.canvas.width = Math.floor(window.innerWidth * dpr);
this.canvas.height = Math.floor(window.innerHeight * dpr);
this.canvas.style.width = window.innerWidth + 'px';
this.canvas.style.height = window.innerHeight + 'px';
this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
setupEventListeners() {
// 点击放烟花(非自动)
this.canvas.addEventListener('click', (e) => {
if (!this.autoMode) {
const rect = this.canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
this.createFirework(x, y);
}
});
// 触摸支持(短触摸发射)
let touchStartTime = 0;
this.canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
touchStartTime = Date.now();
}, {
passive: false
});
this.canvas.addEventListener('touchend', (e) => {
e.preventDefault();
const duration = Date.now() - touchStartTime;
if (duration < 500) {
const t = e.changedTouches[0];
const rect = e.target.getBoundingClientRect();
const x = t.clientX - rect.left;
const y = t.clientY - rect.top;
this.createFirework(x, y);
}
}, {
passive: false
});
this.setupControlListeners();
}
setupControlListeners() {
// 滑块(使用 rAF 合并 UI 更新,避免拖动时抖)
['particleCount', 'explosionForce', 'gravity', 'trailLength', 'glowIntensity'].forEach(id => {
const slider = document.getElementById(id);
const valueDisplay = document.getElementById(id + 'Value');
let rafLock = false;
slider.addEventListener('input', (e) => {
const value = parseFloat(e.target.value);
this.settings[id] = value;
if (!rafLock) {
rafLock = true;
requestAnimationFrame(() => {
valueDisplay.textContent =
(id === 'explosionForce' || id === 'gravity' || id === 'trailLength') ?
value.toFixed(2) :
Math.round(value);
rafLock = false;
});
}
});
});
// 类型按钮
document.querySelectorAll('.firework-type-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.firework-type-btn').forEach(b => b.classList.remove('active'));
e.currentTarget.classList.add('active');
this.settings.currentType = e.currentTarget.dataset.type;
});
});
// 颜色按钮
document.querySelectorAll('.color-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
document.querySelectorAll('.color-btn').forEach(b => b.classList.remove('active'));
e.currentTarget.classList.add('active');
this.settings.colors = e.currentTarget.dataset.colors.split(',');
});
});
// 自动模式按钮
document.getElementById('autoMode').addEventListener('click', () => this.toggleAutoMode());
}
createParticle(x, y, vx, vy, color) {
return {
x,
y,
vx,
vy,
color,
life: 1,
decay: 0.015,
size: 2,
alpha: 1,
trail: [],
isRocket: false,
_trailAcc: 0,
_lodTrailMax: 20
};
}
getParticleFromPool() {
return this.performance.particlePool.pop() || this.createParticle(0, 0, 0, 0, '#ffffff');
}
returnParticleToPool(p) {
// 重置到可复用状态
p.trail.length = 0;
p.life = 1;
p.alpha = 1;
p.isRocket = false;
p._trailAcc = 0;
p._lodTrailMax = 20;
this.performance.particlePool.push(p);
}
createFirework(x, y) {
const colors = this.settings.colors;
// 发射火箭
const rocket = this.getParticleFromPool();
Object.assign(rocket, {
x: x,
y: window.innerHeight,
vx: (Math.random() - 0.5) * 1.8,
vy: -15,
color: colors[(Math.random() * colors.length) | 0],
isRocket: true,
targetY: y,
life: 1,
decay: 0.004,
size: 3,
alpha: 1,
trail: [],
_trailAcc: 0
});
this.particles.push(rocket);
}
explodeFirework(x, y) {
const colors = this.settings.colors;
const count = Math.min(260, Math.max(20, this.settings.particleCount | 0));
const force = this.settings.explosionForce;
const type = this.settings.currentType;
for (let i = 0; i < count; i++) {
const p = this.getParticleFromPool();
const color = colors[(Math.random() * colors.length) | 0];
let vx = 0,
vy = 0;
switch (type) {
case 'burst': {
const angle = (Math.PI * 2 * i) / count;
const speed = (Math.random() * force + 2);
vx = Math.cos(angle) * speed;
vy = Math.sin(angle) * speed;
break;
}
case 'ring': {
const a = (Math.PI * 2 * i) / count;
const s = force * 0.85;
vx = Math.cos(a) * s;
vy = Math.sin(a) * s;
break;
}
case 'heart': {
const t = (i / count) * Math.PI * 2;
const hx = 16 * Math.pow(Math.sin(t), 3);
const hy = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t));
vx = hx * force * 0.11;
vy = hy * force * 0.11;
break;
}
case 'star': {
const a = (Math.PI * 2 * i) / count;
const s = (i % 2 === 0) ? force * 1.25 : force * 0.6;
vx = Math.cos(a) * s;
vy = Math.sin(a) * s;
break;
}
case 'spiral': {
const a = (Math.PI * 4 * i) / count;
const r = (i / count) * force * 1.2;
vx = Math.cos(a) * r;
vy = Math.sin(a) * r;
break;
}
case 'fountain': {
vx = (Math.random() - 0.5) * force * 0.55;
vy = -Math.random() * force * 1.6;
break;
}
default: {
vx = (Math.random() - 0.5) * force * 2;
vy = (Math.random() - 0.5) * force * 2;
}
}
Object.assign(p, {
x,
y,
vx,
vy,
color,
life: 1,
decay: Math.random() * 0.018 + 0.010,
size: Math.random() * 2.6 + 1.0,
alpha: 1,
trail: [],
isRocket: false,
_trailAcc: 0,
_lodTrailMax: 20
});
this.particles.push(p);
}
}
// ====== 性能优化:dt 更新 + 拖尾采样节流(关键)======
updateParticles(dt) {
const step = dt / 16.67; // 以 60fps 为基准缩放
const g = this.settings.gravity * 0.1 * step;
const width = window.innerWidth;
const height = window.innerHeight;
for (let i = this.particles.length - 1; i >= 0; i--) {
const p = this.particles[i];
if (p.isRocket) {
p.x += p.vx * step;
p.y += p.vy * step;
p.vy += 0.2 * step;
// 拖尾按速度累计采样(减少 push 频率)
p._trailAcc += Math.abs(p.vx) + Math.abs(p.vy);
if (p._trailAcc > 6) {
p._trailAcc = 0;
p.trail.push({
x: p.x,
y: p.y,
alpha: p.alpha
});
if (p.trail.length > 10) p.trail.shift();
}
if (p.y <= p.targetY || p.vy >= 0) {
this.explodeFirework(p.x, p.y);
this.returnParticleToPool(p);
this.particles.splice(i, 1);
}
} else {
p.x += p.vx * step;
p.y += p.vy * step;
p.vy += g;
p.vx *= Math.pow(0.99, step);
p.life -= p.decay * step;
p.alpha = p.life;
p._trailAcc += Math.abs(p.vx) + Math.abs(p.vy);
if (p._trailAcc > 4) {
p._trailAcc = 0;
p.trail.push({
x: p.x,
y: p.y,
alpha: p.alpha
});
if (p.trail.length > p._lodTrailMax) p.trail.shift();
}
// 死亡或超出屏幕
if (p.life <= 0 || p.y > height + 80 || p.x < -80 || p.x > width + 80) {
this.returnParticleToPool(p);
this.particles.splice(i, 1);
}
}
}
}
// ====== 性能优化:每个粒子拖尾只 stroke 1 次(关键)======
renderParticles() {
const ctx = this.ctx;
ctx.save();
ctx.globalCompositeOperation = 'lighter';
const trailMul = this.settings.trailLength;
const glow = this.settings.glowIntensity;
for (const p of this.particles) {
const t = p.trail;
// 拖尾:一条 path 一次 stroke(避免每点 stroke)
if (t && t.length > 1) {
ctx.beginPath();
ctx.moveTo(t[0].x, t[0].y);
for (let i = 1; i < t.length; i++) ctx.lineTo(t[i].x, t[i].y);
const tailAlpha = Math.max(0, Math.min(1, (t[t.length - 1].alpha ?? p.alpha) * 0.7 * trailMul));
ctx.globalAlpha = tailAlpha;
ctx.strokeStyle = p.color;
ctx.lineWidth = Math.max(0.5, p.size * 0.5);
ctx.stroke();
}
// 粒子本体
ctx.globalAlpha = Math.max(0, Math.min(1, p.alpha));
if (glow > 0) {
ctx.shadowColor = p.color;
ctx.shadowBlur = glow;
} else {
ctx.shadowBlur = 0;
}
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
clearCanvasFade() {
// 通过透明叠加实现“拖尾残影”背景清理
const ctx = this.ctx;
const alpha = 1 - this.settings.trailLength;
// 固定底色(与默认背景协调),不会覆盖 canvas style 背景渐变
ctx.fillStyle = `rgba(0, 4, 40, ${Math.max(0.02, Math.min(0.35, alpha))})`;
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
}
toggleAutoMode() {
this.autoMode = !this.autoMode;
const btn = document.getElementById('autoMode');
if (this.autoMode) {
btn.classList.add('active');
btn.textContent = '停止自动';
this.startAutoMode();
} else {
btn.classList.remove('active');
btn.textContent = '自动模式';
this.stopAutoMode();
}
}
startAutoMode() {
// 先清掉旧的
this.stopAutoMode();
const tick = () => {
if (!this.autoMode) return;
const x = Math.random() * window.innerWidth;
const y = Math.random() * window.innerHeight * 0.6 + window.innerHeight * 0.1;
this.createFirework(x, y);
const next = 700 + Math.random() * 1400;
this.autoInterval = setTimeout(tick, next);
};
tick();
}
stopAutoMode() {
if (this.autoInterval) {
clearTimeout(this.autoInterval);
this.autoInterval = null;
}
}
// FPS:用滑动窗口平均,更稳定
sampleFPS(dt) {
const fps = 1000 / Math.max(1, dt);
const arr = this.performance.frameSamples;
arr.push(fps);
if (arr.length > 20) arr.shift();
const avg = arr.reduce((a, b) => a + b, 0) / arr.length;
this.performance.fps = Math.round(avg);
}
startLoops() {
const perfUI = () => {
document.getElementById('fpsCounter').textContent = this.performance.fps;
document.getElementById('particleCounter').textContent = this.particles.length;
// 内存:优先使用 performance.memory,否则用估算
const memEl = document.getElementById('memoryUsage');
const m = performance.memory;
if (m && m.usedJSHeapSize) {
memEl.textContent = (m.usedJSHeapSize / 1024 / 1024).toFixed(1) + 'MB';
} else {
const est = Math.min(999, (this.particles.length * 0.004 + 8)).toFixed(1);
memEl.textContent = est + 'MB*';
}
};
setInterval(perfUI, 500);
const animate = (t) => {
if (!this._lastT) this._lastT = t;
const dt = Math.min(40, t - this._lastT); // 防止切后台回来 dt 超大
this._lastT = t;
this.sampleFPS(dt);
// 清屏(残影)
this.clearCanvasFade();
// 背景星空
if (window.advancedEffects) window.advancedEffects.updateStarField();
// 更新+绘制
this.updateParticles(dt);
this.renderParticles();
// 优化器(剔除+LOD+动态质量)
if (window.optimizer) {
window.optimizer.cullParticles();
window.optimizer.applyLOD();
window.optimizer.adjustQuality();
}
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}
}
/* ==========================
高级特效:星空 + 震动 + 音乐模式
========================== */
class AdvancedEffects {
constructor(simulator) {
this.simulator = simulator;
this.backgroundStars = [];
this.musicMode = false;
this._musicTimer = null;
this.shakeIntensity = 0;
}
createStarField() {
const count = 110;
this.backgroundStars.length = 0;
for (let i = 0; i < count; i++) {
this.backgroundStars.push({
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
size: Math.random() * 1.8 + 0.2,
twinkle: Math.random() * 0.02 + 0.004,
alpha: Math.random()
});
}
}
updateStarField() {
const ctx = this.simulator.ctx;
ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = '#ffffff';
for (const s of this.backgroundStars) {
s.alpha += s.twinkle;
if (s.alpha > 1 || s.alpha < 0) s.twinkle = -s.twinkle;
ctx.globalAlpha = Math.abs(s.alpha) * 0.25;
ctx.beginPath();
ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
screenShake(intensity = 6) {
this.shakeIntensity = Math.max(this.shakeIntensity, intensity);
const canvas = this.simulator.canvas;
const run = () => {
if (this.shakeIntensity > 0.2) {
const sx = (Math.random() - 0.5) * this.shakeIntensity;
const sy = (Math.random() - 0.5) * this.shakeIntensity;
canvas.style.transform = `translate(${sx}px, ${sy}px) translateZ(0)`;
this.shakeIntensity *= 0.88;
requestAnimationFrame(run);
} else {
canvas.style.transform = 'translate(0,0) translateZ(0)';
this.shakeIntensity = 0;
}
};
run();
}
toggleMusicMode() {
this.musicMode = !this.musicMode;
if (this.musicMode) this.startMusicVisualization();
else this.stopMusicVisualization();
}
startMusicVisualization() {
this.stopMusicVisualization();
const tick = () => {
if (!this.musicMode) return;
const intensity = Math.random() * 0.8 + 0.2;
const count = Math.floor(intensity * 3) + 1;
for (let i = 0; i < count; i++) {
const x = window.innerWidth * (0.18 + Math.random() * 0.64);
const y = window.innerHeight * (0.1 + Math.random() * 0.55);
this.simulator.createFirework(x, y);
}
if (intensity > 0.72) this.screenShake(8);
const next = 420 + Math.random() * 420;
this._musicTimer = setTimeout(tick, next);
};
tick();
}
stopMusicVisualization() {
if (this._musicTimer) {
clearTimeout(this._musicTimer);
this._musicTimer = null;
}
}
}
/* ==========================
粒子系统优化:剔除 + LOD + 动态质量
========================== */
class ParticleSystemOptimizer {
constructor(simulator) {
this.simulator = simulator;
this.cullingEnabled = true;
this.lodEnabled = true;
}
cullParticles() {
if (!this.cullingEnabled) return;
const w = window.innerWidth,
h = window.innerHeight;
const margin = 120;
// 这里用 filter 会分配新数组;但频率不高且可读性好。
// 如果你要极限性能,可以改成原地倒序 splice。
this.simulator.particles = this.simulator.particles.filter(p =>
p.x > -margin && p.x < w + margin && p.y > -margin && p.y < h + margin
);
}
applyLOD() {
if (!this.lodEnabled) return;
const w = window.innerWidth,
h = window.innerHeight;
const cx = w / 2,
cy = h / 2;
const maxD = Math.sqrt(w * w + h * h) / 2;
for (const p of this.simulator.particles) {
const dx = p.x - cx,
dy = p.y - cy;
const d = Math.sqrt(dx * dx + dy * dy);
const lod = d / maxD;
// 不直接改 size(会不断缩小),只调整“最大拖尾长度”
if (lod > 0.82) p._lodTrailMax = 6;
else if (lod > 0.65) p._lodTrailMax = 10;
else p._lodTrailMax = 20;
}
}
adjustQuality() {
const fps = this.simulator.performance.fps;
const s = this.simulator.settings;
// fps 低:收敛拖尾/发光
if (fps < 32) {
s.trailLength = Math.max(0.80, s.trailLength - 0.03);
s.glowIntensity = Math.max(6, s.glowIntensity - 2);
}
// fps 高:稍微拉回效果
else if (fps > 56) {
s.trailLength = Math.min(0.98, s.trailLength + 0.01);
s.glowIntensity = Math.min(30, s.glowIntensity + 1);
}
// 同步 slider UI(不强制每次都刷,只在明显变化时刷)
// 这里不做太重的 DOM 操作,避免反作用。
}
}
/* ==========================
全局功能:预设/文字/清屏/导入导出/截图
========================== */
function updateSliderValues() {
const sim = window.fireworkSimulator;
if (!sim) return;
const s = sim.settings;
['particleCount', 'explosionForce', 'gravity', 'trailLength', 'glowIntensity'].forEach(key => {
const slider = document.getElementById(key);
const valueDisplay = document.getElementById(key + 'Value');
if (!slider) return;
slider.value = s[key];
valueDisplay.textContent =
(key === 'explosionForce' || key === 'gravity' || key === 'trailLength') ?
Number(s[key]).toFixed(2) :
Math.round(s[key]);
});
// 颜色按钮不强制高亮(你也可以做:匹配 data-colors)
}
function setPreset(type) {
const sim = window.fireworkSimulator;
if (!sim) return;
switch (type) {
case 'romantic':
sim.settings.colors = ['#ff9a9e', '#fecfef', '#ffecd2', '#fcb69f'];
sim.settings.particleCount = 60;
sim.settings.explosionForce = 3;
sim.settings.gravity = 0.2;
document.querySelector('[data-type="heart"]').click();
break;
case 'celebration':
sim.settings.colors = ['#ff6b6b', '#feca57', '#48dbfb', '#ff9ff3'];
sim.settings.particleCount = 120;
sim.settings.explosionForce = 7;
sim.settings.gravity = 0.4;
document.querySelector('[data-type="burst"]').click();
break;
case 'gentle':
sim.settings.colors = ['#a8edea', '#fed6e3', '#ffeaa7', '#fab1a0'];
sim.settings.particleCount = 40;
sim.settings.explosionForce = 2;
sim.settings.gravity = 0.12;
document.querySelector('[data-type="ring"]').click();
break;
case 'intense':
sim.settings.colors = ['#667eea', '#764ba2', '#f093fb', '#f5576c'];
sim.settings.particleCount = 200;
sim.settings.explosionForce = 10;
sim.settings.gravity = 0.6;
document.querySelector('[data-type="star"]').click();
break;
}
updateSliderValues();
}
function showText(text) {
const textDisplay = document.getElementById('textDisplay');
textDisplay.textContent = text;
textDisplay.classList.remove('show');
// 触发重排以重置动画
void textDisplay.offsetHeight;
textDisplay.classList.add('show');
// 同时连发几组烟花
const sim = window.fireworkSimulator;
if (!sim) return;
setTimeout(() => {
for (let i = 0; i < 5; i++) {
setTimeout(() => {
const x = window.innerWidth * 0.2 + Math.random() * window.innerWidth * 0.6;
const y = window.innerHeight * 0.18 + Math.random() * window.innerHeight * 0.45;
sim.createFirework(x, y);
}, i * 160);
}
}, 420);
}
function clearCanvasHard() {
const sim = window.fireworkSimulator;
if (!sim) return;
sim.particles.length = 0;
const ctx = sim.ctx;
ctx.save();
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = 'rgba(0,0,0,1)';
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
ctx.restore();
}
function exportSettings() {
const sim = window.fireworkSimulator;
if (!sim) return;
const data = {
...sim.settings,
bg: localStorage.getItem('fireworks_bg') || 'default'
};
const dataStr = JSON.stringify(data, null, 2);
const blob = new Blob([dataStr], {
type: 'application/json'
});
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'fireworks-settings.json';
link.click();
URL.revokeObjectURL(link.href);
}
function importSettings(file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const obj = JSON.parse(e.target.result);
const sim = window.fireworkSimulator;
if (!sim) return;
// 只合并我们允许的字段
const allowed = ['particleCount', 'explosionForce', 'gravity', 'trailLength', 'glowIntensity', 'currentType', 'colors'];
for (const k of allowed) {
if (obj[k] !== undefined) sim.settings[k] = obj[k];
}
// 类型按钮高亮
if (obj.currentType) {
const btn = document.querySelector(`[data-type="${obj.currentType}"]`);
if (btn) btn.click();
}
// 背景
if (obj.bg) {
document.getElementById('bgSelect').value = obj.bg;
applyBackground(obj.bg);
}
updateSliderValues();
console.log('✅ 设置已导入成功');
} catch (err) {
console.error('导入设置失败:', err);
alert('导入失败:文件不是有效的 JSON 设置。');
}
};
reader.readAsText(file);
}
function captureScreenshot() {
const sim = window.fireworkSimulator;
if (!sim) return;
const canvas = sim.canvas;
const link = document.createElement('a');
link.download = `fireworks-${Date.now()}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
}
/* ==========================
UI:设置面板开关(自由弹出/关闭)
========================== */
function setUIOpen(open) {
document.body.classList.toggle('ui-open', open);
document.body.classList.toggle('ui-closed', !open);
localStorage.setItem('fireworks_ui_open', open ? '1' : '0');
}
/* ==========================
初始化
========================== */
document.addEventListener('DOMContentLoaded', () => {
// 初始化模拟器
window.fireworkSimulator = new FireworkSimulator();
window.advancedEffects = new AdvancedEffects(window.fireworkSimulator);
window.optimizer = new ParticleSystemOptimizer(window.fireworkSimulator);
// 星空背景
window.advancedEffects.createStarField();
// 背景(默认=原先背景)
const savedBg = localStorage.getItem('fireworks_bg') || 'default';
document.getElementById('bgSelect').value = savedBg;
applyBackground(savedBg);
// 设置面板:记住状态(默认打开)
const savedUI = localStorage.getItem('fireworks_ui_open');
setUIOpen(savedUI === null ? true : savedUI === '1');
// UI 事件:开关/遮罩
const toggleBtn = document.getElementById('uiToggleBtn');
const backdrop = document.getElementById('uiBackdrop');
toggleBtn.addEventListener('click', () => {
const open = document.body.classList.contains('ui-open');
setUIOpen(!open);
});
backdrop.addEventListener('click', () => setUIOpen(false));
// 背景切换
document.getElementById('bgSelect').addEventListener('change', (e) => {
applyBackground(e.target.value);
});
// 预设按钮(文字/预设)
document.querySelectorAll('button.preset-btn').forEach(btn => {
btn.addEventListener('click', () => {
const text = btn.dataset.text;
const preset = btn.dataset.preset;
if (text) showText(text);
if (preset) setPreset(preset);
});
});
// 功能按钮(已接通)
const musicBtn = document.getElementById('musicModeBtn');
musicBtn.addEventListener('click', () => {
window.advancedEffects.toggleMusicMode();
const on = window.advancedEffects.musicMode;
musicBtn.classList.toggle('active', on);
musicBtn.textContent = on ? '音乐模式:开启(模拟节拍) M' : '音乐模式(模拟节拍) M';
});
document.getElementById('shakeBtn').addEventListener('click', () => {
window.advancedEffects.screenShake(10);
});
document.getElementById('screenshotBtn').addEventListener('click', captureScreenshot);
document.getElementById('exportBtn').addEventListener('click', exportSettings);
document.getElementById('importFile').addEventListener('change', (e) => {
const file = e.target.files && e.target.files[0];
if (file) importSettings(file);
e.target.value = ''; // 允许再次导入同名文件
});
document.getElementById('clearBtn').addEventListener('click', clearCanvasHard);
// 初始滑块值同步
updateSliderValues();
// 键盘快捷键
document.addEventListener('keydown', (e) => {
switch (e.key) {
case ' ':
e.preventDefault();
document.getElementById('autoMode').click();
break;
case 'Escape':
setUIOpen(false);
break;
case 'c':
case 'C':
clearCanvasHard();
break;
case 'm':
case 'M':
document.getElementById('musicModeBtn').click();
break;
case '1':
document.querySelector('[data-type="burst"]').click();
break;
case '2':
document.querySelector('[data-type="ring"]').click();
break;
case '3':
document.querySelector('[data-type="heart"]').click();
break;
case '4':
document.querySelector('[data-type="star"]').click();
break;
case '5':
document.querySelector('[data-type="spiral"]').click();
break;
case '6':
document.querySelector('[data-type="fountain"]').click();
break;
}
});
console.log('🎆 Super Fireworks Simulator (Optimized) Loaded!');
console.log('快捷键: 空格(自动) C(清屏) M(音乐) Esc(关闭设置) 1-6(类型)');
});
</script>
</body>
</html>
index.html
style.css
index.js
index.html