Skip to content

Instantly share code, notes, and snippets.

@zxhfighter
Created January 4, 2026 13:37
Show Gist options
  • Select an option

  • Save zxhfighter/bed91b9a2e673b77f63437364df0fc44 to your computer and use it in GitHub Desktop.

Select an option

Save zxhfighter/bed91b9a2e673b77f63437364df0fc44 to your computer and use it in GitHub Desktop.
awesome fireworks in a html file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Fireworks Display</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #000000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
#canvas-container {
position: relative;
width: 100vw;
height: 100vh;
}
canvas {
position: absolute;
top: 0;
left: 0;
}
#main-canvas {
z-index: 1;
}
#glow-canvas {
z-index: 2;
mix-blend-mode: screen;
pointer-events: none;
}
#controls {
position: fixed;
top: 20px;
left: 20px;
z-index: 100;
display: flex;
flex-direction: column;
gap: 10px;
}
.control-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.btn {
padding: 10px 16px;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
background: rgba(255, 255, 255, 0.1);
color: white;
font-size: 14px;
cursor: pointer;
backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
.btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.btn.active {
background: rgba(255, 200, 100, 0.3);
border-color: rgba(255, 200, 100, 0.8);
}
.btn.paused {
background: rgba(255, 50, 50, 0.4);
border-color: rgba(255, 50, 50, 0.8);
}
#top-controls {
position: fixed;
top: 20px;
right: 20px;
z-index: 100;
display: flex;
gap: 10px;
}
#sound-btn {
position: fixed;
top: 20px;
right: 20px;
}
#pause-btn {
position: fixed;
top: 20px;
right: 80px;
}
#top-controls .btn {
padding: 12px 20px;
font-size: 16px;
}
@media (max-width: 768px) {
#controls {
top: 70px;
}
.btn {
padding: 8px 12px;
font-size: 12px;
}
#sound-btn {
right: 10px;
}
#pause-btn {
right: 60px;
}
}
</style>
</head>
<body>
<div id="canvas-container">
<canvas id="main-canvas"></canvas>
<canvas id="glow-canvas"></canvas>
</div>
<button id="sound-btn" class="btn">🔊</button>
<button id="pause-btn" class="btn">⏸️</button>
<div id="controls">
<div class="control-group">
<button class="btn mode-btn active" data-mode="random">Random Shapes</button>
<button class="btn mode-btn" data-mode="rapid">Rapid Fire</button>
<button class="btn mode-btn" data-mode="finale">Grand Finale</button>
<button class="btn mode-btn" data-mode="auto">Auto Show</button>
</div>
<div class="control-group">
<button class="btn shape-btn" data-shape="heart">Heart</button>
<button class="btn shape-btn" data-shape="star">Star</button>
<button class="btn shape-btn" data-shape="ring">Ring</button>
<button class="btn shape-btn" data-shape="spiral">Spiral</button>
<button class="btn shape-btn" data-shape="flower">Flower</button>
<button class="btn shape-btn" data-shape="smiley">Smiley</button>
<button class="btn shape-btn" data-shape="frog">Frog</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script>
// Configuration
const CONFIG = {
GRAVITY: 0.03,
FRICTION: 0.99,
CHARACTER_SIZE: 90,
CHARACTER_DURATION: 120,
PARTICLES_PER_EXPLOSION: 500,
CHARACTER_PARTICLES: 150,
COLORS: [
'#ff1744', '#ff4081', '#e040fb', '#7c4dff',
'#536dfe', '#448aff', '#00b0ff', '#00e5ff',
'#1de9b6', '#00e676', '#76ff03', '#c6ff00',
'#ffea00', '#ffc400', '#ff9100', '#ff3d00'
]
};
// State
let canvas, ctx, glowCanvas, glowCtx;
let width, height;
let particles = [];
let rockets = [];
let stars = [];
let isPaused = false;
let soundEnabled = true;
let currentMode = 'random';
let lockedShape = null;
let autoShowInterval = null;
let audioContext = null;
let isMouseDown = false;
let lastLaunchTime = 0;
// Shape functions - return [[x1, y1], [x2, y2], ...] centered at (0,0)
function getHeartPoints(numPoints) {
const points = [];
for (let i = 0; i < numPoints; i++) {
const t = (i / numPoints) * Math.PI * 2;
const x = 16 * Math.pow(Math.sin(t), 3);
const y = -(13 * Math.cos(t) - 5 * Math.cos(2*t) - 2 * Math.cos(3*t) - Math.cos(4*t));
points.push([x * 2.5, y * 2.5]);
}
return points;
}
function getStarPoints(numPoints) {
const points = [];
const outerRadius = 50;
const innerRadius = 20;
const numVertices = 10; // 5 outer + 5 inner vertices
// Generate the 10 vertices of a 5-pointed star
const vertices = [];
for (let i = 0; i < numVertices; i++) {
const angle = (i / numVertices) * Math.PI * 2 - Math.PI / 2; // Rotate so star points up
const radius = (i % 2 === 0) ? outerRadius : innerRadius;
vertices.push([
Math.cos(angle) * radius,
Math.sin(angle) * radius
]);
}
// Interpolate between vertices to get numPoints
const pointsPerEdge = Math.floor(numPoints / numVertices);
for (let i = 0; i < numVertices; i++) {
const currentVertex = vertices[i];
const nextVertex = vertices[(i + 1) % numVertices];
for (let j = 0; j < pointsPerEdge; j++) {
const t = j / pointsPerEdge;
const x = currentVertex[0] + (nextVertex[0] - currentVertex[0]) * t;
const y = currentVertex[1] + (nextVertex[1] - currentVertex[1]) * t;
points.push([x, y]);
}
}
return points;
}
function getRingPoints(numPoints) {
const points = [];
const radius = 50;
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 2;
points.push([
Math.cos(angle) * radius,
Math.sin(angle) * radius
]);
}
return points;
}
function getSpiralPoints(numPoints) {
const points = [];
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 6;
const radius = (i / numPoints) * 50;
points.push([
Math.cos(angle) * radius,
Math.sin(angle) * radius
]);
}
return points;
}
function getDoubleRingPoints(numPoints) {
const points = [];
const halfPoints = Math.floor(numPoints / 2);
// Outer ring
for (let i = 0; i < halfPoints; i++) {
const angle = (i / halfPoints) * Math.PI * 2;
points.push([
Math.cos(angle) * 50,
Math.sin(angle) * 50
]);
}
// Inner ring
for (let i = 0; i < halfPoints; i++) {
const angle = (i / halfPoints) * Math.PI * 2;
points.push([
Math.cos(angle) * 25,
Math.sin(angle) * 25
]);
}
return points;
}
function getDiamondPoints(numPoints) {
const points = [];
const pointsPerSide = numPoints / 4;
// Top to right
for (let i = 0; i < pointsPerSide; i++) {
const t = i / pointsPerSide;
points.push([t * 50, -(1 - t) * 50]);
}
// Right to bottom
for (let i = 0; i < pointsPerSide; i++) {
const t = i / pointsPerSide;
points.push([(1 - t) * 50, t * 50]);
}
// Bottom to left
for (let i = 0; i < pointsPerSide; i++) {
const t = i / pointsPerSide;
points.push([-t * 50, (1 - t) * 50]);
}
// Left to top
for (let i = 0; i < pointsPerSide; i++) {
const t = i / pointsPerSide;
points.push([-(1 - t) * 50, -t * 50]);
}
return points;
}
function getFlowerPoints(numPoints) {
const points = [];
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 2;
const r = 30 + 20 * Math.cos(6 * angle);
points.push([
Math.cos(angle) * r,
Math.sin(angle) * r
]);
}
return points;
}
function getSmileyPoints(numPoints) {
const points = [];
// Face circle (50% of points)
const facePoints = Math.floor(numPoints * 0.45);
const faceRadius = 50;
for (let i = 0; i < facePoints; i++) {
const angle = (i / facePoints) * Math.PI * 2;
points.push([
Math.cos(angle) * faceRadius,
-Math.sin(angle) * faceRadius
]);
}
// Left eye (15% of points)
const eyePoints = Math.floor(numPoints * 0.15);
const eyeRadius = 8;
const leftEyeX = -18;
const leftEyeY = -10;
for (let i = 0; i < eyePoints; i++) {
const angle = (i / eyePoints) * Math.PI * 2;
points.push([
leftEyeX + Math.cos(angle) * eyeRadius,
leftEyeY - Math.sin(angle) * eyeRadius
]);
}
// Right eye (15% of points)
const rightEyeX = 18;
const rightEyeY = -10;
for (let i = 0; i < eyePoints; i++) {
const angle = (i / eyePoints) * Math.PI * 2;
points.push([
rightEyeX + Math.cos(angle) * eyeRadius,
rightEyeY - Math.sin(angle) * eyeRadius
]);
}
// Smile mouth (25% of points) - arc from 0 to PI
const mouthPoints = numPoints - facePoints - eyePoints * 2;
const mouthRadius = 25;
const mouthY = 10;
for (let i = 0; i < mouthPoints; i++) {
const angle = (i / (mouthPoints - 1)) * Math.PI;
points.push([
Math.cos(angle) * mouthRadius,
mouthY + Math.sin(angle) * mouthRadius
]);
}
return points;
}
function getFrogPoints(numPoints) {
const points = [];
// Head ellipse (40% of points)
const headPoints = Math.floor(numPoints * 0.40);
const headRadiusX = 50;
const headRadiusY = 40;
for (let i = 0; i < headPoints; i++) {
const angle = (i / headPoints) * Math.PI * 2;
points.push([
Math.cos(angle) * headRadiusX,
-Math.sin(angle) * headRadiusY
]);
}
// Left eye (20% of points) - prominent and sticking out
const leftEyePoints = Math.floor(numPoints * 0.20);
const leftEyeRadius = 18;
const leftEyeX = -28;
const leftEyeY = -45;
for (let i = 0; i < leftEyePoints; i++) {
const angle = (i / leftEyePoints) * Math.PI * 2;
points.push([
leftEyeX + Math.cos(angle) * leftEyeRadius,
leftEyeY - Math.sin(angle) * leftEyeRadius
]);
}
// Right eye (20% of points) - prominent and sticking out
const rightEyePoints = Math.floor(numPoints * 0.20);
const rightEyeRadius = 18;
const rightEyeX = 28;
const rightEyeY = -45;
for (let i = 0; i < rightEyePoints; i++) {
const angle = (i / rightEyePoints) * Math.PI * 2;
points.push([
rightEyeX + Math.cos(angle) * rightEyeRadius,
rightEyeY - Math.sin(angle) * rightEyeRadius
]);
}
// Smile mouth (20% of points) - wide smile arc inside head
const mouthPoints = numPoints - headPoints - leftEyePoints - rightEyePoints;
const mouthRadius = 25;
const mouthY = -10;
for (let i = 0; i < mouthPoints; i++) {
const angle = Math.PI * 0.15 + (i / (mouthPoints - 1)) * Math.PI * 0.7;
points.push([
Math.cos(angle) * mouthRadius,
mouthY + Math.sin(angle) * mouthRadius
]);
}
return points;
}
function getShapePoints(shapeType, numPoints) {
switch (shapeType) {
case 'heart': return getHeartPoints(numPoints);
case 'star': return getStarPoints(numPoints);
case 'ring': return getRingPoints(numPoints);
case 'spiral': return getSpiralPoints(numPoints);
case 'doubleRing': return getDoubleRingPoints(numPoints);
case 'diamond': return getDiamondPoints(numPoints);
case 'flower': return getFlowerPoints(numPoints);
case 'smiley': return getSmileyPoints(numPoints);
case 'frog': return getFrogPoints(numPoints);
default: return null;
}
}
// Audio functions
function initAudio() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
}
function playLaunchSound() {
if (!soundEnabled || !audioContext) return;
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.type = 'sine';
oscillator.frequency.setValueAtTime(150, audioContext.currentTime);
oscillator.frequency.exponentialRampToValueAtTime(300, audioContext.currentTime + 0.5);
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.5);
}
function playExplosionSound() {
if (!soundEnabled || !audioContext) return;
const bufferSize = audioContext.sampleRate * 0.5;
const buffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * 0.5;
}
const source = audioContext.createBufferSource();
source.buffer = buffer;
const filter = audioContext.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.setValueAtTime(1000, audioContext.currentTime);
filter.frequency.exponentialRampToValueAtTime(100, audioContext.currentTime + 0.5);
const gainNode = audioContext.createGain();
gainNode.gain.setValueAtTime(0.15, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
source.connect(filter);
filter.connect(gainNode);
gainNode.connect(audioContext.destination);
source.start();
source.stop(audioContext.currentTime + 0.5);
}
// Particle class
class Particle {
constructor(x, y, color, options = {}) {
this.x = x;
this.y = y;
this.vx = options.vx || 0;
this.vy = options.vy || 0;
this.color = color;
this.alpha = 1;
this.size = 2.5;
this.isShapeParticle = options.isShapeParticle || false;
this.shapeFrame = 0;
this.targetVx = options.targetVx || (Math.random() - 0.5) * 10;
this.targetVy = options.targetVy || (Math.random() - 0.5) * 10;
this.trail = [];
this.maxTrail = 8;
this.sparkle = options.sparkle || (Math.random() < 0.3);
this.sparklePhase = Math.random() * Math.PI * 2;
}
update() {
// Store trail position
this.trail.push({ x: this.x, y: this.y, alpha: this.alpha });
if (this.trail.length > this.maxTrail) {
this.trail.shift();
}
if (this.isShapeParticle) {
this.shapeFrame++;
// Shimmer effect during formation
if (this.shapeFrame < CONFIG.CHARACTER_DURATION) {
this.x += (Math.random() - 0.5) * 0.2;
this.y += (Math.random() - 0.5) * 0.2;
// Gentle gravity during formation (0.1x normal)
this.vy = CONFIG.GRAVITY * 0.1;
this.y += this.vy;
// After 70% of formation, start transitioning
if (this.shapeFrame > CONFIG.CHARACTER_DURATION * 0.7) {
const transitionProgress = (this.shapeFrame - CONFIG.CHARACTER_DURATION * 0.7) / (CONFIG.CHARACTER_DURATION * 0.3);
this.vx = this.targetVx * transitionProgress * 0.3;
this.vy = this.targetVy * transitionProgress * 0.3;
this.x += this.vx;
this.y += this.vy;
}
this.alpha -= 0.004;
} else {
// Formation ended, switch to normal physics
this.vx = this.targetVx;
this.vy = this.targetVy;
this.isShapeParticle = false;
}
} else {
// Normal physics
this.vy += CONFIG.GRAVITY;
this.vx *= CONFIG.FRICTION;
this.vy *= CONFIG.FRICTION;
this.x += this.vx;
this.y += this.vy;
this.alpha -= 0.008;
}
// Update sparkle phase
this.sparklePhase += 0.1;
}
draw(ctx) {
// Draw trail
if (this.trail.length > 1) {
ctx.beginPath();
ctx.moveTo(this.trail[0].x, this.trail[0].y);
for (let i = 1; i < this.trail.length; i++) {
ctx.lineTo(this.trail[i].x, this.trail[i].y);
}
ctx.strokeStyle = this.color;
ctx.lineWidth = this.size * 0.5;
ctx.globalAlpha = this.alpha * 0.3;
ctx.stroke();
}
// Draw particle
let sparkleIntensity = 1;
if (this.sparkle) {
sparkleIntensity = 0.5 + 0.5 * Math.sin(this.sparklePhase);
}
ctx.beginPath();
ctx.arc(this.x, this.y, this.size * sparkleIntensity, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.globalAlpha = this.alpha;
ctx.fill();
ctx.globalAlpha = 1;
}
}
// Rocket class
class Rocket {
constructor(x, y, targetX, targetY) {
this.x = x;
this.y = y;
this.targetX = targetX;
this.targetY = targetY;
const dx = targetX - x;
const dy = targetY - y;
const distance = Math.sqrt(dx * dx + dy * dy);
const speed = Math.min(distance / 40, 15);
const angle = Math.atan2(dy, dx);
this.vx = Math.cos(angle) * speed;
this.vy = Math.sin(angle) * speed;
this.trail = [];
this.maxTrail = 15;
this.color = '#ffffff';
this.exploded = false;
playLaunchSound();
}
update() {
this.trail.push({ x: this.x, y: this.y });
if (this.trail.length > this.maxTrail) {
this.trail.shift();
}
// Calculate distance to target
const dx = this.targetX - this.x;
const dy = this.targetY - this.y;
const distanceToTarget = Math.sqrt(dx * dx + dy * dy);
this.vy += CONFIG.GRAVITY;
this.x += this.vx;
this.y += this.vy;
// Explode when:
// 1. Close enough to target (within 30px), OR
// 2. Moving away from target (reached peak and passed it)
const newDx = this.targetX - this.x;
const newDy = this.targetY - this.y;
const newDistanceToTarget = Math.sqrt(newDx * newDx + newDy * newDy);
if (distanceToTarget <= 30 || (this.vy >= 0 && newDistanceToTarget > distanceToTarget)) {
this.explode();
return false;
}
return true;
}
explode() {
this.exploded = true;
playExplosionSound();
// Screen shake
const container = document.getElementById('canvas-container');
gsap.to(container, {
x: (Math.random() - 0.5) * 10,
y: (Math.random() - 0.5) * 10,
duration: 0.05,
repeat: 3,
yoyo: true,
onComplete: () => {
gsap.set(container, { x: 0, y: 0 });
}
});
// Determine shape type
let shapeType = lockedShape;
if (!shapeType || currentMode === 'random') {
const shapes = ['heart', 'star', 'ring', 'spiral', 'doubleRing', 'diamond', 'flower', 'smiley', 'frog'];
shapeType = shapes[Math.floor(Math.random() * shapes.length)];
}
// Create shape particles (white, form shape)
const shapePoints = getShapePoints(shapeType, CONFIG.CHARACTER_PARTICLES);
if (shapePoints) {
for (const point of shapePoints) {
const px = this.x + point[0] * (CONFIG.CHARACTER_SIZE / 50);
const py = this.y + point[1] * (CONFIG.CHARACTER_SIZE / 50);
particles.push(new Particle(px, py, '#ffffff', {
isShapeParticle: true,
targetVx: (Math.random() - 0.5) * 10,
targetVy: (Math.random() - 0.5) * 10,
sparkle: Math.random() < 0.3
}));
}
}
// Create burst particles (colored, explode immediately)
const burstCount = CONFIG.PARTICLES_PER_EXPLOSION;
// Random scale for this explosion (0.3 to 1)
const explosionScale = 0.3 + Math.random() * 0.7;
for (let i = 0; i < burstCount; i++) {
const angle = (i / burstCount) * Math.PI * 2;
const speed = (Math.random() * 8 + 2) * explosionScale;
const color = CONFIG.COLORS[Math.floor(Math.random() * CONFIG.COLORS.length)];
particles.push(new Particle(this.x, this.y, color, {
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
sparkle: Math.random() < 0.3
}));
}
}
draw(ctx) {
// Draw trail
if (this.trail.length > 1) {
ctx.beginPath();
ctx.moveTo(this.trail[0].x, this.trail[0].y);
for (let i = 1; i < this.trail.length; i++) {
ctx.lineTo(this.trail[i].x, this.trail[i].y);
}
ctx.strokeStyle = this.color;
ctx.lineWidth = 2;
ctx.globalAlpha = 0.6;
ctx.stroke();
ctx.globalAlpha = 1;
}
// Draw rocket
ctx.beginPath();
ctx.arc(this.x, this.y, 3, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
}
}
// Initialize stars
function initStars() {
stars = [];
for (let i = 0; i < 80; i++) {
stars.push({
x: Math.random() * width,
y: Math.random() * height,
size: Math.random() * 2,
twinkleOffset: Math.random() * Math.PI * 2
});
}
}
// Draw stars
function drawStars(ctx, time) {
for (const star of stars) {
const twinkle = 0.3 + 0.7 * (0.5 + 0.5 * Math.sin(time * 0.002 + star.twinkleOffset));
ctx.beginPath();
ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255, 255, 255, ${twinkle})`;
ctx.fill();
}
}
// Launch firework
function launchFirework(targetX, targetY) {
const startX = width / 2;
const startY = height;
rockets.push(new Rocket(startX, startY, targetX, targetY));
}
// Grand finale
function triggerGrandFinale() {
for (let i = 0; i < 20; i++) {
setTimeout(() => {
if (isPaused) return;
const x = Math.random() * width * 0.6 + width * 0.2;
const y = Math.random() * height * 0.4 + height * 0.1;
launchFirework(x, y);
}, i * 100);
}
}
// Auto show
function startAutoShow() {
stopAutoShow();
autoShowInterval = setInterval(() => {
if (isPaused) return;
const x = Math.random() * width * 0.6 + width * 0.2;
const y = Math.random() * height * 0.4 + height * 0.1;
launchFirework(x, y);
}, 800);
}
function stopAutoShow() {
if (autoShowInterval) {
clearInterval(autoShowInterval);
autoShowInterval = null;
}
}
// Resize handler
function resize() {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
glowCanvas.width = width;
glowCanvas.height = height;
initStars();
}
// Animation loop
let lastTime = 0;
function animate(time) {
requestAnimationFrame(animate);
if (isPaused) return;
const deltaTime = time - lastTime;
lastTime = time;
// Clear canvases
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.fillRect(0, 0, width, height);
glowCtx.clearRect(0, 0, width, height);
// Draw stars
drawStars(glowCtx, time);
// Update and draw rockets
rockets = rockets.filter(rocket => {
const alive = rocket.update();
rocket.draw(ctx);
rocket.draw(glowCtx);
return alive;
});
// Update and draw particles
particles = particles.filter(particle => {
particle.update();
if (particle.alpha > 0) {
particle.draw(ctx);
particle.draw(glowCtx);
return true;
}
return false;
});
}
// Setup controls
function setupControls() {
// Pause button
const pauseBtn = document.getElementById('pause-btn');
pauseBtn.addEventListener('click', () => {
isPaused = !isPaused;
pauseBtn.textContent = isPaused ? '▶️' : '⏸️';
pauseBtn.classList.toggle('paused', isPaused);
if (isPaused) {
stopAutoShow();
} else if (currentMode === 'auto') {
startAutoShow();
}
});
// Sound button
const soundBtn = document.getElementById('sound-btn');
soundBtn.addEventListener('click', () => {
soundEnabled = !soundEnabled;
soundBtn.textContent = soundEnabled ? '🔊' : '🔇';
if (soundEnabled) {
initAudio();
}
});
// Mode buttons
const modeBtns = document.querySelectorAll('.mode-btn');
modeBtns.forEach(btn => {
btn.addEventListener('click', () => {
stopAutoShow();
modeBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentMode = btn.dataset.mode;
if (currentMode === 'auto') {
startAutoShow();
}
});
});
// Shape buttons
const shapeBtns = document.querySelectorAll('.shape-btn');
shapeBtns.forEach(btn => {
btn.addEventListener('click', () => {
const shape = btn.dataset.shape;
if (lockedShape === shape) {
// Unlock
lockedShape = null;
btn.classList.remove('active');
} else {
// Lock new shape
lockedShape = shape;
shapeBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
});
});
// Canvas click/touch
canvas.addEventListener('mousedown', (e) => {
isMouseDown = true;
handleLaunch(e);
});
canvas.addEventListener('mousemove', (e) => {
if (isMouseDown && currentMode === 'rapid') {
const now = Date.now();
if (now - lastLaunchTime > 50) {
handleLaunch(e);
lastLaunchTime = now;
}
}
});
canvas.addEventListener('mouseup', () => {
isMouseDown = false;
});
canvas.addEventListener('mouseleave', () => {
isMouseDown = false;
});
// Touch events
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
isMouseDown = true;
handleLaunch(e.touches[0]);
});
canvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (isMouseDown && currentMode === 'rapid') {
const now = Date.now();
if (now - lastLaunchTime > 50) {
handleLaunch(e.touches[0]);
lastLaunchTime = now;
}
}
});
canvas.addEventListener('touchend', () => {
isMouseDown = false;
});
}
function handleLaunch(e) {
if (isPaused) return;
initAudio();
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (currentMode === 'finale') {
triggerGrandFinale();
} else {
launchFirework(x, y);
}
}
// Initialize
function init() {
canvas = document.getElementById('main-canvas');
ctx = canvas.getContext('2d');
glowCanvas = document.getElementById('glow-canvas');
glowCtx = glowCanvas.getContext('2d');
resize();
window.addEventListener('resize', resize);
setupControls();
// Initial firework - only ONE on page load
setTimeout(() => {
initAudio();
launchFirework(width / 2, height * 0.35);
}, 500);
animate(0);
}
// Start when page loads
window.addEventListener('load', init);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment