BDubbs Posted July 31, 2024 Posted July 31, 2024 Hello everyone, I am trying to create a slideshow based on the demo found at the end of this post. Instead, my version uses React to try to recreate the same effect. However I am running into problems. Here is my code: slides.jsx import { useEffect, useRef } from "react"; import gsap from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger"; import { Observer } from "gsap/Observer"; import { projects } from "./projects"; import "./slideshow.scss"; gsap.registerPlugin(ScrollTrigger, Observer); const SlideShow = () => { const sectionsRef = useRef([]); const imagesRef = useRef([]); const slideImagesRef = useRef([]); const outerWrappersRef = useRef([]); const innerWrappersRef = useRef([]); const countRef = useRef(null); const currentIndex = useRef(0); const animating = useRef(false); useEffect(() => { const sections = sectionsRef.current; const images = imagesRef.current.reverse(); const slideImages = slideImagesRef.current; const outerWrappers = outerWrappersRef.current; const innerWrappers = innerWrappersRef.current; const count = countRef.current; const wrap = gsap.utils.wrap(0, sections.length); gsap.set(outerWrappers, { xPercent: 100 }); gsap.set(innerWrappers, { xPercent: -100 }); gsap.set(sections[0].querySelector(".slide__outer"), { xPercent: 0 }); gsap.set(sections[0].querySelector(".slide__inner"), { xPercent: 0 }); function gotoSection(index, direction) { animating.current = true; index = wrap(index); let tl = gsap.timeline({ defaults: { duration: 1, ease: "expo.inOut" }, onComplete: () => { animating.current = false; console.log("slide changed"); }, }); const currentSection = sections[currentIndex.current]; const heading = currentSection.querySelector(".slide__heading"); const nextSection = sections[index]; const nextHeading = nextSection.querySelector(".slide__heading"); gsap.set([sections, images], { zIndex: 0, autoAlpha: 0 }); gsap.set([sections[currentIndex.current], images[index]], { zIndex: 1, autoAlpha: 1, }); gsap.set([sections[index], images[currentIndex.current]], { zIndex: 2, autoAlpha: 1, }); tl.set(count, { text: index + 1 }, 0.32) .fromTo( outerWrappers[index], { xPercent: 100 * direction }, { xPercent: 0 }, 0 ) .fromTo( innerWrappers[index], { xPercent: -100 * direction }, { xPercent: 0 }, 0 ) .to(heading, { "--width": 800, xPercent: 30 * direction }, 0) .fromTo( nextHeading, { "--width": 800, xPercent: -30 * direction }, { "--width": 200, xPercent: 0 }, 0 ) .fromTo( images[index], { xPercent: 125 * direction, scaleX: 1.5, scaleY: 1.3 }, { xPercent: 0, scaleX: 1, scaleY: 1, duration: 1 }, 0 ) .fromTo( images[currentIndex.current], { xPercent: 0, scaleX: 1, scaleY: 1 }, { xPercent: -125 * direction, scaleX: 1.5, scaleY: 1.3 }, 0 ) .fromTo(slideImages[index], { scale: 2 }, { scale: 1 }, 0) .timeScale(0.8); currentIndex.current = index; } Observer.create({ type: "wheel,touch,pointer", preventDefault: true, wheelSpeed: -1, onUp: () => { if (animating.current) return; gotoSection(currentIndex.current + 1, 1); }, onDown: () => { if (animating.current) return; gotoSection(currentIndex.current - 1, -1); }, tolerance: 10, }); const logKey = (e) => { if ( (e.code === "ArrowUp" || e.code === "ArrowLeft") && !animating.current ) { gotoSection(currentIndex.current - 1, -1); } if ( (e.code === "ArrowDown" || e.code === "ArrowRight" || e.code === "Space" || e.code === "Enter") && !animating.current ) { gotoSection(currentIndex.current + 1, 1); } }; document.addEventListener("keydown", logKey); return () => { document.removeEventListener("keydown", logKey); }; }, []); return ( <div className="slideshow_container"> {projects.map((project, i) => ( <section className="slide" key={project.id} ref={(el) => (sectionsRef.current[i] = el)} > <div className="slide__outer" ref={(el) => (outerWrappersRef.current[i] = el)} > <div className="slide__inner" ref={(el) => (innerWrappersRef.current[i] = el)} > <div className="slide__content"> <div className="slide__container"> <h2 className="slide__heading">{project.projName}</h2> <figure className="slide__img-cont" ref={(el) => (slideImagesRef.current[i] = el)} > <img src={project.codeImgSrc} alt={project.codeImgAlt} /> </figure> </div> </div> </div> </div> <div className="overlay"> <div className="overlay__content"> <p className="overlay__count">{i + 1}</p> <figure className="overlay__img-cont" ref={(el) => (imagesRef.current[i] = el)} > <img src={project.demoImgSrc} alt={project.demoImgAlt} /> </figure> </div> </div> </section> ))} <div className="link_buttons"> <button>Code</button> <button>Demo</button> </div> </div> ); }; export default SlideShow; slideshow.scss I want this slideshow to take up only half the screen @font-face { font-family: "Bandeins Sans & Strange Variable"; src: url("https://res.cloudinary.com/dldmpwpcp/raw/upload/v1566406079/BandeinsStrangeVariable_esetvq.ttf"); } @import url("https://fonts.googleapis.com/css2?family=Sora&display=swap"); * { box-sizing: border-box; user-select: none; } ::-webkit-scrollbar { display: none; } figure { margin: 0; overflow: hidden; } html, body { overflow: hidden; margin: 0; padding: 0; height: 100vh; height: -webkit-fill-available; } .slide { height: 100%; width: 100%; top: 0; left: 50%; position: fixed; visibility: hidden; &__outer, &__inner { width: 100%; height: 100%; overflow-y: hidden; } &__content { display: flex; align-items: center; justify-content: center; position: absolute; height: 100%; width: 100%; top: 0; } &__container { position: relative; max-width: 1400px; width: 100vw; margin: 0 auto; height: 90vh; margin-bottom: 10vh; display: grid; grid-template-columns: repeat(10, 1fr); grid-template-rows: repeat(10, 1fr); grid-column-gap: 0px; grid-row-gap: 0px; padding: 0 1rem; } &__heading { --width: 200; display: block; text-align: left; font-family: "Bandeins Sans & Strange Variable"; font-size: clamp(5rem, 5vw, 5rem); font-weight: 900; font-variation-settings: "wdth" var(--width); margin: 0; padding: 0; color: #f2f1fc; z-index: 999; mix-blend-mode: difference; grid-area: 2 / 2 / 3 / 10; align-self: baseline; } &__img-cont { margin-top: 4rem; grid-area: 2 / 1 / 7 / 8; img { width: 100%; height: 100%; object-fit: cover; } } } .slide:nth-of-type(1) { visibility: visible; .slide__content { backdrop-filter: blur(10px); } } .slide:nth-of-type(2) { .slide__content { backdrop-filter: blur(10px); } } .slide:nth-of-type(3) { .slide__content { backdrop-filter: blur(10px); } } .slide:nth-of-type(4) { .slide__content { backdrop-filter: blur(10px); } } .overlay { position: fixed; top: 0; bottom: 0; left: 50%; right: 0; z-index: 2; &__content { max-width: 1400px; width: 100vw; margin: 0 auto; padding: 0 1rem; height: 90vh; margin-bottom: 10vh; display: grid; grid-template-columns: repeat(10, 1fr); grid-template-rows: repeat(10, 1fr); grid-column-gap: 0px; grid-row-gap: 0px; } &__img-cont { position: relative; overflow: hidden; margin: 0; grid-area: 4 / 3 / 9 / 5; img { position: absolute; width: 100%; height: 100%; object-fit: cover; object-position: 50% 50%; } } &__count { grid-area: 3 / 5 / 4 / 5; font-family: "Bandeins Sans & Strange Variable"; font-size: clamp(3rem, 4vw, 15rem); margin: 0; padding: 0; text-align: right; border-bottom: 7px white solid; } } @media screen and (min-width: 900px) { .overlay__content, .slide__container { padding: 0 3rem; margin-top: 10vh; height: 80vh; } .overlay__img-cont { grid-area: 6 / 2 / 10 / 6; } .overlay__count { grid-area: 3 / 5 / 4 / 5; font-family: "Bandeins Sans & Strange Variable"; color: white; } .slide__img-cont { margin-top: 0; grid-area: 2 / 1 / 8 / 7; } .slide__heading { grid-area: 1 / 1 / 4 / 10; } } projects.js export const projects = [ { id: 1, projName: "test1", codeLink: "(some link)", demoLink: "(some other link)", codeImgSrc: "(some picture src)", codeImgAlt: "", demoImgSrc: "(some other picture src)", demoImgAlt: "", }, { id: 2, projName: "test2", codeLink: "(some link)", demoLink: "(some other link)", codeImgSrc: "(some picture src)", codeImgAlt: "", demoImgSrc: "(some other picture src)", demoImgAlt: "", }, { id: 3, projName: "test3", codeLink: "(some link)", demoLink: "(some other link)", codeImgSrc: "(some picture src)", codeImgAlt: "", demoImgSrc: "(some other picture src)", demoImgAlt: "", }, { id: 4, projName: "test4", codeLink: "(some link)", demoLink: "(some other link)", codeImgSrc: "(some picture src)", codeImgAlt: "", demoImgSrc: "(some other picture src)", demoImgAlt: "", }, ]; My major issue right now is that I cannot get the overflow: hidden; to apply correctly causing the old text and pictures to remain on the screen instead of being hidden. Also when the next slide animates, the current and next numbers on the counter render at the same time, overlapping for a moment. If anyone can help it would be appreciated. Thanks in advance! See the Pen vYWvwXV by cassie-codes (@cassie-codes) on CodePen.
GSAP Helper Posted July 31, 2024 Posted July 31, 2024 Hi @BDubbs and welcome to the GSAP Forums! I see you're using React. Proper cleanup is very important with frameworks, but especially with React. React 18 runs in strict mode locally by default which causes your useEffect() and useLayoutEffect() to get called TWICE. Since GSAP 3.12, we have the useGSAP() hook (the NPM package is here) that simplifies creating and cleaning up animations in React (including Next, Remix, etc). It's a drop-in replacement for useEffect()/useLayoutEffect(). All the GSAP-related objects (animations, ScrollTriggers, etc.) created while the function executes get collected and then reverted when the hook gets torn down. Here is how it works: const container = useRef(); // the root level element of your component (for scoping selector text which is optional) useGSAP(() => { // gsap code here... }, { dependencies: [endX], scope: container }); // config object offers maximum flexibility Or if you prefer, you can use the same method signature as useEffect(): useGSAP(() => { // gsap code here... }, [endX]); // simple dependency Array setup like useEffect() This pattern follows React's best practices. We strongly recommend reading the React guide we've put together at https://gsap.com/resources/React/ If you still need help, here's a React starter template that you can fork to create a minimal demo illustrating whatever issue you're running into. Post a link to your fork back here and we'd be happy to take a peek and answer any GSAP-related questions you have. Just use simple colored <div> elements in your demo; no need to recreate your whole project with client artwork, etc. The simpler the better.
BDubbs Posted August 1, 2024 Author Posted August 1, 2024 So if I have multiple useRefs saved to individual constants, do I need to have multiple useGSAPs? Or can I just make one, put all my gsap code in there, and list all the scope at the end of that code?
Rodrigo Posted August 2, 2024 Posted August 2, 2024 Hi, That will depend on how your code is working. If you're creating animations as a result of a state update, then you'll need more than one useGSAP instance. If you're creating animations based on user interactions (click, mouse over/out, etc.) then you can use contextSafe. Check our learning center in order to understand how it works and different use cases https://gsap.com/resources/React/ Happy Tweening!
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now