Skip to content

Instantly share code, notes, and snippets.

@peduarte
Created April 16, 2022 20:06
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save peduarte/76e8db43a756a994d4bc0802f89096fd to your computer and use it in GitHub Desktop.
Save peduarte/76e8db43a756a994d4bc0802f89096fd to your computer and use it in GitHub Desktop.
//https://cdn.jsdelivr.net/gh/fatbattk/dom-confetti@master/src/main.js
const defaultColors = ['#a864fd', '#29cdff', '#78ff44', '#ff718d', '#fdff6a'];
function createElements(root, elementCount, colors, image, width, height) {
return Array.from({ length: elementCount }).map((_, index) => {
const element = document.createElement('div');
element.style.width = width;
element.style.height = height;
element.style.position = 'absolute';
element.style.willChange = 'transform, opacity';
element.style.visibility = 'hidden';
element.style.pointerEvents = 'none';
if (image) {
element.style['background-image'] = `url("${image}")`;
element.style['background-color'] = 'transparent'; // eslint-disable-line space-infix-ops
element.style['background-repeat'] = 'no-repeat';
element.style['background-size'] = 'contain';
} else {
const color = colors[index % colors.length];
element.style['background-color'] = color; // eslint-disable-line space-infix-ops
}
root.appendChild(element);
return element;
});
}
function randomPhysics(angle, spread, startVelocity, random) {
const radAngle = angle * (Math.PI / 180);
const radSpread = spread * (Math.PI / 180);
return {
x: 0,
y: 0,
z: 0,
wobble: random() * 10,
// wobble: random() * 5,
wobbleSpeed: 0.1 + random() * 0.1,
velocity: startVelocity * 0.5 + random() * startVelocity,
// velocity: startVelocity * 0.3 + random() * startVelocity,
angle2D: -radAngle + (0.5 * radSpread - random() * radSpread),
angle3D: -(Math.PI / 4) + random() * (Math.PI / 2),
tiltAngle: random() * Math.PI,
// tiltAngleSpeed: 0.1 + random() * 0.3,
tiltAngleSpeed: 0.05 + random() * 0.01,
};
}
function updateFetti(fetti, progress, dragFriction, decay) {
/* eslint-disable no-param-reassign */
fetti.physics.x += Math.cos(fetti.physics.angle2D) * fetti.physics.velocity;
fetti.physics.y += Math.sin(fetti.physics.angle2D) * fetti.physics.velocity;
// fetti.physics.z += Math.sin(fetti.physics.angle3D) * fetti.physics.velocity;
fetti.physics.z += 0;
fetti.physics.wobble += fetti.physics.wobbleSpeed;
// Backward compatibility
if (decay) {
fetti.physics.velocity *= decay;
} else {
fetti.physics.velocity -= fetti.physics.velocity * dragFriction;
}
fetti.physics.y += 3;
fetti.physics.tiltAngle += fetti.physics.tiltAngleSpeed;
// fetti.physics.tiltAngle += 0;
const { x, y, z, tiltAngle, wobble } = fetti.physics;
const wobbleX = x + 10 * Math.cos(wobble);
// const wobbleY = y + 10 * Math.sin(wobble);
const wobbleY = y + 10 * Math.sin(wobble);
const transform = `translate3d(${wobbleX}px, ${wobbleY}px, ${z}px) rotate(${tiltAngle}rad)`;
// const transform = `translate3d(${wobbleX}px, ${wobbleY}px, ${z}px) rotate3d(1, 1, 1, ${tiltAngle}rad)`;
fetti.element.style.visibility = 'visible';
fetti.element.style.transform = transform;
fetti.element.style.opacity = 1 - progress;
// fetti.element.style.opacity = 1 - progress / 2;
// fetti.element.style.opacity = 1;
/* eslint-enable */
}
function animate(root, fettis, dragFriction, decay, duration, stagger) {
let startTime;
return new Promise((resolve) => {
function update(time) {
if (!startTime) startTime = time;
const elapsed = time - startTime;
const progress = startTime === time ? 0 : (time - startTime) / duration;
fettis.slice(0, Math.ceil(elapsed / stagger)).forEach((fetti) => {
updateFetti(fetti, progress, dragFriction, decay);
});
if (time - startTime < duration) {
requestAnimationFrame(update);
} else {
fettis.forEach((fetti) => {
if (fetti.element.parentNode === root) {
return root.removeChild(fetti.element);
}
});
resolve();
}
}
requestAnimationFrame(update);
});
}
const defaults = {
angle: 90,
spread: 45,
startVelocity: 45,
elementCount: 50,
width: '10px',
height: '10px',
perspective: '',
colors: defaultColors,
image: '',
duration: 3000,
stagger: 0,
dragFriction: 0.1,
random: Math.random,
};
function backwardPatch(config) {
if (!config.stagger && config.delay) {
config.stagger = config.delay;
}
return config;
}
export function confetti(root, config = {}) {
const {
elementCount,
colors,
image,
width,
height,
perspective,
angle,
spread,
startVelocity,
decay,
dragFriction,
duration,
stagger,
random,
} = Object.assign({}, defaults, backwardPatch(config));
root.style.perspective = perspective;
const elements = createElements(root, elementCount, colors, image, width, height);
const fettis = elements.map((element) => ({
element,
physics: randomPhysics(angle, spread, startVelocity, random),
}));
return animate(root, fettis, dragFriction, decay, duration, stagger);
}
// usage
confetti(element, {
image: 'https://image.url',
width: `20px`,
height: `20px`,
elementCount: 7,
duration: 3000,
angle: 90,
spread: 360,
startVelocity: 20,
stagger: 3,
perspective: '500px',
dragFriction: 0.12,
})
// on my site, i do it on mouseover:
/**
React.useEffect(() => {
let interval;
if (rainbowLinkRef.current && rainbowTriggerRef.current) {
rainbowLinkRef.current.addEventListener('mouseover', () => {
interval = setInterval(() => {
confetti(rainbowTriggerRef.current, {
image: 'https://ped.ro/rainbow.png',
width: `${Math.floor(Math.random() * 25) + 20}px`,
height: `${Math.floor(Math.random() * 25) + 20}px`,
elementCount: 7,
duration: 3000,
angle: 90,
spread: 360,
startVelocity: 20,
stagger: 3,
perspective: '500px',
dragFriction: 0.12,
});
}, 160);
});
rainbowLinkRef.current.addEventListener('mouseout', () => {
clearInterval(interval);
});
}
return () => clearInterval(interval);
}, [rainbowLinkRef, rainbowTriggerRef]);
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment