Jump to content
Search Community

Problems with callbacks execution order

Skid X 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

Hi,
I'm having some troubles because the order of execution of callbacks and events seems different when I have nested timelines.

I have reproduced a simple scenario:
I have a timeline with a tween, then a call to a function that pauses the timeline, then another tween. Both tweens are "fromTo" with immediateRender = false. I have added also complete callback on the first tween and start callback on the second one in order to log them.
The result is:
- first tween completed
- timeline paused

The second tween is not starting and its start callback is not executed, as I would expect.


Then, I take an identical timeline, and I nest it inside another one.
In this case the result is:
- first tween completed
- run the callback that pauses the timeline
- first frame of the second tween executed and its start callback executed
- timeline actually paused

So only nesting the same timeline, the behavior is different.
Consider also that this happens also putting the pause action inside the complete callback of the first tween. The second tween starts in any case before the pause takes effect on the timeline.

You can see it clearly in the codepen.

Is there something wrong in my setup?
Is there a way for having the same execution order in any nested or not nested scenario, except putting tweens and callbacks in slightly different positions?

Thanks in advance
 

See the Pen EjwaGQ by SkidX (@SkidX) on CodePen

Link to comment
Share on other sites

Thanks for the demo and clear explanation. 

I can understand the confusion, but things are working as designed.

 

In short the issue relates to the fact that if you do something like:

timeline.to(obj, 1, {x:100});
timeline.call(myPauseFunction);
timeline.to(obj, 1, {x:200});

myPauseFunction() {
  timeline.pause();
}

There is going to be some fraction of time that transpires between the time call() happens and myPauseFunction() fully executes. During these few milliseconds timeline will still be advancing and the next tween may advance a tiny bit or perhaps a callback that immediately follows call(myPauseFunction) will have time to fire.

 

For this reason I'm actually a little surprised that your first timeline pauses properly without box-b's tween progressing or firing its onStart.

 

To solve this issue we created addPause() which strictly enforces that the timeline is paused() at the proper time.

More details in docs: http://greensock.com/docs/#/HTML5/GSAP/TimelineLite/addPause/

 

So to solve the problem in the second set of timelines do this

 

var line2 = new TimelineMax({paused: true});
line2.fromTo('.box-c', 1, {left: '20px'}, {left: '500px', immediateRender: false, onComplete: function() { log2('C complete');
                                                                                                         log2('PAUSE 2');} });
//use addPause() to immediately pause the timeline without any time drift that happens when an external function is called to pause the timeline
line2.addPause();

//since this next tween has a startTime() that was the same as addPause()  I moved it forward in time just a teensy weensy bit using "+=0.0001"
line2.fromTo('.box-d', 1, {left: '300px'}, {left: '500px', immediateRender: false, onStart: function() { log2('D start')} }, "+=0.0001");

line2wrap.add(line2, 0);
line2.paused(false);
line2wrap.play();

Does that clear things up?

  • Like 3
Link to comment
Share on other sites

here is the pen: http://codepen.io/GreenSock/pen/rVGxEp?editors=001

 

and note I fixed the first timeline to call a function at the same time you use addPause() like

 

var line1 = new TimelineMax({paused: true});
line1.fromTo('.box-a', 1, {left: '20px'}, {left: '500px', immediateRender: false, onComplete: function() { log1('A complete'); } });
line1.addPause("+=0", log1, ['pause1'])
line1.fromTo('.box-b', 1, {left: '300px'}, {left: '500px', immediateRender: false, onStart: function() { log1('B start')}}, "+=0.0001" );
line1.play();
  • Like 1
Link to comment
Share on other sites

There is going to be some fraction of time that transpires between the time call() happens and myPauseFunction() fully executes. During these few milliseconds timeline will still be advancing and the next tween may advance a tiny bit or perhaps a callback that immediately follows call(myPauseFunction) will have time to fire.

 

For this reason I'm actually a little surprised that your first timeline pauses properly without box-b's tween progressing or firing its onStart.

 

To solve this issue we created addPause() which strictly enforces that the timeline is paused() at the proper time.

Thanks a lot, your explanation is very clear.

 

This thing brings me to another doubt: is it correct to suppose that also if I put two callbacks at slightly different positions - let's say the first at 1.0s and the second at 1.001s, with the first callback changing the main playhead position (it could be not only a pause() but also a reverse() or a jump to a different time position) it's NOT guaranteed that the second callback wil not be fired?

 

I'm wondering how to obtain a general way to handle a strictly serial execution of an arbitrary sequence of callbacks / tweens honoring also any actions that change the playhead.

Knowing in advance the kind of actions involved, the solution is easy (use the addPause method as you suggested, execute the linked callback and then change the playhead OR resume the timeline execution) but there could be cases in which you don't know in advance what the callback needs to do...

I have to think about it. Any suggestion is welcome.

 

Anyway thanks a lot, your explanation will help me a lot.

 

Link to comment
Share on other sites

This thing brings me to another doubt: is it correct to suppose that also if I put two callbacks at slightly different positions - let's say the first at 1.0s and the second at 1.001s, with the first callback changing the main playhead position (it could be not only a pause() but also a reverse() or a jump to a different time position) it's NOT guaranteed that the second callback wil not be fired?

 

 

 

That is correct.

 

And your description of using addPause() before doing any playhead moving is sound as well.

If you come across any weird scenarios let us know. It sounds like you have a great understanding of the mechanics now.

Link to comment
Share on other sites

Hi,
I've just made a little change in your pen, and it seems to not work as expected also using the addPause() method.
If you move the addPause() from the inner timeline to the outer one, you have exactly the same problem that I had in my first example, the second tween starts in any case, also if it begins on a time position after the pause action.
Plus, it seems also weird, because the first time when the pen runs automatically, it executes both the onStart event and the first frame of the second tween; when you hit "restart" it executes only the onStart event without changing the element's position, and executes it before the callback associated to the addPause action. Using "restart" sometimes randomly runs correctly as expected, most of the times it doesn't, so it seems strictly related to how quick it is running the addPause, as it was in my first example that used a generic callback.

You can see it here:

See the Pen yNzqeR by SkidX (@SkidX) on CodePen



Unfortunately, this scenario with nested items with the pause on the top level is exactly my use case, so is a real issue for me.

Link to comment
Share on other sites

We established that in order to prevent the nested fromTo() tween from rendering its first frame or calling its onStart it can not have its startTime() the same as the addPause().

 

I think the problem boils down the fact that I was using a an offset of "+=0.0001" to push the nested tween away from the addPause(), but that was still within the same tick (or frame duration) of the addPause() so it was pretty much as if it was the same startTime().

 

Please try this and see if if the results are consistent: http://codepen.io/GreenSock/pen/qdPyya?editors=001

I removed everything not related to the problem to make it more clear. 

  • Like 1
Link to comment
Share on other sites

yes, I know it worked incrementing the time offset, but reaching a "safe" offset value, it brings us to the fact that in that way it works also replacing the addPause() with a plain callback having a timeline.pause() as its first instruction, which was our starting point, making the addPause() not so reliable in the way I need it.

