Jump to content
Search Community

Syncing infinite ScrollTrigger with Draggable on a 3d Carousel animation

coldgroove test
Moderator Tag

Go to solution Solved by Rodrigo,

Recommended Posts

Hi! We really like this 3d carousel idea and tried to implement our own version. We have two main problems:

1 - Can't figure out how they made the scrolltrigger infinite

2 - Tried to add a drag option as well with Draggable plugin and wanted to bind it to the scrolltrigger so the card positions are in sync with it.

 

Here is our implementation (couldnt make it as a pen because I'm lazy and dont know how to create a react envo in codepen but hey, here is the live demo if you wanna see how it looks and behaves):

 

vars and useEffect:

 

const numberOfCards = allCases.length;
const angleOfCards = 360 / numberOfCards;
const cardGaps =
    numberOfCards > 6
      ? Math.pow(numberOfCards, 2.2)
      : -angleOfCards / numberOfCards;

useEffect(() => {
    gsap.registerPlugin(ScrollTrigger, Draggable);
    const root = rootRef.current;
    const container = containerRef.current;
    const cards = cardRefs.current;
    const dragProxy = dragProxyRef.current;
    // Mouse Tracking animation
    const onMouseMove = (e) => {
      if (!root || !container || !cards) return;
      const { clientX, clientY } = e;
      const { width: clientWidth, height: clientHeight } =
        root.getBoundingClientRect();
      const rotateXVal = (clientY - clientHeight / 2) * 0.06;
      const rotateYVal = (clientWidth / 2 - clientX) * 0.01;

      container.style.transform = `rotateX(${rotateXVal}deg) rotate(${rotateYVal}deg)`;
    };
    document.addEventListener("mousemove", onMouseMove);
    // Gsap Animations with scroll and drag
    const ctx = gsap.context(() => {
      if (!root || !container || !cards || !dragProxy) return;
      // Drag
      Draggable.create(dragProxy, {
        type: "x",
        trigger: cards,
        bounds: container,
        onDrag: function () {
          const progress = (this.startX - this.x) * 0.008;
          cards.forEach((card, idx) => {
            card.style.transform = `rotateY(${
              (idx + progress) * angleOfCards
            }deg) translateZ(320px) translate3d(0px,0px,${cardGaps}px)`;
          });
        },
      });
      // Scroll
      ScrollTrigger.create({
        pin: true,
        scrub: true,
        invalidateOnRefresh: false,
        start: "top top",
        end: "+=10000", //i set the end as a big number to at least rotate the carousel one whole turn since i can't make it infinite tried to mimic it
        trigger: root,
        // markers: true,
      });
      cards.forEach((card, idx) => {
        ScrollTrigger.create({
          scrub: true,
          start: "top top",
          end: "max",
          trigger: container,
          onUpdate: (self) => {
            gsap.delayedCall(0.1, () => {
              card.style.transform = `rotateY(${
                (idx + self.progress * 10) * angleOfCards
              }deg) translateZ(320px)  translate3d(0px,0px,${Math.abs(
                (cardGaps * (cards.length + 5)) / cards.length
              )}px)`;
            });
            gsap.delayedCall(1, () => {
              card.style.transform = `rotateY(${
                (idx + self.progress * 10) * angleOfCards
              }deg) translateZ(320px) translate3d(0px,0px,${cardGaps}px)`;
            });
          },
        });
      });
    });

    return () => {
      ctx.revert();
      document.removeEventListener("mousemove", onMouseMove);
    };
  }, []);

JSX:

 

<section className={s.root} ref={rootRef}>
      <div className={s.carouselContainer} ref={containerRef}>
        {allCases &&
          allCases.map((c, idx) => {
            return (
              <div
                key={c.id}
                onMouseEnter={() => setSelectedCaseIdx(idx)}
                style={{
                  transform: `rotateY(${
                    idx * angleOfCards
                  }deg) translateZ(320px) translate3d(0px,0px,${cardGaps}px)`,
                }}
                ref={(e) => createRefs(cardRefs, e, idx)}
                className={s.carouselCard}
              >
                <CustomLink link={c.caseLink} className="z-10">
                  <NextImage
                    layout="fill"
                    media={c.bgImg}
                    objectFit="cover"
                    className="rounded-lg"
                  />
                  <div className={s.content}>
                    <p className={classNames(s.description, "text-lg")}>
                      {c.description}
                    </p>
                    <h3 className={s.bigTitle}>{c.bigTitle}</h3>
                  </div>
                </CustomLink>
              </div>
            );
          })}
      </div>
      <div ref={dragProxyRef} className="invisible absolute" />
</section>

 

Here is the related css classes just in case something is wrong with it?

 

.root {
  @apply pt-20 2xl:pt-32 px-20 min-h-screen text-primary relative;
}

.carouselContainer {
  @apply relative flex justify-center items-center;
  min-height: 650px;
  width: 100%;
  transform-style: preserve-3d;
  perspective: 1300px;
  /* transform-origin: center center calc(-300px / 100); */
}

.carouselCard {
  @apply rounded-lg;
  overflow: hidden;
  width: 320px;
  aspect-ratio: 0.67;
  position: absolute;
  transition: transform 1s;
  -webkit-box-reflect: below 10px
    linear-gradient(transparent, transparent, #0005);
}

 

  • Like 1
Link to comment
Share on other sites

We love to see minimal demo's, that way we can see your thought process and thus better help you. 

 

Sorry we can't really debug live websites, there is just no way to modify the code. Try creating a minimal demo of what you're trying to do, this has two benefits. First this allows you to experiment and try out new ideas. By making it simple people usually solve 90% of their own bugs. Second, you have an easy version you can share in which anyone could edit and modify the code.

 

If you want to use React check out our StackBlitz React template here

Link to comment
Share on other sites

  • Solution

Hi,

 

I wouldn't use ScrollTrigger for that particular case, especially if the element will use the entire page. You can detect scroll direction and speed to tween the progress of an instance that rotates the elements or it's parent.

 

I would use the Observer Plugin:

https://greensock.com/docs/v3/Plugins/Observer

 

Here is a simple example of how it works:

See the Pen XWzRraJ by GreenSock (@GreenSock) on CodePen

 

Also you already have a setup using Draggable, but maybe you can get a few extra ideas from this one:

See the Pen RwLepdQ by GreenSock (@GreenSock) on CodePen

 

Now if you want to use infinite scrolling (personally I still think Observer is a better option) you can check this example:

See the Pen LYRwgPo by GreenSock (@GreenSock) on CodePen

 

Hopefully this helps.

Happy Tweening!

  • Like 1
Link to comment
Share on other sites

Hey @Rodrigo! Thanks a lot for the Observer advice. I had no clue about that plugin. It's awesome! As much as I'm not sure if gsap is a perfect match w/React, there is always a solution in GSAP if devs know about their needs. Observer solved all the problems mentioned. Thanks once again! 

  • Like 2
Link to comment
Share on other sites

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 account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...