Jump to content
Search Community

Missing Something w/ Horizontal ScrollTrigger 😅

jh3y test
Moderator Tag

Go to solution Solved by GreenSock,

Recommended Posts

Hey y'all!

 

I'm messing around with this horizontal scrolling demo where I'm using ScrollTrigger and setting the body width to an excessive width. The effect works as expected when scrolling (I've not styled things up yet, POC 😅).

 

But, I want to add "Next/Previous" buttons to the UI and I can't see what obvious thing I'm missing 🤦‍♂️ 

"Previous" works as expected. But, "Next" is never able to reach the last item. I'm assuming I'm not doing something quite right in `SCROLLER.scroll` at Line ~#150. Or, my `start` and `end` isn't quite right? Could even be how I'm determing which item is the "Current" one. For that, I'm using `Math.floor(mapRange(0, 1, 0, CARDS.length, self.progress))`. And the snap "seems" to work.

 

Anyone able to point out what obvious thing I'm missing? 🙏😅

See the Pen 9087b98b088d025b16fc50aff45ba9f1 by jh3y (@jh3y) on CodePen

Link to comment
Share on other sites

  • Solution

It looks like the browser won't allow you to go quite to the maximum scroll value which is calculated like document.documentElement.scrollWidth - window.innerWidth. If you literally try setting window.pageXOffset to that, the browser is like "ummm...nope. I won't move at all. Completely ignoring you." An easy solution is to subtract a tiny portion from the end value, like:

// BEFORE (bad)
SCROLLER.scroll((INDEX + 1) * (SCROLLER.end / (CARDS.length - 1)));

// AFTER (good)
SCROLLER.scroll((INDEX + 1) * ((SCROLLER.end - 0.1) / (CARDS.length - 1)))

Also, one minor tip - you could simplify this:

Math.floor(mapRange(0, 1, 0, CARDS.length, self.progress))

By using the gsap.utils.pipe() function to create a reusable function that does all that work for you:

const mapProgToIndex = pipe(
      mapRange(0, 1, 0, CARDS.length),
      Math.floor
  );

// then later...
mapProgToIndex(self.progress);

Here's a fork: 

See the Pen 47957fe9703cbe2e4ee4ae29d43aa1fc?editors=0010 by GreenSock (@GreenSock) on CodePen

 

Better? 

  • Like 2
Link to comment
Share on other sites

Ahh. That's great! Thanks. I did wonder if doing a little subtraction was the answer. But, I think I was subtracting from the whole thing which wouldn't work 🤦‍♂️

Ooo. I like that utility method approach. That's not one I've seen/tried. Thank you. I'll try that 👍

 

One other question/clarification. I'm not sure if this is possible. I tried some "Hacky" ways to try and get it there. But, is there a way to get infinite scrolling working with something like this using `onLeave` and `onLeaveBack`? with `snap`. I figure that would be hard because `snap` effects the progress and then it ends up jumping back and forth between the first and last getting caught in a snap loop. It's not a requirement for this, more so a curiosity of how to do it. I'm making some materials around making this and I can start the scroll at the mid-point so it starts with things on either side 👍

Link to comment
Share on other sites

The hard part about infinite things on scroll is that the scroll bar is limited while the effect that you're wanting is not. So you have to either loop the scroll position like this demo (found in the ScrollTrigger demos section) or hook directly into the scroll-related navigation events (like the wheel event) instead of actually using the actual scroll position.

Link to comment
Share on other sites

Yeah, that's what I was doing and it works for the most part if I disable `snap`. I did see that demo and also referred to how we used it for the DJ scratching demo. It was more curiosity for this one. I think I'm going to keep it as non-infinite and start it at an offset.

 

Thanks again.

Link to comment
Share on other sites

@jh3y have you thought about building things a bit differently so that users scroll vertically instead of horizontally? It seems much more natural to me; every time I opened your demo, I've tried to scroll down and nothing happens and it takes me a few seconds to realize "oh yeah, I have to scroll horizontally in this one..." :)

 

Building in that way may also open up some possibilities with the infinite scroll and snapping. I couldn't help myself - I had to build a little demo for you and it was pretty fun:

See the Pen LYRwgPo?editors=0010 by GreenSock (@GreenSock) on CodePen

 

There are some strategies in there for the infinitely looping stuff that might be worth using/studying. I'm doing custom snapping and smooth scrubbing as well as using one "raw" timeline to lay out the staggered animations (with extra ones) and then another one to essentially move its playhead in a way that makes it appear to seamlessly loop. I think you're the kind of developer who might enjoy more advanced stuff like this. 

 

It's fun to scroll through and it seems to perform quite well. 

 

Anyway, I hope this is helpful. 🙌

  • Like 6
Link to comment
Share on other sites

Ooo, that works seamlessly 👏 Thank you @GreenSock

 

I did change the demo a little to use a single tween. Then I use function based values to map how each visible element should appear. I'll see if I can fit that in 🤓

 

This is really cool. Thanks for taking another look at it. Yeah, this sort of thing I love digging into!

 

See the Pen YzGmNLL by jh3y (@jh3y) on CodePen

  • Like 1
Link to comment
Share on other sites

@GreenSock I love it! 😍

 

See the Pen xxRKggV by jh3y (@jh3y) on CodePen

 

I've gone through the code and retyped it to walk through. I think I understand most parts. Very clever! 👏

 

I'm looking to write this up/make a video on it at some point. It would be cool to show the evolution of implementing it. Of course, I'll be crediting this forum heavily 😅 I love the solution you've presented because it's GSAP'ping GSAP if I'm understanding it right which I think is brilliant.

 

I've made some notes and things I've tried to understand. I'm more than happy with an ELI5 on some of these if you're able to. I'd love to properly understand it. I love this kind of thing as it gives me something new to dig into/challenge myself with understanding.

 

- raw timeline powers card appearance. This is neat. Although my "Map" approach worked, this is more GSAPPY and accurate. If we wanted to offset the scale, etc. We'd introduce another `fromTo` with a `yoyo` and maybe a reduced duration with a delay/position?

- Wrapping. This part seems straighforward. If we go forward, increment, if we go backward, decrement. Keep  going backwards and we keep counting down from 10. The iteration is used for accuracy with the scrub?

- The main mechanism. I think this is something I need to type out to clarify my thinking which could be off 😅 We use scrollTrigger to update the totalTime of the scrub. And that puts the loop playhead into different positions which in turn updates the position of the raw timeline? There's a bit to swallow there 😅

- The overlap part. I'm not 100% sure about how this works. This part I probably need a little ELI5 on as well. It's mainly this part. The overlap adds extra time playing the raw timeline which makes it loop round better. And that's because inside there would be a wrap in either direction?

 

  

LOOP
    .to(RAW, {
      time: LOOP_TIME,
      duration: LOOP_TIME - START,
      ease: 'none',
    })
    .fromTo(
      RAW,
      { time: OVERLAP * SPACING + 1 },
      {
        time: START,
        duration: START - (OVERLAP * SPACING + 1),
        immediateRender: false,
        ease: 'none',
      }
    )

 

 

Sorry if these are obvious questions. I'd love to get a proper handle on this before attempting to write about it. Think it's a real cool thing to show. The idea behind building something like this was to use it to showcase the CodePen on my site with 😎

 

 

  • Like 1
Link to comment
Share on other sites

3 hours ago, jh3y said:

I'm looking to write this up/make a video on it at some point. It would be cool to show the evolution of implementing it. Of course, I'll be crediting this forum heavily 😅 I love the solution you've presented because it's GSAP'ping GSAP if I'm understanding it right which I think is brilliant.

Yep, exactly - we've actually got 3 levels of GSAP-ing; we've got the raw timeline which is the base which has "extra" animations because we want things to be "infinite", so we can't start the animation at the very beginning - there won't be any elements showing to the LEFT of that first element. So, for example, if we've got 5 elements to animate, it'd look like:

1-2-3-4-5-1-2-3-4-5-1
          ^ start here

The "overlap" is just the number of elements on each side of the start/end that we'll pad it with (so we'll add 2 * overlap number of extra animations). It really just depends how many elements are shown on the screen at any given time. The goal: cover the extra space. Here the overlap is 3 (thus 6 extra animations tagged onto the end). The tighter the spacing, the more elements/animations you'll need to cover the gaps which is why I've got it set to Math.ceil(1 / spacing)

 

