RolandSoos

addPause: the behavior is unexpected

Recommended Posts

I know my example is silly and there is no real reason in these examples to use addPause. 

 

I'm writing an algorithm which will create timeline based on a specific data structure. There are multiple animation stages -> Incoming, Loop, Outgoing and some of them might be empty and they might pause the timeline at a specific label.

var tl = new TimelineLite({
  onComplete: function() {
    tl.play("incoming");
  }
});
tl.addLabel("incoming");
tl.addPause("incoming", function() {
  // Resume the timeline if the related Promise resolved
});
// Add the incoming animations if any

tl.addLabel("loop-incoming");
tl.addPause("loop-incoming", function() {
  // Resume the timeline if the related Promise resolved
});

// Add the loop-incoming animations if any

tl.addLabel("loop");
// Add the loop animations if any

tl.addLabel("loop-outgoing");
tl.addPause("loop-outgoing", function() {
  // If loop set for multiple iterations -> Jump back to the "loop" label and play
  // Resume the timeline if the related Promise resolved
});
// Add the loop-outgoing animations if any

tl.addLabel("outgoing");
tl.addPause("outgoing", function() {
  // Resume the timeline if the related Promise resolved
});
// Add the outgoing animations if any

 

 

For example if there is no incoming animations, then the addPause for the "incoming" label will be at the same time as the "addPause" for the loop-incoming label.

 

The documentation states that addPause is a null tween with a pause at the onComplete event. So In my pens I set the suppressEvents to false.

 

Problem #1: https://codepen.io/anon/pen/PXEpYY?editors=0011

The console output should be Pause 1 -> Pause 3 -> Pause 1 -> Pause 3 etc...

The actual output is: Pause 1 -> Pause 3 -> Pause 1 -> Pause 1 -> Pause 3 -> Pause 1 -> Pause 1 -> Pause 3

 

Problem #2: https://codepen.io/anon/pen/KbZaEM?editors=0011

Pause 2 should come instantly after Pause 1, but it happens after pause 3. I'm not sure why.

 

Problem #3: https://codepen.io/anon/pen/wRpJBK?editors=0011

There is no way to play the timeline if the addPause placed at the 0 position. I tried with and without supressEvents.

 

Problem #4: https://codepen.io/anon/pen/YdYZOP?editors=0011

If the timeline plays from a label, then  addPause fired, but it does not pause the timeline. It reports in the addPause callback that the timeline state is paused, so there is no way to force pause the timeline. Probably the solution for this is to use seek instead of play and play the timeline from the addPause callback when the promise fulfilled: https://codepen.io/anon/pen/KbZWbQ?editors=0011

 

Problem #5: https://codepen.io/anon/pen/MZrmwP?editors=0011

Tried to solve Problem #4 with seek, but stuck again. If I seek to a new label inside the original play call stack, even Pause 2 skipped and the timeline plays.

Then I tried to "escape" from the play's call stack with setTimeout, but the result is even more unexpected, the first tween continues to play and the seek does not happen: https://codepen.io/anon/pen/dwJWOM?editors=0011

 

 

Share this post


Link to post
Share on other sites

Hi @RolandSoos,

 

While I do not understand your thoughts and codes,
but suspect that several pauses at the same time point a conflict.


So here's just a try: If you use TimelineMax: .addPause and .removePause ...

 

 

 

Just another point of view ...

 

Mikel

 

 

Share this post


Link to post
Share on other sites

Hi @mikel,

thanks for your thoughts. In my addPause callbacks, I placed a simple play calls. I think that was a mistake on my side, but if I would add a promise for everyone of them that would add a lot of unnecessary codes. 

 

Here is a simple example with promise. When the promise resolved, the timeline can continue from from pause1 state. If the promise resolved during the first tween, then the timeline simply continues. If it is resolved after reached addPause callback, the timeline will wait until the promise gets resolved and continues to play.

 

https://codepen.io/anon/pen/pqprJe?editors=0011

 

Update: I edited the examples in my first comment. Now the every play action depends on a promise.

 

Share this post


Link to post
Share on other sites

Hi RolandSoos,

 

I can't go over all of your issues in one go (my lunch break is only so long)  Ops, someone got carried away and is being naughty at work...

 

Problem #1, you said yourself, you are turnning off the supressEvent thus, whenever resolve#2 is clicked, the virtual playhead jumps from the second addPause() all the way back to time 0, passing over the first addPause(), triggering its callback. If you leave the suppressEvent turned on, you will have the behaviour you expect.

 

