Created
September 21, 2025 11:18
-
-
Save AlbertMarashi/c55c6eb125ea8af4c373ae5f0d5d7d37 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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