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.
<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 & 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 & 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 & 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 & 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 & 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 & 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.
/* =========================================================
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:
(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
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
4. How to Run Locally on Your Computer
- 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. - Extract the archive: Unzip the downloaded file. Inside you will find separate
index.html,style.css,script.js,index-standalone.html, and a comprehensiveREADME.md. - Open in any browser: Simply double-click
index.html(orindex-standalone.html) to open and test immediately in Chrome, Firefox, Safari, or Edge. - Edit in Visual Studio Code: Open the extracted folder in VS Code. Right-click
index.htmland 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.