Jump to content
RolandSoos

Missing frames after addPause callback resumed

Recommended Posts

Well, first of all I'm sorry that every day I have a problem. For the current one, I was not able to create  a Codepen which produces this issue.

In my real world example, I have an addPause at position 0.001, so when the timeline reach this position, it will wait until the click promise resolved:

See the Pen QzZZpo?editors=0110 by anon (@anon) on CodePen

 

 

Also in my scenario, I have a complex scene with lot of animated elements ~100 which repeated all the time. When the event happen which resolves the promise of the addPause and plays the timeline, it seems like that the animation is skipping several frames. The second, third etc. time when it reaches the addPause  and the play promise resolved again, the animation is running smoothly.

 

I tried to reproduce the very same issue at CodePen, but it only happened when I animated 10000 other small boxes, so that might be unrelated.

 

Screen recording (watch in full hd)

  • 0:05 I click on the zeppelin. The Zeppelin's promise resolved, so it starts to play from the addPause. This part is important as this is where you will see that the zeppelin jumps instantly in the middle of the screen. The first part of the animation is missing.

 

Console contains the following timings:

  • 0:19 in resolved, this is when the zeppelin was clicked, after this line the timings when this event happened:
    • current js time: 1547202313240  .time() of the zeppelin's timeline: 2.4220000000000006
  • The next timings are from the onUpdate event of the zeppelin's timeline:
    • current js time is first and the zeppelin's timeline the second

 

Here is a more detailed console which shows the same issue as the video:

____		JS time______	.time()	.progress()
Play		1547203186098 	0.001	0.00031201248049921997
onUpdate 	1547203186121 	0.2559	0.07987519500780024
onUpdate 	1547203186139 	0.274	0.08549141965678628
onUpdate 	1547203186156 	0.2919	0.09110764430577217
onUpdate 	1547203186170 	0.3060	0.09547581903276132
onUpdate 	1547203186189 	0.3239	0.10109204368174722
onUpdate 	1547203186206 	0.3420	0.10670826833073326
onUpdate 	1547203186222 	0.3559	0.11107644305772227
onUpdate 	1547203186239 	0.375	0.11700468018720749
onUpdate 	1547203186255 	0.391	0.12199687987519502
onUpdate 	1547203186271 	0.4070	0.12698907956318253
onUpdate 	1547203186289 	0.4249	0.13260530421216843
onUpdate 	1547203186304 	0.4399	0.13728549141965676
onUpdate 	1547203186322 	0.4580	0.1429017160686428
onUpdate 	1547203186339 	0.4750	0.1482059282371295

 

It seems like the first onUpdate jumped ~0.252s which I think could result that huge jump as the original animation's duration is 0.8s. The timelines whole duration 3.205

 

 

If I change the addPause position from 0.001 to 1s the animation plays nicely without jump and this is the console output. There is only ~0.04s jump between the first two.

 

____	 JS time______ .time()	.progress()
play____ 1547203837755 1     0.23786869647954326
onUpdate 1547203837780 1.041 0.24762131303520452
onUpdate 1547203837798 1.058 0.25166508087535683
onUpdate 1547203837813 1.074 0.2557088487155089
onUpdate 1547203837829 1.090 0.25951474785918166
onUpdate 1547203837846 1.108 0.263558515699334
onUpdate 1547203837862 1.124 0.26736441484300666
onUpdate 1547203837881 1.143 0.271883920076118

 

 

I tried several position values and it seems like it has this frame skipping behavior, when the position is smaller than 0.2s. Above 0.2s I get 0.04s between the first two. Under 0.2s, the delay is increasing with lower values.

 

I tried to place addPause to 1.001s and seek the timeline to 1s, but this does not solved the issue.

 

 

Ps.: if I won't be able to solve this, I will remove the addPause from the start of the timeline and I will start the timeline initially paused and will start to play when the event happens.

Share this post


Link to post
Share on other sites

More details while debugging:

 