Pause 1 -> Pause 3 -> Pause 1 -> Pause 3 etc...

 

Problem #2, as you know, the addPause is a "zero duration" tween. So, you are adding two pauses on top of each other at the exact same timestamp. But, when the playhead hits that timestamp, GSAP only fires the first callback as it is based on a onComplete callback. The second addPause() never gets a change to run. When the user clicks on resolve #1, the playhead moves from that timestamp but the second callback is not fired because the it sits at the exact timestamp so, on the next GSAP tick, the playhead has moved away from it so, the onComplete call does not fire. If you were to add the second addPause with a miniscule offset, for example "+=0.01", it will fire when clicking on resolve #2.

 

This example also has the supressEvents issue as responded above. So, you will need to adjust accordingly.

 

Problem #3 builds on top of the concepts of Problem #2, the addPause is sitting at time 0 but it never runs because the timeline is already set to paused. Given that it never runs, the .then() part of the promise never gets initialized and thus, everything else falls apart. The workaround is the same as before, have the addPause() sit sligthly offset from 0, say 0.001, and things should work as you expect.

 

Problem #4, is regarding the supressEvent, that you are disabling. Again, the addPause works off a onComplete call. The callback is firing in this case because you are removing the supression of events when the playhead is scrubbing around, not playing. When you set the timeline to play from 'test' onwards, you're scrubbing the timeline from 0 past the addPause() and then, GSAP starts playing from the label. The callback is fired because no events were supressed.

 

Problem #5 is on the same vein as #4, where you are turning off the supression of events and scrubbing the timeline over those events thus firing them.

 

Ultimately, the behaviour is as expected, but only IF you understand the callback in addPause() happens during a onComplete(), you cannot stack several of those on top of each other as only the first one will fire. You also need to have in mind the concept of "tick" in GSAP, where certain things happen at one "tick", then the playhead moves to another time position before the other "tick" running.

 

Hopefully, this helps clear out your questions and you will be able to adjust the code to suit your needs.

  • Like 5

Share this post


Link to post
Share on other sites

Thanks @Dipscom!

 

Problem #1: Well, I was not aware the if I seek backward all in-between events are fired. I thought the .seek(0, false) only fires events which happens at 0 position. I think it is a good to remember thing...

 

Problem #2: I thought their are linear in the original add tween order. So If if my addPause stops the timeline, the timeline does not render other tween at the very same position which added after the addPause. If the timeline resumed, then it would continue at the same tick with the next tween after the current pause. Yeah, I see, it is too complicated to manage properly :)

 

As you mentioned, other problems are all based on this two misunderstanding. Probably my use case is to specific and I see I won't be able to solve it with in-built addPause and labels. Probably I will use addCallback strictly one at one position to prevent problems and I will manage the label transitions in these functions.

 

Share this post


Link to post
Share on other sites

I'm not clear what you need to achieve but, I feel you could have all your addPause() calls lined up nicely if you were to always offset them by "+=0.01" relative amount of time. Then, they should all trigger, one by one and you should see no different in the animation as the amount of time they would add to the total timeline is negligible.

 

  • Like 1

Share this post


Link to post
Share on other sites

Thanks @Dipscom!

 

I started to work with your suggestion. Well, if I assume 60fps 1/60=~0.01666, the offset should be smaller than that number. So the suggested 0.01 should be fine, but I tried it with 0.0001. I'm not sure if the following error related to float rounding or something wrong with my code. I might need to add multiple labels/callback at the "same" offset position, so I would like to go 0.001 as that would give 16 extra spaces for a tick at 60fps.

 

https://codepen.io/anon/pen/vvdqEm

 

Normal console output would be:

Start

pause at IN

pause at Complete

pause at IN

pause at Complete

pause at IN

pause at Complete

pause at IN

pause at Complete

 

But sometimes I get the following:

image.png.0567f13ffc334a9371589279f6931621.png

 

If it is really a float rounding issue, is there any chance that it happens when the small offset is 0.01 or 0.001?

Share this post


Link to post
Share on other sites

I can't autoratively say if it is or it isn't a float rounding issue. It certainly smells like one from what you are reporting and from looking at your code.

 

When looking at your pen, I had it running for a while and reloaded it a few times myself. I only managed to see that repeated "pause at IN" once. And I have changed the smallPosition to be '+=0.0000000001'. It got so small that I started seeing the onStart callback again. But not the double "pause at IN".

 

Maybe it is a CPU/Browser issue? Whereas some combos the error occurs more frequently than others?  My initial thought was that it was the setTimeout() that would be messing things up but I don't see how it would be the case. I know the setTimeout() will fire even when you don't have the tab on focus but not sure how that would cause the call for GSAP's callback to run twice.

 

Out of curiosity, will your system run over those empty pauses automatically? If not, I don't see how it matters how long the whole timeline is because, the user will not be able to click fast enough for that to matter. Have you tried to play devil's advocate to your concept there and see if you are not overengienierring this bit of the logic?

 

 

  • Like 1

Share this post


Link to post
Share on other sites

Well, I can reproduce it in every 3-5 Codepen run, it happens after 5-7 iteration. Also The window was focused all the time, so I do not think it is relevant.

 

In my old system I have 3 simple timelines and they contain tweens for the very same element. The system manages to play, pause, seek etc... all of them. The timelines play only when their dependency met.

Here is an example (events are optional in the system)

TL #1 Plays when user clicked a button and if this is the first time when TL #1 plays

TL #2 Plays when TL #1 completed and this timeline repeats itself until the user clicks another button. If the button clicked TL #2 plays until onComplete

TL #3 Plays when TL #2 completed

 

When I add them to a single timeline, it gets a little complex, but I think it is possible with GSAP. This is why I experience with these edge cases and examples :)

  1. addLabel -> 'TL1' -> Position 0
  2. addCallback -> Position +=0.001 -> Pause the timeline if TL #1 dependency not met (Also play the timeline in the future when dependency met). Position offset added so when I seek to TL1 label in the future I do not need suppressEvents:false
  3. add TL1 tweens
  4. addCallback -> Notify system that TL1 completed
  5. addLabel -> 'INITIAL' -> Position +=0.001
  6. addLabel -> 'TL2' -> Position +=0.001
  7. addCallback -> Position +=0.001 -> Pause the timeline if TL #2 dependency not met (Also play the timeline in the future when dependency met).
  8. addLabel -> 'LOOP' -> Position +=0.001
  9. add TL2 tweens
  10. addCallback -> Position +=0.001 -> if the dependency not met seek back to loop label all the time. .seek('LOOP')
  11. addCallback -> Position +=0.001 -> Notify system that TL2 completed
  12. addLabel -> 'TL3' -> Position +=0.001
  13. addCallback -> Position +=0.001 -> Pause the timeline if TL #3 dependency not met (Also play the timeline in the future when dependency met).
  14. add TL3 tweens
  15. addCallback -> Position +=0.001 -> Notify system that TL3 completed. Based on the options, might .seek('TL1')

Public actions allowed on this timeline are:

  • play
  • pause
  • seek('TL1')
  • seek('INITIAL')

Share this post


Link to post
Share on other sites

Something's definitely going on here, I did see it once.

 

Right now I am knees deep on something at work and can't spare the attention but I'll come back and have a think at some point...

 

Maybe someone else will have a suggestion in the meantime.

Share this post


Link to post
Share on other sites

Hello @RolandSoos

 

Just a thought, just so you can rule out codepen's website causing any shenanigans try running your codepen in Debug mode. Debug mode runs your codepen without an iframe, unlike with Edit and Full modes which display your code within an iframe.

 

To use Codepen's Debug mode with no iframe, in your codepen url change /pen/ to /debug/.

 

https://codepen.io/anon/debug/vvdqEm

 

See if it still has that issue, this way you can rule out codepen's nested iframes as causing your issue.
 

Happy Tweening ! :)

Share this post


Link to post
Share on other sites

Thanks @Jonathan,

it happened once in debug mode too :(

image.png.3cf06a3df1702a4343917a38dee0ac76.png

Share this post


Link to post
Share on other sites

I removed every loaded JS files except TweenMax.min.js, also set the postition offset to +=0.01 and reduces/improved the test. It will die with error message on the console when the callback fired twice.

 

https://codepen.io/mm00/pen/REMGre?editors=0011

 

Share this post


Link to post
Share on other sites

Hey RolandSoos,

 

Sorry for the disappearance, deadlines, babies and general life, you know... Gotta earn that bread else, I shall starve.

 

So, for all intents and purposes we can all agree this is some sort of float rounding error - At least this is everybody's best guess.

 

I still have difficulties reproducing the double callback but I have spotted it once so, we'll go with it happening.

 

My question to you is: do you really need to create one big timeline that holds all of those animations and has to account for so many variations? Can't you just create/update a timeline whenever the conditions change? In general it is pretty cheap to create and overwrite timelines. I know you said in your real application everything is more complex but still, I fail to see why this would need to be one single big timeline. Have you considered having your system create tiny timelines with onComplete callbacks that ask a central state manager for the where to go next?

Share this post


Link to post
Share on other sites

Thanks @Dipscom!

 

My current system use tiny timelines and manages their statuses and such. There is no real problem with it, but with a single timeline, I would be able to achieve better result in the following areas. Single timeline would allow to

  • use the same timeline as linear or event based mode. I would be able to simply change progress based on scroll for example.
  • when the canvas in my system changes,  I could simply invalidate the timeline without errors. Currently I have to invalidate all timelines in the right order as their happen on the same element/props also I must restart the animations from the start point. With a single timeline I could invalidate and just "resume" from the current point.
  • not care for immediateRender. As the current system animates the same element's prop in different timelines, I must use the proper tween and it results in lot of testing.
  • use timeScale easily.
  • throw out a lot of code related to timeline managing.

Probably I would be able to solve all of these things with tiny timelines and a custom timeline manager implementation. But, why would I write a new timeline controller if GSAP's might be able to solve this issue, right? :)

 

BTW: I near to the finish with my single timeline and it looks and works great. I haven't seen any bug related to this double firing, probably because I still manage statuses and my callbacks verify if the actual status is the right one. So it is not so important for me, it would be just nice to know if this bug could result any other anomalies.

Share this post


Link to post
Share on other sites

I see. Well, I'm glad it is working and that you got your system completed. Hopefully you won't have issues with the rounding number when the time comes. 

Share this post


Link to post
Share on other sites

I had a little time for debugging. I used the current version of uncompressed tweenmax.js.

Stacktrace

 

First column is the first console log

Second column is the last console log right before the callback fired twice.

Third column is the console log when the event fired twice

vGCNRrt.png

 

It first and the third call is fine and the exception is the second call stack. It seems like the double firing happens when the ticker is not active an GSAP wakes it up.

 

tl.pause(); in the last callback sometimes sends the ticker to sleep

 

    Animation._updateRoot = TweenLite.render = function() {
        var i, a, p;
        if (_lazyTweens.length) { //if code is run outside of the requestAnimationFrame loop, there may be tweens queued AFTER the engine refreshed, so we need to ensure any pending renders occur before we refresh again.
            _lazyRender();
        }
        _rootTimeline.render((_ticker.time - _rootTimeline._startTime) * _rootTimeline._timeScale, false, false);
        _rootFramesTimeline.render((_ticker.frame - _rootFramesTimeline._startTime) * _rootFramesTimeline._timeScale, false, false);
        if (_lazyTweens.length) {
            _lazyRender();
        }
        console.log(_ticker.frame , _nextGCFrame);
        if (_ticker.frame >= _nextGCFrame) { //dump garbage every 120 frames or whatever the user sets TweenLite.autoSleep to
            _nextGCFrame = _ticker.frame + (parseInt(TweenLite.autoSleep, 10) || 120);
            for (p in _tweenLookup) {
                a = _tweenLookup[p].tweens;
                i = a.length;
                while (--i > -1) {
                    if (a[i]._gc) {
                        a.splice(i, 1);
                    }
                }
                if (a.length === 0) {
                    delete _tweenLookup[p];
                }
            }
            //if there are no more tweens in the root timelines, or if they're all paused, make the _timer sleep to reduce load on the CPU slightly
            p = _rootTimeline._first;
            if (!p || p._paused) if (TweenLite.autoSleep && !_rootFramesTimeline._first && _ticker._listeners.tick.length === 1) {
                    while (p && p._paused) {
                        p = p._next;
                    }

                    if (!p) {
                        _ticker.sleep();
                    }
                }

        }
    };

 

Which outputs:

11STuHX.png

 

So I think it is related with the garbage collection I think.

 

Another interesting details, that the .play() suppressEvents is default to true, so there should not be any events, but when this wake exception happens at garbage collection, the _rootTimeline.render((_ticker.time - _rootTimeline._startTime) * _rootTimeline._timeScale, false, false); gets called which does not suppress the events.

 

I hope it helps :)

Share this post


Link to post
Share on other sites

Well, I got my first issue related this bug, so I debug deeper. Values are from the Chrome debugger.

 

R2Tx65H.png

 

4.624-0.001 = 4.622999999999999  // Note: Lost the precision. In a perfect world, it should be 4.623

 

