@@ -2530,9 +2530,13 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
25302530
25312531 for ( var i = 0 ; i < frameList . length ; i ++ ) {
25322532 var computedFrame ;
2533+
25332534 if ( frameList [ i ] . name ) {
2535+ // If it's a named frame, compute it:
25342536 computedFrame = Plots . computeFrame ( gd , frameList [ i ] . name ) ;
25352537 } else {
2538+ // Otherwise we must have been given a simple object, so treat
2539+ // the input itself as the computed frame.
25362540 computedFrame = frameList [ i ] . frame ;
25372541 }
25382542
@@ -2544,32 +2548,50 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
25442548 } ;
25452549
25462550 if ( i === frameList . length - 1 ) {
2551+ // The last frame in this .animate call stores the promise resolve
2552+ // and reject callbacks. This is how we ensure that the animation
2553+ // loop (which may exist as a result of a *different* .animate call)
2554+ // still resolves or rejecdts this .animate call's promise. once it's
2555+ // complete.
25472556 nextFrame . onComplete = resolve ;
25482557 nextFrame . onInterrupt = reject ;
25492558 }
25502559
25512560 trans . _frameQueue . push ( nextFrame ) ;
25522561 }
25532562
2563+ // Set it as never having transitioned to a frame. This will cause the animation
2564+ // loop to immediately transition to the next frame (which, for immediate mode,
2565+ // is the first frame in the list since all others would have been discarded
2566+ // below)
25542567 if ( animationOpts . mode === 'immediate' ) {
25552568 trans . _lastFrameAt = - Infinity ;
25562569 }
25572570
2571+ // Only it's not already running, start a RAF loop. This could be avoided in the
2572+ // case that there's only one frame, but it significantly complicated the logic
2573+ // and only sped things up by about 5% or so for a lorenz attractor simulation.
2574+ // It would be a fine thing to implement, but the benefit of that optimization
2575+ // doesn't seem worth the extra complexity.
25582576 if ( ! trans . _animationRaf ) {
25592577 beginAnimationLoop ( ) ;
25602578 }
25612579 }
25622580
25632581 function stopAnimationLoop ( ) {
2582+ gd . emit ( 'plotly_animated' ) ;
2583+
2584+ // Be sure to unset also since it's how we know whether a loop is already running:
25642585 window . cancelAnimationFrame ( trans . _animationRaf ) ;
25652586 trans . _animationRaf = null ;
25662587 }
25672588
25682589 function nextFrame ( ) {
2569- if ( trans . _currentFrame ) {
2570- if ( trans . _currentFrame . onComplete ) {
2571- trans . _currentFrame . onComplete ( ) ;
2572- }
2590+ if ( trans . _currentFrame && trans . _currentFrame . onComplete ) {
2591+ // Execute the callback and unset it to ensure it doesn't
2592+ // accidentally get called twice
2593+ trans . _currentFrame . onComplete ( ) ;
2594+ trans . _currentFrame . onComplete = null ;
25732595 }
25742596
25752597 var newFrame = trans . _currentFrame = trans . _frameQueue . shift ( ) ;
@@ -2578,61 +2600,60 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
25782600 trans . _lastFrameAt = Date . now ( ) ;
25792601 trans . _timeToNext = newFrame . frameOpts . duration ;
25802602
2603+ // This is simply called and it's left to .transition to decide how to manage
2604+ // interrupting current transitions. That means we don't need to worry about
2605+ // how it resolves or what happens after this:
25812606 Plots . transition ( gd ,
25822607 newFrame . frame . data ,
25832608 newFrame . frame . layout ,
25842609 newFrame . frame . traces ,
25852610 newFrame . frameOpts ,
25862611 newFrame . transitionOpts
2587- ) . then ( function ( ) {
2588- if ( trans . _frameQueue . length === 0 ) {
2589- gd . emit ( 'plotly_animated' ) ;
2590- if ( trans . _currentFrame && trans . _currentFrame . onComplete ) {
2591- trans . _currentFrame . onComplete ( ) ;
2592- trans . _currentFrame = null ;
2593- }
2594- }
2595- } ) ;
2596- }
2597-
2598- if ( trans . _frameQueue . length === 0 ) {
2612+ ) ;
2613+ } else {
2614+ // If there are no more frames, then stop the RAF loop:
25992615 stopAnimationLoop ( ) ;
26002616 }
26012617 }
26022618
26032619 function beginAnimationLoop ( ) {
26042620 gd . emit ( 'plotly_animating' ) ;
26052621
2606- // If no timer is running, then set last frame = long ago:
2622+ // If no timer is running, then set last frame = long ago so that the next
2623+ // frame is immediately transitioned:
26072624 trans . _lastFrameAt = - Infinity ;
26082625 trans . _timeToNext = 0 ;
26092626 trans . _runningTransitions = 0 ;
26102627 trans . _currentFrame = null ;
26112628
26122629 var doFrame = function ( ) {
2613- // Check if we need to pop a frame:
2630+ // This *must* be requested before nextFrame since nextFrame may decide
2631+ // to cancel it if there's nothing more to animated:
2632+ trans . _animationRaf = window . requestAnimationFrame ( doFrame ) ;
2633+
2634+ // Check if we're ready for a new frame:
26142635 if ( Date . now ( ) - trans . _lastFrameAt > trans . _timeToNext ) {
26152636 nextFrame ( ) ;
26162637 }
2617-
2618- trans . _animationRaf = window . requestAnimationFrame ( doFrame ) ;
26192638 } ;
26202639
2621- return doFrame ( ) ;
2640+ doFrame ( ) ;
26222641 }
26232642
2624- var counter = 0 ;
2643+ // This is an animate-local counter that helps match up option input list
2644+ // items with the particular frame.
2645+ var configCounter = 0 ;
26252646 function setTransitionConfig ( frame ) {
26262647 if ( Array . isArray ( transitionOpts ) ) {
2627- if ( counter >= transitionOpts . length ) {
2628- frame . transitionOpts = transitionOpts [ counter ] ;
2648+ if ( configCounter >= transitionOpts . length ) {
2649+ frame . transitionOpts = transitionOpts [ configCounter ] ;
26292650 } else {
26302651 frame . transitionOpts = transitionOpts [ 0 ] ;
26312652 }
26322653 } else {
26332654 frame . transitionOpts = transitionOpts ;
26342655 }
2635- counter ++ ;
2656+ configCounter ++ ;
26362657 return frame ;
26372658 }
26382659
@@ -2683,13 +2704,17 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) {
26832704 }
26842705 }
26852706
2707+ // If the mode is either next or immediate, then all currently queued frames must
2708+ // be dumped and the corresponding .animate promises rejected.
26862709 if ( [ 'next' , 'immediate' ] . indexOf ( animationOpts . mode ) !== - 1 ) {
26872710 discardExistingFrames ( ) ;
26882711 }
26892712
26902713 if ( frameList . length > 0 ) {
26912714 queueFrames ( frameList ) ;
26922715 } else {
2716+ // This is the case where there were simply no frames. It's a little strange
2717+ // since there's not much to do:
26932718 gd . emit ( 'plotly_animated' ) ;
26942719 resolve ( ) ;
26952720 }
0 commit comments