/*global window: false */ "use strict"; module.exports = function(Chart) { var helpers = Chart.helpers; Chart.defaults.global.animation = { duration: 1000, easing: "easeOutQuart", onProgress: helpers.noop, onComplete: helpers.noop }; Chart.Animation = Chart.Element.extend({ currentStep: null, // the current animation step numSteps: 60, // default number of steps easing: "", // the easing to use for this animation render: null, // render function used by the animation service onAnimationProgress: null, // user specified callback to fire on each step of the animation onAnimationComplete: null // user specified callback to fire when the animation finishes }); Chart.animationService = { frameDuration: 17, animations: [], dropFrames: 0, addAnimation: function(chartInstance, animationObject, duration, lazy) { if (!lazy) { chartInstance.animating = true; } for (var index = 0; index < this.animations.length; ++index) { if (this.animations[index].chartInstance === chartInstance) { // replacing an in progress animation this.animations[index].animationObject = animationObject; return; } } this.animations.push({ chartInstance: chartInstance, animationObject: animationObject }); // If there are no animations queued, manually kickstart a digest, for lack of a better word if (this.animations.length === 1) { helpers.requestAnimFrame.call(window, this.digestWrapper); } }, // Cancel the animation for a given chart instance cancelAnimation: function(chartInstance) { var index = helpers.findNextWhere(this.animations, function(animationWrapper) { return animationWrapper.chartInstance === chartInstance; }); if (index) { this.animations.splice(index, 1); chartInstance.animating = false; } }, // calls startDigest with the proper context digestWrapper: function() { Chart.animationService.startDigest.call(Chart.animationService); }, startDigest: function() { var startTime = Date.now(); var framesToDrop = 0; if (this.dropFrames > 1) { framesToDrop = Math.floor(this.dropFrames); this.dropFrames = this.dropFrames % 1; } for (var i = 0; i < this.animations.length; i++) { if (this.animations[i].animationObject.currentStep === null) { this.animations[i].animationObject.currentStep = 0; } this.animations[i].animationObject.currentStep += 1 + framesToDrop; if (this.animations[i].animationObject.currentStep > this.animations[i].animationObject.numSteps) { this.animations[i].animationObject.currentStep = this.animations[i].animationObject.numSteps; } this.animations[i].animationObject.render(this.animations[i].chartInstance, this.animations[i].animationObject); if (this.animations[i].animationObject.onAnimationProgress && this.animations[i].animationObject.onAnimationProgress.call) { this.animations[i].animationObject.onAnimationProgress.call(this.animations[i].chartInstance, this.animations[i]); } if (this.animations[i].animationObject.currentStep === this.animations[i].animationObject.numSteps) { if (this.animations[i].animationObject.onAnimationComplete && this.animations[i].animationObject.onAnimationComplete.call) { this.animations[i].animationObject.onAnimationComplete.call(this.animations[i].chartInstance, this.animations[i]); } // executed the last frame. Remove the animation. this.animations[i].chartInstance.animating = false; this.animations.splice(i, 1); // Keep the index in place to offset the splice i--; } } var endTime = Date.now(); var dropFrames = (endTime - startTime) / this.frameDuration; this.dropFrames += dropFrames; // Do we have more stuff to animate? if (this.animations.length > 0) { helpers.requestAnimFrame.call(window, this.digestWrapper); } } }; };