5Rz5Umy.png

 

4.622999999999999-4.6129999999999995 = 0.009999999999999787

 

Bc9BZB6.png

 

0.009999999999999787-0.01 = -2.1337098754514727e-16
 

and then GSAP end up thinking its a reverseComplete

 

pQcParK.png

 

I tried the following code and solved this issue:

Line 1766

var renderTime = time - tween._startTime;

if(renderTime < _tinyNum && renderTime > -_tinyNum){
  renderTime = 0
}


if (!tween._reversed) {
  tween.render(renderTime * tween._timeScale, suppressEvents, force);
} else {
  tween.render(((!tween._dirty) ? tween._totalDuration : tween.totalDuration()) - (renderTime * tween._timeScale), suppressEvents, force);
}

 

 

In my real world scenario this bug caused trouble. I had an addPause callback at some position. To prevent another issue I mentioned in the forum earlier, my addPause callback seeks back the timeline to the real position:

var pausePosition;
timeline.addPause(function(){
	timeline.seek(pausePosition);
});
pausePosition = timeline.recent().startTime();

 

It pauses the timeline properly. Then in the future, when I play this timeline, because of this rounding issue the addPause callback gets fired again as rounding messed up the time and GSAP indentifies that we are in the past and calls the pause again. (It is rare, but happened several times.)

 

Update #1:

This float problem is not only related to my small position (0.01s) what I use in my examples. The same can happen with real world values: https://codepen.io/mm00/pen/jdWVGL?editors=0011

 

Share this post


Link to post
Share on other sites

I would have thought @GreenSock would have chimed in by now but I guess he's been bogged down with other stuff these days. I am finding this issue fascinating but, unfortunately, I can't think of anything at the moment to help remediate that. Keep bumping this up whenever you have more info and hopefully we'll manage to work something out.

  • Thanks 1

Share this post


Link to post
Share on other sites

Yep, sorry, I've had a lot on my to-do list and this is far from being a simple thing to address. It requires deep-dives into the code and it can get quite complex with lots of different scenarios to accommodate. I do appreciate the details and the suggested fix too, although I'm pretty uncomfortable with adding that to the codebase because it'd be costly performance-wise (not to mention that almost nobody would run into this edge case in their projects). I'll dig into it, though, as soon as I can and let you know what I find. Zero-duration tweens and pauses like this are much, much more tricky than most people would anticipate. Thanks for your patience. I really appreciate the reduced test cases too.

  • Like 2

Share this post


Link to post
Share on other sites

Thanks @GreenSock! Well, fixing this issue is not important for me, I will fix double fires on my own without touching the source of GSAP. I just needed to know the reason of this bug to be able to produce proper solution for my usecase. :)

  • Like 1

Share this post


Link to post
Share on other sites

I dug into this and if I let your test case run for long enough (sometimes over an hour), it did hit that issue. I chased it down and implemented what I believe is a fix that's more efficient than the temporary one you had in place. I let it run for a long time and it never happened again, but I'd appreciate it if you'd give it a whirl and see if you can break anything. Here's an uncompressed TweenMax from the upcoming release: 

https://s3-us-west-2.amazonaws.com/s.cdpn.io/16327/TweenMax-latest-beta.js

 

Better? 

  • Like 3

Share this post


Link to post
Share on other sites

Well done @GreenSock! It seems like the fix eliminated the issue. 

 

BTW, What do you think what was the reason that it happened for you and for @Dipscom less frequently? In Win 10 Latest Chrome and MacBook Air latest Safari, I was able to reproduce with 30% chance in 20 seconds.

Share this post


Link to post
Share on other sites

I was looking at it from a 2016 MacBook Pro on Firefox, on a Nokia running Android One on the Brave browser (Chromium). Although I do have a Windows machine here as well, I did not go as far as testing with it.

 

It did not take me a whole hour to trigger the bug but it was not easy nor consistent.

Share this post


Link to post
Share on other sites
14 hours ago, RolandSoos said:

BTW, What do you think what was the reason that it happened for you and for @Dipscom less frequently? In Win 10 Latest Chrome and MacBook Air latest Safari, I was able to reproduce with 30% chance in 20 seconds.

 

Tough to say. I'm on a MacBook Pro that's pretty beefy. I let your test go overnight in one Chrome window and it never hit the glitch. 

 

Anyway, glad the new version resolves things completely. Thanks for verifying. 

  • Like 2

Share this post


Link to post
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.