Code:
Javascript
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script src="https://unpkg.com/lenis@1.1.18/dist/lenis.min.js"></script>
<script src="./script.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => {
gsap.registerPlugin(ScrollTrigger);
const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
const stickySection = document.querySelector(".steps");
const stickyHeight = window.innerHeight * 7;
const cards = document.querySelectorAll(".card");
const countContainer = document.querySelector(".count-container");
const totalCards = cards.length;
ScrollTrigger.create({
trigger: stickySection,
start: "top top",
end: `+=${stickyHeight}px`,
pin: true,
pinSpacing: true,
onUpdate: (self) => {
positionCards(self.progress);
},
});
const getRadius = () => {
return window.innerWidth < 900
? window.innerWidth * 7.5
: window.innerWidth * 2.5;
};
const arcAngle = Math.PI * 0.4;
const startAngle = Math.PI / 2 - arcAngle / 2;
function positionCards(progress = 0) {
const radius = getRadius();
const totalTravel = 1 + totalCards / 7.5;
const adjustedProgress = (progress * totalTravel - 1) * 0.75;
cards.forEach((card, i) => {
const normalizedProgress = (totalCards - 1 - i) / totalCards;
const cardProgress = normalizedProgress + adjustedProgress;
const angle = startAngle + arcAngle * cardProgress;
const x = Math.cos(angle) * radius;
const y = Math.sin(angle) * radius;
const rotation = (angle - Math.PI / 2) * (180 / Math.PI);
gsap.set(card, {
x: x,
y: -y + radius,
rotation: -rotation,
transformOrigin: "center center",
});
});
}
positionCards(0);
let currentCardIndex = 0;
const options = {
root: null,
rootMargin: "0% 0%",
threshold: 0.5,
};
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
lastScrollY = window.scrollY;
let cardIndex = Array.from(cards).indexOf(entry.target);
currentCardIndex = cardIndex;
console.log(currentCardIndex);
const targetY = 150 - currentCardIndex * 150;
gsap.to(countContainer, {
y: targetY,
duration: 0.3,
ease: "power1.out",
overwrite: true,
});
}
});
}, options);
cards.forEach((card) => {
observer.observe(card);
});
window.addEventListener("resize", () => positionCards(0));
});
</script>
CSS
section {
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
}
.cards {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 550vw;
height: 600px;
will-change: transform;
}
.card {
position: absolute;
width: 500px;
height: 50px;
left: 50%;
top: 130%;
transform-origin: center center;
margin-left: -150px;
display: flex;
flex-direction: column;
gap: 1em;
will-change: transform;
}
.card-content {
width: 100%;
height: 60px;
}
.step-counter {
position: absolute;
display: flex;
flex-direction: column;
margin: 2em;
}
.counter-title,
.count {
position: relative;
/*width: 1200px;*/
height: 100px;
clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
overflow: hidden;
}
.count {
position: relative;
top: -30px;
}
.count-container {
position: relative;
transform: translateY(150px);
will-change: transform;
}
.step-counter h2 {
width: 100%;
position: relative;
color: #fff;
text-transform: uppercase;
font-weight: 900;
font-size: 90px;
line-height: 1;
letter-spacing: -0.04em;
will-change: transform;
}
.empty {
opacity: 0;
}
.lenis.lenis-smooth {
scroll-behavior: auto !important;
}
.lenis.lenis-smooth [data-lenis-prevent] {
overscroll-behavior: contain;
}
.lenis.lenis-stopped {
overflow: clip;
}
.lenis.lenis-smooth iframe {
pointer-events: none;
}
@media (max-width: 900px) {
.counter-title {
height: 30px;
}
.counter-title h2 {
font-size: 30px;
}
.count {
top: 0px;
left: -10px;
}
.cards {
top: 27.5%;
}
.card {
width: 375px;
height: 500px;
}
}