Skip to content

Instantly share code, notes, and snippets.

@AlbertMarashi
Created September 21, 2025 11:18
Show Gist options
  • Select an option

  • Save AlbertMarashi/c55c6eb125ea8af4c373ae5f0d5d7d37 to your computer and use it in GitHub Desktop.

Select an option

Save AlbertMarashi/c55c6eb125ea8af4c373ae5f0d5d7d37 to your computer and use it in GitHub Desktop.
// Define the interface for configuration options
interface ConfettiOptions {
colors?: string[]; // Array of color strings (e.g., hex codes)
spawnRate?: number; // Number of particles to spawn per second
particleSpeed?: number; // Speed of the confetti particles (pixels per second)
size?: number; // Size of confetti (in pixels)
angularSpeed?: number; // Rotational speed of the confetti (degrees per second)
gravity?: number; // Gravity effect applied to the confetti's vertical movement (pixels per second squared)
spread?: number; // Spread of confetti (controls the random deviation in velocities, in degrees)
duration?: number; // Duration (in milliseconds) to spawn confetti
timeScale?: number; // Time scaling factor for the simulation speed,
std?: number // Standard deviation of normal distribution
upward_bias?: number // Upward bias of the particles
height?: number // Height of the confetti
}
export function launchConfetti(options: ConfettiOptions = {}): Promise<void> {
return new Promise<void>(resolve => {
// Default configuration
const defaultOptions: Required<ConfettiOptions> = {
colors: [
"var(--brand)",
"var(--purple)",
"var(--green)",
"var(--blue)",
],
spawnRate: 100, // Particles per second
particleSpeed: window.innerWidth * 0.6, // pixels per second
size: 12, // pixels
angularSpeed: 360, // degrees per second
gravity: window.innerHeight * 0.8, // pixels per second squared
spread: 30, // degrees
duration: 750, // milliseconds
timeScale: 1, // Normal time scale
std: 0.1, // Standard deviation of normal distribution,
upward_bias: Math.PI * 0.35, // Upward bias of the particles
height: 1, // Height of the confetti
}
// Merge user options with default options
const config: Required<ConfettiOptions> = {
...defaultOptions,
...options,
}
const {
colors,
spawnRate,
particleSpeed,
size,
angularSpeed,
gravity,
spread,
duration,
timeScale,
std,
upward_bias,
height,
} = config
// Utility functions
const random = Math.random
const PI = Math.PI
const windowHeight = window.innerHeight
const windowWidth = window.innerWidth
// Normal distribution function
function normalRandom(mean: number, stdDev: number): number {
let u = 0, v = 0
while (u === 0) u = random() // Convert [0,1) to (0,1)
while (v === 0) v = random()
const num = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * PI * v)
return Math.max(0, Math.min(windowHeight, mean + num * stdDev))
}
// Create the container for confetti
const container = document.createElement("div")
container.style.position = "fixed"
container.style.top = "0"
container.style.left = "0"
container.style.width = "100%"
container.style.height = "0"
container.style.pointerEvents = "none"
container.style.overflow = "visible"
container.style.zIndex = "9999"
document.body.appendChild(container)
// Confetto class
class Confetto {
x: number
y: number
rotation: number
size: number
color: string
vx: number
vy: number
ax: number
ay: number
tiltAngle: number
tiltAngleIncrement: number
element: HTMLDivElement
constructor() {
const r = random()
this.size = size * (r + 0.5)
this.color = colors[Math.floor(random() * colors.length)]
const speed = particleSpeed * (0.5 + random()) * 1 // Vary speed between 50% and 150%
this.rotation = random() * 360
// Calculate angle for movement
const angleSpread = spread * (PI / 180) // Convert spread to radians
const startFromLeft = random() < 0.5
const baseAngle = startFromLeft ? 0 : PI // Left or right side
const upwardBias = startFromLeft ? -upward_bias : upward_bias
const angle = baseAngle + upwardBias + (random() - 0.5) * angleSpread
// Start from the side
this.x = startFromLeft ? -this.size : windowWidth + this.size
this.y = Math.max(0, normalRandom(windowHeight * height, windowHeight * std))
// Velocity components
this.vx = speed * Math.cos(angle)
this.vy = speed * Math.sin(angle)
// Acceleration
this.ax = 0
this.ay = gravity
// Tilt angle for rotation effect
this.tiltAngle = random() * 360
this.tiltAngleIncrement = (random() - 0.5) * 2 * angularSpeed // Rotate in either direction
// Create confetto element
this.element = document.createElement("div")
this.element.style.position = "absolute"
this.element.style.width = `${this.size}px`
this.element.style.height = `${this.size * 0.4}px` // Rectangular shape
this.element.style.backgroundColor = this.color
this.element.style.left = `${this.x}px`
this.element.style.top = `${this.y}px`
this.element.style.transform = `rotate(${this.rotation}deg) skew(15deg)`
this.element.style.willChange = "transform, opacity"
container.appendChild(this.element)
}
update(deltaTime: number): boolean {
// Update velocities
this.vx += this.ax * deltaTime
this.vy += this.ay * deltaTime
// Update position
this.x += this.vx * deltaTime
this.y += this.vy * deltaTime
// Update rotation
this.tiltAngle += this.tiltAngleIncrement * deltaTime
this.rotation += this.tiltAngleIncrement * deltaTime
// Apply styles
this.element.style.left = `${this.x}px`
this.element.style.top = `${this.y}px`
this.element.style.transform = `rotate(${this.rotation}deg) skew(15deg)`
// Remove confetto if it's out of bounds
return this.y > windowHeight + this.size || this.x < -this.size || this.x > windowWidth + this.size
}
}
// Array to hold all confetti
const confetti: Confetto[] = []
const startTime = performance.now()
let elapsedTime = 0
let spawning = true
let accumulatedSpawnTime = 0 // Time accumulator for spawning confetti
// Animation loop
let lastTime = performance.now()
const update = (currentTime: number) => {
const deltaTime = ((currentTime - lastTime) / 1000) * timeScale // Convert to seconds and apply time scale
lastTime = currentTime
elapsedTime = currentTime - startTime
// Remove confetti that are out of bounds
for (let i = confetti.length - 1; i >= 0; i--) {
const confetto = confetti[i]
if (confetto.update(deltaTime)) {
// Remove confetto
container.removeChild(confetto.element)
confetti.splice(i, 1)
}
}
// Spawn new confetti with exponential burst effect
if (spawning) {
accumulatedSpawnTime += deltaTime
const timeRatio = elapsedTime / duration
const burstFactor = Math.exp(-5 * timeRatio) // Exponential decay
const adjustedSpawnRate = spawnRate * burstFactor * 5 // Multiply for initial burst
const particlesToSpawn = Math.floor(accumulatedSpawnTime * adjustedSpawnRate)
if (particlesToSpawn > 0) {
accumulatedSpawnTime -= particlesToSpawn / adjustedSpawnRate
for (let i = 0; i < particlesToSpawn; i++) {
confetti.push(new Confetto())
}
}
}
// Stop spawning new confetti after duration
if (elapsedTime >= duration) {
spawning = false
}
if (confetti.length > 0 || spawning) {
requestAnimationFrame(update)
} else {
// Clean up and resolve the promise
document.body.removeChild(container)
resolve()
}
}
requestAnimationFrame(update)
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment