Copy URL or Code
Paste to your AI coding assistant and customize:
Done. Your AI handles the rest.
Fully customizable. Features Three.js particle system with bloom effects, deep bass audio, and 4 color schemes. Perfect for celebrations and landing pages.
"use client";
import React, { useRef, useEffect, useState, useCallback } from "react";
export interface BoomBoomSkyProps {
width?: number;
height?: number;
className?: string;
autoStart?: boolean;
showOverlay?: boolean;
enableSound?: boolean;
colorScheme?: "vivid" | "pastel" | "warm" | "cool";
burstInterval?: number;
sparkCount?: number;
}
const COLOR_PALETTES = {
vivid: { saturation: 1.0, lightness: 0.6 },
pastel: { saturation: 0.6, lightness: 0.75 },
warm: { saturation: 0.9, lightness: 0.55, hueRange: [0, 0.15] },
cool: { saturation: 0.85, lightness: 0.6, hueRange: [0.5, 0.75] },
};
export default function BoomBoomSky({
width = 400,
height = 400,
className = "",
autoStart = false,
showOverlay = true,
enableSound = true,
colorScheme = "vivid",
burstInterval = 3200,
sparkCount = 18500,
}: BoomBoomSkyProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [isStarted, setIsStarted] = useState(autoStart);
const cleanupRef = useRef<(() => void) | null>(null);
const handleStart = useCallback(() => {
setIsStarted(true);
}, []);
useEffect(() => {
if (!isStarted || !containerRef.current) return;
let isMounted = true;
const initScene = async () => {
const THREE = await import("three");
const { EffectComposer } = await import("three/examples/jsm/postprocessing/EffectComposer.js");
const { RenderPass } = await import("three/examples/jsm/postprocessing/RenderPass.js");
const { UnrealBloomPass } = await import("three/examples/jsm/postprocessing/UnrealBloomPass.js");
if (!isMounted || !containerRef.current) return;
const SoundEngine = {
audioCtx: null as AudioContext | null,
active: false,
gain: 0.55,
compressor: null as DynamicsCompressorNode | null,
activate() {
if (!this.audioCtx) {
this.audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)();
this.compressor = this.audioCtx.createDynamicsCompressor();
this.compressor.threshold.value = -12;
this.compressor.knee.value = 35;
this.compressor.ratio.value = 10;
this.compressor.connect(this.audioCtx.destination);
this.active = true;
}
if (this.audioCtx.state === "suspended") this.audioCtx.resume();
},
boom() {
if (!this.active || !this.audioCtx || !this.compressor) return;
const now = this.audioCtx.currentTime;
const bass = this.audioCtx.createOscillator();
const bassVol = this.audioCtx.createGain();
bass.type = "sine";
bass.frequency.setValueAtTime(45, now);
bass.frequency.exponentialRampToValueAtTime(18, now + 2.8);
bassVol.gain.setValueAtTime(this.gain * 1.4, now);
bassVol.gain.exponentialRampToValueAtTime(0.01, now + 4.5);
bass.connect(bassVol);
bassVol.connect(this.compressor);
bass.start(now);
bass.stop(now + 4.5);
const rumbleLen = this.audioCtx.sampleRate * 4.5;
const rumbleBuf = this.audioCtx.createBuffer(1, rumbleLen, this.audioCtx.sampleRate);
const rumbleData = rumbleBuf.getChannelData(0);
for (let i = 0; i < rumbleLen; i++) rumbleData[i] = Math.random() * 2 - 1;
const rumble = this.audioCtx.createBufferSource();
rumble.buffer = rumbleBuf;
const rumbleLP = this.audioCtx.createBiquadFilter();
rumbleLP.type = "lowpass";
rumbleLP.frequency.setValueAtTime(140, now);
rumbleLP.frequency.exponentialRampToValueAtTime(25, now + 3.8);
const rumbleVol = this.audioCtx.createGain();
rumbleVol.gain.setValueAtTime(this.gain * 0.9, now);
rumbleVol.gain.exponentialRampToValueAtTime(0.01, now + 4.2);
rumble.connect(rumbleLP);
rumbleLP.connect(rumbleVol);
rumbleVol.connect(this.compressor);
rumble.start(now);
const snap = this.audioCtx.createOscillator();
snap.type = "triangle";
const snapVol = this.audioCtx.createGain();
snap.frequency.setValueAtTime(180, now);
snap.frequency.exponentialRampToValueAtTime(45, now + 0.12);
snapVol.gain.setValueAtTime(this.gain * 0.25, now);
snapVol.gain.exponentialRampToValueAtTime(0.01, now + 0.12);
snap.connect(snapVol);
snapVol.connect(this.compressor);
snap.start(now);
snap.stop(now + 0.12);
},
};
function createGlowTexture() {
const canvas = document.createElement("canvas");
canvas.width = 32;
canvas.height = 32;
const ctx = canvas.getContext("2d")!;
const grad = ctx.createRadialGradient(16, 16, 0, 16, 16, 16);
grad.addColorStop(0, "rgba(255,255,255,1)");
grad.addColorStop(0.25, "rgba(255,255,255,0.92)");
grad.addColorStop(0.55, "rgba(255,255,255,0.45)");
grad.addColorStop(1, "rgba(0,0,0,0)");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, 32, 32);
return new THREE.CanvasTexture(canvas);
}
const glowSprite = createGlowTexture();
const SETTINGS = {
sparkCount: Math.min(sparkCount, 25000),
sparkSize: 0.75,
decayRate: 0.0052,
burstForce: 3.2,
suspendTime: 1.35,
pullDown: 0.0028,
drag: 0.952,
launcherSpeed: 0.95,
launcherSize: 1.8,
glowIntensity: 1.55,
glowSpread: 0.45,
trailFade: 0.42,
autoFire: true,
fireInterval: burstInterval,
soundOn: enableSound,
};
const container = containerRef.current;
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x000000, 0.0018);
const cam = new THREE.PerspectiveCamera(60, width / height, 0.1, 4000);
cam.position.set(0, 0, 150);
const renderer = new THREE.WebGLRenderer({ antialias: false, preserveDrawingBuffer: true });
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
renderer.autoClearColor = false;
container.appendChild(renderer.domElement);
const renderPass = new RenderPass(scene, cam);
const bloomEffect = new UnrealBloomPass(new THREE.Vector2(width, height), SETTINGS.glowIntensity, SETTINGS.glowSpread, 0.0);
const postProcessor = new EffectComposer(renderer);
postProcessor.addPass(renderPass);
postProcessor.addPass(bloomEffect);
const starGeo = new THREE.BufferGeometry();
const starPositions = new Float32Array(2500 * 3);
for (let i = 0; i < 2500 * 3; i++) starPositions[i] = (Math.random() - 0.5) * 1100;
starGeo.setAttribute("position", new THREE.BufferAttribute(starPositions, 3));
const starMat = new THREE.PointsMaterial({ size: 1.3, color: 0x999999, map: glowSprite, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending });
scene.add(new THREE.Points(starGeo, starMat));
const fadePlane = new THREE.Mesh(new THREE.PlaneGeometry(4000, 4000), new THREE.MeshBasicMaterial({ color: 0x000000, transparent: true, opacity: SETTINGS.trailFade }));
fadePlane.position.z = cam.position.z - 50;
fadePlane.lookAt(cam.position);
scene.add(fadePlane);
const palette = COLOR_PALETTES[colorScheme];
function getHue(): number {
if ("hueRange" in palette) {
const [min, max] = palette.hueRange as [number, number];
return min + Math.random() * (max - min);
}
return Math.random();
}
class SkyBurst {
finished = false;
stage: "launch" | "burst" = "launch";
elapsed = 0;
hues: any[] = [];
position: any;
velocity: any;
peakY: number = 0;
projectile: any;
particles: any = null;
activeCount = 0;
speeds: Float32Array = new Float32Array(0);
lives: Float32Array = new Float32Array(0);
tints: Float32Array = new Float32Array(0);
constructor(originX: number) {
const primaryHue = getHue();
const colorRoll = Math.random();
if (colorRoll < 0.35) {
this.hues.push(new THREE.Color().setHSL(primaryHue, palette.saturation, palette.lightness));
} else if (colorRoll < 0.7) {
this.hues.push(new THREE.Color().setHSL(primaryHue, palette.saturation, palette.lightness));
this.hues.push(new THREE.Color().setHSL((primaryHue + 0.5) % 1.0, palette.saturation, palette.lightness * 0.9));
} else {
this.hues.push(new THREE.Color().setHSL(primaryHue, palette.saturation, palette.lightness));
this.hues.push(new THREE.Color().setHSL((primaryHue + 0.33) % 1.0, palette.saturation, palette.lightness));
this.hues.push(new THREE.Color().setHSL((primaryHue + 0.66) % 1.0, palette.saturation, palette.lightness));
}
this.position = new THREE.Vector3(originX, -75, (Math.random() - 0.5) * 45);
this.velocity = new THREE.Vector3((Math.random() - 0.5) * 0.45, SETTINGS.launcherSpeed * (0.88 + Math.random() * 0.28), (Math.random() - 0.5) * 0.45);
this.peakY = -8 + Math.random() * 28;
const projGeo = new THREE.BufferGeometry();
projGeo.setAttribute("position", new THREE.Float32BufferAttribute(this.position.toArray(), 3));
this.projectile = new THREE.Points(projGeo, new THREE.PointsMaterial({ size: SETTINGS.launcherSize, color: this.hues[0], map: glowSprite, transparent: true, depthWrite: false, blending: THREE.AdditiveBlending }));
scene.add(this.projectile);
}
tick(delta: number) {
if (this.particles) (this.particles.material as any).size = SETTINGS.sparkSize;
if (this.projectile) (this.projectile.material as any).size = SETTINGS.launcherSize;
if (this.stage === "launch") {
this.position.add(this.velocity);
this.velocity.y *= 0.988;
this.projectile.geometry.attributes.position.setXYZ(0, this.position.x, this.position.y, this.position.z);
this.projectile.geometry.attributes.position.needsUpdate = true;
if (this.velocity.y < 0.18 || this.position.y >= this.peakY) this.detonate();
} else {
this.elapsed += delta;
const posArr = this.particles.geometry.attributes.position.array as Float32Array;
const colArr = this.particles.geometry.attributes.color.array as Float32Array;
let remaining = 0;
const suspended = this.elapsed < SETTINGS.suspendTime;
const gravityMult = THREE.MathUtils.smoothstep(this.elapsed, SETTINGS.suspendTime, SETTINGS.suspendTime + 0.45);
for (let i = 0; i < this.activeCount; i++) {
if (this.lives[i] > 0) {
remaining++;
const idx = i * 3;
posArr[idx] += this.speeds[idx];
posArr[idx + 1] += this.speeds[idx + 1];
posArr[idx + 2] += this.speeds[idx + 2];
if (suspended) {
this.speeds[idx] *= SETTINGS.drag;
this.speeds[idx + 1] *= SETTINGS.drag;
this.speeds[idx + 2] *= SETTINGS.drag;
} else {
this.speeds[idx + 1] -= SETTINGS.pullDown * gravityMult;
this.speeds[idx] *= 0.978;
this.speeds[idx + 1] *= 0.978;
this.speeds[idx + 2] *= 0.978;
this.lives[i] -= SETTINGS.decayRate;
}
const fade = Math.max(0, this.lives[i]);
colArr[idx] = this.tints[idx] * fade * 1.45;
colArr[idx + 1] = this.tints[idx + 1] * fade * 1.45;
colArr[idx + 2] = this.tints[idx + 2] * fade * 1.45;
}
}
this.particles.geometry.attributes.position.needsUpdate = true;
this.particles.geometry.attributes.color.needsUpdate = true;
if (remaining === 0) this.dispose();
}
}
detonate() {
if (SETTINGS.soundOn) SoundEngine.boom();
scene.remove(this.projectile);
this.stage = "burst";
this.elapsed = 0;
this.activeCount = SETTINGS.sparkCount;
const geo = new THREE.BufferGeometry();
const positions = new Float32Array(this.activeCount * 3);
const colors = new Float32Array(this.activeCount * 3);
this.tints = new Float32Array(this.activeCount * 3);
this.speeds = new Float32Array(this.activeCount * 3);
this.lives = new Float32Array(this.activeCount);
const power = SETTINGS.burstForce * (0.82 + Math.random() * 0.38);
for (let i = 0; i < this.activeCount; i++) {
const idx = i * 3;
positions[idx] = this.position.x;
positions[idx + 1] = this.position.y;
positions[idx + 2] = this.position.z;
const azimuth = Math.random() * Math.PI * 2;
const inclination = Math.acos(2 * Math.random() - 1);
const magnitude = power * (0.78 + Math.random() * 0.42);
this.speeds[idx] = magnitude * Math.sin(inclination) * Math.cos(azimuth);
this.speeds[idx + 1] = magnitude * Math.sin(inclination) * Math.sin(azimuth);
this.speeds[idx + 2] = magnitude * Math.cos(inclination);
const chosenHue = this.hues[Math.floor(Math.random() * this.hues.length)];
const luminance = 0.55 + Math.random() * 0.75;
this.tints[idx] = chosenHue.r * luminance;
this.tints[idx + 1] = chosenHue.g * luminance;
this.tints[idx + 2] = chosenHue.b * luminance;
colors[idx] = this.tints[idx];
colors[idx + 1] = this.tints[idx + 1];
colors[idx + 2] = this.tints[idx + 2];
this.lives[i] = 1.0;
}
geo.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geo.setAttribute("color", new THREE.BufferAttribute(colors, 3));
this.particles = new THREE.Points(geo, new THREE.PointsMaterial({ size: SETTINGS.sparkSize, map: glowSprite, transparent: true, depthWrite: false, vertexColors: true, blending: THREE.AdditiveBlending }));
scene.add(this.particles);
}
dispose() {
this.finished = true;
if (this.particles) { scene.remove(this.particles); this.particles.geometry.dispose(); (this.particles.material as any).dispose(); }
if (this.projectile) { scene.remove(this.projectile); this.projectile.geometry.dispose(); (this.projectile.material as any).dispose(); }
}
}
const bursts: SkyBurst[] = [];
let lastFireTime = 0;
let nextFireDelay = 0;
function fireBurst() { bursts.push(new SkyBurst((Math.random() - 0.5) * 140)); }
function checkAutoFire(time: number) {
if (!SETTINGS.autoFire) return;
if (time - lastFireTime > nextFireDelay) { lastFireTime = time; nextFireDelay = SETTINGS.fireInterval + Math.random() * 800; fireBurst(); }
}
const timer = new THREE.Clock();
let frameId: number;
function loop() {
frameId = requestAnimationFrame(loop);
const dt = timer.getDelta();
checkAutoFire(performance.now());
for (let i = bursts.length - 1; i >= 0; i--) { bursts[i].tick(dt); if (bursts[i].finished) bursts.splice(i, 1); }
postProcessor.render();
}
SoundEngine.activate();
fireBurst();
loop();
cleanupRef.current = () => {
cancelAnimationFrame(frameId);
renderer.dispose();
postProcessor.dispose();
scene.clear();
if (container.contains(renderer.domElement)) container.removeChild(renderer.domElement);
};
};
initScene();
return () => { isMounted = false; if (cleanupRef.current) cleanupRef.current(); };
}, [isStarted, width, height, enableSound, colorScheme, burstInterval, sparkCount]);
return (
<div className={`relative ${className}`} style={{ width, height, backgroundColor: "#000" }}>
<div ref={containerRef} style={{ width: "100%", height: "100%", overflow: "hidden" }} />
{showOverlay && !isStarted && (
<div onClick={handleStart} style={{ position: "absolute", top: 0, left: 0, width: "100%", height: "100%", background: "rgba(0,0,0,0.85)", display: "flex", justifyContent: "center", alignItems: "center", color: "white", fontFamily: "system-ui, sans-serif", fontSize: "16px", cursor: "pointer", zIndex: 1000, flexDirection: "column", textAlign: "center", borderRadius: "inherit" }}>
<div style={{ fontWeight: 500 }}>CLICK TO START</div>
<span style={{ fontSize: "11px", color: "#777", marginTop: "12px" }}>Fireworks with <span style={{ color: "#ff9500", fontWeight: 600 }}>Deep Boom Sound</span></span>
</div>
)}
</div>
);
}
export { BoomBoomSky };import BoomBoomSky from "@/components/BoomBoomSky";
// Basic usage with click overlay
<BoomBoomSky width={600} height={400} />
// Auto-start without overlay
<BoomBoomSky width={800} height={600} autoStart showOverlay={false} />
// Color schemes: "vivid" | "pastel" | "warm" | "cool"
<BoomBoomSky colorScheme="pastel" /> // Soft pastel colors
<BoomBoomSky colorScheme="warm" /> // Orange/red tones
<BoomBoomSky colorScheme="cool" /> // Blue/cyan tones
// Custom burst interval (ms) and spark count
<BoomBoomSky burstInterval={2000} sparkCount={12000} />
// Disable sound
<BoomBoomSky enableSound={false} />
// Fullscreen background
<div className="fixed inset-0">
<BoomBoomSky
width={typeof window !== 'undefined' ? window.innerWidth : 1920}
height={typeof window !== 'undefined' ? window.innerHeight : 1080}
autoStart
showOverlay={false}
/>
</div>