We pause that timeline because we'll control its playhead with ANOTHER timeline we're creating - "seamlessLoop". As indicated above, we'll start at the first "extra" animation so that there's stuff to the left ("5-4-3..."). Then we tween forward by the "overlap" amount (3 here) at which point we jump the playhead backward that same spot in the "normal" set of animations (non-extra). 

1-2-3-4-5-1-2-3-4-5-1
      ^ <------ ^ jump from here

Then it tweens forward to where we started, thus if you were to play the seamlessLoop timeline (which just contains two tweens that scrub the raw timeline's playhead), it'd appear to seamlessly loop the "raw" timeline. 

 

Cool, huh? 

 

Most people really struggle with figuring out how to do a seamless loop where elements go off one side of the screen and come in the other side later. If you can explain this approach well in your tutorial (or whatever), it might help a lot of developers. 

 

Smooth scrubbing

So now we've got a seamless loop that'd play, but we want to link that playhead to the scrollbar and have it SMOOTHLY move to a new position whenever the user scrolls, so we create a simple "scrub" tween to handle that which we reuse over and over again whenever we're going to a new position. That's cheaper than creating a new tween instance on every update (which could be quite frequent while scrolling). No need to manage overwrites that way too. 

 

Snapping

Since we spaced all the animations out on the "raw" timeline with the "spacing" value, and we set up our "seamlessLoop" to start at exactly the spot where the first element is in the middle of the screen, snapping is easy. If "spacing" is 0.1, for example, we know that element #2 will be centered on the screen at a time of 0.1, the third would be at 0.2, etc. So it's index * spacing. 

 

Infinite looping & "iteration"

We set the seamlessLoop to a repeat: -1 to allow us to just keep pushing the totalTime forward however far we want, and the timeline handles the looping at the appropriate spots. 

 

I'm using scrub.vars.totalTime is the destination value for the scrubbing. It's a convenient way to keep track of that too. So when you hit the "next" or "previous" buttons, all we've gotta do is add/subtract whatever our "spacing" amount is to get to that next spot on the timeline. The only catch is that you can't really go backwards past 0 (the start of the timeline), so we sense that condition and shove the playhead forward 10 iterations (it could be any number, there's no reason it has to be 10). 

 

We need to track the "iteration" in order to appropriately handle the wrapping around for the infinite scrolling effect. Otherwise, think about what'd happen if you scrolled all the way down (progress: 1) and it shot you back to the start (progress: 0) - the scrub tween would end up REWINDING all that distance instead of acting like it's continuing forward. 

 

Note: I based everything off of a tween duration of 1 second. Originally I had that as a variable, but then I realized it's not very helpful. It seemed cleaner to just use 1 across the board, and then you can adjust the "spacing" variable for a different effect, and/or alter the tweens, of course, but there seemed no benefit to having yet another "duration" variable floating around. So if you see the value "1" in various spots, it's likely related to duration. 

 

4 hours ago, jh3y said:

. Although my "Map" approach worked, this is more GSAPPY and accurate.

The down side of your previous solution was that it had to run a bunch of logic and create a set of new tweens each and every time you reached a new "slot". If someone scrolls really fast, it's expensive and you were actually creating conflicting tweens (easily remedied by setting overwrite: true, but still). You'd also get inconsistent timing because imagine what'd happen if one of the elements is halfway to its destination, and then the user scrolls so now it's gotta go to a new destination...if you've got a duration of 0.5, for example, it'd move a lot faster if that spans 800px than if it spans 50px. So the spacing between things could get a bit inconsistent. If your tweens are fast enough, people probably wouldn't really notice. But I obsess about this stuff. 

 

When you use a single timeline like this, all the tweens are created ONCE and interpolation is very fast. Plus you get totally consistent spacing no matter how fast the user scrolls or clicks the buttons. Cheaper, faster, more accurate and consistent. 🎉

 

Answers to your comments in the code

Regarding the onRepeat that works around a super rare edge case bug that's fixed GSAP 3.6.1 - I'd keep it in there because 3.6.1 isn't out yet, and people may use older versions. Without getting into the weeds too much, if you go backwards over a loop threshold in a paused timeline (before 3.6.1), it could render (just for that one time) at the very beginning of the iteration. Since our demo is constantly tweening that playhead (with "scrub" tween), there would be situations where going backwards past the first element would have that tween start at the wrong value because the tween happened to snag the starting value at just that moment, thus things would appear to go back a whole iteration. 

 

// There are two fromTo because the properties have a different duration.
// We want the cards to appear quicker.

No, it's just because we have some properties that yoyo from the right side of the screen to the left (opacity and scale, for example) so they get big until the center of the screen, then go back down whereas the "xPercent" tween is consistently decreasing that whole time. So we need TWO simultaneous tweens for all that. 

 

5 hours ago, jh3y said:

Sorry if these are obvious questions. I'd love to get a proper handle on this before attempting to write about it. Think it's a real cool thing to show.

These are NOT obvious questions and I'm glad you asked. It's a relatively advanced concept. I don't think I've seen anyone else take this approach, but in my opinion it has a lot of merit. 

 

Phew! Longest post in a long time. But I'm excited about what has been accomplished here.

  • Like 4
  • Thanks 1
Link to comment
Share on other sites

Hey @GreenSock 👋

 

I've sat down to try and get my head around things 😁 It might take me a couple of rounds to fully get the picture accurately in my head.

My goal though, is to make this easier to understand for people. If I can make it make sense to people who've barely used GSAP, that's a huge win!

 

With that in mind. I'm going to fire out some random notes I took from reading your response.

 

  • Yep. We're "Meta GSAP" 😅 3x
  • The way the gap is covered is based on the overlap defined and the amount of items available. In our example, 10 because `Math.ceil(1/0.1)`. With a number of items as low as 5, the spacing would need to be increased or the overlap reduced? Because you need enough elements to wrap the front and back at the right time. And that's how the RAW timeline works because the modulo grabs the overlaps and tacks them onto the loop?
  • The loop's job is to move the playhead on raw. And because raw infinitely repeats and our scrolling increases the `totalTime`, it'll keep moving the playhead?
  • Raw.time sets that center point for us
  • I think I need to spend more time looking at what loopTime, start, etc. are doing with regards to the fromTo on raw. But, a high level explanation is that. It's still that code snippet I posted before that I need to get my head around for the timing.
  • ScrollTrigger part. '+=3000' could be whatever we want in reason? I think the wrapping part I understand. I like how that and the next/prev part are done. It's neat. The actual movement with ScrollTrigger. Is that because increasing/reducing `totalTime` affects the playhead position of the loop? Or is it because the scrub keeps tweening the time up and down of the loop incrementally/decrementally?
  • Yeah, the way it's all done on one makes a lot of sense. I can feel the performance difference between the two. It's noticeable. The obsession is appreciated. I'd rather do it right than get it out the door quick and hit hurdles later.
  • Yep, I got that `yoyo` part. Don't think I worded myself very well. Working on it!

 

Oof. A lot to digest 😅

 

Sorry if I sound like a broken record with some of them pointers. I'm keen to make something visual to help people out with this. Heck, I understand it better than I did at first glance. Progress is being made!

 

Thanks again for all this. I really appreciate your help.

Link to comment
Share on other sites

1 hour ago, jh3y said:
  • The way the gap is covered is based on the overlap defined and the amount of items available. In our example, 10 because `Math.ceil(1/0.1)`. With a number of items as low as 5, the spacing would need to be increased or the overlap reduced? Because you need enough elements to wrap the front and back at the right time. And that's how the RAW timeline works because the modulo grabs the overlaps and tacks them onto the loop?
  • The loop's job is to move the playhead on raw. And because raw infinitely repeats and our scrolling increases the `totalTime`, it'll keep moving the playhead?

I think it might help you visualize things if you comment-out everything except the "raw" timeline (and unpause it, maybe set its timeScale to 0.1 to really slow it down) and just watch what it's doing. It's basically like a staggered animation with some extra iterations slapped onto the end. 

 

Let that sink in.

 

Now we can move to the next step - the seamlessLoop. Why do we even need it? 

 

Notice that the raw timeline starts with everything off to the right, then it starts moving the first element, then the second, etc. But in our final version, we don't want it to look like that - it's supposed to look like everything wraps, so you'd never see a bunch of empty space on the left of the first element - you should see the LAST element in the parade just to the left of the first element (as if it wrapped around). How the heck can we do that? 

 

That's the point of putting those extra iterations at the end. Notice that if you let "raw" play to the end of the first iteration, that first element shows up right after the last one - THAT is how everything should look at the start! So we need a way to make the playhead start there. I calculate that position as the "startTime" variable. And then find a safe spot AFTER that to jump back to the corresponding spot in the first iteration. For example, when the "4" is in the middle of the screen on the 2nd iteration ("extra"), we can jump back to exactly that spot in the FIRST iteration and everything will look identical (except now we're on the first iteration, non-extra). I calculate that time as "loopTime" - it's the time on the raw timeline where we can loop back safely. Now we can play forward to where we started. Two tweens. Done. And when I say "play forward" I mean that we're literally tweening the playhead with a linear ease (the "raw" timeline remains paused technically). 

 

I'd recommend getting rid of all the extra fluff and let the seamlessLoop run (unpause it). Visualize what it's doing. Sit with it. If it's not clear yet, ping me back and we can walk through it. Don't move on to the ScrollTrigger or the scrub stuff until you really grasp what these two timelines are doing.

 

1 hour ago, jh3y said:

ScrollTrigger part. '+=3000' could be whatever we want in reason?

Yep, I just randomly chose that number. I wanted there to be enough scrolling space to feel good. Not too little, not too much. But yeah, choose whatever you want. It's just the total scrolling distance. 

 

1 hour ago, jh3y said:

the way it's all done on one makes a lot of sense. I can feel the performance difference between the two. It's noticeable. The obsession is appreciated. I'd rather do it right than get it out the door quick and hit hurdles later.

🎉

 

All good questions. Don't hesitate to ask more if you're stuck. I recognize this is a pretty advanced demo, but once it clicks and you get the concept, I think you'll really dig it and it could be useful for a lot of stuff you do in the future. 

  • Like 3
Link to comment
Share on other sites

Hey @GreenSock!

 

I think we're starting 2021 off great with an attempt for longest thread 😅

 

I think I get the overlap. Perhaps my wording wasn't quite right again. I had a play around with overlaps and manually adjusting the spacing, etc.

I think typing out the calculations and their values may help me understand it a bunch more. 

 

I've been messing with unpausing the timeline and slowing down Raw here

 

See the Pen WNoNoLZ by jh3y (@jh3y) on CodePen

 

And now I'm going to walkthrough as I understand it.

 

For example, with 50 cards, an overlap of 10 and a spacing of 0.1.

 

See the Pen zYoYoaM by jh3y (@jh3y) on CodePen

 

Loop time: (50 + 10) * 0.1 + 1 = 7

Start time: 50 * 0.1 + 0.5 = 5.5

 

Then the LOOP tweens RAW to a time and duration of

 

time: 7

duration: 1.5

 

That first part is responsible for moving the items from right to left. To a time of 7 equates to the 70 cards in this case which may be coincidence. As a loop time for 30 cards would be.

 

(30 + 10) * 0.1 + 1 = 5

 

Maybe not then. That number is equal to (Cards + (2 * overlap))?

 

But, the start time would be at the 55th position in the tween. Minus the overlaps that would mean showing the 45th card and up? Why a duration of 1.5 though?

 

See the Pen zYoYoaM by jh3y (@jh3y) on CodePen

 

I could adjust that to offset the positioning, etc.?

 

That first part of the loop goes from the first overlap to card 20. That first part of the loop using a duration of 1.5 with a time of 7 means we go from position 45 to 20. But, I'd have assumed it would be travelling only 15 places not 25.

 

See the Pen BaQapXq by jh3y (@jh3y) on CodePen

 

Then we set `fromTo` to Raw

 

Go from `time`

 

time: 10 * 0.1 + 1 = 2

 

to

 

time: 5.5

duration: 5.5 - 2

 

The time is moving to the start position from 20 positions in? Is there a reason for being 20 positions in? In this case, that's 2 lots of overlap but I'm not sure why that works. The extra + 1 on times. In this case it shows 0,1,2,3,4 then jumps to 20 which makes sense. The .5 gives us those 5 starting positions.

 

The tween goes from card 20 to card 5. That little bit of wrap.

 

See the Pen bGBGgaK by jh3y (@jh3y) on CodePen

 

I think I get the concept of the overlap. I think it's the Math part that lets me down a little here with wrapping my head around the different calculated times/duration 😅

 

Also, the way loop works. The way it's split into this `to` and `fromTo`. The durations having + 1 on them or being split in a particular way. The `to` and `fromTo` are tweening a different end of the raw timeline. Then when pushed together they do this:

 

See the Pen poNoejo by jh3y (@jh3y) on CodePen

 

If I'm right in thinking it's because logically looking at it. The to is handling our start position + the second overlap and the the fromTo handles the first part of the loop and the first overlap. But, the missing "Eurekah" I think for me is the Math of the time linking to the duration. Although, the raw time is explicitly set above actually so that means we are going.

 

I read the LOOP as: 5.5 > 7 > 2 > 5.5 > 5.5 > 7 > 2 > 5.5 > 5.5 > 7 etc. Which I think... makes sense?

 

That leaves me to think about the durations. We know that Raw's stagger gives 8. 70 * 0.1 + 1. Is that that +1 in our durations for padding out the duration of the movement?

 

LOOP has durations of

 

(7 - 5.5) and (5.5 - 2) 

 

1.5 and 3.5. I think I'm trying to sus what these are tied to to and then I'm pretty much there 🤞

 

The scrub part I think makes sense to me. I needed to reference the docs for `totalTime` to see that it's a method for scrubbing the playhead to a certain spot 🤦‍♂️

 

Another huge response I'm sorry. I'm adament I'm going to learn this. You're right. It's advanced, but, it's cool! And it will be an awesome thing to have in my toolbelt.

 

I think I'm getting there with the understanding. It's thinking on the meta level 😅

  • Like 1
Link to comment
Share on other sites

FYI, I created a version that waits to do any snapping until after you stop scrolling, so you get a smoother effect while scrolling (because it's not always snapping): 

See the Pen qBqEZKg?editors=0010 by GreenSock (@GreenSock) on CodePen

 

The change was very simple - I removed the snap() wrapper in the ScrollTrigger's onUpdate and then added this: 

// when the user stops scrolling, snap to the closest item.
ScrollTrigger.addEventListener("scrollEnd", () => scrubTo(snap((iteration + trigger.progress) * seamlessLoop.duration())));

 

On 1/29/2021 at 6:08 AM, jh3y said:

Why a duration of 1.5 though?

Oh, that's just because we want it to play at "normal speed", like a ratio of 1:1. So if we're tweening the playhead from a time of 5.5 to 7, for example, that's 1.5 seconds. If, for example, we made that tween have a duration of 3 it would play at half-speed (taking 3 seconds to move the playhead 1.5 seconds). 

 

So it's super easy when you're tweening the playhead of another animation - you just use an ease of "none" and make the duration match the distance you're traveling. Make sense? 

 

Looping math

I'm not sure I can explain it any better than my original post: 

1-2-3-4-5-1-2-3-4-5-1
          ^ start here

We always start right after the end of the normal (non-overlap) animations. So here we've got 5 items. So When the first element is halfway through its sweep across the screen, thus: 

items.length * spacing + 0.5

Remember, that 0.5 is based on a duration of 1. So it's just half the duration because that's when it'll be in the middle of the screen. 

 

Why do we start here? Because the whole goal here is to never have empty space on the left or right - things are supposed to appear to "wrap". This is the first spot on the timeline where the first element would be on the screen WHILE the last ones are next to it (to its left). 

 

We could have just put 3x the whole items list and then just tweened in the middle:

1-2-3-4-5-1-2-3-4-5-1-2-3-4-5
          ^-------^

I guess that'd make the math easier, and it'd be simpler to visualize - the whole goal is to make sure there's never empty space to the left or right, so this accomplishes that goal. But again, I'm a performance nut so I wanted to keep the number of animations to a minimum so I came up with this whole overlap thing.

 

Now we'd tween forward to the earliest spot we could safely jump back to the corresponding position in the "normal" set of animations: 

(items.length + overlap) * spacing + 1, // 1 is the duration, so it's when the last overlap finishes

Which is visualized like:

1-2-3-4-5-1-2-3-4-5-1
              ^ loop back here (at the end of this tween)

 

Now we need to jump back to the corresponding spot in the "normal" set of animations. I suppose we could subtract items.length * spacing + 1 (the duration of the "normal" set of animations), but I just thought in terms of the overlap: 

overlap * spacing + 1 // again, 1 is the duration

Think of "overlap" like the number of extra animations we're cramming onto each side of the timeline to fill up the space on the screen.

1-2-3-4-5-1-2-3-4-5-1
    ^ jump here (end of this tween)

So now we've covered a total of 5 items but we seamlessly jumped back at one spot.

 

On 1/29/2021 at 6:08 AM, jh3y said:

Is that that +1 in our durations for padding out the duration of the movement?

Yep, I coded it all with the duration being 1 to go from the right side of the screen to the left. 

 

On 1/29/2021 at 6:08 AM, jh3y said:

I needed to reference the docs for `totalTime` to see that it's a method for scrubbing the playhead to a certain spot 🤦‍♂️

Yeah, totalTime() is just time() but it factors in repeats. So if you've got a repeat of 3 and a duration of 1, totalTime would go from 0 to 4 over the course of 4 seconds, but time would go from 0 to 1 and then back to 0 to 1, etc., a total of 4 times in the same 4 seconds.

 

Does that clear things up? 

 

  • Like 5
Link to comment
Share on other sites

Hey @GreenSock!

 

Yeah, that clears some things up. I think the best way for me to try and fully have a handle on this is to start writing up the article for it. I think it might be neat for me to create a non-looping version too using the same technique. That will likely make it easier to see the progression from having the scroll to adding the overlap.

 

One thing with your new snapping version. If you scroll it to "0", it'll infinitely bounce slightly. Or, at least did for me above.

Link to comment
Share on other sites

  • 4 weeks later...

Hey @GreenSock

 

Bringing the thread back to life! 😅

 

I'm trying to write up the article for this at the moment. Not going to lie, this is one of the trickiest articles I've tried to write. Happy to send you a draft link for it. Would be good to get your eyes on it.

 

Anyway. I've got to a point where I'm explaining hooking up ScrollTrigger without the looping. I'm trying to make the technique as "verbose" as possible. The technique I'm using is adding a tween three times to a timeline and replaying a window of that timeline.

 

But, when I hook this up to scroll I get a flicker if I try scrolling back up quick from the end. 

 

See the Pen jOVzExq by jh3y (@jh3y) on CodePen

 

I'm not 100% sure what's causing that. Any ideas? Or is this a drawback of trying to go about doing it this way?

 

I'm thinking this might cause more issues when it comes to wrapping , etc.

 

I notice that this way, I've also made a mess of the snapping 🤔 Perhaps I need to go back to the drawing board and find a better way of explaining the original approach. I felt like doing the three timelines was a good way to explain it and then introduce the padding/overlap concept once the idea has sunk in. I'm trying to find a "nice" way of explaining building the loop and why it's done the way it is instead of cutting out a section of a repeated timeline.

 

If you get a chance to check this out, thanks in advance!

Link to comment
Share on other sites

No problem at all! I'm going to take another look at the original method too and see if I can break it down again. I'll also get the snapping back that way too. I was intrigued to know if this way was also possible though 🤔

 

As always, big thanks in advance with this.

Link to comment
Share on other sites

Alright, @jh3y I was finally able to look at this. Here's what I noticed: 

  • There's a bit of a logic issue going on because you start everything off at xPercent: 5000 and then you have a bunch of .fromTo() tweens with immediateRender: false that make those go from xPercent: 100 to xPercent: -200. Now the .fromTo() tweens with immediateRender: false must technically record an extra state at the very beginning so that if you rewind the parent playhead backwards before that tween, it renders things in the pre-tween state. 

    Imagine box.x starts at 0, for example, and then you do this: 
    let tl = gsap.timeline();
    tl.fromTo(box, {x: 100}, {x: 200, duration: 1, immediateRender: false}, 1);

    It'll basically keep box.x at 0 for 1 second, and then as soon as that tween starts, box.x JUMPS to 100 and animates to 200. Simple. But if we rewind that timeline to a time less than 1, box.x should be 0. See how that tween needed to record that pre-tween state for that reason? 

    So in your example, you've got a scenario where if you rewind, those tweens will put xPercent back to 5000. That "flash" you saw was a tiny spot between two tweens, where the last one was reverting xPercent to 5000.
     

  • The way you built your timeline was odd to me and somewhat logic-challenging because you created an INFINITELY repeating timeline (and in GSAP, it isn't actually infinite - it's just a very large number, otherwise it'd be impossible to accommodate certain other functionality). But you were inserting sub-timelines at the "end" minus 0.75 seconds (`>-${duration - stagger}`) which is sorta odd to think of an infinitely-repeating timeline's "end", you know? I think it'd be much cleaner to get rid of the infinite repeating stuff. 

This is what I would consider to be a better way to build the raw timeline: 

See the Pen c7bee24884f9387f6bd9ca5486388421?editors=0010 by GreenSock (@GreenSock) on CodePen

 

Notice I'm passing in the zIndex to getShift() so that we can ensure that when the first element (bottom-most) loops back, it's sitting on top of the final element in the stacking order. It might not be necessary because you're scaling things such that they're never really overlapping, but I wanted to show this technique in case you needed to have some overlapping at some point. 

 

Does that clear things up? 

  • Like 4
Link to comment
Share on other sites

Hey @GreenSock 👋

 

Ahh, that's perfect. Thanks! Makes a bunch of sense. That's my bad on leaving it as `repeat: -1` on the shifts. Completely right that it wouldn't make sense there 👍 That was the 17th part and I'd likely got a little flustered putting it together.

 

That z-index part and positioning makes complete sense too. Looking at it again, not sure what I'm thinking there 👍😅

I likely needed to step back for a moment!

 

Do you think it makes sense to approach the scroll in this way? As in stacking the three timelines? I've found this maybe a little easier to incrementally build up to for people. I will revisit the original approach again though and see if I can break that down and relate it too 👍

 

This is all really useful. Thank you again Jack for all the help with this! 🙏

 

 ʕ •ᴥ•ʔ

  • Like 1
Link to comment
Share on other sites

13 hours ago, jh3y said:

Do you think it makes sense to approach the scroll in this way? As in stacking the three timelines? I've found this maybe a little easier to incrementally build up to for people. I will revisit the original approach again though and see if I can break that down and relate it too 👍

 

Sure, I think it's totally valid. The approach I took originally was very similar - I just chopped off the pieces of the surrounding two (before/after) timelines that weren't necessary in order to optimize things, but it's not like leaving those extra parts in would significantly slow anything down or even be noticeable in the real world. 

 

It sounds like you are basically doing what I mentioned here: 

On 1/31/2021 at 12:40 AM, GreenSock said:

We could have just put 3x the whole items list and then just tweened in the middle:


1-2-3-4-5-1-2-3-4-5-1-2-3-4-5
          ^-------^

👍

  • Like 2
Link to comment
Share on other sites

Yep! Exactly that 💯

 

Thrilled we're on the same page. This is quite the thread 😅

I can totally mention that optimization though. I think that's the part I originally struggled to visualise. Where that chop gets calculated.

 

Now, I've been practicing the technique in the wild some more, it's kinda sunk in. I figured that might be what I needed to do. It's a real cool technique and definitely feels like unlocking a superpower when you start doing these "meta" things.

  • Like 1
Link to comment
Share on other sites

Looks like I've got hung up on the original issue @GreenSock hahaha 😅

 

Next/Prev buttons with this method. I have snapping in place. But, I can't get past an iteration with this method for some reason. I'm sure I'm missing something straightforward. I'm determined to get a draft of this article complete this week!

 

See the Pen WNoYoMY by jh3y (@jh3y) on CodePen

 

This works fine until I try to wrap 🤔 Then it gets caught understandably I guess in an infinite loop. Is there a way to get out of the loop? My snap is set up based on the amount of elements which seems to work until I try scrubbing with the buttons.

 

NEXT.addEventListener('click', () => {
  const DESTINATION = SNAP(SCRUB.vars.totalTime + (1 / BOXES.length))
  scrubTo(DESTINATION)
})
PREV.addEventListener('click', () => {
  const DESTINATION = SNAP(SCRUB.vars.totalTime - (1 / BOXES.length))
  scrubTo(DESTINATION)
})

 

I guess that's one issue I've not sussed out yet. It has the ability to get stuck in a loop of wrapping when progress is 1. Would it make sense to change the conditionals slightly?

Link to comment
Share on other sites

Hmm. I've tried a couple of things here @GreenSock 🤔

 

But to no avail. Perhaps there's a trick to stopping the snap from going into a loop or I'm missing a piece of logic. I noticed also in this demo you posted previously, I can get it to sit in an infinite loop where it rocks the scroll position.

 

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

 

I have this issue in my demo too. The other issue I have though is that my buttons don't work for wrapping which is strange because the logic is the same from what I can see. Unless I'm missing a line somewhere.

 

See the Pen gOLQLYW by jh3y (@jh3y) on CodePen

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...