4

I have an svg which forms the basis of my horizontal scroller.

Within this svg, I have added the class .animate to the elements which I want to fade in up as the item comes into view. The .animate class for reference has been added to all the text items in the svg.

Currently, only the .animate elements that are in view initially fade in up. When I scroll down to continue the scroller, the other elements are static. They're not fading in or translating up or down in any way?

TL;DR, here is what I'm trying to achieve:

  • When the scroller pins in place, and the user continued to scroll down, start fading away .horizontalScroller__intro.
  • Once .horizontalScroller__intro has faded away, start the horizontal scroll for .horizontalScroller__items
  • Any elements with the class of .animate in my scroller will fade in up to its original position.

Note: I understand SO rules and preferences to post code here. But, my demo's contain a length SVG, which I cannot post here as it exceeds SO's character limit.

Here is a demo of my latest approach

From the scrollTrigger docs, containerAnimation is what helps achieve animations on horizontal scrollers, and is what I've tried to achieve.

However, in my demo above, I have the following issues:

  1. .horizontalScroller__intro doesn't show initially, when it should, and should fade out on scroll.
  2. The horizontal scroller doesn't work anymore
  3. The .animate elements that are in view, do not fade in up

If I use timeline (see below snippet), then the intro fade out and scroller works. But, doesn't animate in the child elements, which is where I need containerAnimation

$(function() {

  let container = document.querySelector(".horizontalScroller__items");

  let tl = gsap.timeline({
    scrollTrigger: {
      trigger: ".horizontalScroller",
      pin: ".horizontalScroller",
      anticipatePin: 1,
      scrub: true,
      invalidateOnRefresh: true,
      refreshPriority: 1,
      end: '+=4000px',
      markers: true,
    }
  });

  tl.to('.horizontalScroller__intro', {
    opacity: 0,
  })

  tl.to(container, {
    x: () => -(container.scrollWidth - document.documentElement.clientWidth) + "px",
    ease: "none",
  })

});

I'm struggling to find a way in which I can make the intro fade in, the scroller scroll horizontally, and the .animate elements to fade in, or fade in up.

Edit:

@onkar ruikar, see notes based on your sandbox below:

  1. When you scroll down and the comes into view, I want the initial .animate elements to scroll up into view (currently, once the text fade away, and then the horizontal scroller starts working, only then does the .animate that are suppose to be in view, fade in up
  2. After the initial .animate elements have loaded, the next .animate elements that are part of the scroller, they do not fade in up. They're static. As each .animate element comes into view, then it should fade in up (I think it's currently triggering once, for all the elements).

See visual here:

enter image description here

In the above gif, you can see the first two text blocks are hidden, as soon as they're in view, I want them to fade up. Then the 3rd and 4th text blocks are static, when they should fade up as the user scrolls to that section.

Freddy
  • 683
  • 4
  • 35
  • 114

1 Answers1

2

You need to use onUpdate method on the scroll trigger.

onUpdate: self => console.log("progress", self.progress)

Based on the self.progress set opacity, x position etc.

Full demo on codesandbox. Click on "Open Sandbox" button on bottom right to see the code.

if ("scrollRestoration" in history) {
  history.scrollRestoration = "manual";
}
$(function() {
  let container = document.querySelector(".horizontalScroller__items");
  let elements = gsap.utils.toArray(
    document.querySelectorAll(".animate")
  );
  let intro = document.querySelector(".horizontalScroller__intro");
  let svg = document.querySelector("svg");
  let animDone = false;
  window.scrollPercent = -1;

  var scrollTween = gsap.to(container, {
    ease: "none",
    scrollTrigger: {
      trigger: ".horizontalScroller",
      pin: ".horizontalScroller",
      anticipatePin: 1,
      scrub: true,
      invalidateOnRefresh: true,
      refreshPriority: 1,
      end: "+=600%",
      markers: true,
      onEnter: (self) => {
        moveAnimate();
      },
      onLeaveBack: (self) => {
        resetAnimate();
      },
      onUpdate: (self) => {
        let p = self.progress;
        if (p <= 0.25) {
          let op = 1 - p / 0.23;
          intro.style.opacity = op;
          animDone = false;
        }

        if (p > 0.23) {
          moveAnimate();
          // we do not want to shift the svg by 100% to left
          // want to shift it only by 100% - browser width
          let scrollPercent =
            (1 - window.innerWidth / svg.scrollWidth) * 100;
          let shift = ((p - 0.22) * scrollPercent) / 0.78;
          gsap.to(svg, {
            xPercent: -shift
          });
        }
      }
    }
  });

  function resetAnimate() {
    gsap.set(".animate", {
      y: 150,
      opacity: 0
    });
  }
  resetAnimate();

  function moveAnimate() {
    for (let e of elements) {
      if (ScrollTrigger.isInViewport(e, 0.4, true))
        gsap.to(e, {
          y: 0,
          opacity: 1,
          duration: 2
        });
    }
  }
});

