    /**
     * @class Ext.fx.Animator
     * Animation instance

    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 Usage__
    In the example below, we modify the values of the element at each fifth throughout the animation.

        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'
                }
            }
        });

     * @markdown
     */
    Ext.define('Ext.fx.Animator', {

        /* Begin Definitions */

        mixins: {
            observable: 'Ext.util.Observable'
        },

        requires: ['Ext.fx.Manager'],

        /* End Definitions */

        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

         * @markdown
         */
        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 int
         */
        currentIteration: 0,

        /**
         * Current keyframe step of the animation.
         * @property keyframeStep
         * @type Number
         */
        keyframeStep: 0,

        /**
         * @private
         */
        animKeyFramesRE: /^(from|to|\d+%?)$/,

        /**
         * @cfg {Ext.fx.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.
         */

         /**
          * @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:
     <pre><code>
    keyframes : {
        '0%': {
            left: 100
        },
        '40%': {
            left: 150
        },
        '60%': {
            left: 75
        },
        '100%': {
            left: 100
        }
    }
     </code></pre>
          */
        constructor: function(config) {
            var me = this;
            config = Ext.apply(me, config || {});
            me.config = config;
            me.id = Ext.id(null, 'ext-animator-');
            me.addEvents(
                /**
                 * @event beforeanimate
                 * Fires before the animation starts. A handler can return false to cancel the animation.
                 * @param {Ext.fx.Animator} this
                 */
                'beforeanimate',
                /**
                  * @event keyframe
                  * Fires at each keyframe.
                  * @param {Ext.fx.Animator} this
                  * @param {Number} keyframe step number
                  */
                'keyframe',
                /**
                 * @event afteranimate
                 * Fires when the animation is complete.
                 * @param {Ext.fx.Animator} this
                 * @param {Date} startTime
                 */
                'afteranimate'
            );
            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, anim, nextAnim, 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);
            // Only an end
            //if (attrs[0].pct) {
            //    attrs.unshift({pct: 0, attrs: element.attrs});
            //}

            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
         * @private
         * @param target
         * @type string/object
         */
        applyAnimator: function(target) {
            var me = this,
                anims = [],
                timeline = me.timeline,
                reverse = me.reverse,
                ln = timeline.length,
                anim, easing, damper, initial, attrs, lastAttrs, 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 = Ext.create('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
         * @returns 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);
        }
    });