Fire works Simulator – Interactive Fireworks & Particle Engine 🎆

A stunning, customizable Fireworks Simulator created inspired by fire works. Features live GUI controls (Hue ranges, Rockets Point, Acceleration, Friction, Gravity, Particles, Explosion count, Trace length/speed, Flickering, Sound FX, Line Styles), instant Presets, click & stream barrage, and live code generator.

Difficulty: Advanced Est. Time: 45 mins Vanilla JS (ES6+) Includes HTML, CSS, JS & README (.ZIP)
Live UI Runner

Fire works Simulator – Interactive Fireworks & Particle Engine 🎆

live-sandbox://3d-fireworks-simulator

1. Project Overview & Features

The Fire works Simulator – Interactive Fireworks & Particle Engine 🎆 is an essential frontend project designed to help you bridge the gap between learning JavaScript syntax and building interactive, responsive web applications.

Key Features Implemented:

  • Interactive full-screen fireworks simulation inspired by fire works with customizable physics
  • Floating Tweakpane-style GUI control panel with collapsible folders for Rockets, Physics, Particles, Colors & Sound
  • Configurable launch position range (rocketsPoint min/max %) and variable ballistic ascent velocity
  • Fine-tuned particle physics: gravity, friction, acceleration, flickering twinkle, and trail length/speed
  • Interactive Hue & Color sliders with real-time rainbow spectrum preview and brightness/decay controls
  • 8+ Instant Presets: Default Classic, Heavy Gold Rain, Fast Barrage, Neon Cyberpunk, Birthday Cake 🎂, Heart ❤️, Saturn 🪐, and Grand Salvo
  • Click to launch anywhere or click & drag to stream high-intensity continuous barrages
  • Synthesized Web Audio API sound engine (whistles, deep sub-bass explosion booms, crackles, stereo panning)
  • Live code & JSON config generator with 1-click copy for seamless integration into any web project

2. Step-by-Step Code Walkthrough

A Step 1: Semantic HTML Structure

We structure the interface using clean, semantic HTML elements. This ensures maximum accessibility and provides intuitive hooks for CSS classes and JavaScript query selectors.