When the timeline created, GSAP sets its _startTime:

    p.add = p.insert = function (child, position, align, stagger) {
        var prevTween, st;
        /*if(window.aaaa) {
            console.log((new Date()).getTime(), 'add to timeline', child._totalDuration);
        }*/
        child._startTime = Number(position || 0) + child._delay;

 

At 60ps, there is a frame in every 0.01666s

 

Example:

new TimelineLite()

_startTime becomes 0.236 | timeline.rawTime() -> 0 | timeline._timeline._time -> 0.236

here the timeline created and starts to play

as there is an addPause at 0.001, the timeline will pause:

_pauseTime becomes 0.318 | timeline.rawTime() -> 0.089 | timeline._timeline._time -> 0.318

 

Exception:

In an ideal world _pauseTime should be _startTime + 0.001 as we stopped there => 0.236+0.001=0.237

In real, it took 0.089s which is ~5 frame if we would have the 60fps at page load.

 

Conclusion:

When the fps is constant, it does not matter if the _pauseTime is perfectly aligned with addPause position or not as in the worst case we lose only 1 frame by this which you are unable to notice.

The problems comes when you have low fps at the start of the timeline and high fps when the paused time resumed, I think this is the scenario with my complex page. There is less than 60fps during new TimelineLite(), addPause(0.001, ...) (I think we can calculate the fps as it took 5 "60fps frames" to reach the addPause, so 60/5 = 12fps). Then when the play happens, GSAP thinks that those 5 frames are already rendered and they will be skipped.

 

 

 

 

 

Share this post


Link to post
Share on other sites

Possible solution could be:

 

p.render = function (time, suppressEvents, force) {
  ........
  ........
                  while (tween) {
                    next = tween._next; //record it here because the value could change after rendering...
                    if (curTime !== this._time || (this._paused && !prevPaused)) { //in case a tween pauses or seeks the timeline when rendering, like inside of an onUpdate/onComplete
                        break;
                    } else if (tween._active || (tween._startTime <= curTime && !tween._paused && !tween._gc)) {
                        if (pauseTween === tween) {
                            this.pause();

                            // If we pause the animation by a pauseTween, then set the proper _pauseTime to prevent frame skipping
                            this._pauseTime = this._startTime + pauseTween._startTime;
                        }
                        if (!tween._reversed) {
                            tween.render((time - tween._startTime) * tween._timeScale, suppressEvents, force);
                        } else {
                            tween.render(((!tween._dirty) ? tween._totalDuration : tween.totalDuration()) - ((time - tween._startTime) * tween._timeScale), suppressEvents, force);
                        }
                    }
                    tween = next;
                }

 

 

The pause() call sets the this._pauseTime, which might be not good. So if there is a pauseTween, then set the value of this._pauseTime to the sum of the _startTime of the timeline and the _startTime of the pauseTween.

 

this._pauseTime = this._startTime + pauseTween._startTime;

 

Share this post


Link to post
Share on other sites

Are you familiar with lagSmoothing? 

https://greensock.com/docs/TweenMax/static.lagSmoothing

 

GSAP is built to honor timings primarily, but it'll also adjust for some amount of lag. It's a trade-off of course and you can control it to be whatever you prefer. 

 

As far as I can tell, things are working as they should. Remember that you cannot assume things will always run at 60fps. GSAP was built to be completely resolution-independent in terms of time, so you you jump anywhere. If you've got tons of things animating and there's a lag in the browser for .25 seconds, for example, on the next tick GSAP will jump .25 seconds ahead (as it should). It kinda sounds like you were expecting it to act as if .25 seconds didn't elapse, but only 1/60th of a second (assuming 60fps) - is that right? Again, you can tweak lagSmoothing() to give you that behavior but that also means that when the browser bogs down, your animations will get slower and slower. Hopefully docs and the video there make it clear. 

  • Like 2

Share this post


Link to post
Share on other sites

Thanks Jack!

 

I was able to create a Codepen as I know the reason: 

See the Pen qLJevO?editors=0010 by anon (@anon) on CodePen

  1. Open up the console
  2. Watch for the error line: _pauseTime set 0.161 Run the pen again until you do not get bigger _pauseTime than ~0.04. Best if bigger than 0.1
  3. Click on Resolve #1 and watch that the animation will jump several frames.

 

 

Well if I have the following timeline

var tl = new TimelineLite();

tl.addPause("+=0.001", function() {
  button1.promise.then(function() {
    tl.play(null, false);
  });
});

tl.to("div", 2, {
  x: 1000
});

 

I see your point about lag smoothing and that would be a great reason, but:

I assume that the timeline gets paused at that position and when I resolve the promise, the play will continue from that position. I think it makes no sense to honor timings in an addPause case as the timeline is paused.

 

So addPause should be an exception. If there was a lag between the timeline creation and addPause's pause(), I do not think that lag should affect the future when the timeline plays again. There is a reason why you paused the timeline at that given point of time

 

addPause is already an exception in GSAP, try the following pen: 

See the Pen ebQOVN?editors=1010 by anon (@anon) on CodePen

 

If GSAP would honor the timing, then the second red box should jump near to the black line instantly as it has the same position as the addPause. But the addPause was an exception and it does not allow future tweens to render on that timeline in the very same tick.

 

Share this post


Link to post
Share on other sites

I'm traveling at the moment, but I'll look into this when I'm back. Thanks for the detailed info and your patience. 

  • Like 1

Share this post


Link to post
Share on other sites

As I know the cause of the problem, I was able to find solution for this issue, but I think it still worth some of your time to see if GSAP should be improved with the previous idea.

 

First of all I was able to find a more easier method to simulate the problem. Just limit the fps while the playhead reaches the addPause tween.

TweenMax.ticker.useRAF(false);
TweenMax.ticker.fps(2);

 

Then in the addPause callback remove the fps limitation

tl.addPause(..., function(){
  TweenMax.ticker.useRAF(true);
  TweenMax.ticker.fps(undefined);
});

 

The solution

get the tween's exact position on the timeline and in the addPause callback, seek to that position. It will result that the playhead will be in the right position. Downside: I have to store every addPause's position after adding them, to be able to know where to seek on the timeline.

var pauseTime2;
tl2.addPause("+=0.001", function() {
  this.seek(pauseTime2);
  button1.promise.then(function() {
    tl2.play(null, false);
  });
});
pauseTime2 = tl2.recent().startTime();

 

When the solution fails

If in the addPause callback we instantly play the timeline, then seeking back -> GSAP  will not honor the timings, which is bad and I see why the current implementation of GSAP is good.

var pauseTime2;
tl2.addPause("+=0.001", function() {
  this.seek(pauseTime2);
  //button1.promise.then(function() {
    tl2.play(null, false);
  //});
});
pauseTime2 = tl2.recent().startTime();

 

 

So to properly manage this situation, the seek should be conditional.

  • If the promise already resolved when the addPause callback happens, we should continue to play and GSAP will honor timing. 
  • If it is not resolved then we do not need to honor timing, so we can simply seek back and play when the promise resolved.
var pauseTime2;
tl2.addPause("+=10.001", function() {
  if (!button1.promise.resolved) {
    this.seek(pauseTime2);
    button1.promise.then(function() {
      tl2.play(null, false);
    });
  } else {
    tl2.play(null, false);
  }
});
pauseTime2 = tl2.recent().startTime();

 

So, I can manage this situation on my own.

 

Final pen where red box is the bad result and green is the right result

 

See the Pen EGGpXW?editors=0010 by mm00 (@mm00) on CodePen

 

 

How could GSAP manage this situation? Two possible solution...

 

#1 solution

addPause callback could have an optional boolean return value. This return value could indicate if we need this pause or not.

tl2.addPause("+=10.001", function() {
  if (!button1.promise.resolved) {
    button1.promise.then(function() {
      tl2.play(null, false);
    });
    
    // tell GSAP that it is a real pause, so pause the timeline, so 
    // GSAP could adjust the position of the timeline to the exact position of the pause.
    return true;
  }
  
  // we do not need the pause, so the timeline can continue to play.
  return false;
});

 

 

#2 solution

If the pause reason of the timeline is addPause, then GSAP should check on the next tick if there way a play call on this timeline.

  • If there was a play call on the timeline until the next tick, GSAP should continue to play without affecting the timings.
  • If there was not any play call on the timeline until the next tick, GSAP can adjust the playhead position back to the addPause position as there is no honorable timing anymore for this pause.

In my opinion #2 solution is best to solve race condition issues and works best with promises.

 

tl2.addPause("+=10.001", function() {
  var ret = false;
  
  button1.promise.then(function() {
    ret = true;
    tl2.play(null, false);
  });
  
  // return will be always as the promise.then is async and will set ret to true later
  return ret;
});

 

Share this post


Link to post
Share on other sites

This is certainly an edge case, but I do see an issue that seemed worthy of fixing. Here's the most reduced test case I could come up with to illustrate/test:

function test() {
    var released = false,
        tl = new TimelineMax({onUpdate:function() {
                if (released) {
                    if (tl.time() > 0.8) {
                        console.log("FAILED!")
                    } else {
                        console.log("WORKS!");
                    }
                    released = false;
                    tl.pause(0).kill();
                    TweenLite.ticker.fps(null);
                }
            }});

    TweenLite.ticker.fps(2);

    tl.addPause(0.0001, function() {
        setTimeout(function() {
            released = true;
            tl.play();
        }, 500);
    }).to({x:0}}, 2, {
        x: 1000
    });
}
test();

 

This should be resolved in the next release which you can preview (uncompressed) at: 

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

 

Better? 

Share this post


Link to post
Share on other sites

Thanks @GreenShock! I will be able to check this on Monday.

Share this post


Link to post
Share on other sites

Yes, it seems like the fix solved the issue. Thank you!

 

Old:

See the Pen bzbgpd?editors=0011 by mm00 (@mm00) on CodePen

 

New:

See the Pen omvBJR?editors=0011 by mm00 (@mm00) on CodePen

 

 

 

I'm not sure if you are interested in another hard to noticeable bug to fix in addPause :)

 

There might happen that a condition already met when I reach addPause. First though, simply tl.play() in the addPause callback and I will be fine. But, as the addPause happens earlier than tweens and the pause cancels the rendering of that frame. It results if you instantly resume the tween, you will notice a skipped frame if you watch closely. On my example watch the middle of the animation. Hard to notice but you will see the result of the one missing frame:

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

 

 

I know, I could use removePause, but that would take too much effort as I would need to place that back on the onComplete event of the timeline also I need that callback. :)

 

As a temporary solution I get the tween created with addPause and I play with the data property:

// If I allow pause
tween['data'] = 'isPause';

// If I want to skip the pause
tween['data'] = '';

 

 

 

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...

  • Recently Browsing   0 members

    No registered users viewing this page.