I really appreciate your efforts, you have a great customer support here (not talking about the fact that it's saturday!), but probably telling you something more about my needs would make it clearer why I'm not so much comfortable with this kind of solution.

I'm the author of Tweene (tweene.com), it's a library that works as a proxy of animation libraries (currently your lib, velocity, transit and jquery).
Basically it could be used as a layer of abstraction on top of your lib, so the users could arbritrary create any kind of animation with it. My task here is to make the more consistent result across different libraries, so the users can switch, for example, from css transitions or jquery to gsap without changing a single line in their animation code, expecting a reliable result. Since I'm able to solve this pause issue on all the other libraries (because they have not native support for timelines, so I implemented them inside Tweene), it would be really a pity to add an exception for gsap saying something like "to be sure to achieve the same result in gsap, you need to put an offset of 20ms after any pause", My aim is to give more power and more freedom to users, not more constraints. Your lib is the most powerful on the market and my personal favourite too, so I clearly don't want to make it work worse nor perceived as working worse when used together with Tweene.
I'm already using internally an offset in gsap driver for solving some corner cases, but I'm using a value of 1microsecond, correcting each duration accordingly so it's totally transparent for the final user. Introducing an extra offset of 20ms or something similar would be really harder to handle in a coherent way.

This is the only reason for which I'm struggling (and probably bothering you, sorry for that) to find a robust and general solution that could be applied to any scenario. If it was needed only for a personal specific animation on which I had total control, that solution you have proposed would fit perfectly.
 

Link to comment
Share on other sites

Searching for a solution, I've found a method that seems perfect when running in forward direction, but it seems really strange (buggy?) when the animation runs reversed.

This is my idea: since the addPause() works very well when it is at the same nesting level of other events (tweens that begin almost at the same time or callbacks) while it has some issues when it is positioned on an upper level (that is where I need it), I've put an addPause() at both levels, with the inner one that is "auto resuming" (I've put a child.paused(false) inside the pause callback).