HTML (index.html)
<div class="fw-app" id="fireworksApp">
  <!-- Interactive Canvas Viewport -->
  <canvas id="fireworksCanvas"></canvas>

  <!-- Top Glass Navigation Bar -->
  <header class="fw-nav">
    <div class="fw-brand">
      <span class="fw-brand-icon">🎆</span>
      <div class="fw-brand-text">
        <span class="fw-brand-title">fireworks</span>
        <span class="fw-brand-badge">Simulator</span>
      </div>
    </div>

    <div class="fw-nav-actions">
      <button type="button" class="fw-btn fw-btn-primary" id="btnPlayPause" title="Play / Pause Simulation">
        <i class="fas fa-pause" id="iconPlayPause"></i> <span id="textPlayPause">Pause</span>
      </button>
      <button type="button" class="fw-btn" id="btnClear" title="Clear Screen">
        <i class="fas fa-trash-can"></i> <span class="d-none d-md-inline">Clear</span>
      </button>
      <button type="button" class="fw-btn" id="btnRandomize" title="Randomize Parameters">
        <i class="fas fa-dice"></i> <span class="d-none d-md-inline">Random</span>
      </button>
      <button type="button" class="fw-btn" id="btnSound" title="Toggle Sound FX (Web Audio API)">
        <i class="fas fa-volume-up text-cyan-400" id="iconSound"></i> <span class="d-none d-md-inline" id="textSound">Sound ON</span>
      </button>
      <button type="button" class="fw-btn fw-btn-accent" id="btnExportCode" title="Export Config & Code">
        <i class="fas fa-code"></i> <span class="d-none d-md-inline">Export Code</span>
      </button>
      <button type="button" class="fw-btn fw-btn-icon" id="btnSnapshot" title="Take Screenshot">
        <i class="fas fa-camera"></i>
      </button>
      <button type="button" class="fw-btn fw-btn-icon" id="btnFullscreen" title="Toggle Fullscreen">
        <i class="fas fa-expand"></i>
      </button>
      <button type="button" class="fw-btn fw-btn-icon" id="btnToggleGui" title="Toggle Control Panel">
        <i class="fas fa-sliders"></i>
      </button>
    </div>
  </header>

  <!-- Center Stream Barrage Hint -->
  <div class="fw-stream-hint" id="fwStreamHint">
    <span class="fw-pulse-dot"></span>
    <span>CLICK ANYWHERE OR CLICK &amp; DRAG TO STREAM BARRAGE</span>
    <span class="fw-pulse-dot"></span>
  </div>

  <!-- Live Stats HUD -->
  <div class="fw-stats-hud">
    <div class="stat-pill"><span class="stat-k">FPS:</span> <span class="stat-v" id="hudFps">60</span></div>
    <div class="stat-pill"><span class="stat-k">Particles:</span> <span class="stat-v" id="hudParticles">0</span></div>
    <div class="stat-pill"><span class="stat-k">Rockets:</span> <span class="stat-v" id="hudRockets">0</span></div>
    <div class="stat-pill d-none d-sm-inline-flex"><span class="stat-k">Res:</span> <span class="stat-v" id="hudResolution">1920x1080</span></div>
  </div>

  <!-- Tweakpane-style Floating Glass Control Panel (GUI) -->
  <aside class="fw-gui-panel" id="fwGuiPanel">
    <div class="gui-header">
      <div class="gui-title"><i class="fas fa-sliders text-cyan-400 me-2"></i> fire works controls</div>
      <button type="button" class="gui-close-btn" id="btnCloseGui" title="Close Panel"><i class="fas fa-times"></i></button>
    </div>

    <div class="gui-body">
      <!-- 1. Presets Folder -->
      <div class="gui-folder open" data-folder="presets">
        <div class="gui-folder-header">
          <span class="folder-title"><i class="fas fa-wand-magic-sparkles me-2 text-pink-400"></i> Presets</span>
          <i class="fas fa-chevron-down folder-arrow"></i>
        </div>
        <div class="gui-folder-content">
          <div class="gui-row">
            <label class="gui-label">Load Preset</label>
            <select class="gui-select" id="guiPresetSelect">
              <option value="default">Default Classic 🎆</option>
              <option value="heavy-rain">Heavy Gold Rain 🎇</option>
              <option value="fast-barrage">Fast Barrage 🚀</option>
              <option value="neon-cyber">Neon Cyberpunk ⚡</option>
              <option value="birthday">Birthday Cake 🎂</option>
              <option value="heart">Heart Romantic ❤️</option>
              <option value="saturn">Saturn Planetary 🪐</option>
              <option value="slow-sparkler">Slow-Mo Sparkler ✨</option>
              <option value="grand-salvo">Grand Finale Salvo 👑</option>
            </select>
          </div>
        </div>
      </div>

      <!-- 2. Rockets & Launcher Folder -->
      <div class="gui-folder open" data-folder="rockets">
        <div class="gui-folder-header">
          <span class="folder-title"><i class="fas fa-rocket me-2 text-amber-400"></i> Rockets &amp; Launch</span>
          <i class="fas fa-chevron-down folder-arrow"></i>
        </div>
        <div class="gui-folder-content">
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Intensity</span> <span class="gui-val" id="valIntensity">30</span></div>
            <input type="range" class="gui-range" id="paramIntensity" min="1" max="60" value="30">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Rockets Point Min (%)</span> <span class="gui-val" id="valRocketsPointMin">50%</span></div>
            <input type="range" class="gui-range" id="paramRocketsPointMin" min="0" max="100" value="50">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Rockets Point Max (%)</span> <span class="gui-val" id="valRocketsPointMax">50%</span></div>
            <input type="range" class="gui-range" id="paramRocketsPointMax" min="0" max="100" value="50">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Launch Delay Min (ms)</span> <span class="gui-val" id="valDelayMin">30</span></div>
            <input type="range" class="gui-range" id="paramDelayMin" min="10" max="100" value="30">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Launch Delay Max (ms)</span> <span class="gui-val" id="valDelayMax">60</span></div>
            <input type="range" class="gui-range" id="paramDelayMax" min="10" max="120" value="60">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Rocket Speed Min</span> <span class="gui-val" id="valSpeedMin">8</span></div>
            <input type="range" class="gui-range" id="paramSpeedMin" min="2" max="25" value="8">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Rocket Speed Max</span> <span class="gui-val" id="valSpeedMax">14</span></div>
            <input type="range" class="gui-range" id="paramSpeedMax" min="5" max="35" value="14">
          </div>
        </div>
      </div>

      <!-- 3. Physics & Mechanics Folder -->
      <div class="gui-folder open" data-folder="physics">
        <div class="gui-folder-header">
          <span class="folder-title"><i class="fas fa-atom me-2 text-emerald-400"></i> Physics &amp; Motion</span>
          <i class="fas fa-chevron-down folder-arrow"></i>
        </div>
        <div class="gui-folder-content">
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Acceleration</span> <span class="gui-val" id="valAcceleration">1.05</span></div>
            <input type="range" class="gui-range" id="paramAcceleration" min="1.00" max="1.20" step="0.01" value="1.05">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Friction</span> <span class="gui-val" id="valFriction">0.96</span></div>
            <input type="range" class="gui-range" id="paramFriction" min="0.88" max="0.99" step="0.005" value="0.96">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Gravity</span> <span class="gui-val" id="valGravity">1.50</span></div>
            <input type="range" class="gui-range" id="paramGravity" min="0.1" max="5.0" step="0.1" value="1.5">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Trail Opacity</span> <span class="gui-val" id="valOpacity">0.50</span></div>
            <input type="range" class="gui-range" id="paramOpacity" min="0.05" max="0.95" step="0.05" value="0.50">
          </div>
        </div>
      </div>

      <!-- 4. Particles & Explosion Folder -->
      <div class="gui-folder open" data-folder="particles">
        <div class="gui-folder-header">
          <span class="folder-title"><i class="fas fa-burst me-2 text-rose-400"></i> Particles &amp; Burst</span>
          <i class="fas fa-chevron-down folder-arrow"></i>
        </div>
        <div class="gui-folder-content">
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Particles Count</span> <span class="gui-val" id="valParticles">90</span></div>
            <input type="range" class="gui-range" id="paramParticles" min="10" max="280" value="90">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Explosion Power</span> <span class="gui-val" id="valExplosion">5</span></div>
            <input type="range" class="gui-range" id="paramExplosion" min="1" max="10" value="5">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Trace Length</span> <span class="gui-val" id="valTraceLength">3</span></div>
            <input type="range" class="gui-range" id="paramTraceLength" min="1" max="10" value="3">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Trace Speed</span> <span class="gui-val" id="valTraceSpeed">10</span></div>
            <input type="range" class="gui-range" id="paramTraceSpeed" min="1" max="25" value="10">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Flickering / Twinkle (%)</span> <span class="gui-val" id="valFlickering">50%</span></div>
            <input type="range" class="gui-range" id="paramFlickering" min="0" max="100" value="50">
          </div>
          <div class="gui-row">
            <label class="gui-label">Line Style</label>
            <select class="gui-select" id="paramLineStyle">
              <option value="round">Round</option>
              <option value="square">Square</option>
            </select>
          </div>
        </div>
      </div>

      <!-- 5. Hue & Color Folder -->
      <div class="gui-folder open" data-folder="colors">
        <div class="gui-folder-header">
          <span class="folder-title"><i class="fas fa-palette me-2 text-indigo-400"></i> Colors &amp; Hues</span>
          <i class="fas fa-chevron-down folder-arrow"></i>
        </div>
        <div class="gui-folder-content">
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Hue Min</span> <span class="gui-val" id="valHueMin">0°</span></div>
            <input type="range" class="gui-range hue-slider" id="paramHueMin" min="0" max="360" value="0">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Hue Max</span> <span class="gui-val" id="valHueMax">360°</span></div>
            <input type="range" class="gui-range hue-slider" id="paramHueMax" min="0" max="360" value="360">
          </div>
          <div class="gui-hue-preview" id="guiHuePreview"></div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Brightness Min (%)</span> <span class="gui-val" id="valBrightnessMin">50%</span></div>
            <input type="range" class="gui-range" id="paramBrightnessMin" min="10" max="100" value="50">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Brightness Max (%)</span> <span class="gui-val" id="valBrightnessMax">80%</span></div>
            <input type="range" class="gui-range" id="paramBrightnessMax" min="10" max="100" value="80">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Decay Min</span> <span class="gui-val" id="valDecayMin">0.015</span></div>
            <input type="range" class="gui-range" id="paramDecayMin" min="0.005" max="0.05" step="0.001" value="0.015">
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Decay Max</span> <span class="gui-val" id="valDecayMax">0.030</span></div>
            <input type="range" class="gui-range" id="paramDecayMax" min="0.005" max="0.05" step="0.001" value="0.030">
          </div>
        </div>
      </div>

      <!-- 6. Sound & Interaction Folder -->
      <div class="gui-folder" data-folder="sound">
        <div class="gui-folder-header">
          <span class="folder-title"><i class="fas fa-headphones me-2 text-cyan-400"></i> Sound &amp; Mouse</span>
          <i class="fas fa-chevron-down folder-arrow"></i>
        </div>
        <div class="gui-folder-content">
          <div class="gui-toggle-row">
            <span class="gui-label">Sound FX Enabled</span>
            <label class="gui-switch">
              <input type="checkbox" id="paramSoundEnabled" checked>
              <span class="slider"></span>
            </label>
          </div>
          <div class="gui-row">
            <div class="gui-row-header"><span class="gui-label">Volume</span> <span class="gui-val" id="valVolume">50%</span></div>
            <input type="range" class="gui-range" id="paramVolume" min="0" max="100" value="50">
          </div>
          <div class="gui-toggle-row">
            <span class="gui-label">Mouse Click Launch</span>
            <label class="gui-switch">
              <input type="checkbox" id="paramMouseClick" checked>
              <span class="slider"></span>
            </label>
          </div>
          <div class="gui-toggle-row">
            <span class="gui-label">Mouse Drag Stream</span>
            <label class="gui-switch">
              <input type="checkbox" id="paramMouseMove" checked>
              <span class="slider"></span>
            </label>
          </div>
        </div>
      </div>
    </div>
  </aside>

  <!-- Export Code Modal -->
  <div class="fw-modal-backdrop hidden" id="exportModal">
    <div class="fw-modal-card">
      <div class="fw-modal-header">
        <div class="fw-modal-title"><i class="fas fa-code text-cyan-400 me-2"></i> Export Fire works Configuration</div>
        <button type="button" class="fw-modal-close" id="btnCloseModal"><i class="fas fa-times"></i></button>
      </div>
      <div class="fw-modal-body">
        <p class="text-xs text-slate-400 mb-2">Initialize fireworks on any HTML container element with your customized options:</p>
        <div class="fw-code-box">
          <pre><code id="exportCodeOutput"></code></pre>
        </div>
      </div>
      <div class="fw-modal-footer">
        <button type="button" class="fw-btn fw-btn-primary" id="btnCopyConfig">
          <i class="far fa-copy me-1"></i> Copy Code
        </button>
        <button type="button" class="fw-btn" id="btnDismissModal">Close</button>
      </div>
    </div>
  </div>
</div>

B Step 2: Styling & Responsive CSS

Modern CSS flexbox and grid layouts are used to make the UI look polished, centered, and responsive across desktop, tablet, and mobile screens.

CSS (style.css)
/* =========================================================
   Fire works Simulator - Modern Glassmorphism Styling
   Inspired by fireworks.js.org
   ========================================================= */
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  user-select: none;
  -webkit-user-select: none;
}

.fw-app {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  background-color: #030712;
  background: radial-gradient(circle at 50% 80%, #0b1329 0%, #030712 100%);
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  color: #f8fafc;
  cursor: crosshair;
}

#fireworksCanvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 1;
}

/* --- Top Floating Navigation Bar --- */
.fw-nav {
  position: absolute;
  top: 14px;
  left: 14px;
  right: 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  z-index: 20;
  pointer-events: none;
}

.fw-brand {
  display: flex;
  align-items: center;
  gap: 10px;
  background: rgba(15, 23, 42, 0.82);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid rgba(255, 255, 255, 0.15);
  border-radius: 9999px;
  padding: 6px 14px 6px 10px;
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
  pointer-events: auto;
}

.fw-brand-icon {
  font-size: 1.3rem;
  line-height: 1;
}

