Jump to content


Tween delay ignored after tab switch

Recommended Posts

Hi. I'm writing a particle tweening script that tweens a bunch of dom elements, which I each give a slightly more delay. Everything looks fine, but when I switch tabs in my browser (chrome, but same in safari) and after that go back to the tab with my particle tweens, the delay seems to be ignored..

All the tweens start at the same time, without the delay!


Any idea why this is? If you don't understand what I mean, I could upload the script to my hosting as an example.


Oh, and I thought about using timelinelite for this, but I really want the sort of plugin I'm writing to not be dependent of timelinelite, only of tweenlite.


Edit: I've uploaded the test project to my webhost so you can see the problem yourself. http://sanderbruggem...cles/index.html


Open the page in a tab, then click on another tab, wait a few seconds and go back to the tab with my test project: the particles "explode" now instead of continuously loop..


I think I know why: the first time the tween is played, the delay is taken into account, but then when I call "restart()" on the tween in onComplete, the delay is not added anymore. Which in general is good: the tween already has an offset regarding the other tweens because of the initial delay. However, probably for speed reasons, when navigating away from the page, all the tweens are being reset/paused or whatever, and on focus again all the tweens are played again (but this time without the initial delay, because it practically fires "restart()" but this time on all the tweens at the same time).


So my question actually is: what type of event is used to determine if the window/tab is focussed, so I can intercept this (i've had partial success with window.onfocus() and window.onblur()) OR is there maybe another solution for this?

Link to comment
Share on other sites



First, very cool effect you have built. Very happy you chose to use GSAP and its plugin architecture.


A few things to consider:


You are absolutely correct in your assumption that restart() does not account for the delay. delay, only offsets the initial start time of the tween, there is no time or space added to the actual tween that would affect its duration. When restart() is called, a tween starts playing with no concern for the delay.


It would be fairly natural to assume that if 10 tweens had delays that had them all starting at .5-second intervals via incrementally increasing delays that they would also appear to stagger as they restarted as the first one would restart() before the second, and the second before the third and so-on. Under normal circumstances they would.


In your case, I'm quite certain that onComplete's are scheduled to run at a time that an update is not taking place due to the throttled frame-rate or tick. When the next tick happens, TweenLite realizes that some onComplete's have been missed and fires them off at once.


Very long explanation:


What's most likely happening in your case is that when you switch tabs, requestAnimationFrame is properly slowing the update cycle down to around 2fps (browsers may vary) in order to take the load off the processor. Since TweenLite is adamant about making sure all tweens are always synchronized as intended and that timing is always honored, it always renders based on how much time has elapsed. So even though the updates were throttled while your tab lost focus, TweenLite is still keeping track of time. When the browser's RAF kicks the frame rate or tick back up, TweenLite knows immediately how much time has transpired since the tab lost focus. On TweenLite's next render it places all the tweened elements exactly where they should be.


To be really clear, if you have a 10 second linear TweenLite like so


TweenLite.to(element, 10, {css:{left:1000}, ease:Linear.easeNone}); // assume starting left = 0;


Your object would be moving at 100px per second.


Imagine if at 1 second into the tween (100px) you switch tabs and visit greensock.com for 6 seconds and then come back. While you are away TweenLite's ticker (which is tied to requestAnimationFrame) may only fire a dozen or so times but the instant your tab regains focus, TweenLite will say "hey it been 7 seconds since this animation started, I have no idea what happened, but I'd better do my job and render this tween where it should be" and you will see the element rendered at 700px.


Now, that little part about "i have no idea what happened", doesn't mean that TweenLite is slacking or somehow forgot to do something, its just there to illustrate that TweenLite is not aware that the tick got throttled or that the browser lost focus. There is nothing baked into TweenLite that listens for the window losing focus or the framerate dropping. It always goes about its business of making sure each update renders according to the amount time that has transpired.


Whew, all that to say, TweenLite honors time above all else.


To address your assumption:


However, probably for speed reasons, when navigating away from the page, all the tweens are being reset/paused or whatever, and on focus again all the tweens are played again (but this time without the initial delay, because it practically fires "restart()"


I totally can see how it appears that way, but that is not the case.



So why are your restart()s all happening at the same time?


As much as TweenLite cares about time, it also wants to assure that all callbacks get fired.

Now remember, when you are away from your tab, very few updates are taking place behind the scenes. For the sake of argument let's say the tick speed drops to 1 tick per second.


Imagine you have 5 tweens that are .2 seconds long and are staggered at .1 second intervals and each has an onComplete callback. All of those tweens are going to start and finish within 1 second of time.


If as soon as these tweens start you switch tabs you will cause the tick rate to throttle down to let's say for the sack or argument 1 tick per second. When the next tick fires, a whole second has transpired. TweenLite says, "oh snap, there were 5 callbacks that never had a chance to run, They had better run now!". At that instant all the onComplete callbacks fire at the same time.


In a nutshell, I believe this is what you are experiencing. While your particle effect is running in the background tweens are completing in between update cycles. When the next tick fires, all of the onCompletes are firing at once forcing all the tweens to restart at the same time.


So although in your case this behavior is causing some unfortunate results, it is what also makes the engine so solid. Imagine those callbacks just got lost between updates? that would be VERY frustrating.


Using TweenMaxes that are set to repeat (and don't rely on onComplete callbacks) would definitely not exhibit this behavior, but I can understand your resistance to increase the size of your plugin's dependencies.


You could also try using TweenLite.ticker.useRAF(false) which will most likely fire the onComplete's when you expect but you'll lose the performance benefits.




Hopefully this helps clear things up.


Let us know if there is more that we can clarify for you.

Link to comment
Share on other sites

Aha, very clear explaination, thanks! So basically, easy explained, the onComplete events are "saved up" when navigating away from the page and THEY are fired at once when navigating back.


I must say that I have tried setting useRAF to false, but that didn't seem to solve anything. I will try it again though.


Because I really don't want to be dependent of TweenMax or TimelineLite, I will try to take care of it by writing some code myself. I will get back here when I make some progress or when I have another question.


Thanks a lot!

Link to comment
Share on other sites

The other thing you could do is leverage the fact that all tweens are placed on the root timeline by default, and you can check the tween's timeline.time() and compare it to the tween's startTime() and then do the math to figure out what position to jump to and play() based on any gap you sense there.


For example:


TweenLite.to(obj1, 1, {x:100, onComplete:repeat, onCompleteParams:["{self}"]});
TweenLite.to(obj2, 1, {delay:0.5, x:100, onComplete:repeat, onCompleteParams:["{self}"]});

function repeat(tween) {
   tween.play(); //just to make sure the tween's "timeline" property is populated - it gets set to null after the tween completes by default because it is removed from the root timeline to make it eligible for garbage collection
var start = tween.startTime(),
	parentTime = tween.timeline.time(),
	duration = tween.duration(),
	gap = (parentTime - (start + duration)) % duration;


Make sense?

  • Like 1
Link to comment
Share on other sites

Beautiful! Works like a charm. Thanks!


I had to change one thing though: tween.timeline was null, so I used tween._timeline.


Works, but all variables and function starting with _ should be considered private functions, right? Why is tween.timeline null?

Link to comment
Share on other sites

Ah yes - I'm sorry about that - "timeline" is null at that point because by default, all tweens are removed from the root timeline as soon as they finish (to make them eligible for garbage collection and keep things fast). You can force a tween back onto the timeline from which it was removed by simply calling its play() method, or paused(false). That'll populate the "timeline" property again.


My apologies for missing that important detail.

Link to comment
Share on other sites

No problem! Things like that will only help me understand the GSAP structure better. The more you learn. :)



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.