From 84769d1542a69d7214ecbb09f08141d4cbdcd54a Mon Sep 17 00:00:00 2001 From: Ricky Reusser Date: Wed, 26 Oct 2016 17:58:34 -0400 Subject: [PATCH] Animation direction and fromcurrent --- src/plot_api/plot_api.js | 27 +++++++ src/plots/animation_attributes.js | 18 +++++ src/plots/plots.js | 2 + test/jasmine/tests/animate_test.js | 111 ++++++++++++++++++++++++++++- 4 files changed, 157 insertions(+), 1 deletion(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index c576d1c46ba..ae088ee58ff 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2431,6 +2431,33 @@ Plotly.animate = function(gd, frameOrGroupNameOrFrameList, animationOpts) { discardExistingFrames(); } + if(animationOpts.direction === 'reverse') { + frameList.reverse(); + } + + var currentFrame = gd._fullLayout._currentFrame; + if(currentFrame && animationOpts.fromcurrent) { + var idx = -1; + for(i = 0; i < frameList.length; i++) { + frame = frameList[i]; + if(frame.type === 'byname' && frame.name === currentFrame) { + idx = i; + break; + } + } + + if(idx > 0 && idx < frameList.length - 1) { + var filteredFrameList = []; + for(i = 0; i < frameList.length; i++) { + frame = frameList[i]; + if(frameList[i].type !== 'byname' || i > idx) { + filteredFrameList.push(frame); + } + } + frameList = filteredFrameList; + } + } + if(frameList.length > 0) { queueFrames(frameList); } else { diff --git a/src/plots/animation_attributes.js b/src/plots/animation_attributes.js index a1999b4ba51..b6cd0dc8d95 100644 --- a/src/plots/animation_attributes.js +++ b/src/plots/animation_attributes.js @@ -12,6 +12,7 @@ module.exports = { mode: { valType: 'enumerated', dflt: 'afterall', + role: 'info', values: ['immediate', 'next', 'afterall'], description: [ 'Describes how a new animate call interacts with currently-running', @@ -22,6 +23,23 @@ module.exports = { 'is started.' ].join(' ') }, + direction: { + valType: 'enumerated', + role: 'info', + values: ['forward', 'reverse'], + dflt: 'forward', + description: [ + 'The direction in which to play the frames triggered by the animation call' + ].join(' ') + }, + fromcurrent: { + valType: 'boolean', + dflt: false, + role: 'info', + description: [ + 'Play frames starting at the current frame instead of the beginning.' + ].join(' ') + }, frame: { duration: { valType: 'number', diff --git a/src/plots/plots.js b/src/plots/plots.js index 33e499cc443..8655a4cfd67 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -668,6 +668,8 @@ plots.supplyAnimationDefaults = function(opts) { } coerce('mode'); + coerce('direction'); + coerce('fromcurrent'); if(Array.isArray(opts.frame)) { optsOut.frame = []; diff --git a/test/jasmine/tests/animate_test.js b/test/jasmine/tests/animate_test.js index 092cb31ca07..f7bf5050893 100644 --- a/test/jasmine/tests/animate_test.js +++ b/test/jasmine/tests/animate_test.js @@ -14,7 +14,9 @@ describe('Plots.supplyAnimationDefaults', function() { it('supplies transition defaults', function() { expect(Plots.supplyAnimationDefaults({})).toEqual({ + fromcurrent: false, mode: 'afterall', + direction: 'forward', transition: { duration: 500, easing: 'cubic-in-out' @@ -29,6 +31,8 @@ describe('Plots.supplyAnimationDefaults', function() { it('uses provided values', function() { expect(Plots.supplyAnimationDefaults({ mode: 'next', + fromcurrent: true, + direction: 'reverse', transition: { duration: 600, easing: 'elastic-in-out' @@ -39,6 +43,8 @@ describe('Plots.supplyAnimationDefaults', function() { } })).toEqual({ mode: 'next', + fromcurrent: true, + direction: 'reverse', transition: { duration: 600, easing: 'elastic-in-out' @@ -63,7 +69,12 @@ describe('Test animate API', function() { function verifyFrameTransitionOrder(gd, expectedFrames) { var calls = Plots.transition.calls; - expect(calls.count()).toEqual(expectedFrames.length); + var c1 = calls.count(); + var c2 = expectedFrames.length; + expect(c1).toEqual(c2); + + // Prevent lots of ugly logging when it's already failed: + if(c1 !== c2) return; for(var i = 0; i < calls.count(); i++) { expect(calls.argsFor(i)[1]).toEqual( @@ -315,6 +326,104 @@ describe('Test animate API', function() { }); } + describe('Animation direction', function() { + var animOpts; + + beforeEach(function() { + animOpts = { + frame: {duration: 0}, + transition: {duration: 0} + }; + }); + + it('animates frames by name in reverse', function(done) { + animOpts.direction = 'reverse'; + + Plotly.animate(gd, ['frame0', 'frame2', 'frame1', 'frame3'], animOpts).then(function() { + verifyFrameTransitionOrder(gd, ['frame3', 'frame1', 'frame2', 'frame0']); + verifyQueueEmpty(gd); + }).catch(fail).then(done); + }); + + it('animates a group in reverse', function(done) { + animOpts.direction = 'reverse'; + Plotly.animate(gd, 'even-frames', animOpts).then(function() { + verifyFrameTransitionOrder(gd, ['frame2', 'frame0']); + verifyQueueEmpty(gd); + }).catch(fail).then(done); + }); + }); + + describe('Animation fromcurrent', function() { + var animOpts; + + beforeEach(function() { + animOpts = { + frame: {duration: 0}, + transition: {duration: 0}, + fromcurrent: true + }; + }); + + it('animates starting at the current frame', function(done) { + Plotly.animate(gd, ['frame1'], animOpts).then(function() { + verifyFrameTransitionOrder(gd, ['frame1']); + verifyQueueEmpty(gd); + + return Plotly.animate(gd, null, animOpts); + }).then(function() { + verifyFrameTransitionOrder(gd, ['frame1', 'frame2', 'frame3']); + verifyQueueEmpty(gd); + }).catch(fail).then(done); + }); + + it('plays from the start when current frame = last frame', function(done) { + Plotly.animate(gd, null, animOpts).then(function() { + verifyFrameTransitionOrder(gd, ['base', 'frame0', 'frame1', 'frame2', 'frame3']); + verifyQueueEmpty(gd); + + return Plotly.animate(gd, null, animOpts); + }).then(function() { + verifyFrameTransitionOrder(gd, [ + 'base', 'frame0', 'frame1', 'frame2', 'frame3', + 'base', 'frame0', 'frame1', 'frame2', 'frame3' + ]); + + verifyQueueEmpty(gd); + }).catch(fail).then(done); + }); + + it('animates in reverse starting at the current frame', function(done) { + animOpts.direction = 'reverse'; + + Plotly.animate(gd, ['frame1'], animOpts).then(function() { + verifyFrameTransitionOrder(gd, ['frame1']); + verifyQueueEmpty(gd); + return Plotly.animate(gd, null, animOpts); + }).then(function() { + verifyFrameTransitionOrder(gd, ['frame1', 'frame0', 'base']); + verifyQueueEmpty(gd); + }).catch(fail).then(done); + }); + + it('plays in reverse from the end when current frame = first frame', function(done) { + animOpts.direction = 'reverse'; + + Plotly.animate(gd, ['base'], animOpts).then(function() { + verifyFrameTransitionOrder(gd, ['base']); + verifyQueueEmpty(gd); + + return Plotly.animate(gd, null, animOpts); + }).then(function() { + verifyFrameTransitionOrder(gd, [ + 'base', 'frame3', 'frame2', 'frame1', 'frame0', 'base' + ]); + + verifyQueueEmpty(gd); + }).catch(fail).then(done); + }); + }); + // The tests above use promises to ensure ordering, but the tests below this call Plotly.animate // without chaining promises which would result in race conditions. This is not invalid behavior, // but it doesn't ensure proper ordering and completion, so these must be performed with finite