.fw-brand-text {
  display: flex;
  flex-direction: column;
}

.fw-brand-title {
  font-size: 0.88rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  color: #ffffff;
  line-height: 1.15;
}

.fw-brand-badge {
  font-size: 0.65rem;
  font-weight: 600;
  color: #38bdf8;
  letter-spacing: 0.03em;
}

.fw-nav-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  pointer-events: auto;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.fw-btn {
  background: rgba(15, 23, 42, 0.82);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid rgba(255, 255, 255, 0.14);
  color: #f1f5f9;
  font-size: 0.8rem;
  font-weight: 600;
  padding: 8px 14px;
  border-radius: 9999px;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
}

.fw-btn:hover {
  background: rgba(30, 41, 59, 0.95);
  border-color: rgba(255, 255, 255, 0.28);
  transform: translateY(-1px);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5);
}

.fw-btn:active {
  transform: translateY(0);
}

.fw-btn-primary {
  background: linear-gradient(135deg, #059669 0%, #10b981 100%);
  border-color: rgba(16, 185, 129, 0.5);
  color: #ffffff;
}

.fw-btn-primary:hover {
  background: linear-gradient(135deg, #047857 0%, #059669 100%);
  border-color: rgba(16, 185, 129, 0.8);
}

.fw-btn-primary.paused {
  background: linear-gradient(135deg, #d97706 0%, #f59e0b 100%);
  border-color: rgba(245, 158, 11, 0.5);
}

.fw-btn-accent {
  background: linear-gradient(135deg, #4f46e5 0%, #6366f1 100%);
  border-color: rgba(99, 102, 241, 0.5);
  color: #ffffff;
}

.fw-btn-accent:hover {
  background: linear-gradient(135deg, #4338ca 0%, #4f46e5 100%);
}

.fw-btn-icon {
  width: 36px;
  height: 36px;
  padding: 0;
  justify-content: center;
  border-radius: 50%;
}

/* --- Center Stream Hint --- */
.fw-stream-hint {
  position: absolute;
  top: 68px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(15, 23, 42, 0.72);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  padding: 6px 16px;
  border-radius: 9999px;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: #94a3b8;
  display: flex;
  align-items: center;
  gap: 8px;
  z-index: 10;
  pointer-events: none;
  box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}

.fw-pulse-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #10b981;
  animation: pulseDot 1.5s infinite;
}

@keyframes pulseDot {
  0%, 100% { transform: scale(1); opacity: 1; box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.6); }
  50% { transform: scale(1.3); opacity: 0.7; box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
}

/* --- Stats HUD --- */
.fw-stats-hud {
  position: absolute;
  bottom: 16px;
  left: 16px;
  display: flex;
  align-items: center;
  gap: 8px;
  z-index: 15;
  pointer-events: none;
}

.stat-pill {
  background: rgba(15, 23, 42, 0.75);
  backdrop-filter: blur(8px);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 9999px;
  padding: 4px 10px;
  font-size: 0.72rem;
  font-family: 'Fira Code', monospace;
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

.stat-k { color: #64748b; }
.stat-v { color: #38bdf8; font-weight: 600; }

/* --- Tweakpane-style Floating Glass Control Panel --- */
.fw-gui-panel {
  position: absolute;
  top: 68px;
  right: 16px;
  width: 320px;
  max-height: calc(100vh - 88px);
  background: rgba(15, 23, 42, 0.88);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 16px;
  z-index: 30;
  display: flex;
  flex-direction: column;
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.6);
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
  overflow: hidden;
}

.fw-gui-panel.closed {
  transform: translateX(340px);
  opacity: 0;
  pointer-events: none;
}

.gui-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 14px;
  background: rgba(30, 41, 59, 0.5);
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.gui-title {
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: #e2e8f0;
  display: flex;
  align-items: center;
}

.gui-close-btn {
  background: none;
  border: none;
  color: #94a3b8;
  cursor: pointer;
  font-size: 0.9rem;
  padding: 4px;
  transition: color 0.15s;
}

.gui-close-btn:hover {
  color: #ffffff;
}

.gui-body {
  padding: 8px 10px;
  overflow-y: auto;
  overflow-x: hidden;
  max-height: calc(100vh - 150px);
}

.gui-body::-webkit-scrollbar {
  width: 5px;
}
.gui-body::-webkit-scrollbar-track {
  background: rgba(15, 23, 42, 0.3);
}
.gui-body::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.15);
  border-radius: 9999px;
}

/* --- Accordion Folders --- */
.gui-folder {
  margin-bottom: 6px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 10px;
  background: rgba(15, 23, 42, 0.5);
  overflow: hidden;
}

.gui-folder-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 9px 12px;
  cursor: pointer;
  background: rgba(30, 41, 59, 0.4);
  transition: background 0.15s;
}

.gui-folder-header:hover {
  background: rgba(51, 65, 85, 0.5);
}

.folder-title {
  font-size: 0.78rem;
  font-weight: 700;
  color: #f1f5f9;
}

.folder-arrow {
  font-size: 0.7rem;
  color: #94a3b8;
  transition: transform 0.2s ease;
}

.gui-folder.open .folder-arrow {
  transform: rotate(0deg);
}

.gui-folder:not(.open) .folder-arrow {
  transform: rotate(-90deg);
}

.gui-folder-content {
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.gui-folder:not(.open) .gui-folder-content {
  display: none;
}

/* --- GUI Rows & Inputs --- */
.gui-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.gui-row-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.gui-label {
  font-size: 0.72rem;
  font-weight: 600;
  color: #94a3b8;
}

.gui-val {
  font-size: 0.72rem;
  font-weight: 600;
  font-family: 'Fira Code', monospace;
  color: #38bdf8;
}

.gui-select {
  background: rgba(30, 41, 59, 0.8);
  border: 1px solid rgba(255, 255, 255, 0.15);
  color: #f8fafc;
  font-size: 0.78rem;
  font-weight: 600;
  border-radius: 8px;
  padding: 6px 10px;
  outline: none;
  cursor: pointer;
  transition: border-color 0.15s;
}

.gui-select:focus {
  border-color: #38bdf8;
}

.gui-range {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 5px;
  border-radius: 9999px;
  background: rgba(51, 65, 85, 0.8);
  outline: none;
  cursor: pointer;
}

.gui-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #38bdf8;
  box-shadow: 0 0 10px rgba(56, 189, 248, 0.7);
  cursor: pointer;
  transition: transform 0.1s ease;
}

.gui-range::-webkit-slider-thumb:hover {
  transform: scale(1.2);
}

.gui-hue-preview {
  height: 6px;
  border-radius: 9999px;
  background: linear-gradient(to right, red, yellow, lime, cyan, blue, magenta, red);
  margin: 2px 0 6px 0;
  box-shadow: 0 0 8px rgba(255, 255, 255, 0.2);
}

.gui-toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 2px 0;
}

/* Switch */
.gui-switch {
  position: relative;
  display: inline-block;
  width: 36px;
  height: 20px;
}
.gui-switch input {
  opacity: 0;
  width: 0;
  height: 0;
}
.gui-switch .slider {
  position: absolute;
  cursor: pointer;
  top: 0; left: 0; right: 0; bottom: 0;
  background-color: rgba(51, 65, 85, 0.8);
  transition: 0.2s;
  border-radius: 9999px;
  border: 1px solid rgba(255, 255, 255, 0.15);
}
.gui-switch .slider:before {
  position: absolute;
  content: "";
  height: 14px;
  width: 14px;
  left: 2px;
  bottom: 2px;
  background-color: white;
  transition: 0.2s;
  border-radius: 50%;
}
.gui-switch input:checked + .slider {
  background-color: #10b981;
}
.gui-switch input:checked + .slider:before {
  transform: translateX(16px);
}

/* --- Export Code Modal --- */
.fw-modal-backdrop {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(3, 7, 18, 0.8);
  backdrop-filter: blur(12px);
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
}
.fw-modal-backdrop.hidden {
  display: none !important;
}

.fw-modal-card {
  width: 100%;
  max-width: 620px;
  background: rgba(15, 23, 42, 0.95);
  border: 1px solid rgba(255, 255, 255, 0.18);
  border-radius: 18px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.8);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.fw-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 18px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(30, 41, 59, 0.5);
}

.fw-modal-title {
  font-size: 0.95rem;
  font-weight: 700;
  color: #f1f5f9;
}

.fw-modal-close {
  background: none;
  border: none;
  color: #94a3b8;
  font-size: 1.1rem;
  cursor: pointer;
}
.fw-modal-close:hover { color: #ffffff; }

.fw-modal-body {
  padding: 16px 18px;
}

.fw-code-box {
  background: #020617;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 10px;
  padding: 14px;
  max-height: 320px;
  overflow-y: auto;
  font-family: 'Fira Code', monospace;
  font-size: 0.78rem;
  color: #e2e8f0;
  line-height: 1.5;
}

.fw-modal-footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  padding: 12px 18px;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(30, 41, 59, 0.4);
}

/* Responsive adjustments */
@media (max-width: 640px) {
  .fw-gui-panel {
    top: auto;
    bottom: 0;
    right: 0;
    left: 0;
    width: 100vw;
    max-height: 65vh;
    border-radius: 20px 20px 0 0;
  }
  .fw-gui-panel.closed {
    transform: translateY(100%);
  }
}

C Step 3: JavaScript Logic & Event Handling

Here is the complete JavaScript code handling the state, event listeners, mathematical logic, and dynamic DOM rendering:

JavaScript (script.js)
(function() {
  'use strict';

  // =========================================================
  // 1. Fireworks-js Engine & Audio Synthesizer
  // =========================================================
  const canvas = document.getElementById('fireworksCanvas');
  const ctx = canvas.getContext('2d');

  let width = 0;
  let height = 0;

  function resize() {
    width = canvas.width = window.innerWidth;
    height = canvas.height = window.innerHeight;
    document.getElementById('hudResolution').textContent = `${width}x${height}`;
  }
  resize();
  window.addEventListener('resize', resize);

  // Web Audio API Procedural Sound Engine
  let audioCtx = null;
  function getAudioContext() {
    if (!audioCtx) {
      const AudioContextClass = window.AudioContext || window.webkitAudioContext;
      if (AudioContextClass) {
        audioCtx = new AudioContextClass();
      }
    }
    if (audioCtx && audioCtx.state === 'suspended') {
      audioCtx.resume();
    }
    return audioCtx;
  }

  function playLaunchSound(pan = 0, volume = 0.5) {
    if (!options.sound.enabled || volume <= 0) return;
    const ctxA = getAudioContext();
    if (!ctxA) return;

    try {
      const now = ctxA.currentTime;
      const osc = ctxA.createOscillator();
      const gain = ctxA.createGain();
      const panner = ctxA.createStereoPanner ? ctxA.createStereoPanner() : null;

      osc.type = 'sine';
      osc.frequency.setValueAtTime(320 + Math.random() * 80, now);
      osc.frequency.exponentialRampToValueAtTime(1100 + Math.random() * 200, now + 0.5);

      gain.gain.setValueAtTime(0.001, now);
      gain.gain.linearRampToValueAtTime(0.12 * volume, now + 0.05);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.55);

      if (panner) {
        panner.pan.setValueAtTime(Math.max(-1, Math.min(1, pan)), now);
        osc.connect(gain);
        gain.connect(panner);
        panner.connect(ctxA.destination);
      } else {
        osc.connect(gain);
        gain.connect(ctxA.destination);
      }

      osc.start(now);
      osc.stop(now + 0.6);
    } catch(e) {}
  }

  function playExplosionSound(intensity = 1.0, pan = 0, volume = 0.5) {
    if (!options.sound.enabled || volume <= 0) return;
    const ctxA = getAudioContext();
    if (!ctxA) return;

    try {
      const now = ctxA.currentTime;
      const osc = ctxA.createOscillator();
      const gain = ctxA.createGain();
      const panner = ctxA.createStereoPanner ? ctxA.createStereoPanner() : null;

      // Low frequency punch
      osc.type = 'triangle';
      osc.frequency.setValueAtTime(140 + Math.random() * 40, now);
      osc.frequency.exponentialRampToValueAtTime(30, now + 0.35);

      const maxG = Math.min(0.8, (0.28 + intensity * 0.05) * volume);
      gain.gain.setValueAtTime(maxG, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.45);

      // Crackle noise generator
      const bufferSize = ctxA.sampleRate * 0.25;
      const noiseBuffer = ctxA.createBuffer(1, bufferSize, ctxA.sampleRate);
      const output = noiseBuffer.getChannelData(0);
      for (let i = 0; i < bufferSize; i++) {
        output[i] = (Math.random() * 2 - 1) * Math.exp(-i / (ctxA.sampleRate * 0.06));
      }
      const noise = ctxA.createBufferSource();
      noise.buffer = noiseBuffer;
      const noiseGain = ctxA.createGain();
      noiseGain.gain.setValueAtTime(0.18 * volume, now + 0.03);
      noiseGain.gain.exponentialRampToValueAtTime(0.001, now + 0.28);

      if (panner) {
        panner.pan.setValueAtTime(Math.max(-1, Math.min(1, pan)), now);
        osc.connect(gain);
        gain.connect(panner);
        noise.connect(noiseGain);
        noiseGain.connect(panner);
        panner.connect(ctxA.destination);
      } else {
        osc.connect(gain);
        gain.connect(ctxA.destination);
        noise.connect(noiseGain);
        noiseGain.connect(ctxA.destination);
      }

      osc.start(now);
      osc.stop(now + 0.45);
      noise.start(now + 0.02);
      noise.stop(now + 0.3);
    } catch(e) {}
  }

  // =========================================================
  // 2. Fireworks Configuration State
  // =========================================================
  const options = {
    autoresize: true,
    opacity: 0.5,
    acceleration: 1.05,
    friction: 0.96,
    gravity: 1.5,
    particles: 90,
    traceLength: 3,
    traceSpeed: 10,
    explosion: 5,
    intensity: 30,
    flickering: 50,
    lineStyle: 'round',
    hue: {
      min: 0,
      max: 360
    },
    delay: {
      min: 30,
      max: 60
    },
    rocketsPoint: {
      min: 50,
      max: 50
    },
    lineWidth: {
      explosion: { min: 1, max: 3 },
      trace: { min: 1, max: 2 }
    },
    brightness: {
      min: 50,
      max: 80
    },
    decay: {
      min: 0.015,
      max: 0.03
    },
    mouse: {
      click: true,
      move: true,
      max: 1
    },
    sound: {
      enabled: true,
      volume: 50
    }
  };

  const presets = {
    'default': {
      intensity: 30, rocketsPoint: { min: 50, max: 50 }, delay: { min: 30, max: 60 },
      acceleration: 1.05, friction: 0.96, gravity: 1.5, opacity: 0.5,
      particles: 90, explosion: 5, traceLength: 3, traceSpeed: 10, flickering: 50, lineStyle: 'round',
      hue: { min: 0, max: 360 }, brightness: { min: 50, max: 80 }, decay: { min: 0.015, max: 0.03 }
    },
    'heavy-rain': {
      intensity: 45, rocketsPoint: { min: 10, max: 90 }, delay: { min: 15, max: 35 },
      acceleration: 1.03, friction: 0.98, gravity: 2.8, opacity: 0.35,
      particles: 180, explosion: 7, traceLength: 8, traceSpeed: 16, flickering: 65, lineStyle: 'round',
      hue: { min: 35, max: 55 }, brightness: { min: 65, max: 95 }, decay: { min: 0.008, max: 0.018 }
    },
    'fast-barrage': {
      intensity: 60, rocketsPoint: { min: 20, max: 80 }, delay: { min: 10, max: 25 },
      acceleration: 1.10, friction: 0.95, gravity: 1.2, opacity: 0.6,
      particles: 120, explosion: 6, traceLength: 4, traceSpeed: 18, flickering: 40, lineStyle: 'round',
      hue: { min: 0, max: 360 }, brightness: { min: 60, max: 90 }, decay: { min: 0.02, max: 0.04 }
    },
    'neon-cyber': {
      intensity: 35, rocketsPoint: { min: 40, max: 60 }, delay: { min: 25, max: 50 },
      acceleration: 1.06, friction: 0.96, gravity: 1.3, opacity: 0.45,
      particles: 130, explosion: 6, traceLength: 5, traceSpeed: 12, flickering: 70, lineStyle: 'round',
      hue: { min: 180, max: 310 }, brightness: { min: 70, max: 100 }, decay: { min: 0.012, max: 0.025 }
    },
    'birthday': {
      intensity: 40, rocketsPoint: { min: 30, max: 70 }, delay: { min: 20, max: 45 },
      acceleration: 1.05, friction: 0.96, gravity: 1.4, opacity: 0.5,
      particles: 150, explosion: 7, traceLength: 4, traceSpeed: 11, flickering: 80, lineStyle: 'round',
      hue: { min: 290, max: 360 }, brightness: { min: 60, max: 90 }, decay: { min: 0.015, max: 0.03 }
    },
    'heart': {
      intensity: 25, rocketsPoint: { min: 45, max: 55 }, delay: { min: 35, max: 70 },
      acceleration: 1.05, friction: 0.96, gravity: 1.2, opacity: 0.48,
      particles: 160, explosion: 6, traceLength: 4, traceSpeed: 10, flickering: 60, lineStyle: 'round',
      hue: { min: 330, max: 360 }, brightness: { min: 60, max: 95 }, decay: { min: 0.012, max: 0.025 }
    },
    'saturn': {
      intensity: 30, rocketsPoint: { min: 30, max: 70 }, delay: { min: 30, max: 60 },
      acceleration: 1.05, friction: 0.96, gravity: 1.3, opacity: 0.5,
      particles: 140, explosion: 6, traceLength: 4, traceSpeed: 12, flickering: 50, lineStyle: 'round',
      hue: { min: 25, max: 210 }, brightness: { min: 55, max: 85 }, decay: { min: 0.014, max: 0.028 }
    },
    'slow-sparkler': {
      intensity: 20, rocketsPoint: { min: 20, max: 80 }, delay: { min: 40, max: 80 },
      acceleration: 1.02, friction: 0.985, gravity: 0.8, opacity: 0.35,
      particles: 100, explosion: 4, traceLength: 6, traceSpeed: 6, flickering: 90, lineStyle: 'round',
      hue: { min: 40, max: 70 }, brightness: { min: 70, max: 100 }, decay: { min: 0.008, max: 0.018 }
    },
    'grand-salvo': {
      intensity: 60, rocketsPoint: { min: 5, max: 95 }, delay: { min: 10, max: 25 },
      acceleration: 1.08, friction: 0.96, gravity: 1.5, opacity: 0.4,
      particles: 220, explosion: 8, traceLength: 6, traceSpeed: 16, flickering: 75, lineStyle: 'round',
      hue: { min: 0, max: 360 }, brightness: { min: 65, max: 100 }, decay: { min: 0.01, max: 0.025 }
    }
  };

  // =========================================================
  // 3. Mathematical Shapes & Fireworks Physics Classes
  // =========================================================
  function randomRange(min, max) {
    return Math.random() * (max - min) + min;
  }

  function randomHue() {
    return randomRange(options.hue.min, options.hue.max);
  }

  class DetonationFlash {
    constructor(x, y, hue, maxRadius = 150) {
      this.x = x;
      this.y = y;
      this.hue = hue;
      this.maxRadius = maxRadius;
      this.age = 0;
      this.duration = 16;
      this.dead = false;
    }

    update(dt) {
      this.age += dt;
      if (this.age >= this.duration) {
        this.dead = true;
      }
    }

    draw(ctx) {
      const progress = this.age / this.duration;
      const alpha = Math.max(0, 1 - progress);
      const r = this.maxRadius * Math.sin(progress * Math.PI * 0.5);

      // Bloom Glow
      const grad = ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, r * 1.4);
      grad.addColorStop(0, `hsla(${this.hue}, 100%, 90%, ${alpha * 0.9})`);
      grad.addColorStop(0.3, `hsla(${this.hue}, 100%, 65%, ${alpha * 0.7})`);
      grad.addColorStop(1, `hsla(${this.hue}, 100%, 50%, 0)`);
      ctx.fillStyle = grad;
      ctx.beginPath();
      ctx.arc(this.x, this.y, r * 1.4, 0, Math.PI * 2);
      ctx.fill();

      // Shockwave ring
      ctx.beginPath();
      ctx.arc(this.x, this.y, r, 0, Math.PI * 2);
      ctx.strokeStyle = `hsla(${this.hue}, 100%, 85%, ${alpha * 0.85})`;
      ctx.lineWidth = Math.max(1, 3.5 * (1 - progress));
      ctx.stroke();
    }
  }

  class TargetCrosshair {
    constructor(x, y) {
      this.x = x;
      this.y = y;
      this.age = 0;
      this.duration = 20;
      this.dead = false;
    }

    update(dt) {
      this.age += dt;
      if (this.age >= this.duration) this.dead = true;
    }

    draw(ctx) {
      const progress = this.age / this.duration;
      const alpha = 1 - progress;
      const r = 8 + progress * 24;

      ctx.strokeStyle = `rgba(56, 189, 248, ${alpha * 0.9})`;
      ctx.lineWidth = 1.5;
      ctx.beginPath();
      ctx.arc(this.x, this.y, r, 0, Math.PI * 2);
      ctx.stroke();

      const cross = 6;
      ctx.beginPath();
      ctx.moveTo(this.x - cross, this.y); ctx.lineTo(this.x + cross, this.y);
      ctx.moveTo(this.x, this.y - cross); ctx.lineTo(this.x, this.y + cross);
      ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.9})`;
      ctx.stroke();
    }
  }

  class Particle {
    constructor(x, y, hue, angle, speed, customOptions = {}) {
      this.x = x;
      this.y = y;
      this.hue = hue;
      this.angle = angle;
      this.speed = speed;
      this.friction = customOptions.friction || options.friction;
      this.gravity = customOptions.gravity !== undefined ? customOptions.gravity : options.gravity;
      this.brightness = randomRange(options.brightness.min, options.brightness.max);
      this.alpha = 1;
      this.decay = randomRange(options.decay.min, options.decay.max);
      this.flickering = customOptions.flickering !== undefined ? customOptions.flickering : options.flickering;
      this.lineStyle = options.lineStyle;

      this.coordinates = [];
      this.coordinateCount = options.traceLength;
      while (this.coordinates.length < this.coordinateCount) {
        this.coordinates.push([this.x, this.y]);
      }

      this.dead = false;
    }

    update(dt) {
      this.coordinates.pop();
      this.coordinates.unshift([this.x, this.y]);

      this.speed *= this.friction;
      this.x += Math.cos(this.angle) * this.speed * dt;
      this.y += (Math.sin(this.angle) * this.speed + this.gravity) * dt;

      this.alpha -= this.decay * dt;
      if (this.alpha <= this.decay) {
        this.dead = true;
      }
    }

    draw(ctx) {
      ctx.beginPath();
      const lastCoord = this.coordinates[this.coordinates.length - 1];
      ctx.moveTo(lastCoord[0], lastCoord[1]);
      ctx.lineTo(this.x, this.y);

      let effectiveBrightness = this.brightness;
      let effectiveAlpha = this.alpha;

      if (this.flickering > 0 && Math.random() * 100 < this.flickering) {
        effectiveBrightness = Math.min(100, this.brightness + randomRange(10, 30));
        effectiveAlpha = Math.min(1, this.alpha * (0.8 + Math.random() * 0.4));
      }

      ctx.strokeStyle = `hsla(${this.hue}, 100%, ${effectiveBrightness}%, ${effectiveAlpha})`;
      ctx.lineWidth = randomRange(options.lineWidth.explosion.min, options.lineWidth.explosion.max);
      ctx.lineCap = this.lineStyle;
      ctx.stroke();

      // Spark glow dot
      ctx.fillStyle = `hsla(${this.hue}, 100%, 95%, ${effectiveAlpha})`;
      ctx.beginPath();
      ctx.arc(this.x, this.y, 1.2, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  class Rocket {
    constructor(startX, startY, targetX, targetY, customShape = null) {
      this.x = startX;
      this.y = startY;
      this.startX = startX;
      this.startY = startY;
      this.targetX = targetX;
      this.targetY = targetY;
      this.customShape = customShape;

      this.distanceToTarget = Math.hypot(targetX - startX, targetY - startY);
      this.distanceTraveled = 0;

      this.coordinates = [];
      this.coordinateCount = options.traceLength;
      while (this.coordinates.length < this.coordinateCount) {
        this.coordinates.push([this.x, this.y]);
      }

      this.angle = Math.atan2(targetY - startY, targetX - startX);
      this.speed = randomRange(options.speedMin || 8, options.speedMax || 14);
      this.acceleration = options.acceleration;
      this.hue = randomHue();
      this.brightness = randomRange(options.brightness.min, options.brightness.max);
      this.dead = false;

      const pan = (startX - width / 2) / (width / 2);
      playLaunchSound(pan, options.sound.volume / 100);
    }

    update(dt) {
      this.coordinates.pop();
      this.coordinates.unshift([this.x, this.y]);

      this.speed *= this.acceleration;
      const vx = Math.cos(this.angle) * this.speed * dt;
      const vy = Math.sin(this.angle) * this.speed * dt;

      this.distanceTraveled = Math.hypot(this.x - this.startX, this.y - this.startY);

      if (this.distanceTraveled >= this.distanceToTarget || this.y <= this.targetY) {
        this.explode();
        this.dead = true;
      } else {
        this.x += vx;
        this.y += vy;
      }
    }

    explode() {
      const pan = (this.x - width / 2) / (width / 2);
      playExplosionSound(options.explosion, pan, options.sound.volume / 100);

      // Detonation shockwave bloom
      flashes.push(new DetonationFlash(this.targetX, this.targetY, this.hue, 140 * (options.explosion / 5)));

      if (this.customShape === 'heart') {
        createHeartBurst(this.targetX, this.targetY, this.hue);
      } else if (this.customShape === 'birthday') {
        createBirthdayBurst(this.targetX, this.targetY, this.hue);
      } else if (this.customShape === 'saturn') {
        createSaturnBurst(this.targetX, this.targetY, this.hue);
      } else {
        createStandardBurst(this.targetX, this.targetY, this.hue);
      }
    }

    draw(ctx) {
      ctx.beginPath();
      const lastCoord = this.coordinates[this.coordinates.length - 1];
      ctx.moveTo(lastCoord[0], lastCoord[1]);
      ctx.lineTo(this.x, this.y);
      ctx.strokeStyle = `hsla(${this.hue}, 100%, ${this.brightness}%, 1)`;
      ctx.lineWidth = randomRange(options.lineWidth.trace.min, options.lineWidth.trace.max);
      ctx.lineCap = options.lineStyle;
      ctx.stroke();

      // Rocket head white flare
      ctx.fillStyle = '#ffffff';
      ctx.beginPath();
      ctx.arc(this.x, this.y, 2.2, 0, Math.PI * 2);
      ctx.fill();

      ctx.fillStyle = `hsla(${this.hue}, 100%, 60%, 0.5)`;
      ctx.beginPath();
      ctx.arc(this.x, this.y, 5, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  // Burst Generators
  function createStandardBurst(x, y, hue) {
    const count = options.particles;
    for (let i = 0; i < count; i++) {
      const angle = randomRange(0, Math.PI * 2);
      const speed = Math.cos(randomRange(0, Math.PI / 2)) * randomRange(2, 8) * (options.explosion / 5);
      particles.push(new Particle(x, y, hue + randomRange(-15, 15), angle, speed));
    }
  }

  function createHeartBurst(x, y, hue) {
    const count = Math.max(120, options.particles * 1.5);
    for (let i = 0; i < count; i++) {
      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));
      const spd = 0.3 * (options.explosion / 5) * randomRange(0.85, 1.15);
      const angle = Math.atan2(hy, hx);
      const speed = Math.hypot(hx, hy) * spd;
      particles.push(new Particle(x, y, hue + randomRange(-10, 10), angle, speed, { friction: 0.95 }));
    }
  }

  function createBirthdayBurst(x, y, hue) {
    const count = options.particles;
    for (let i = 0; i < count; i++) {
      const angle = randomRange(0, Math.PI * 2);
      const speed = randomRange(2, 7) * (options.explosion / 5);
      const colHue = randomRange(0, 360);
      particles.push(new Particle(x, y, colHue, angle, speed, { flickering: 80 }));
    }
  }

  function createSaturnBurst(x, y, hue) {
    const ringCount = Math.floor(options.particles * 0.7);
    for (let i = 0; i < ringCount; i++) {
      const angle = (i / ringCount) * Math.PI * 2;
      const speed = randomRange(4.5, 6.5) * (options.explosion / 5);
      const vx = Math.cos(angle) * speed;
      const vy = Math.sin(angle) * speed * 0.35;
      const finalAngle = Math.atan2(vy, vx);
      const finalSpeed = Math.hypot(vx, vy);
      particles.push(new Particle(x, y, hue, finalAngle, finalSpeed, { gravity: 0.5 }));
    }
    const sphereCount = Math.floor(options.particles * 0.3);
    for (let i = 0; i < sphereCount; i++) {
      const angle = randomRange(0, Math.PI * 2);
      const speed = randomRange(1, 3);
      particles.push(new Particle(x, y, (hue + 180) % 360, angle, speed));
    }
  }

  // Collections
  const rockets = [];
  const particles = [];
  const flashes = [];
  const crosshairs = [];

  // =========================================================
  // 4. Main Fireworks Manager Engine
  // =========================================================
  let isRunning = true;
  let launchTimer = 0;
  let nextLaunchInterval = 30;

  function scheduleNextLaunch() {
    const baseInterval = (60 / options.intensity) * 15;
    nextLaunchInterval = randomRange(
      Math.max(5, baseInterval * (options.delay.min / 50)),
      Math.max(10, baseInterval * (options.delay.max / 50))
    );
  }
  scheduleNextLaunch();

  function launchRocket(customTargetX = null, customTargetY = null, shape = null) {
    const minX = (width * options.rocketsPoint.min) / 100;
    const maxX = (width * options.rocketsPoint.max) / 100;
    const startX = randomRange(minX, maxX);
    const startY = height;

    const targetX = customTargetX !== null ? customTargetX : randomRange(width * 0.1, width * 0.9);
    const targetY = customTargetY !== null ? customTargetY : randomRange(height * 0.1, height * 0.5);

    if (customTargetX !== null) {
      crosshairs.push(new TargetCrosshair(targetX, targetY));
    }

    rockets.push(new Rocket(startX, startY, targetX, targetY, shape));
  }

  // Pointer Interaction
  let isPointerDown = false;
  let streamInterval = null;
  let lastPointerX = 0;
  let lastPointerY = 0;

  canvas.addEventListener('pointerdown', (e) => {
    if (!options.mouse.click && !options.mouse.move) return;
    getAudioContext();
    isPointerDown = true;
    lastPointerX = e.clientX;
    lastPointerY = e.clientY;

    if (options.mouse.click) {
      launchRocket(e.clientX, e.clientY);
    }

    if (options.mouse.move) {
      clearInterval(streamInterval);
      streamInterval = setInterval(() => {
        if (!isPointerDown) return;
        launchRocket(
          lastPointerX + randomRange(-40, 40),
          lastPointerY + randomRange(-30, 30)
        );
      }, 160);
    }
  });

  window.addEventListener('pointermove', (e) => {
    lastPointerX = e.clientX;
    lastPointerY = e.clientY;
  });

  window.addEventListener('pointerup', () => {
    isPointerDown = false;
    clearInterval(streamInterval);
  });

  // =========================================================
  // 5. Render Loop & FPS Meter
  // =========================================================
  let lastTime = performance.now();
  let frameCount = 0;
  let fpsTimer = 0;

  function render(currentTime) {
    requestAnimationFrame(render);

    const delta = Math.min(3.0, (currentTime - lastTime) / 16.666);
    lastTime = currentTime;

    // Update FPS HUD
    frameCount++;
    fpsTimer += delta;
    if (fpsTimer >= 30) {
      const fps = Math.round((frameCount / fpsTimer) * 60);
      document.getElementById('hudFps').textContent = fps;
      document.getElementById('hudParticles').textContent = particles.length;
      document.getElementById('hudRockets').textContent = rockets.length;
      frameCount = 0;
      fpsTimer = 0;
    }

    // Motion Blur Canvas Clear (using options.opacity)
    ctx.globalCompositeOperation = 'source-over';
    ctx.fillStyle = `rgba(3, 7, 18, ${options.opacity})`;
    ctx.fillRect(0, 0, width, height);

    // Additive Blending for blinding sparks glow
    ctx.globalCompositeOperation = 'lighter';

    // Auto launcher
    if (isRunning) {
      launchTimer += delta;
      if (launchTimer >= nextLaunchInterval) {
        launchTimer = 0;
        scheduleNextLaunch();
        launchRocket();
      }
    }

    // Crosshairs
    for (let i = crosshairs.length - 1; i >= 0; i--) {
      crosshairs[i].update(delta);
      crosshairs[i].draw(ctx);
      if (crosshairs[i].dead) crosshairs.splice(i, 1);
    }

    // Flashes
    for (let i = flashes.length - 1; i >= 0; i--) {
      flashes[i].update(delta);
      flashes[i].draw(ctx);
      if (flashes[i].dead) flashes.splice(i, 1);
    }

    // Rockets
    for (let i = rockets.length - 1; i >= 0; i--) {
      rockets[i].update(delta);
      rockets[i].draw(ctx);
      if (rockets[i].dead) rockets.splice(i, 1);
    }

    // Particles
    for (let i = particles.length - 1; i >= 0; i--) {
      particles[i].update(delta);
      particles[i].draw(ctx);
      if (particles[i].dead) particles.splice(i, 1);
    }
  }

  // =========================================================
  // 6. UI Controls & Synchronization
  // =========================================================
  const btnPlayPause = document.getElementById('btnPlayPause');
  const iconPlayPause = document.getElementById('iconPlayPause');
  const textPlayPause = document.getElementById('textPlayPause');
  const btnClear = document.getElementById('btnClear');
  const btnRandomize = document.getElementById('btnRandomize');
  const btnSound = document.getElementById('btnSound');
  const iconSound = document.getElementById('iconSound');
  const textSound = document.getElementById('textSound');
  const btnSnapshot = document.getElementById('btnSnapshot');
  const btnFullscreen = document.getElementById('btnFullscreen');
  const btnToggleGui = document.getElementById('btnToggleGui');
  const fwGuiPanel = document.getElementById('fwGuiPanel');
  const btnCloseGui = document.getElementById('btnCloseGui');
  const btnExportCode = document.getElementById('btnExportCode');
  const exportModal = document.getElementById('exportModal');
  const btnCloseModal = document.getElementById('btnCloseModal');
  const btnDismissModal = document.getElementById('btnDismissModal');
  const btnCopyConfig = document.getElementById('btnCopyConfig');
  const exportCodeOutput = document.getElementById('exportCodeOutput');

  // Play / Pause
  btnPlayPause.addEventListener('click', () => {
    getAudioContext();
    isRunning = !isRunning;
    iconPlayPause.className = isRunning ? 'fas fa-pause' : 'fas fa-play';
    textPlayPause.textContent = isRunning ? 'Pause' : 'Start';
    btnPlayPause.classList.toggle('paused', !isRunning);
  });

  // Clear
  btnClear.addEventListener('click', () => {
    rockets.length = 0;
    particles.length = 0;
    flashes.length = 0;
    crosshairs.length = 0;
    ctx.clearRect(0, 0, width, height);
  });

  // Sound Toggle
  btnSound.addEventListener('click', () => {
    getAudioContext();
    options.sound.enabled = !options.sound.enabled;
    document.getElementById('paramSoundEnabled').checked = options.sound.enabled;
    updateSoundButton();
  });

  function updateSoundButton() {
    iconSound.className = options.sound.enabled ? 'fas fa-volume-up text-cyan-400' : 'fas fa-volume-xmark text-slate-500';
    textSound.textContent = options.sound.enabled ? 'Sound ON' : 'Sound OFF';
  }

  // Randomize
  btnRandomize.addEventListener('click', () => {
    options.hue.min = Math.floor(Math.random() * 180);
    options.hue.max = Math.floor(Math.random() * 180 + 180);
    options.intensity = Math.floor(randomRange(20, 55));
    options.particles = Math.floor(randomRange(60, 200));
    options.explosion = Math.floor(randomRange(3, 8));
    options.gravity = parseFloat(randomRange(0.8, 2.5).toFixed(1));
    options.friction = parseFloat(randomRange(0.93, 0.98).toFixed(3));
    syncUiFromOptions();
    launchRocket(width / 2, height * 0.3);
  });

  // Fullscreen
  btnFullscreen.addEventListener('click', () => {
    if (!document.fullscreenElement) {
      document.documentElement.requestFullscreen().catch(() => {});
    } else {
      document.exitFullscreen().catch(() => {});
    }
  });

  // Snapshot
  btnSnapshot.addEventListener('click', () => {
    const link = document.createElement('a');
    link.download = 'fireworks-js-simulator.png';
    link.href = canvas.toDataURL('image/png');
    link.click();
  });

  // GUI Toggle
  btnToggleGui.addEventListener('click', () => {
    fwGuiPanel.classList.toggle('closed');
  });
  btnCloseGui.addEventListener('click', () => {
    fwGuiPanel.classList.add('closed');
  });

  // Folders Accordion
  document.querySelectorAll('.gui-folder-header').forEach(header => {
    header.addEventListener('click', () => {
      header.parentElement.classList.toggle('open');
    });
  });

  // Sliders & Inputs Binding
  function bindSlider(id, targetObj, key, valId, suffix = '', multiplier = 1, parser = parseFloat) {
    const input = document.getElementById(id);
    const valDisplay = document.getElementById(valId);
    if (!input) return;

    input.addEventListener('input', (e) => {
      const val = parser(e.target.value);
      targetObj[key] = val;
      if (valDisplay) valDisplay.textContent = `${val}${suffix}`;
    });
  }

  bindSlider('paramIntensity', options, 'intensity', 'valIntensity');
  bindSlider('paramRocketsPointMin', options.rocketsPoint, 'min', 'valRocketsPointMin', '%', 1, parseInt);
  bindSlider('paramRocketsPointMax', options.rocketsPoint, 'max', 'valRocketsPointMax', '%', 1, parseInt);
  bindSlider('paramDelayMin', options.delay, 'min', 'valDelayMin', 'ms', 1, parseInt);
  bindSlider('paramDelayMax', options.delay, 'max', 'valDelayMax', 'ms', 1, parseInt);
  bindSlider('paramSpeedMin', options, 'speedMin', 'valSpeedMin', '', 1, parseInt);
  bindSlider('paramSpeedMax', options, 'speedMax', 'valSpeedMax', '', 1, parseInt);

  bindSlider('paramAcceleration', options, 'acceleration', 'valAcceleration', '', 1, parseFloat);
  bindSlider('paramFriction', options, 'friction', 'valFriction', '', 1, parseFloat);
  bindSlider('paramGravity', options, 'gravity', 'valGravity', '', 1, parseFloat);
  bindSlider('paramOpacity', options, 'opacity', 'valOpacity', '', 1, parseFloat);

  bindSlider('paramParticles', options, 'particles', 'valParticles', '', 1, parseInt);
  bindSlider('paramExplosion', options, 'explosion', 'valExplosion', '', 1, parseInt);
  bindSlider('paramTraceLength', options, 'traceLength', 'valTraceLength', '', 1, parseInt);
  bindSlider('paramTraceSpeed', options, 'traceSpeed', 'valTraceSpeed', '', 1, parseInt);
  bindSlider('paramFlickering', options, 'flickering', 'valFlickering', '%', 1, parseInt);

  bindSlider('paramHueMin', options.hue, 'min', 'valHueMin', '°', 1, parseInt);
  bindSlider('paramHueMax', options.hue, 'max', 'valHueMax', '°', 1, parseInt);
  bindSlider('paramBrightnessMin', options.brightness, 'min', 'valBrightnessMin', '%', 1, parseInt);
  bindSlider('paramBrightnessMax', options.brightness, 'max', 'valBrightnessMax', '%', 1, parseInt);
  bindSlider('paramDecayMin', options.decay, 'min', 'valDecayMin', '', 1, parseFloat);
  bindSlider('paramDecayMax', options.decay, 'max', 'valDecayMax', '', 1, parseFloat);

  bindSlider('paramVolume', options.sound, 'volume', 'valVolume', '%', 1, parseInt);

  document.getElementById('paramLineStyle').addEventListener('change', (e) => {
    options.lineStyle = e.target.value;
  });

  document.getElementById('paramSoundEnabled').addEventListener('change', (e) => {
    options.sound.enabled = e.target.checked;
    updateSoundButton();
  });

  document.getElementById('paramMouseClick').addEventListener('change', (e) => {
    options.mouse.click = e.target.checked;
  });

  document.getElementById('paramMouseMove').addEventListener('change', (e) => {
    options.mouse.move = e.target.checked;
  });

  // Preset Selector
  const presetSelect = document.getElementById('guiPresetSelect');
  presetSelect.addEventListener('change', (e) => {
    const key = e.target.value;
    const preset = presets[key];
    if (preset) {
      Object.assign(options, preset);
      syncUiFromOptions();
      let customShape = null;
      if (key === 'heart') customShape = 'heart';
      if (key === 'birthday') customShape = 'birthday';
      if (key === 'saturn') customShape = 'saturn';
      launchRocket(width / 2, height * 0.3, customShape);
    }
  });

  function syncUiFromOptions() {
    const set = (id, val, textId, suffix = '') => {
      const el = document.getElementById(id);
      if (el) el.value = val;
      const t = document.getElementById(textId);
      if (t) t.textContent = `${val}${suffix}`;
    };

    set('paramIntensity', options.intensity, 'valIntensity');
    set('paramRocketsPointMin', options.rocketsPoint.min, 'valRocketsPointMin', '%');
    set('paramRocketsPointMax', options.rocketsPoint.max, 'valRocketsPointMax', '%');
    set('paramDelayMin', options.delay.min, 'valDelayMin', 'ms');
    set('paramDelayMax', options.delay.max, 'valDelayMax', 'ms');
    set('paramAcceleration', options.acceleration, 'valAcceleration');
    set('paramFriction', options.friction, 'valFriction');
    set('paramGravity', options.gravity, 'valGravity');
    set('paramOpacity', options.opacity, 'valOpacity');
    set('paramParticles', options.particles, 'valParticles');
    set('paramExplosion', options.explosion, 'valExplosion');
    set('paramTraceLength', options.traceLength, 'valTraceLength');
    set('paramTraceSpeed', options.traceSpeed, 'valTraceSpeed');
    set('paramFlickering', options.flickering, 'valFlickering', '%');
    set('paramHueMin', options.hue.min, 'valHueMin', '°');
    set('paramHueMax', options.hue.max, 'valHueMax', '°');
    set('paramBrightnessMin', options.brightness.min, 'valBrightnessMin', '%');
    set('paramBrightnessMax', options.brightness.max, 'valBrightnessMax', '%');
    set('paramDecayMin', options.decay.min, 'valDecayMin');
    set('paramDecayMax', options.decay.max, 'valDecayMax');
    set('paramVolume', options.sound.volume, 'valVolume', '%');

    document.getElementById('paramLineStyle').value = options.lineStyle;
    document.getElementById('paramSoundEnabled').checked = options.sound.enabled;
    document.getElementById('paramMouseClick').checked = options.mouse.click;
    document.getElementById('paramMouseMove').checked = options.mouse.move;
    updateSoundButton();
  }

  // =========================================================
  // 7. Export Code & Configuration Generator Modal
  // =========================================================
  function generateExportCode() {
    const cleanConfig = {
      autoresize: true,
      opacity: options.opacity,
      acceleration: options.acceleration,
      friction: options.friction,
      gravity: options.gravity,
      particles: options.particles,
      traceLength: options.traceLength,
      traceSpeed: options.traceSpeed,
      explosion: options.explosion,
      intensity: options.intensity,
      flickering: options.flickering,
      lineStyle: options.lineStyle,
      hue: { min: options.hue.min, max: options.hue.max },
      delay: { min: options.delay.min, max: options.delay.max },
      rocketsPoint: { min: options.rocketsPoint.min, max: options.rocketsPoint.max },
      lineWidth: { explosion: { min: 1, max: 3 }, trace: { min: 1, max: 2 } },
      brightness: { min: options.brightness.min, max: options.brightness.max },
      decay: { min: options.decay.min, max: options.decay.max },
      mouse: { click: options.mouse.click, move: options.mouse.move, max: 1 }
    };

    return '<!-- 1. HTML Container -->\n' +
      '<div id="fireworks-container" style="width: 100vw; height: 100vh; background: #030712;"><' + '/div>\n\n' +
      '<!-- 2. Include Fire works.js -->\n' +
      '<script src="https://cdn.jsdelivr.net/npm/fireworks-js@2.10.8/dist/index.umd.js"><' + '/script>\n\n' +
      '<!-- 3. Initialize with your custom settings -->\n' +
      '<script>\n' +
      'const container = document.getElementById("fireworks-container");\n' +
      'const fireworks = new Fireworks.default(container, ' + JSON.stringify(cleanConfig, null, 2) + ');\n\n' +
      '// Start the fireworks simulator\n' +
      'fireworks.start();\n' +
      '<' + '/script>';
  }

  btnExportCode.addEventListener('click', () => {
    exportCodeOutput.textContent = generateExportCode();
    exportModal.classList.remove('hidden');
  });

  btnCloseModal.addEventListener('click', () => exportModal.classList.add('hidden'));
  btnDismissModal.addEventListener('click', () => exportModal.classList.add('hidden'));
  exportModal.addEventListener('click', (e) => {
    if (e.target === exportModal) exportModal.classList.add('hidden');
  });

  btnCopyConfig.addEventListener('click', function() {
    if (navigator.clipboard) {
      navigator.clipboard.writeText(exportCodeOutput.textContent).then(() => {
        this.innerHTML = '<i class="fas fa-check me-1"></i> Copied!';
        setTimeout(() => { this.innerHTML = '<i class="far fa-copy me-1"></i> Copy Code'; }, 1500);
      });
    }
  });

  // Launch welcome fireworks
  setTimeout(() => {
    launchRocket(width * 0.35, height * 0.3);
    launchRocket(width * 0.65, height * 0.3);
  }, 350);

  // Start animation
  requestAnimationFrame(render);
})();

3. Core JavaScript Concepts You Learned

Canvas 2D Rendering Engine

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Ballistic Particle Kinematics

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Tweakpane / GUI Control Panel

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Procedural Web Audio API

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Physics & Friction Simulation

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

Preset Configuration System

Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.

4. How to Run Locally on Your Computer

  1. Download the project ZIP: Click the Download Project (.ZIP) button at the top or in the live runner to save 3d-fireworks-simulator-project.zip.
  2. Extract the archive: Unzip the downloaded file. Inside you will find separate index.html, style.css, script.js, index-standalone.html, and a comprehensive README.md.
  3. Open in any browser: Simply double-click index.html (or index-standalone.html) to open and test immediately in Chrome, Firefox, Safari, or Edge.
  4. Edit in Visual Studio Code: Open the extracted folder in VS Code. Right-click index.html and select "Open with Live Server" to enjoy instant live reloading as you modify HTML, CSS, or JS!

Bonus Challenges for Learners

Ready to level up? Try adding these extra features on your own:

  • Add sound effects or audio feedback for user button clicks.
  • Add custom theme color customizer with CSS variables.
  • Export the results to PDF or copy as a formatted text summary.

Other Beginner Projects You Might Like

Advanced45 mins
Creative & 3D WebGL

3D Interactive Birthday Heart Tree Surprise

I turned HTML, CSS, and JavaScript into a special birthday surprise. An interactive 3D WebGL heart tree with pull-and-release slingshot, procedural branch growth, 600+ blooming heart petals, and music box melody.

Three.js WebGLProcedural 3D TreesPhysics Slingshot+3
Advanced45 mins
Creative & 3D WebGL

3D 'Add to Cart' Product Delivery Truck & Box Animation

Viral 3D WebGL e-commerce interaction: starts with a floating 3D 'Add to Cart 🛒' button in center stage. When clicked, it morphs into an unfolding cardboard box, glides to the side, and launches the parcel along a 3D parabolic trajectory curve into the open cargo bay of a yellow delivery truck, which seals its doors and drives off for express 24h delivery.

Three.js 3D WebGLInteractive 3D Button MorphingFlap Physics & Box Unfolding+4
Advanced35 mins
Creative & 3D WebGL

3D Radial Share Arc Button Animation

Viral UI component inspired by @_code_and_chill_: clicking the 'SHARE' capsule button triggers a smooth spring-physics radial arc splay of 5 social channel discs (𝕏, LinkedIn, WhatsApp, Email, Copy Link) using trigonometric CSS placement formulas.

Trigonometric Radial Arc Splay3D Interactive Perspective TiltSpring Physics & Harmonic Damping+3