Jump to content
GreenSock

Maarten

OverwriteManager onComplete bug

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

I just stumbled upon this OverwriteManager behaviour that seems to be a bug.

 

When starting a tween with a time of 0.25s and an onComplete handler and then starting a second tween of the same property (that should overwrite/kill the first tween) around the time the first tween completes (i.e. after 250ms via a setTimeout or in a click handler when you click after 250ms), it doesn't fully kill the first tween, so the onComplete handler still fires after the second tween has been started.

 

The onComplete handler is probably called asynchronously in some way. But the asynchronous callback id's (e.g. return values of setTimeout or setInterval) should be saved with a tween untill they are executed, and killed (via clearTimeout / clearInterval) when a new tween overwrites a previous one.

 

I've provided a simple JSFiddle example that displays this behaviour.

http://jsfiddle.net/4Vx4P/

 

Run a few times if it doesn't show the 2 outputs (it's a timing issue).

Tested in Chrome and FireFox.

 

Just thought i'd share this with you.

Otherwise an awesome product obviously!

 

Link to comment
Share on other sites

First thing I would ask here is about order of operations; you are creating a 250ms tween and then setting a timeout for 250ms from that point, but it seems like your preference is for the timeout to finish first in the queue of things to happen in 250ms?
 
What you've found in this 'sometimes it works' setup is not strictly a bug per se, but rather an issue caused by combining GSAP's strict timing with setTimeout's inaccurate timing. Essentially GSAP uses everything at it's disposal to synchronise itself to the exact timing of the computer, while using setTimeout is more like saying "I'd like to trigger in 250ms, but if that's not possible I don't care, as close to that as you can offer will do".
 
CPUs work on a system where CPU time is allocated in chunks to applications as determined by the operating system - applications take turns at using the CPU for a few operations, and then control is wrested for other applications. If the CPU offers, let's say, 10ms to another application between 245ms and 255ms from the original setTimeout, then the first chance the browser has to activate your function is at 255ms from your request i.e. not as accurate, and definitely not accurate for what you're trying...
 
On the otherhand, GSAP does a bunch of magic to compensate for timer inaccuracies by tying itself accurately to the system time, such that issues like this should be well accounted for. Since you are already using GSAP anyway, you'll probably find TweenLite.DelayedCall() to be a much better replacement for setTimeout as it makes use of the much stricter GSAP timing system.

 

(btw thanks for providing the jsfiddle - its a great help to have something to fire up and test immediately :D)

  • Like 1
Link to comment
Share on other sites

Jamie makes some excellent points, particularly about how setTimeout() works and how it can't be relied upon to be accurate. However, there's one more factor at play here...

 

As indicated in the docs, the default overwrite mode is "auto" which runs the first time the tween is rendered. When a to() tween is created, there's no reason to render it at that point because no change would have taken place yet. That would be a waste of CPU cycles. So it renders on the very next frame/tick. In your example, you were basically creating a new tween EXACTLY (or pretty close to) when the other one finishes. So by the time the new one renders on the next tick, that previous tween has already completed. See why this is expected behavior? It's not a bug at all. 

 

The solution is pretty simple: you can set overwrite:"all" as the overwrite mode which makes it kill all tweens of the same target immediately (no waiting for the first render), OR you can set immediateRender:true which will cause it to...um...you'll never guess...render immediately when the tween is created, thus its overwrite logic will kick in right away. :)

 

Like Jamie said, it's much better to tap into GSAP's central ticker for any time-related activities in your code so that everything is synchronized and optimized. TweenLite.delayedCall() is great for that, or if you need some logic run after ever update of the core engine, you can add a "tick" listener to the TweenLite.ticker. Let us know if you need help with that. 

 

Oh, one more minor note about the whole setTimeout() thing and timing issues - by default, GSAP uses requestAnimationFrame for its updates (and automatically falls back to setTimeout() if necessary), so if you're seeing different results in your tests, it would be perfectly normal for setTimeout() calls not to coincide perfectly with requestAnimationFrame events. For animation, it's definitely better to use requestAnimationFrame whenever possible as it syncs up with browser repaints whereas setTimeout() doesn't.

  • Like 2
Link to comment
Share on other sites

Great stuff about the timing guys!

I knew setTimeout isn't reliable, that's why i included:

or in a click handler when you click after 250ms

 

Because that is actually how i discovered this behaviour, by clicking right at the time a tween finishes and starting a new tween in the click handler.

So the point wasn't really about timing or using setTimeout, i just used it in the example because it triggers the exact same behaviour as i had with my click handler (which is a little hard to demonstrate in JSFiddle :))

 

In my case it was like an open/close toggle. Where onOpenClick adds content to an element and tweens it to become visible, and onCloseClick does kind of the reverse, it tweens to become invisible and onComplete removes the content from the element.

 

So when clicking right around the time the close tween ends, caused the open tween to .. well tween ;)  but after that tween started (/was created), the onComplete of the close tween was called which removed content from the element. So it tweened open with empty content.

 

Your post explains the behaviour perfectly :)

When a to() tween is created, there's no reason to render it at that point because no change would have taken place yet. That would be a waste of CPU cycles. So it renders on the very next frame/tick.

 

There's probably precisely one 'tick' difference (in one way or another) in the starting & rendering (thereby overwriting) of the second tween in which the first one finishes (calling the onComplete).

 

 

you can set immediateRender:true which will cause it to...um...you'll never guess...render immediately when the tween is created, thus its overwrite logic will kick in right away

 

Perfect! That indeed solves the issue.

So i guess i'll use this in every tween where i use other tweens on the same target with an onComplete handler.

 

Thanks!

Link to comment
Share on other sites

For the record, you can change the default overwrite mode to "all" for the entire engine if you want:

TweenLite.defaultOverwrite = "all";

Just beware that'll affect all tweens thereafter. 

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