Jump to content
Search Community

Skipping steps in label-based panel animation

katerlouis test
Moderator Tag

Warning: Please note

This thread was started before GSAP 3 was released. Some information, especially the syntax, may be out of date for GSAP 3. Please see the GSAP 3 migration guide and release notes for more information about how to update the code to GSAP 3's syntax. 

Recommended Posts

Hey, long time no read :)


I have a master timeline, which the user can traverse in steps.


Immediate back and forth is cool, but besides that this pattern doesn't feel very responsive. This becomes very noticeable when you want to go down quickly.

That is why I want to give the user the ability to skip animations, but without jumps in the animation.


The plan for skipping:

1) tween to the label after the one we are tweening to right now (or the one after, or the one after that..)

2) immediately increase the timeScale and tween it back to 1 (duration of that tween could be influenced by how many skips etc.)


I am having a very hard time to solve issue 1) .. I wanted to check which label we have passed last with tl.currentLabel(). Unfortunately that doesn't work when you go backwards. As the docs state "Gets the closest label that is at or before the current time"  – In depth: Let's say we are in the middle of the animation from 4 to 5 and want to skip to 6. currentLabel() returns 4 at that point (remember: we are still tweening from 4 to 5). 4 to 6 is more than 1, therefore it's a skip. Hurray!–

But when we go that route in reverse, so tweening from 6 to 5 and skip to 4, currentLabel() returns 5... 5 to 4 is 1, therefore not recognized as a skip, although it is one.. We need it to return 6!


So I need more of a "Last label we crossed" alá `tl.getLastLabel()`


My workaround is to update a `lastState` variable manually like so


var lastState = 1;

var updateLast = function(x) {
	lastState = x;

var tl = new TimelineMax() 
	// initial state
	.call(updateLast, [1])

	// animating to state 2
	.to( ... )
    .call(updateLast, [2])

	// animating to state 3
	.to( ... )
    .call(updateLast, [3])

	// animating to state 4
	.to( ... )
    .call(updateLast, [2]) // I am not editing out this mistake, as it illustrates perfectly why this solution is pain!


This is by far not the most elegant way to do this; 



My idea for solving issue 2) is also really buggy and doesn't have the intended effect. 


I hope things aren't explained to confusingly and fellow Magicians come forward and help me out <3







See the Pen rJqQRK by katerlouis (@katerlouis) on CodePen

Link to comment
Share on other sites

First, thanks for the very nice demo. Had it not been so nicely put together I don't think I would have gotten far.


Although using labels to figure out next / prev navigation is cool, it can get messy in situations like this and I want to suggest a different approach.

  1. Please visit http://johnpolacek.github.io/open-source-for-fame-and-fortune/ 
  2. press the right arrow a few times slowly
  3. press it a bunch of times like a maniac
  4. notice the slideshow doesn't skip a beat and it actually advances faster each time you click the arrow!

What is happening is that every slide's animation is placed in a master timeline. 

There is an array, called positions, keeping track of the startTime of each slide's animation 


There is a tweenTo() function pretty similar to your goTo() that takes an index value which is used to determine which slide's startTime you need to navigate to.


When tweenTo() is called it:


  1. increases a timeScale variable 
  2. updates value of positionIndex (used to grab the startTime of the slide you want to navigate to)
  3. creates a tween that tweens the time() of the masterTimeline in a linear fashion (similar to what TimelineMax.tweenTo() does)
  4. sets the timeScale() of that tween based on the timeScale variable
  5. puts an onComplete callback on that function that resets timeScale to 0 when the tween is done
