Jump to content
Search Community

Issue in v3: Negative timescale on endless loop stops loop after some repeats [3.2.4]

Friebel test
Moderator Tag

Recommended Posts

While creating a codepen thing of another issue with reverse() I am now experiencing while converting projects from gsap 2 to 3 I just bump into another issue which is there when putting a negative timescale value on an endless looping timeline (repeat: -1).

 

What happens?

When the timeline loops in normal direction it repeats endless as expected. But when setting a negative timescale to the timeline the timeline stops running after some repeats. Of course this is not what we might expect, as the loop should keep repeating endlessly, only in a different playhead direction. This looks pretty random, so I made a video of this codepen to see it in action (used browser: Ms Edge Chromium version), but this forum doesn't let me upload the file here because of the filesize, even if I make a very low resolution mp4. 😠 

 

So guess you have to try the codepen and by patient to see it happen sometimes and other times not. It looks like a pretty random issue, although happening a lot.

[edit] It is actually not that random; what seems to happen is that when reversing the direction, it will now only reverse the actually done repeats when it was playing forward. So if we first let the playhead run a timelinesequence three times (so one normal run and two repeats) and then hit enter a negative timescale to reverse the direction, it only runs three times the sequence in the reversed direction and then stops. But instead it should keep repeating endlessly, as the repeat is still set to -1 to have an endless repeating loop.

 

Seen in both gsap 3.2.0 as well as in 3.2.4.

 

See the Pen jOPzYbQ by Friksel (@Friksel) on CodePen

Link to comment
Share on other sites

Hey Friebel. This is expected. If you start then pen then click reverse immediately, you can see that it will reverse for only as many loops as it has completed up until the point where you clicked the reverse button.

 

If you want it to loop infinitely in the reverse direction, set the progress in the onReverseComplete:

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

 

Thanks for the minimal demo to make your problem clear!

Link to comment
Share on other sites

6 minutes ago, ZachSaucier said:

Hey Friebel. This is expected.

I just edited my post and see your reaction now telling what I just saw, so that was a crosspost :)

 

You state this is expected. But it's not expected at all to me and doesn't make sense to me either, so I don't agree with you. We tell a timeline to repeat endlessly, so it should repeat endlessly. No matter the direction it's running in. In my world an endless loop means endless loop. Independent of a direction. Just look at how yoyo loops work. Also endless yoyo loops, runing back and forth, run in two directions and never stop. To me this is really an issue in the lib the way it is now.

Link to comment
Share on other sites

@Friebel I totally get why this would seem unintuitive in your scenario. 

 

Explanation and complicating factors

In general, Timelines (except the global one) can't have animations with NEGATIVE start times. In other words, children can't be placed before the very start of the timeline. There are several reasons: 

  1. animation.progress(0) would become meaningless. As soon as a timeline has an infinitely repeating animation, it effectively has no start or end theoretically. Currently we handle the "end" by just using a very large value, so practically-speaking it's infinite but not really. But doing that for the start too is quite thorny, like the progress(0) thing. 
  2. GSAP has performance optimizations that determine when a particular animation needs to render, otherwise it's ignored. If the parent's playhead is before the start or after the end, it shouldn't need to be rendered (well, there are a couple of edge cases that are exceptions and those are already taken care of). Making a child extend BEFORE the start time of its parent makes this optimization practically impossible.
  3. Currently when a negative startTime is sensed, all of the children are moved forward and the parent timeline is moved backward to adjust things accordingly and keep it "legal" (but this is visually imperceptible). If we allow infinite backwards repeats, this now becomes even more problematic. ALL other animations in the timeline would have their startTimes moved forward by a LOT (assuming we use a big number instead of literally Infinity). That might strike developers as even more unintuitive. 

Furthermore, this could lead to odd behavior. Imagine building a sequence where things fade/move in and then...I dunno...a strobe light fades in and pulses "infinitely"...and then you reverse that animation or you jump to the beginning...the pulse would render at the very start!! That, I think, would be quite unintuitive. Any way you slice it, you're gonna have some scenarios where you can point to a behavior as seeming "unintuitive". 

 

Solution: 

