极致优秀烟花模拟器edit icon

作者:
邓朝元
Fork(复制)
下载
嵌入
BUG反馈
index.html
style.css
index.js
现在支持上传本地图片了!
index.html
            
            <!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>
        
编辑器加载中
预览
控制台