You need to set opacity 0 on .animate elements in CSS. And use end: '+=400%' instead of 4000px. Relative dimensions can be used in position based calculations easily.

the Hutt
  • 16,980
  • 2
  • 14
  • 44
  • 1
    Hi, thanks for your answer :) Is it possible to achieve what I'm after using `containerAnimation` (https://greensock.com/3-8/#containerAnimation), as GSAP have recommended this as best practise when doing animations within a horizontal scroller? – Freddy Feb 03 '22 at 19:11
  • Unfortunately, "Pinning and snapping won't work on ScrollTriggers with a containerAnimation." [ref](https://codepen.io/GreenSock/pen/WNjaxKp). I couldn't keep intro text fixed in center with this. It moves to left as we keep scrolling. – the Hutt Feb 04 '22 at 06:46
  • hmm, thanks for confirming. I had a similar issue and conflicts between `timeline` and `containerAnimation`. I have edited my question to follow up on the demo you have provided. – Freddy Feb 06 '22 at 20:30
  • Using `ScrollTrigger.isInViewport(...)` we can achieve that. See the updated demo. Also added ` history.scrollRestoration = 'manual';` so that the browser won't remember last scroll position and it'll start at start every time.. – the Hutt Feb 07 '22 at 12:36
  • Thanks, I can see progress, which is good! however, as soon as the user scrolls to the `.horizontalScroller` section, I want the elements with the class of `.animate` for start fading up. For example, see this screenshot: https://i.imgur.com/VwUpjrk.png the two text items that are missing in that screenshot need to start appearing as soon as they're in view (they currently only become visible once the user starts scrolling. Then, the `.animate` elements that are not in view (and are only accessible after scrolling), they need to appear as the user scrolls (as your demo is currently doing). – Freddy Feb 07 '22 at 19:11
  • Fix is easy animate them in `onEnter` hook. Check the updated demo. – the Hutt Feb 08 '22 at 09:09
  • 1
    Thanks. I've dissected your code and started to understand it better. There's one bit I cannot get my head around and that is the `end` parameter. So, in your demo, `end: '+=400%'` which makes the last piece of text cut off at the edge of the screen (see screenshot: https://i.imgur.com/pvxVvpJ.png). I'm trying to make keep the section pinned until the last piece of text is near the center of the section. – Freddy Feb 10 '22 at 00:15
  • To address this I've tried `end: () => "+=" + document.querySelector('.horizontalScroller__items').offsetWidth,`, but this makes the scroller unpin sooner rather than later. What must be done here to ensure the scroller isn't fast and also makes sure the last piece of content is comfortably in view before it unpins? Something dynamic is ideal as I need the scroller length to adapt for responsive too. – Freddy Feb 10 '22 at 00:15
  • Because we are not using `containerAnimation` and doing shifting ourselves, the setting `end: +=400%` decides speed of the left shift, and not the amount of shift. How much to shift is done in `onUpdate` method. I've fixed the calculations. – the Hutt Feb 10 '22 at 06:39
  • Thanks for explaining. I've just tested this approach (your demo) in Safari, and it flickers. I stripped the code back to test if it still occurs without the `shift` method, and it worked fine. Is there a way to make this `shift` method work on Safari also? – Freddy Feb 10 '22 at 16:12
  • I think the issue is related to scroll inertia or very high scroll speed on those devices. Gsap [demos](https://codepen.io/GreenSock/pen/WNjaxKp) also do not work well. – the Hutt Feb 10 '22 at 16:47
  • The linked demo works for me in Safari? on my demo, I'm not scrolling at high speeds. It works in Chrome, but doesn't scroll at all in Safari, see gif here: https://i.imgur.com/XJPYKgf.gif - The flicker happens everytime I'm trying to scroll to scroll through the SVG – Freddy Feb 10 '22 at 17:02
  • On further look, it seems like `.pin-spacer` removes and adds itself back to the DOM on scroll. Could the `shift` be causing this in any way? – Freddy Feb 10 '22 at 19:52
  • 1
    Ah, figured it out. I had `ScrollTrigger.refresh();` running on scroll which was causing the glitch. Many thanks for the help and the explanations :) – Freddy Feb 10 '22 at 20:13