Zach's suggestion with setting the progress to 1 is good, but technically there's a risk of a very slight time creep with that because if it updates 60 times per second, there could be a few milliseconds of gap between the second-to-last render and the final one that triggers the onReverseComplete. Most (if not all) other animation engines I've seen don't protect against that time creep, but GSAP does. So you could just use an onReverseComplete to call a function that adjusts the playhead according to the rawTime:

function infiniteReverse() {
  this.totalTime(this.rawTime() + this.duration() + this.repeatDelay());
}

gsap.timeline({repeat: -1, onReverseComplete: infiniteReverse});
...

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

 

Does that help?

  • Like 5
Link to comment
Share on other sites

1 hour ago, GreenSock said:

@Friebel I totally get why this would seem unintuitive in your scenario. 

 

Explanation and complicating factors

 

@GreenSock Thanks a lot for this great in depth explanation Jack. I understand your challenges. Out of curiosity: how do you do it with yoyo than? Or does yoyo work because when it is reversing it always have played once?

Link to comment
Share on other sites

2 minutes ago, Friebel said:

@GreenSock Thanks a lot for this great in depth explanation Jack. I understand your challenges. Out of curiosity: how do you do it with yoyo than?

A yoyo merely affects the direction of the playhead inside the overall totalDuration. So, for example, if you have a 1-second yoyo-ing tween with repeat: 2, the totalDuration is 3, so as the playhead sweeps over it the playhead goes forward, then once it hits 1 second totalTime, it starts going backward, then at 2 seconds it goes forward again, etc. It just translates the totalTime internally to be what it needs to be (in yoyo phase or not). But the overall placement and duration on the parent timeline is no different than if you had yoyo: false. 

 

Does that answer your question? 

  • Like 3
Link to comment
Share on other sites

12 hours ago, GreenSock said:

It just translates the totalTime internally to be what it needs to be (in yoyo phase or not). But the overall placement and duration on the parent timeline is no different than if you had yoyo: false. 

So if I get what you're saying there's only one timeline of one second in your example. And when the totalTime progresses internally the totalTime gets recalculated to a position on the one second timeline, either by moving forward or moving backward, depending on the current state of the yoyo repeat: forward or backward. Do I get that right?

 

If so, why wouldn't that be possible with an endless loop (repeat: -1) where yoyo is false and the timeline is playing in reverse? Isn't that the same as repeating the backwards part of a yoyo loop? So to explain this in your example, let's say we're setting the one second timeline now to { repeat: -1, yoyo: false } and playing it in reverse with reverse() or timeScale(-1).
I don't really understand why the inner workings of a timeline progression would be handled differently for non-yoyo loops vs yoyo-loops. Wouldn't it be possible to calculate the totalTime internally to a position on the reversed timeline as well? So like the yoyo internal re-calculation, but now running in backwards state when timescale is below 0 and forward state when timescale is above 0?

Link to comment
Share on other sites

7 hours ago, Friebel said:

I don't really understand why the inner workings of a timeline progression would be handled differently for non-yoyo loops vs yoyo-loops. Wouldn't it be possible to calculate the totalTime internally to a position on the reversed timeline as well?

No no, I wasn't saying it's somehow too difficult for me to figure out where to map the playhead. That's relatively easy. All 3 of my points listed above still apply and none of them are related to calculating internally where the playhead should be given a totalTime. 

 

Think about the PARENT timeline - if it has a child that extends infinitely in both directions, then where does that timeline START? If I call timeline.progress(0) (or really any progress), what should it render?

 

Also, just to be clear, rendering a yoyo vs. non-yoyo animation has some important differences internally. GSAP has to sense when a boundary (start/end) is crossed so that it can render things accordingly EXACTLY at the boundary in case someone has a callback positioned precisely at the boundary. Or if an onRepeat gets called, the timeline should be precisely at the end state before (on the same tick) being rendered at the new position. Remember, GSAP protects against time drift (unlike other animation engines); a tick almost never happens EXACTLY when the playhead hits the boundary. It's almost always a little bit past the boundary, so GSAP renders at the boundary first and then at the "real" (accurate, wrapped or yoyo'd) position. So a yoyo can just hit the boundary and then the new position but a non-yoyo has to hit the end boundary, then wrap around to the start to render there before going to the new position. That ensures that things render perfectly and every callback gets triggered properly. 👍

 

  • Like 3
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...