This works perfectly in forward direction. The child callbacks are called always before those in the parent (this suggest me that you are using a bottom-up strategy when it is running forwards), so the inner pause is executed first preventing the following events to happen (that is what I need), then it resumes itself but in the same tick it runs the parent pause too, so the overall effect works, and you need just the smallest offset for the following tweens (I'm using one microsecond).
I've put also two callbacks for cheching the order of execution, one just before the pause and another one just after it. In forward direction the order is kept perfectly.

When you resume it reaches the end of the timeline as expected. The problems come when you reverse it.
In backward direction it executes first the parent pause, that seemed coherent to the previous results, bottom-up in forward direction, top-down in backward direction, so I was expecting the child pause to be executed after it. But when you resume it from the pause you see that both the child pause and one of the other callbacks are never executed.
So it seems that running in backward direction a pause at the top level "hides" the execution of any callback or pause in the nested items that belong to the same tick.
Could you confirm this as a bug or I'm missing something here?

Here is the pen when you can see the issue:

See the Pen pJWqjz by SkidX (@SkidX) on CodePen




 

Link to comment
Share on other sites

  • 2 weeks later...

Thanks for your patience, Skid X. Yes, I see what you mean. It's definitely an edge case, but thanks for pointing it out. I see how some tweaks to the rendering algorithm would be beneficial in this situation, so I rewrote pretty much the whole addPause() feature and posted an uncompressed prerelease version of TweenMax 1.17.1 at https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/TweenMax-latest-beta.js so that you could kick the tires a bit. Does that work the way you hoped? 

 

As I'm sure you discovered with your attempts to add timeline functionality to the other engines, it can be a very tricky endeavor with a lot of edge cases to accommodate. For example, how do you handle zero-duration tweens on a timeline and determine if they should render their start or end values when the playhead lands directly on top? What about when there's one at the very start or very end? If you accommodate looping, do you seamlessly interpolate things so that there isn't any time drift between cycles? If so, what if there's an update 0.00001 before the end of the timeline, and the next update is 0.2 seconds later and there's a nested tween with an onComplete positioned at the very end? You must make sure that fires before looping back. What if a pause happens right in the middle of the render loop, from within a callback? There are scores of little things like this that you must take into account, as I'm sure you've discovered. I tip my hat to you for trying to harmonize the various different engines. Frankly, it seems almost impossible since GSAP has a bunch of unique features (like fixing the transform-origin problems in various browsers for SVG, plus svgOrigin or smoothOrigin, plus consistent, independent control of transform components, overwrite management, Bezier tweening, attribute tweening, etc., etc.). 

 

Anyway, thanks again for your patience and insight. Please let me know if the update resolves your concerns. 

  • Like 3
Link to comment
Share on other sites

Thanks for your patience, Skid X. Yes, I see what you mean. It's definitely an edge case, but thanks for pointing it out. I see how some tweaks to the rendering algorithm would be beneficial in this situation, so I rewrote pretty much the whole addPause() feature and posted an uncompressed prerelease version of TweenMax 1.17.1 at https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/TweenMax-latest-beta.js so that you could kick the tires a bit. Does that work the way you hoped?

 

Hi Jack, thanks a lot, I'll make some tests this afternoon (CET timezone here) and let you know.

 

 

As I'm sure you discovered with your attempts to add timeline functionality to the other engines, it can be a very tricky endeavor with a lot of edge cases to accommodate. For example, how do you handle zero-duration tweens on a timeline and determine if they should render their start or end values when the playhead lands directly on top? What about when there's one at the very start or very end? If you accommodate looping, do you seamlessly interpolate things so that there isn't any time drift between cycles? If so, what if there's an update 0.00001 before the end of the timeline, and the next update is 0.2 seconds later and there's a nested tween with an onComplete positioned at the very end? You must make sure that fires before looping back. What if a pause happens right in the middle of the render loop, from within a callback? There are scores of little things like this that you must take into account, as I'm sure you've discovered. I tip my hat to you for trying to harmonize the various different engines. Frankly, it seems almost impossible since GSAP has a bunch of unique features (like fixing the transform-origin problems in various browsers for SVG, plus svgOrigin or smoothOrigin, plus consistent, independent control of transform components, overwrite management, Bezier tweening, attribute tweening, etc., etc.). 

You are totally right when you say that is impossible to achieve exactly the same behavior across different engines, it's very true, but that is also out of the scope of my lib. It would (teorically) require to handle manually any low level aspect of the rendering process, and this is exactly what I do NOT do with Tweene, at the end of the process the single tween is always demanded to the engine of your choice, so it goes out of my control.

The aim is to offer to developers a more standardized approach in their work. You can think at it like web standards work with browsers. You can use the same language (CSS) to define styles for any browser, and in most of the cases you will also achieve the same visual result, but there are also a lot of cases handled differently by different browsers, because they run different rendering engines. Without a standardized CSS language, you would have not only different rendered results, but also to learn different style languages and different approaches for different browsers, which is clearly a worse scenario. With Tweene I'm trying to offer a common language and achieve the more reliable result in most of the common cases, not to solve any differences in the inner animation engines.

For example Tweene supports also CSS Transitions. As you obviously know, you have no guarantee that two or more transitions of same duration will really end at the same instant, so it is basically impossible to guarantee a perfect sync while it's not an issue in gsap in which all things are in perfect sync by design. I cannot solve this and I don't want to. It's out of my scope. Let' say a developer writes a simple animation using Tweene and CSS Transition as inner engine. Later, that developer comes to the idea that achieve a perfect sync is a requirement for that work. It can just switch the engine from "transit" to "gsap", and in most of the cases no more efforts would be required. I think it's a better scenario than rewriting the whole thing having to learn a different API too. If its animation implies also some corner cases due to the internal differences of the two rendering engines, those differences would be there to solve in any case, with or without Tweene.

 

Said that and coming back to timelines, as you can imagine my implementation of timeline on top of different engines that I cannot control at low level can work only as an advanced "scheduler". Basically I organize the whole thing as a doubly linked list of simple tweens (well, the real structure can be more like a graph, but the list probably gives a better idea of the concept). This approach clearly introduce some small delays on "keyframes" on the timeline, while in your native implementation of timelines this is handled perfectly without any additional overhead. I know, there are of course implicit limits in this approach, but again, if this is an issue for the user, he can choice the tweene's gsap driver that relies on your native timeline implementation, he will have just a really small overhead before the very first play, after that the animation is totally powered by your lib.

 

Since I cannot obtain a cross-engine perfect sync, I tried at least to obtain always the same order of execution of different items (tweens and callbacks) and that was the point of the pause issue. A user talked to me of a simple scenario: a sequence of different tweens one after each other (no overlapping) with a pause just at the end of each tween. With my "timeline as a scheduler" I can do this so "complete event > pause > begin of next tween" are intrinsically always executed in the same order, while the inner optimization that you perform inside your code for events that happens inside the same tick (which is totally a feature from a performance point of view!) made difficult to me to achieve the same order of execution.

As told, I'll try today your changes and let you know. Thanks a lot again! (and sorry for my bad english)

  • Like 2
Link to comment
Share on other sites

Here I am, I've tested your update and seems almost perfect in both forward and backward directions.

Just to let you know, the only small difference from what I would love to obtain is that currently if I put callbacks on the same instant of the pause, they are executed in any case regardless their definition order.

 

My test scenario is this (consider each item appended at the end of the timeline without any offset):
1) Tween #1, 1s duration