function tweenTo(i) {

        timeScale++; //speed up if user keeps pushing the button.
        positionIndex = i;
        // Tween playhead to new position using a linear ease.
        TweenMax.to(timeline, Math.abs(positions[i] - timeline.time()), {time:positions[i], ease:Linear.easeNone, onComplete:function() {
                // Reset timeScale when tween is done
                timeScale = 0;



You can see the source of that function on TweenDeck's git repo here



I took your pen and updated it to use this approach.

You should be able to click the arrows pretty fast to see the how it works (give the demo focus first)


See the Pen LQXbWx?editors=1010 by GreenSock (@GreenSock) on CodePen


FWIW there is very little chance I would have been able to do this unless I had been cc'd on an email between @GreenSock and @John Polacek a few years ago when they were discussing how to implement this "fast advance" technique that Jack suggested. Much credit goes to both of them!


  • Like 4
Link to comment
Share on other sites

Amazing how much I learn each time I engage (90% begging for help ^^) with you guys!


Didn't think of using `Math.max()` in `tweenTo()` – much more elegant than using ifs.


a1) When `tweenTo()` gets executed multiple times, multiple TweenMax-instances are trying to tween tl's time, right? I mean, TweenMax.to(tl..) from the first call of `tweenTo()` is not finished yet– Why does this work? (Just realized this is a basic behavior of GSAP which I always took for granted :D – Curious!)


a2) How comes the onComplete-callback of the first TweenMax.to(tl..) doesn't reset the timeScale to 0 for the following calls? It's hard to explain.. imagine ur reflexes are good enough to skip right before the timeline reaches the position. The previous TweenMax.to(tl..) still fires it's onComplete and therefore resets timeScale to 0, resulting in a timeScale that stays at 1 on every skip (because it's reset to 0 beforehand) –


TweenMax.to(tl, Math.abs(positions[i] - tl.time()), {time:positions[i], ease:Linear.easeNone, onComplete:function() {
	// Reset timeScale when tween is done
	timeScale = 0;




This approach is more straight forward. But besides dealing better with the duration of tl's time-tween (although I think maybe .tweenTo() does exactly the same?) and manipulating the timeScale successfully, I don't see why labels make things messy in this case (not defensive behaviour! I just guess I'm missing something here). 


My `tweenTo`-var is the new `positionIndex`

My `states`-var generated from the labels is the new `positions` var

We both tween to the next position (I use .`tweenTo()`, you use `TweenMax.to(tl, ..)`)



Only major difference I see is how we deal with the timeScale manipulation. You could argue that tweening the timeScale won't feel good to the user, but I had no chance to judge that, because my timeScale manipulation doesn't seem to affect the timeline unless you hammer the button like a maniac.


Try my pen again and quickly press right arrow twice. The timeScale of tl tweens, as seen in the output which is generated from the actual timeScale in an onUpdate-callback. But the animations speed definitely isn't 10 times as fast.


Although the idea and implementation with lastState is inelegant,

I really want to understand and fix this mistake.



EDIT: I noticed that the output of the timeScale in your pen comes from the variable. I changed it to `tl.timeScale()` which results a constant 1– I guess this has also to do with my misunderstanding of the situation?



EDIT 2: Okay I am getting closer, the major difference is:

– you change the timeScale of the TweenMax that is tweening tl's time; 

– I was manipulating timeScale of the tl, which doesn't effect anything, since .tweenTo() also manually tweens the time? Like tl's timeScale() only changes speed when tl is "played normally"?

But.. this doesn't explain to me, why the second skip does make it super fast.. 

Link to comment
Share on other sites



a1) When `tweenTo()` gets executed multiple times, multiple TweenMax-instances are trying to tween tl's time, right? I mean, TweenMax.to(tl..) from the first call of `tweenTo()` is not finished yet– Why does this work? (Just realized this is a basic behavior of GSAP which I always took for granted :D – Curious!)


Yeah, the engine realizes that another tween is tweening the time of tl and overwriting kicks in... killing the previous tween


It becomes very difficult to follow what you need if you keep updating the same demo and editing the same posts with updated questions.


Much better to fork your demo for each revision and also do a new reply. We are not notified when you edit a post so 99% of the time we don't see them.


I noticed that timeScale() in the demo I last saw was getting up to 999 which I doubt is what you want:




As for using labels vs time. When I said your initial label way could get messy I was referring to relying on getLabelBefore() / getCurrentLabel() and stuff while a tween was tweening the time() of a timeline with a super fast timeScale(). It becomes tricky too because you also need to know if you are currently moving the playhead forwards or backwards... do you use getLabelAfter or getlabelBefore(). Again just sort of complicated when there are so many different states that can change quickly based on user input.



Yes, you are correct though, the tween that I'm creating on the timeline is very similar to what TimelineMax.tweenTo() does behind the scenes. Also, nice job of using getLabelsArray() to get an array of labels with access to their times. 


If you still have questions, please post a new (forked) demo with a single clear question, so that I can more efficiently understand what you might need.


  • Like 3
Link to comment
Share on other sites

Weird, I specifically did fork my original pen, because I was asking you guys to use this exact one. 

Sorry for the inconvenience. 


To be on the same page again, this is the latest version of what I got:

See the Pen ZrmROd by katerlouis (@katerlouis) on CodePen



I now have another requirement:

Going back only decreases the `tweeningTo` var, which is a mechanic that is not clear to the user. The way I want it to be, when you change direction: "go back to the last state we passed and don't increase the timeScale any further".


Example: initial state is 1, you press the right arrow. Notice when you now press the left arrow halfway through the tween. The timeScale is now 2. 


Example 2: Lets assume you would be fast enough for this.. you press the right arrow 5 times very quickly, so tweening to 6. Now you press the left arrow right between 2 and 3. Two problems with that:

c1) you would go (I assume unexpectedly for most) to 5, which feels weird until you realize whats going on

c2) the timeScale to 5 is now even higher than before where you were on your way to 6 –


There are more weird scenarios, Example 3:

You are on 1 and press right quickly 4 times, going to 5, and press left when you are around step 4. When you didn't keep track of your presses and know exactly that it was 4 times, this feels like the last left-press wasn't recognized. 


This may seem nitpicky in the case of this pen, but the projects animations are about 4 times as long; so I want to get this super sharp.

And all these cases feel way weirder when you change out "keys left and right" with "swipe up and down" on touch devices. "I swiped in the opposite direction, why is it still going there?"


My idea now is to keep the "lastState"-mechanic in tact and then check for a direction change on keydown etc.

But I hope you have a better solution in mind :)

Link to comment
Share on other sites

Thanks for the further clarification.


The use cases you explained are all valid and few people take the time to document and understand the complexities of what could happen when you let a user bang on a keyboard and / or swipe quickly. 


As we have discussed together the GSAP API is loaded with hooks that allow you to:

  • tween to and from any portion of a complex animation (timeline with nested timelines)
  • change the speed of that tween at any time
  • fire callbacks when that tween is done
  • assess the amount of time between where the playhead is and the section (or label) you want to tween to

You've proven yourself very capable of knowing how those things work.


For what you want to do it does sound like you need another layer of logic that tracks currentDirection and previous direction to further decide what to do with the timeScale.


Unfortunately, i just don't have the time to go down the path of coming up with a bullet-proof solution that can account for all these different user interactions and performing all the custom logic and calculations necessary to get it all smooth. It could literally take hours.


I definitely feel like you are close and on the right path.


  • Like 1
Link to comment
Share on other sites

14 hours ago, Carl said:

You've proven yourself very capable of knowing how those things work.


That coming from one of the Elders! <3


–back to topic–

I initially named the thread "how to get playhead direction?" – Is there really no getter for this in GSAP API? I haven't found anything in the DOCS or forums on getting playhead direction. It's hard to imagine being the only one who ever needed that. Is there something I am missing?

Link to comment
Share on other sites

7 hours ago, kreativzirkel said:

Is there really no getter for this in GSAP API?


Sure, you can check the reversed() value, but keep in mind that tweens/timelines can be nested as deeply as you want, so it can get a little tricky. If you've got a reversed timeline inside a reversed timeline, it's technically reversed but it'll APPEAR to play forward (because its parent timeline is reversed).


Also, you can have a paused timeline whose playhead is being tweened by that ANOTHER tween (forward or backward). So in that case, it wouldn't even matter what the reversed() method returns. See what I mean? 


There are ways to get the value you want - it just depends on your scenario. Simplistic is just use the reversed() method, but if you've got the potential of any of those other scenarios, you could walk up the ancestor timelines and figure out the value accordingly or use an onUpdate to track direction. Lots of options.  

  • Like 2
Link to comment
Share on other sites

Of course I thought about reversed() aswell, but see a change in the reversed()-status at the early stages of this implementation. Which confused me a lot!


19 hours ago, GreenSock said:

Also, you can have a paused timeline whose playhead is being tweened by that ANOTHER tween (forward or backward). So in that case, it wouldn't even matter what the reversed() method returns. See what I mean? 


I guess this is the case here.

My tl is paused and goTo() creates a new Tween the tl progress in either direction. 

So this progress-tween is technically going forward and therefore reversed() returns false. That makes sense to me.


But while this progress-tween is happening, why doesn't tl know that it is currently being manipulated backwards?



Here is my approach to it.

I included a reverse variable, that is changed by goTo() depending on the duration of the progress-Tween.

When this duration is negative it means we are going backwards. 

Furthermore I included an if in goTo that cancels the animation, when we goTo() the same element we are on now. Without the cancel the timeScale would be increased, but not decreased, since we only decrease it onPause of the tl.



See the Pen gvqNyq by katerlouis (@katerlouis) on CodePen


The next time I will try to implement going backwards to the element that was last viewed, rather than just decreasing the tweeningTo var.


When there is a more elegant way to the track the direction let me hear it :)

