/**
* @class Ext.fx.Animator
*
* This class is used to run keyframe based animations, which follows the CSS3 based animation
* structure. Keyframe animations differ from typical from/to animations in that they offer
* the ability to specify values at various points throughout the animation.
*
* ## Using Keyframes
*
* The {@link #keyframes} option is the most important part of specifying an animation when using
* this class. A key frame is a point in a particular animation. We represent this as a percentage
* of the total animation duration. At each key frame, we can specify the target values at that
* time. Note that you *must* specify the values at 0% and 100%, the start and ending values.
* There is also a {@link #keyframe} event that fires after each key frame is reached.
*
* ## Example
*
* In the example below, we modify the values of the element at each fifth throughout the animation.
*
* @example
* Ext.create('Ext.fx.Animator', {
* target: Ext.getBody().createChild({
* style: {
* width: '100px',
* height: '100px',
* 'background-color': 'red'
* }
* }),
* duration: 10000, // 10 seconds
* keyframes: {
* 0: {
* opacity: 1,
* backgroundColor: 'FF0000'
* },
* 20: {
* x: 30,
* opacity: 0.5
* },
* 40: {
* x: 130,
* backgroundColor: '0000FF'
* },
* 60: {
* y: 80,
* opacity: 0.3
* },
* 80: {
* width: 200,
* y: 200
* },
* 100: {
* opacity: 1,
* backgroundColor: '00FF00'
* }
* }
* });
*/
Ext.define('Ext.fx.Animator', {
mixins: {
observable: 'Ext.util.Observable'
},
requires: ['Ext.fx.Manager'],
/**
* @property {Boolean} isAnimator
* `true` in this class to identify an object as an instantiated Animator, or subclass thereof.
*/
isAnimator: true,
/**
* @cfg {Number} duration
* Time in milliseconds for the animation to last. Defaults to 250.
*/
duration: 250,
/**
* @cfg {Number} delay
* Time to delay before starting the animation. Defaults to 0.
*/
delay: 0,
// private used to track a delayed starting time
delayStart: 0,
/**
* @cfg {Boolean} dynamic
* Currently only for Component Animation: Only set a component's outer element size bypassing
* layouts. Set to true to do full layouts for every frame of the animation. Defaults to false.
*/
dynamic: false,
/**
* @cfg {String} easing
*
* This describes how the intermediate values used during a transition will be calculated.
* It allows for a transition to change speed over its duration.
*
* - backIn
* - backOut
* - bounceIn
* - bounceOut
* - ease
* - easeIn
* - easeOut
* - easeInOut
* - elasticIn
* - elasticOut
* - cubic-bezier(x1, y1, x2, y2)
*
* Note that cubic-bezier will create a custom easing curve following the CSS3
* [transition-timing-function][0] specification. The four values specify points P1 and P2
* of the curve as (x1, y1, x2, y2). All values must be in the range [0, 1] or the definition
* is invalid.
*
* [0]: http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag
*/
easing: 'ease',
/**
* Flag to determine if the animation has started
* @property running
* @type Boolean
*/
running: false,
/**
* Flag to determine if the animation is paused. Only set this to true if you need to
* keep the Anim instance around to be unpaused later; otherwise call {@link #end}.
* @property paused
* @type Boolean
*/
paused: false,
/**
* @private
*/
damper: 1,
/**
* @cfg {Number} iterations
* Number of times to execute the animation. Defaults to 1.
*/
iterations: 1,
/**
* Current iteration the animation is running.
* @property currentIteration
* @type Number
*/
currentIteration: 0,
/**
* Current keyframe step of the animation.
* @property keyframeStep
* @type Number
*/
keyframeStep: 0,
/**
* @private
*/
animKeyFramesRE: /^(from|to|\d+%?)$/,
/**
* @cfg {Ext.fx.target.Target} target
* The Ext.fx.target to apply the animation to. If not specified during initialization,
* this can be passed to the applyAnimator method to apply the same animation to many targets.
*/
/**
* @event beforeanimate
* Fires before the animation starts. A handler can return false to cancel the animation.
* @param {Ext.fx.Animator} this
*/
/**
* @event keyframe
* Fires at each keyframe.
* @param {Ext.fx.Animator} this
* @param {Number} keyframe step number
*/
/**
* @event afteranimate
* Fires when the animation is complete.
* @param {Ext.fx.Animator} this
* @param {Date} startTime
*/
/**
* @cfg {Object} keyframes
* Animation keyframes follow the CSS3 Animation configuration pattern. 'from' is always
* considered '0%' and 'to' is considered '100%'.<b>Every keyframe declaration must have
* a keyframe rule for 0% and 100%, possibly defined using "from" or "to"</b>.
* A keyframe declaration without these keyframe selectors is invalid and will not be available
* for animation. The keyframe declaration for a keyframe rule consists of properties and
* values. Properties that are unable to be animated are ignored in these rules,
* with the exception of 'easing' which can be changed at each keyframe. For example:
*
* keyframes : {
* '0%': {
* left: 100
* },
* '40%': {
* left: 150
* },
* '60%': {
* left: 75
* },
* '100%': {
* left: 100
* }
* }
*/
constructor: function(config) {
var me = this;
config = Ext.apply(me, config || {});
me.config = config;
me.id = Ext.id(null, 'ext-animator-');
me.mixins.observable.constructor.call(me, config);
me.timeline = [];
me.createTimeline(me.keyframes);
if (me.target) {
me.applyAnimator(me.target);
Ext.fx.Manager.addAnim(me);
}
},
/**
* @private
*/
sorter: function(a, b) {
return a.pct - b.pct;
},
/**
* @private
* Takes the given keyframe configuration object and converts it into an ordered array
* with the passed attributes per keyframe or applying the 'to' configuration to all keyframes.
* Also calculates the proper animation duration per keyframe.
*/
createTimeline: function(keyframes) {
var me = this,
attrs = [],
to = me.to || {},
duration = me.duration,
prevMs, ms, i, ln, pct, attr;
for (pct in keyframes) {
if (keyframes.hasOwnProperty(pct) && me.animKeyFramesRE.test(pct)) {
attr = { attrs: Ext.apply(keyframes[pct], to) };
// CSS3 spec allow for from/to to be specified.
if (pct === "from") {
pct = 0;
}
else if (pct === "to") {
pct = 100;
}
// convert % values into integers
attr.pct = parseInt(pct, 10);
attrs.push(attr);
}
}
// Sort by pct property
Ext.Array.sort(attrs, me.sorter);
ln = attrs.length;
for (i = 0; i < ln; i++) {
prevMs = (attrs[i - 1]) ? duration * (attrs[i - 1].pct / 100) : 0;
ms = duration * (attrs[i].pct / 100);
me.timeline.push({
duration: ms - prevMs,
attrs: attrs[i].attrs
});
}
},
/**
* Applies animation to the Ext.fx.target
* @param {String/Object} target
* @private
*/
applyAnimator: function(target) {
var me = this,
anims = [],
timeline = me.timeline,
ln = timeline.length,
anim, easing, damper, attrs, i;
if (me.fireEvent('beforeanimate', me) !== false) {
for (i = 0; i < ln; i++) {
anim = timeline[i];
attrs = anim.attrs;
easing = attrs.easing || me.easing;
damper = attrs.damper || me.damper;
delete attrs.easing;
delete attrs.damper;
anim = new Ext.fx.Anim({
target: target,
easing: easing,
damper: damper,
duration: anim.duration,
paused: true,
to: attrs
});
anims.push(anim);
}
me.animations = anims;
me.target = anim.target;
for (i = 0; i < ln - 1; i++) {
anim = anims[i];
anim.nextAnim = anims[i + 1];
anim.on('afteranimate', function() {
this.nextAnim.paused = false;
});
anim.on('afteranimate', function() {
this.fireEvent('keyframe', this, ++this.keyframeStep);
}, me);
}
anims[ln - 1].on('afteranimate', function() {
this.lastFrame();
}, me);
}
},
/**
* @private
* Fires beforeanimate and sets the running flag.
*/
start: function(startTime) {
var me = this,
delay = me.delay,
delayStart = me.delayStart,
delayDelta;
if (delay) {
if (!delayStart) {
me.delayStart = startTime;
return;
}
else {
delayDelta = startTime - delayStart;
if (delayDelta < delay) {
return;
}
else {
// Compensate for frame delay;
startTime = new Date(delayStart.getTime() + delay);
}
}
}
if (me.fireEvent('beforeanimate', me) !== false) {
me.startTime = startTime;
me.running = true;
me.animations[me.keyframeStep].paused = false;
}
},
/**
* @private
* Perform lastFrame cleanup and handle iterations
* @return a hash of the new attributes.
*/
lastFrame: function() {
var me = this,
iter = me.iterations,
iterCount = me.currentIteration;
iterCount++;
if (iterCount < iter) {
me.startTime = new Date();
me.currentIteration = iterCount;
me.keyframeStep = 0;
me.applyAnimator(me.target);
me.animations[me.keyframeStep].paused = false;
}
else {
me.currentIteration = 0;
me.end();
}
},
/**
* Fire afteranimate event and end the animation. Usually called automatically when the
* animation reaches its final frame, but can also be called manually to pre-emptively
* stop and destroy the running animation.
*/
end: function() {
var me = this;
me.fireEvent('afteranimate', me, me.startTime, new Date() - me.startTime);
},
isReady: function() {
return this.paused === false && this.running === false && this.iterations > 0;
},
isRunning: function() {
// Explicitly return false, we don't want to be run continuously by the manager
return false;
}
});