ARTICLE AD BOX
Thanks for the help in advance. For the life of me, I cannot get a particular scroll animation to execute which utilizes gsap and lenis.
I've assembled the code to the best of my ability: https://codepen.io/Charles-Kent/pen/XJdZRWy
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>RedoMedia Split-Card Scroll Animation | Codegrid</title> <link rel="stylesheet" href="styles.css" /> </head> <body> <section class="intro"> <h1>Every idea begins as a single image</h1> </section> <section class="sticky"> <div class="sticky-header"> <h1>Three pillars with one purpose</h1> </div> <div class="card-container"> <div class="card" id="card-1"> <div class="card-front"> <img src="https://www.charlesthedesigner.com/images/card-images/card_cover_1.jpg" alt="" /> </div> <div class="card-back"> <span>( 01 )</span> <p>Interactive Web Experiences</p> </div> </div> <div class="card" id="card-2"> <div class="card-front"> <img src="https://www.charlesthedesigner.com/images/card-images/card_cover_2.jpg" alt="" /> </div> <div class="card-back"> <span>( 02 )</span> <p>Thoughtful Design Language</p> </div> </div> <div class="card" id="card-3"> <div class="card-front"> <img src="https://www.charlesthedesigner.com/images/card-images/card_cover_3.jpg" alt="" /> </div> <div class="card-back"> <span>( 03 )</span> <p>Visual Design Systems</p> </div> </div> </div> </section> <section class="outro"> <h1>Every transition leaves a trace</h1> </section> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/ScrollTrigger.min.js"></script> <script src="https://unpkg.com/[email protected]/dist/lenis.min.js"></script> </body> </html> @import url("https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap"); :root { --bg: #0f0f0f; --fg: #fff; --card-1: #b2b2b2; --card-2: #ce2017; --card-3: #2f2f2f; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: "Instrument Serif", sans-serif; } img { width: 100%; height: 100%; object-fit: cover; } h1 { font-size: 4rem; font-weight: 500; line-height: 1; } p { font-size: 2rem; font-weight: 500; line-height: 1; } section { position: relative; width: 100%; height: 100svh; padding: 2rem; background-color: var(--bg); color: var(--fg); } .intro, .outro { text-align: center; align-content: center; } .intro h1, .outro h1 { width: 30%; margin: 0 auto; } .sticky { position: relative; display: flex; justify-content: center; align-items: center; } .sticky-header { position: absolute; top: 20%; left: 50%; transform: translate(-50%, -50%); } .sticky-header h1 { position: relative; text-align: center; will-change: transform, opacity; transform: translateY(40px); opacity: 0; } .card-container { position: relative; width: 75%; display: flex; perspective: 1000px; transform: translateY(40px); will-change: width; } .card { position: relative; flex: 1; aspect-ratio: 5/7; transform-style: preserve-3d; transform-origin: top; } #card-1 { border-radius: 20px 0 0 20px; } #card-3 { border-radius: 0 20px 20px 0; } .card-front, .card-back { position: absolute; width: 100%; height: 100%; backface-visibility: hidden; border-radius: inherit; overflow: hidden; } .card-back { display: flex; justify-content: center; align-items: center; text-align: center; transform: rotateY(180deg); padding: 2rem; } .card-back span { position: absolute; top: 2rem; left: 2rem; opacity: 0.4; } #card-1 .card-back { background-color: var(--card-1); color: var(--bg); } #card-2 .card-back { background-color: var(--card-2); } #card-3 .card-back { background-color: var(--card-3); } @media (max-width: 1000px) { h1 { font-size: 3rem; } .intro h1, .outro h1 { width: 100%; } .sticky { height: max-content; padding: 4rem 2rem; flex-direction: column; } .sticky-header { position: relative; top: 0; left: 0; transform: none; margin-bottom: 4rem; } .sticky-header h1 { opacity: 1; } .card-container { width: 100%; flex-direction: column; gap: 2rem; } .card { width: 100%; max-width: 400px; margin: 0 auto; border-radius: 20px !important; } #card-1, #card-2, #card-3, .card-back { transform: none; } } import { gsap } from 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/gsap.min.js'; import { ScrollTrigger } from 'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.1/ScrollTrigger.min.js'; import { Lenis } from 'https://unpkg.com/[email protected]/dist/lenis.min.js'; 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 cardContainer = document.querySelector(".card-container"); const stickyHeader = document.querySelector(".sticky-header h1"); let isGapAnimationCompleted = false; let isFlipAnimationCompleted = false; function initAnimations() { ScrollTrigger.getAll().forEach((trigger) => trigger.kill()); const mm = gsap.matchMedia(); mm.add("(max-width: 999px)", () => { document .querySelectorAll(".card, .card-container, .sticky-header h1") .forEach((el) => (el.style = "")); return {}; }); mm.add("(min-width: 1000px)", () => { ScrollTrigger.create({ trigger: ".sticky", start: "top top", end: `+=${window.innerHeight * 4}px`, scrub: 1, pin: true, pinSpacing: true, onUpdate: (self) => { const progress = self.progress; if (progress >= 0.1 && progress <= 0.25) { const headerProgress = gsap.utils.mapRange( 0.1, 0.25, 0, 1, progress ); const yValue = gsap.utils.mapRange(0, 1, 40, 0, headerProgress); const opacityValue = gsap.utils.mapRange( 0, 1, 0, 1, headerProgress ); gsap.set(stickyHeader, { y: yValue, opacity: opacityValue, }); } else if (progress < 0.1) { gsap.set(stickyHeader, { y: 40, opacity: 0, }); } else if (progress > 0.25) { gsap.set(stickyHeader, { y: 0, opacity: 1, }); } if (progress <= 0.25) { const widthPercentage = gsap.utils.mapRange( 0, 0.25, 75, 60, progress ); gsap.set(cardContainer, { width: `${widthPercentage}%` }); } else { gsap.set(cardContainer, { width: "60%" }); } if (progress >= 0.35 && !isGapAnimationCompleted) { gsap.to(cardContainer, { gap: "20px", duration: 0.5, ease: "power3.out", }); gsap.to(["#card-1", "#card-2", "#card-3"], { borderRadius: "20px", duration: 0.5, ease: "power3.out", }); isGapAnimationCompleted = true; } else if (progress < 0.35 && isGapAnimationCompleted) { gsap.to(cardContainer, { gap: "0px", duration: 0.5, ease: "power3.out", }); gsap.to("#card-1", { borderRadius: "20px 0 0 20px", duration: 0.5, ease: "power3.out", }); gsap.to("#card-2", { borderRadius: "0px", duration: 0.5, ease: "power3.out", }); gsap.to("#card-3", { borderRadius: "0 20px 20px 0", duration: 0.5, ease: "power3.out", }); isGapAnimationCompleted = false; } if (progress >= 0.7 && !isFlipAnimationCompleted) { gsap.to(".card", { rotationY: 180, duration: 0.75, ease: "power3.inOut", stagger: 0.1, }); gsap.to(["#card-1", "#card-3"], { y: 30, rotationZ: (i) => [-15, 15][i], duration: 0.75, ease: "power3.inOut", }); isFlipAnimationCompleted = true; } else if (progress < 0.7 && isFlipAnimationCompleted) { gsap.to(".card", { rotationY: 0, duration: 0.75, ease: "power3.inOut", stagger: -0.1, }); gsap.to(["#card-1", "#card-3"], { y: 0, rotationZ: 0, duration: 0.75, ease: "power3.inOut", }); isFlipAnimationCompleted = false; } }, }); return () => {}; }); } initAnimations(); let resizeTimer; window.addEventListener("resize", () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { initAnimations(); }, 250); }); });This is based on a CodeGrid tutorial I found on YouTube (which showcases how the final result should look): https://www.youtube.com/watch?v=_F1t2Ux-znk&t
Looks relatively simple on there but fails to execute for me in any way... just my luck, right? Where does the code go wrong for me? Thanks again.