And definitely come forward when you have an idea for a better "lastState" mechanic. This manual .call(update, [x]) just sucks :D

Link to comment
Share on other sites

haha! after seeing this thread several times I finally decided to give it a go, but just when I was this close to finishing a codepen I had to leave to go see friends, and I spent the night with my fingers crossed that it wouldn't all be for naught! luckily no one showed up with a groundbreaking solution so I am now free to post mine without fear of ridicule. (of course I would still have posted it regardless, haha.)


See the Pen JpzPbQ?editors=1000 by Acccent (@Acccent) on CodePen



This uses the 'master timeline' approach highlighted earlier in the thread, but it skips a lot of things by assuming that all the steps will be equally spaced which allowed me to just use tl.time() to see what state the animation currently is in. I'm sure it could easily be reproduced with labels and .getLabelTime(), though.

Ideally, you would add a delay before resetting the timescale, so that if I press → 0.1s after the animation reached its end it still skips ahead at the same speed instead of reverting to the default.


One area where someone more knowledgeable than me could help is, I'm not sure sub.pause().kill(); sub = null; is necessary or if it's overkill when we want to make sure that tween is properly removed from existence. Maybe it'd be possible to use a different method to see if the tween is active and then use something like .updateTo() to change the target state.

  • Like 4
Link to comment
Share on other sites

On 2.3.2018 at 12:44 AM, Acccent said:

but it skips a lot of things by assuming that all the steps will be equally spaced


Thanks for the efforts :) – Unfortunately this assumption is wrong in my case. The steps are all independent in their duration and that is unchangable. 


For the rest I have to look into your pen closer in a couple of days; deadline is pushing hard for a first draft. Decision is to go with the behavior of my latest pen. 

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