Skip to content

Instantly share code, notes, and snippets.

@aldoyh
Created May 5, 2025 14:01
Show Gist options
  • Save aldoyh/415eb88656f13e92a8330e7098a63e14 to your computer and use it in GitHub Desktop.
Save aldoyh/415eb88656f13e92a8330e7098a63e14 to your computer and use it in GitHub Desktop.
Flair Confetti!
<p>click and drag</p>
<section class="hero pricing-hero" data-block="pricing-hero">
<div class="container">
<div class="pricing-hero__content">
<div class="pricing-hero__flair">
<div class="pricing-hero__hand">
<img class="pricing-hero__drag" src="https://assets.codepen.io/16327/hand-drag.png" alt="">
<img class="pricing-hero__rock" src="https://assets.codepen.io/16327/hand-rock.png" alt="">
<img class="pricing-hero__handle" src="https://assets.codepen.io/16327/2D-circle.png" alt="">
<small>drag me</small>
</div>
<div class="image-preload" aria-hidden="true">
<img data-key="combo" src="https://assets.codepen.io/16327/3D-combo.png" width="1" height="1" style="position: absolute; left: -9999px;" />
<img data-key="cone" src="https://assets.codepen.io/16327/3D-cone.png" width="1" height="1" style="position: absolute; left: -9999px;" />
<img data-key="hoop" src="https://assets.codepen.io/16327/3D-hoop.png" width="1" height="1" style="position: absolute; left: -9999px;" />
<img data-key="keyframe" src="https://assets.codepen.io/16327/3D-keyframe.png" width="1" height="1" style="position: absolute; left: -9999px;" />
<img data-key="semi" src="https://assets.codepen.io/16327/3D-semi.png" width="1" height="1" style="position: absolute; left: -9999px;" />
<img data-key="spiral" src="https://assets.codepen.io/16327/3D-spiral.png" width="1" height="1" style="position: absolute; left: -9999px;" />
<img data-key="squish" src="https://assets.codepen.io/16327/3D-squish.png" width="1" height="1" style="position: absolute; left: -9999px;" />
<img data-key="triangle" src="https://assets.codepen.io/16327/3D-triangle.png" width="1" height="1" style="position: absolute; left: -9999px;" />
<img data-key="tunnel" src="https://assets.codepen.io/16327/3D-tunnel.png" width="1" height="1" style="position: absolute; left: -9999px;" />
<img data-key="wat" src="https://assets.codepen.io/16327/3D-poly.png" width="1" height="1" style="position: absolute; left: -9999px;" />
</div>
<div class="explosion-preload" aria-hidden="true">
<img data-key="blue-circle" src="https://assets.codepen.io/16327/2D-circles.png" style="position: absolute; left: -9999px;" />
<img data-key="green-keyframe" src="https://assets.codepen.io/16327/2D-keyframe.png" style="position: absolute; left: -9999px;" />
<img data-key="orange-lightning" src="https://assets.codepen.io/16327/2D-lightning.png" style="position: absolute; left: -9999px;" />
<img data-key="orange-star" src="https://assets.codepen.io/16327/2D-star.png" style="position: absolute; left: -9999px;" />
<img data-key="purple-flower" src="https://assets.codepen.io/16327/2D-flower.png" style="position: absolute; left: -9999px;" />
<img data-key="cone" src="https://assets.codepen.io/16327/3D-cone.png" style="position: absolute; left: -9999px;" />
<img data-key="keyframe" src="https://assets.codepen.io/16327/3D-spiral.png" style="position: absolute; left: -9999px;" />
<img data-key="spiral" src="https://assets.codepen.io/16327/3D-spiral.png" style="position: absolute; left: -9999px;" />
<img data-key="tunnel" src="https://assets.codepen.io/16327/3D-tunnel.png" style="position: absolute; left: -9999px;" />
<img data-key="hoop" src="https://assets.codepen.io/16327/3D-hoop.png" style="position: absolute; left: -9999px;" />
<img data-key="semi" src="https://assets.codepen.io/16327/3D-semi.png" style="position: absolute; left: -9999px;" />
</div>
</div>
</div>
<svg class="pricing-hero__canvas"></svg>
<div class="pricing-hero__proxy"></div>
</div>
</section>
gsap.registerPlugin(
Observer,
CustomEase,
CustomWiggle,
Physics2DPlugin,
ScrollTrigger
);
class confettiCannon {
constructor(el) {
this.el = el;
}
init() {
const hero = this.el;
this.hero = hero;
const el = {
hand: hero.querySelector(".pricing-hero__hand"),
instructions: hero.querySelector(".pricing-hero__hand small"),
rock: hero.querySelector(".pricing-hero__rock"),
drag: hero.querySelector(".pricing-hero__drag"),
handle: hero.querySelector(".pricing-hero__handle"),
canvas: hero.querySelector(".pricing-hero__canvas"),
proxy: hero.querySelector(".pricing-hero__proxy"),
preloadImages: hero.querySelectorAll(".image-preload img"),
xplodePreloadImages: hero.querySelectorAll(".explosion-preload img")
};
this.el = el;
this.isDrawing = false;
this.imageMap = {};
this.imageKeys = [];
this.el.preloadImages.forEach((img) => {
const key = img.dataset.key;
this.imageMap[key] = img;
this.imageKeys.push(key);
});
this.explosionMap = {};
this.explosionKeys = [];
this.el.xplodePreloadImages.forEach((img) => {
const key = img.dataset.key;
this.explosionMap[key] = img;
this.explosionKeys.push(key);
});
this.currentLine = null;
this.startImage = null;
this.circle = null;
this.startX = 0;
this.startY = 0;
this.lastDistance = 0;
this.animationIsOk = window.matchMedia(
"(prefers-reduced-motion: no-preference)"
).matches;
this.wiggle = CustomWiggle.create("myWiggle", { wiggles: 6 });
this.clamper = gsap.utils.clamp(1, 100);
this.xSetter = gsap.quickTo(this.el.hand, "x", { duration: 0.1 });
this.ySetter = gsap.quickTo(this.el.hand, "y", { duration: 0.1 });
this.setpricingMotion();
this.initObserver();
this.initEvents();
}
initEvents() {
if (!this.animationIsOk || ScrollTrigger.isTouch === 1) return;
this.hero.style.cursor = "none";
this.hero.addEventListener("mouseenter", (e) => {
gsap.set(this.el.hand, { opacity: 1 });
this.xSetter(e.x, e.x);
this.ySetter(e.y, e.y);
});
this.hero.addEventListener("mouseleave", (e) => {
gsap.set(this.el.hand, { opacity: 0 });
});
this.hero.addEventListener("mousemove", (e) => {
this.xSetter(e.x);
this.ySetter(e.y);
});
gsap.delayedCall(1, (e) => {
this.createExplosion(window.innerWidth/2, window.innerHeight/2, 600);
})
}
setpricingMotion() {
gsap.set(this.el.hand, { xPercent: -50, yPercent: -50 });
}
initObserver() {
if (!this.animationIsOk) return;
if (ScrollTrigger.isTouch === 1) {
Observer.create({
target: this.el.proxy,
type: "touch",
onPress: (e) => {
this.createExplosion(e.x, e.y, 400);
}
});
} else {
Observer.create({
target: this.el.proxy,
type: "pointer",
onPress: (e) => this.startDrawing(e),
onDrag: (e) => this.isDrawing && this.updateDrawing(e),
onDragEnd: (e) => this.clearDrawing(e),
onRelease: (e) => this.clearDrawing(e)
});
}
}
startDrawing(e) {
this.isDrawing = true;
gsap.set(this.el.instructions, { opacity: 0 });
this.startX = e.x;
this.startY = e.y + window.scrollY;
// Create line
this.currentLine = document.createElementNS(
"http://www.w3.org/2000/svg",
"line"
);
this.currentLine.setAttribute("x1", this.startX);
this.currentLine.setAttribute("y1", this.startY);
this.currentLine.setAttribute("x2", this.startX);
this.currentLine.setAttribute("y2", this.startY);
this.currentLine.setAttribute("stroke", "#fffce1");
this.currentLine.setAttribute("stroke-width", "2");
this.currentLine.setAttribute("stroke-dasharray", "4");
this.circle = document.createElementNS(
"http://www.w3.org/2000/svg",
"circle"
);
this.circle.setAttribute("cx", this.startX);
this.circle.setAttribute("cy", this.startY);
this.circle.setAttribute("r", "30");
this.circle.setAttribute("fill", "#0e100f");
// Create image at start point
const randomKey = gsap.utils.random(this.imageKeys);
const original = this.imageMap[randomKey];
const clone = document.createElementNS(
"http://www.w3.org/2000/svg",
"image"
);
clone.setAttribute("x", this.startX - 25);
clone.setAttribute("y", this.startY - 25);
clone.setAttribute("width", "50");
clone.setAttribute("height", "50");
clone.setAttributeNS("http://www.w3.org/1999/xlink", "href", original.src);
this.startImage = clone;
this.el.canvas.appendChild(this.currentLine);
this.el.canvas.appendChild(this.circle);
this.el.canvas.appendChild(this.startImage);
gsap.set(this.el.drag, { opacity: 1 });
gsap.set(this.el.handle, { opacity: 1 });
gsap.set(this.el.rock, { opacity: 0 });
}
updateDrawing(e) {
if (!this.currentLine || !this.startImage) return;
let cursorX = e.x;
let cursorY = e.y + window.scrollY;
let dx = cursorX - this.startX;
let dy = cursorY - this.startY;
let distance = Math.sqrt(dx * dx + dy * dy);
let shrink = (distance - 30) / distance;
let x2 = this.startX + dx * shrink;
let y2 = this.startY + dy * shrink;
if (distance < 30) {
x2 = this.startX;
y2 = this.startY;
}
let angle = Math.atan2(dy, dx) * (180 / Math.PI);
gsap.to(this.currentLine, {
attr: { x2, y2 },
duration: 0.1,
ease: "none"
});
// Eased scale (starts fast, slows down)
let raw = distance / 100;
let eased = Math.pow(raw, 0.5);
let clamped = this.clamper(eased);
gsap.set([this.startImage, this.circle], {
scale: clamped,
rotation: `${angle + -45}_short`,
transformOrigin: "center center"
});
// Move & rotate hand
gsap.to(this.el.hand, {
rotation: `${angle + -90}_short`,
duration: 0.1,
ease: "none"
});
this.lastDistance = distance;
}
createExplosion(x, y, distance = 100) {
const count = Math.round(gsap.utils.clamp(3, 100, distance / 20));
const angleSpread = Math.PI * 2;
const explosion = gsap.timeline();
const gravity = 5;
const speed = gsap.utils.mapRange(0, 500, 0.3, 1.5, distance);
const sizeRange = gsap.utils.mapRange(0, 500, 20, 60, distance);
for (let i = 0; i < count; i++) {
const randomKey = gsap.utils.random(this.explosionKeys);
const original = this.explosionMap[randomKey];
const img = original.cloneNode(true);
img.className = "explosion-img";
img.style.position = "absolute";
img.style.pointerEvents = "none";
img.style.height = `${gsap.utils.random(20, sizeRange)}px`;
img.style.left = `${x}px`;
img.style.top = `${y}px`;
img.style.zIndex = 4;
this.hero.appendChild(img);
const angle = Math.random() * angleSpread;
const velocity = gsap.utils.random(500, 1500) * speed;
explosion
.to(
img,
{
physics2D: {
angle: angle * (180 / Math.PI),
velocity: velocity,
gravity: 3000
},
rotation: gsap.utils.random(-180, 180),
duration: 1 + Math.random()
},
0
)
.to(
img,
{
opacity: 0,
duration: 0.2,
ease: "power1.out",
onComplete: () => img.remove()
},
1
);
}
return explosion;
}
clearDrawing(e) {
if (!this.isDrawing) return;
this.createExplosion(this.startX, this.startY, this.lastDistance);
gsap.set(this.el.drag, { opacity: 0 });
gsap.set(this.el.handle, { opacity: 0 });
gsap.set(this.el.rock, { opacity: 1 });
gsap.to(this.el.rock, {
duration: 0.4,
rotation: "+=30",
ease: "myWiggle",
onComplete: () => {
gsap.set(this.el.rock, { opacity: 0 });
gsap.set(this.el.hand, { rotation: 0, overwrite: "auto" });
gsap.to(this.el.instructions, { opacity: 1 });
gsap.set(this.el.drag, { opacity: 1 });
}
});
this.isDrawing = false;
// Clear all elements from SVG and reset references
this.el.canvas.innerHTML = "";
this.currentLine = null;
this.startImage = null;
}
}
const cannon = new confettiCannon(document.body);
cannon.init();
<script src="https://unpkg.com/gsap@3/dist/gsap.min.js"></script>
<script src="https://unpkg.com/gsap/dist/Observer.min.js"></script>
<script src="https://assets.codepen.io/16327/CustomEase3.min.js"></script>
<script src="https://assets.codepen.io/16327/CustomWiggle3.min.js"></script>
<script src="https://unpkg.com/gsap@3/dist/ScrollTrigger.min.js"></script>
<script src="https://assets.codepen.io/16327/Physics2DPlugin3.min.js"></script>
p {
position: fixed;
top: 48%;
left: 0; right: 0;
width: 100%;
text-align: center;
z-index: 999;
}
.pricing-hero {
align-items: center;
background: var(--color-just-black);
display: flex;
flex-direction: column;
justify-content: center;
max-width: 100vw;
min-height: 100vh;
overflow: hidden;
position: relative;
width: 100%;
z-index: 2;
}
img {
max-width 100%;
}
.pricing-hero .subtitle {
color: var(--color-surface-white);
display: inline-block;
z-index: 2;
}
.pricing-hero__flair {
display: block;
margin: max(2rem, min(2.0712vw + 1.51456rem, 4rem)) auto
max(2rem, min(6.21359vw + 0.543689rem, 8rem));
width: 100%;
}
.pricing-hero__content {
padding-bottom: max(4rem, min(10.7443vw + 1.4818rem, 14.375rem));
padding-top: max(4rem, min(10.7443vw + 1.4818rem, 14.375rem));
text-align: center;
width: 100%;
}
.pricing-hero__heading-container {
position: relative;
width: 100%;
}
.pricing-hero__heading--free {
left: 0;
opacity: 0;
position: absolute;
top: 0;
width: 100%;
}
.pricing-hero__heading {
line-height: 1.13 !important;
margin: 0;
}
.pricing-hero__heading > * {
-webkit-text-fill-color: transparent;
background: var(--color-ui-gradient);
-webkit-background-clip: text;
background-clip: text;
will-change: transform;
}
.pricing-hero__hand {
left: 0;
opacity: 0;
pointer-events: none;
position: fixed;
top: 0;
width: 30px;
z-index: 4;
}
.pricing-hero__hand small {
left: -60%;
position: absolute;
top: 20px;
width: 200%;
}
.pricing-hero__drag,
.pricing-hero__rock {
position: absolute;
z-index: 4;
}
.pricing-hero__rock, .pricing-hero__drag {
max-width: 141%;
opacity: 0;
right: 1px;
top: -22px;
width: 131%;
}
.pricing-hero__drag {
opacity: 1;
}
.pricing-hero__handle {
left: 0;
opacity: 0;
position: absolute;
right: 0;
top: -40px;
width: 100%;
}
.pricing-hero__canvas {
z-index: -1;
}
.pricing-hero__canvas,
.pricing-hero__proxy {
bottom: 0;
height: 100vh;
left: 0;
position: absolute;
right: 0;
top: 0;
width: 100vw;
}
.pricing-hero__proxy {
z-index: 3;
}
.explosion-img {
will-change: transform;
}
.pricing-intro {
align-items: center;
background: var(--color-ui-blue-lt);
color: var(--color-just-black);
overflow: hidden;
padding-top: max(4rem, min(7.63754vw + 2.20995rem, 11.375rem));
position: relative;
z-index: 2;
}
@media only screen and (min-width: 77.5rem) {
.pricing-intro {
padding-bottom: max(2rem, min(9.64401vw - 0.260316rem, 11.3125rem));
}
}
.pricing-intro .heading-r {
-webkit-text-fill-color: transparent;
background: var(--color-ui-text-gradient);
-webkit-background-clip: text;
background-clip: text;
line-height: 1.2;
margin-bottom: max(1rem, min(1.0356vw + 0.757282rem, 2rem));
}
.pricing-intro:after {
background-image: url(/tf-assets/noise-e82662fe.png);
bottom: 0;
content: "";
display: block;
height: 100%;
left: 0;
opacity: 0.2;
pointer-events: none;
position: absolute;
right: 0;
top: 0;
width: 100%;
}
.pricing-intro__heading {
margin-bottom: max(2rem, min(2.0712vw + 1.51456rem, 4rem));
}
.pricing-intro__flair {
margin-bottom: 0;
margin-top: 64px;
}
.pricing-intro__flair svg {
margin: 0 auto;
max-width: max(16.875rem, min(17.4757vw + 12.7791rem, 33.75rem));
width: 100%;
}
.explosion-img {
will-change: transform;
}
<link href="https://codepen.io/GreenSock/pen/xxmzBrw/fcaef74061bb7a76e5263dfc076c363e.css" rel="stylesheet" />
@KuRRe8
Copy link

KuRRe8 commented May 5, 2025

I can drag the fist and the circle but nothing else happens, is it right

@aldoyh
Copy link
Author

aldoyh commented May 6, 2025

@KuRRe8 Just checked it, you're supposed to click and drag to shoot confetti bombs.
Checkout the codepen here: https://codepen.io/aldoyh/pen/emmrrqw

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment