Jump to content
Search Community

Reversing timeline produces unexpected results

SteveS test
Moderator Tag

Recommended Posts

I have a horizontal scrolling section with several sub-sections. On scroll, I want to scroll the section sideways until the next element is in view. I create a timeline by iterating over the items and creating the tween as follows:
 

const tween = gsap.to(hscroll, {
        x: () => {
          return `-=${item.offsetWidth}`
        },
        delay: 3,
      });
      scrollTl.add(tween);

When I play the TL normally, it works as expected. However, if I were to use scrollTl.reverse() the animation breaks completely. Am I misusing reverse?

Link to comment
Share on other sites

@GreenSock

Here is a codepen:

See the Pen wvmWZyL by SteveSDaBest (@SteveSDaBest) on CodePen

 

I think I know what the issue is. invalidateOnRefresh doesn't work on .from() tweens and when I reverse the tl it treats it like a from (if I'm remembering correctly)

The alternative is to use fromTo tweens but my use case is more complicated than the example and calculating the fromTo's isn't trivial, so if this would work it would be great.

Of course I am invalidating in order to accommodate scrollTrigger resizing.

Link to comment
Share on other sites

I'm having a tough time deciphering this, Steve. You didn't describe what's supposed to happen, what isn't happening (how exactly it "breaks completely"), etc. 

 

It's not that invalidateOnRefresh doesn't work on .from() tweens - it's a logic thing: let's say you x starts at 0 and you do this: 

let tween = gsap.from(... {x: 100});

Well of course that immediately jumps to 100 and it uses the CURRENT value as the destination (0 in this case), so it starts animating back to 0. However, let's say it gets to 98 and then you do this:

tween.invalidate();

That flushes any recorded start/end values, thus when it renders on the next tick it'll record 100 as the start, and the CURRENT value as the end...which means it'll animate from 100 to 98 (barely any change). Also keep in mind that the playhead on the tween doesn't change. So if it was 50% into the tween when you invalidate() and then it renders on the next tick, the playhead continues right along, thus the interpolated value is accurately reflected (it doesn't start over from scratch). 

 

My guess is that you just didn't understand how it works(?) and it's more of a logic issue in the way you set things up? 

Link to comment
Share on other sites

I've updated the codepen to better show the issue. It seems that a reversed TL will not work correctly if it is invalidated. Is there any way to deal with this? I was hoping that since I was starting with a forward timeline, everything would calculate out based on the starting values, and that when it is reversed it would retain those starting values. Where a .from() tween uses the current animation state, I would expect .to to have the correct starting values and THEN reverse.

Link to comment
Share on other sites

Yep, it's a logic thing. Let me walk you through it...

 

First, keep in mind that ScrollTrigger always scrubs the animation from a progress of 0 at the start to 1 at the end

 

Let's take that simple case again of x starting at 0 and a to() tween is set up to do x: "+=100". The first time it renders, it records the current value (0) as the start, and adds 100 to that, thus the end is set to 100. Great.

 

But now you're reversing that and nesting it into another timeline. So when that timeline plays forward, its child is playing backward. You wire that timeline up to a ScrollTrigger, thus it scrubs from progress of 0 on that timeline to 1. When you set invalidateOnRefresh: true, that basically tells ScrollTrigger "when you refresh, rewind that animation to progress 0 (start) and then call invalidate() on it to flush any recorded start/end values". 

 

But when ScrollTrigger rewinds that parent timeline, you've got its child reversed! So x will render at 100 in this case at the start! So when invalidate() is called, it flushes all start/end values internally for all child tweens, and on the next render it re-initializes the tween and records the start/end values. Since it's a .to() tween, it says "use the CURRENT value as the start"...and that's 100 at this point! So it'll animate from 100, and then "+=100" would be 200, thus it'll animate from 100 to 200. And if you refresh() again, it'll go from 200 to 300, and so on. 

 

Relative tweens are super convenient, but you just have to keep in mind what's happening so you don't create a logic issue. 

 

There are many solution options:

  • Use .fromTo() tweens so you can control both ends and you're not having the tween conveniently use "whatever the current value is..." for either end. 
  • Instead building the timeline backwards and reversing it, just build it the way you want it to go without reversing. That way you aren't inverting things and having the "rewind" actually act as a "jump to the end" 
  • Don't use invalidateOnRefresh

Does that clear things up? 

 

Side note: you could make your code more concise by using the convenience methods: 

// long
tl.add(gsap.to("#inner-2", {
  x: `-=${item.offsetWidth}`,
  delay: 2
}));

// short
tl.to("#inner-2", {
  x: `-=${item.offsetWidth}`
}, "+=2")

 

Link to comment
Share on other sites

I see. I was assuming that since I had been using .to that it would get the starting values of the unreversed tweens. It's not that .reverse() takes a tween and plays backwards from 1, it sets the end to 0 and plays "forward" to the beginning. The difference being that if it switched the play direction, progress 0 would still be the unanimated state, but since reversing sets 0 to the end it is actually the opposite.

No matter, I actually solved my problem with CSS. My brain was fried after working a few hours too long and I was making things more complicated than they needed to be.

Link to comment
Share on other sites

1 hour ago, SteveS said:

I was assuming that since I had been using .to that it would get the starting values of the unreversed tweens. It's not that .reverse() takes a tween and plays backwards from 1, it sets the end to 0 and plays "forward" to the beginning. The difference being that if it switched the play direction, progress 0 would still be the unanimated state, but since reversing sets 0 to the end it is actually the opposite.

Yeah, I don't think it'd be intuitive if ScrollTrigger rewound the main animation to a progress of 0 but then INSIDE that animation, some of the children's playheads went to the opposite end so that the playheads are completely misaligned based on their reversed state. 

 

Glad to hear you figured out a solution! 🙌

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