2) callback "A"
3) addPause
4) callback "B"
5) Tween #2, 1s duration

My ideal behavior is having this order of execution (in forward direction):
- onComplete of #1 > callback A > Pause
then after resuming
- callback B > onBegin of #2

Obviously I expect the reverse order in backward direction (excluding onComplete and onBegin that will not fire).

With your last update I have this:
- onComplete of #1 > callback A > callbackB > Pause
then after resuming
onBegin of #2

Putting just a microsecond of offset among each callbacks and the pause solves the order, it is not a big issue, so feel free to ignore me on this thing if you don't find any value added in changing it :D
Please let me know only if you would consider it as a future change.

Thanks a lot for your update, it definitely helps me.

Link to comment
Share on other sites

Glad it's working better for you.

 

Would you mind showing me how to reproduce the behavior you described with callbackA > callbackB > pause? I tried this: 

var tl = new TimelineMax();
tl.to({}, 0.5, {})
.call(function() {
    console.log("1");
})
.addPause()
.call(function() {
    console.log("2");
})
.to({}, 0.5, {onStart:function() {console.log("next tween start");}});

TweenLite.delayedCall(2, function() {
    console.log("resume");
    tl.resume();
});

And I got "1", then 1.5 seconds later, I saw "resume" and then "2" and "next tween start". Maybe I'm missing something. 

Link to comment
Share on other sites

Sorry, it's my fault, I was not totally clear in my previous description. It happens when you have the callbacks on a nested timeline while the pause is on the outer one (all in the same instant). I have a parent timeline in my scenario due to other not related reasons and I forgot to mention it.

Putting it in this way the lib behavior seems totally reasonable, so I was going to fix my use case moving all the callbacks on the parent to be on the same level of the pause, and I've noticed something strange, I've reproduced it here:

See the Pen QbQzzY by SkidX (@SkidX) on CodePen



In the first timeline (A and B boxes tweened) I reproduced my bad scenario with the pause in the parent timeline and the callbacks in the child.
The call order is that I have said before, and the tweens animation in backward direction is perfectly symmetrical to that one in forward direction, as expected.

In the second timeline (C and D boxes tweened) I just moved the callbacks on the parent, and I noticed two different strange things.
1) the "post pause" is never called before the pause (correct) but the order of execution of "D begin" and "post pause" is not always the same, sometimes it runs the callback first, sometimes refreshing the page and restarting it calls the tween begin event first. It's strange considering that the tween position is forced to a different time offset.
2) Take a look to what happens when you resume after the pause, reach the end and then reverse the animation. In backward direction the D box stops on a different position, it is not symmetrical to the forward animation. The only difference is the position of the callbacks, why is changing also the behavior of the tween?

I know that we are talking of some edge cases, but just to understand how it is working. Thanks for any feedback.

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