# Yahoo! UI Library

### anim 3.0.0b1

Yahoo! UI Library > anim > anim.js (source view)
Search:
Filters Show Private Show Protected Show Deprecated

    /**
    * The Animation Utility provides an API for creating advanced transitions.
    * @module anim
    */

    /**
    * Provides the base Anim class, for animating numeric properties.
    *
    * @module anim
    * @submodule anim-base
    */

        /**
         * A class for constructing animation instances.
         * @class Anim
         * @for Anim
         * @constructor
         * @extends Base
         */

        var RUNNING = 'running',
            START_TIME = 'startTime',
            ELAPSED_TIME = 'elapsedTime',
            /**
            * @for Anim
            * @event start
            * @description fires when an animation begins.
            * @param {Event} ev The start event.
            * @type Event.Custom
            */
            START = 'start',

            /**
            * @event tween
            * @description fires every frame of the animation.
            * @param {Event} ev The tween event.
            * @type Event.Custom
            */
            TWEEN = 'tween',

            /**
            * @event end
            * @description fires after the animation completes.
            * @param {Event} ev The end event.
            * @type Event.Custom
            */
            END = 'end',
            NODE = 'node',
            PAUSED = 'paused',
            REVERSE = 'reverse', // TODO: cleanup
            ITERATION_COUNT = 'iterationCount',

            NUM = Number;

        var _running = {},
            _instances = {},
            _timer;

        Y.Anim = function() {
            Y.Anim.superclass.constructor.apply(this, arguments);
            _instances[Y.stamp(this)] = this;
        };

        Y.Anim.NAME = 'anim';

        /**
         * Regex of properties that should use the default unit.
         *
         * @property RE_DEFAULT_UNIT
         * @static
         */
        Y.Anim.RE_DEFAULT_UNIT = /^width|height|top|right|bottom|left|margin.*|padding.*|border.*$/i;

        /**
         * The default unit to use with properties that pass the RE_DEFAULT_UNIT test.
         *
         * @property DEFAULT_UNIT
         * @static
         */
        Y.Anim.DEFAULT_UNIT = 'px';

        Y.Anim.DEFAULT_EASING = function (t, b, c, d) {
            return c * t / d + b; // linear easing
        };

        /**
         * Bucket for custom getters and setters
         *
         * @property behaviors
         * @static
         */
        Y.Anim.behaviors = {
            left: {
                get: function(anim, attr) {
                    return anim._getOffset(attr);
                }
            }
        };

        Y.Anim.behaviors.top = Y.Anim.behaviors.left;

        /**
         * The default setter to use when setting object properties.
         *
         * @property DEFAULT_SETTER
         * @static
         */
        Y.Anim.DEFAULT_SETTER = function(anim, att, from, to, elapsed, duration, fn, unit) {
            unit = unit || '';
            anim._node.setStyle(att, fn(elapsed, NUM(from), NUM(to) - NUM(from), duration) + unit);
        };

        /**
         * The default getter to use when getting object properties.
         *
         * @property DEFAULT_GETTER
         * @static
         */
        Y.Anim.DEFAULT_GETTER = function(anim, prop) {
            return anim._node.getComputedStyle(prop);
        };

        Y.Anim.ATTRS = {
            /**
             * The object to be animated.
             * @attribute node
             * @type Node
             */
            node: {
                setter: function(node) {
                    node = Y.get(node);
                    this._node = node;
                    if (!node) {
                        Y.log(node + ' is not a valid node', 'warn', 'Anim');
                    }
                    return node;
                }
            },

            /**
             * The length of the animation.  Defaults to "1" (second).
             * @attribute duration
             * @type NUM
             */
            duration: {
                value: 1
            },

            /**
             * The method that will provide values to the attribute(s) during the animation.
             * Defaults to "Easing.easeNone".
             * @attribute easing
             * @type Function
             */
            easing: {
                value: Y.Anim.DEFAULT_EASING,

                setter: function(val) {
                    if (typeof val === 'string' && Y.Easing) {
                        return Y.Easing[val];
                    }
                }
            },

            /**
             * The starting values for the animated properties.
             * Fields may be strings, numbers, or functions.
             * If a function is used, the return value becomes the from value.
             * If no from value is specified, the DEFAULT_GETTER will be used.
             * @attribute from
             * @type Object
             */
            from: {},

            /**
             * The ending values for the animated properties.
             * Fields may be strings, numbers, or functions.
             * @attribute to
             * @type Object
             */
            to: {},

            /**
             * Date stamp for the first frame of the animation.
             * @attribute startTime
             * @type Int
             * @default 0
             * @readOnly
             */
            startTime: {
                value: 0,
                readOnly: true
            },

            /**
             * Current time the animation has been running.
             * @attribute elapsedTime
             * @type Int
             * @default 0
             * @readOnly
             */
            elapsedTime: {
                value: 0,
                readOnly: true
            },

            /**
             * Whether or not the animation is currently running.
             * @attribute running
             * @type Boolean
             * @default false
             * @readOnly
             */
            running: {
                getter: function() {
                    return !!_running[Y.stamp(this)];
                },
                value: false,
                readOnly: true
            },

            /**
             * The number of times the animation should run
             * @attribute iterations
             * @type Int
             * @default 1
             */
            iterations: {
                value: 1
            },

            /**
             * The number of iterations that have occurred.
             * Resets when an animation ends (reaches iteration count or stop() called).
             * @attribute iterationCount
             * @type Int
             * @default 0
             * @readOnly
             */
            iterationCount: {
                value: 0,
                readOnly: true
            },

            /**
             * How iterations of the animation should behave.
             * Possible values are "normal" and "alternate".
             * Normal will repeat the animation, alternate will reverse on every other pass.
             *
             * @attribute direction
             * @type String
             * @default "normal"
             */
            direction: {
                value: 'normal' // | alternate (fwd on odd, rev on even per spec)
            },

            /**
             * Whether or not the animation is currently paused.
             * @attribute running
             * @type Boolean
             * @default false
             * @readOnly
             */
            paused: {
                readOnly: true,
                value: false
            },

            /**
             * If true, animation begins from last frame
             * @attribute reverse
             * @type Boolean
             * @default false
             */
            reverse: {
                value: false
            }

        };

        /**
         * Runs all animation instances.
         * @method run
         * @static
         */
        Y.Anim.run = function() {
            for (var i in _instances) {
                if (_instances[i].run) {
                    _instances[i].run();
                }
            }
        };

        /**
         * Pauses all animation instances.
         * @method pause
         * @static
         */
        Y.Anim.pause = function() {
            for (var i in _running) { // stop timer if nothing running
                if (_running[i].pause) {
                    _running[i].pause();
                }
            }
            Y.Anim._stopTimer();
        };

        /**
         * Stops all animation instances.
         * @method stop
         * @static
         */
        Y.Anim.stop = function() {
            for (var i in _running) { // stop timer if nothing running
                if (_running[i].stop) {
                    _running[i].stop();
                }
            }
            Y.Anim._stopTimer();
        };

        Y.Anim._startTimer = function() {
            if (!_timer) {
                _timer = setInterval(Y.Anim._runFrame, 1);
            }
        };

        Y.Anim._stopTimer = function() {
            clearInterval(_timer);
            _timer = 0;
        };

        /**
         * Called per Interval to handle each animation frame.
         * @method _runFrame
         * @private
         * @static
         */
        Y.Anim._runFrame = function() {
            var done = true;
            for (var anim in _running) {
                if (_running[anim]._runFrame) {
                    done = false;
                    _running[anim]._runFrame();
                }
            }

            if (done) {
                Y.Anim._stopTimer();
            }
        };

        Y.Anim.RE_UNITS = /^(-?\d*\.?\d*){1}(em|ex|px|in|cm|mm|pt|pc|%)*$/;

        var proto = {
            /**
             * Starts or resumes an animation.
             * percent start time marker.
             * @method run
             * @chainable
             */
            run: function() {
                if (!this.get(RUNNING)) {
                    this._start();
                } else if (this.get(PAUSED)) {
                    this._resume();
                }
                return this;
            },

            /**
             * Pauses the animation and
             * freezes it in its current state and time.
             * Calling run() will continue where it left off.
             * @method pause
             * @chainable
             */
            pause: function() {
                if (this.get(RUNNING)) {
                    this._pause();
                }
                return this;
            },

            /**
             * Stops the animation and resets its time.
             * @method stop
             * @chainable
             */
            stop: function(finish) {
                if (this.get(RUNNING) || this.get(PAUSED)) {
                    this._end(finish);
                }
                return this;
            },

            _added: false,

            _start: function() {
                this._set(START_TIME, new Date() - this.get(ELAPSED_TIME));
                this._actualFrames = 0;
                if (!this.get(PAUSED)) {
                    this._initAnimAttr();
                }
                _running[Y.stamp(this)] = this;
                Y.Anim._startTimer();

                this.fire(START);
            },

            _pause: function() {
                this._set(START_TIME, null);
                this._set(PAUSED, true);
                delete _running[Y.stamp(this)];

                /**
                * @event pause
                * @description fires when an animation is paused.
                * @param {Event} ev The pause event.
                * @type Event.Custom
                */
                this.fire('pause');
            },

            _resume: function() {
                this._set(PAUSED, false);
                _running[Y.stamp(this)] = this;

                /**
                * @event resume
                * @description fires when an animation is resumed (run from pause).
                * @param {Event} ev The pause event.
                * @type Event.Custom
                */
                this.fire('resume');
            },

            _end: function(finish) {
                this._set(START_TIME, null);
                this._set(ELAPSED_TIME, 0);
                this._set(PAUSED, false);

                delete _running[Y.stamp(this)];
                this.fire(END, {elapsed: this.get(ELAPSED_TIME)});
            },

            _runFrame: function() {
                var attr = this._runtimeAttr,
                    customAttr = Y.Anim.behaviors,
                    easing = attr.easing,
                    d = attr.duration,
                    t = new Date() - this.get(START_TIME),
                    reversed = this.get(REVERSE),
                    done = (t >= d),
                    lastFrame = d,
                    attribute,
                    setter;

                if (reversed) {
                    t = d - t;
                    done = (t <= 0);
                    lastFrame = 0;
                }

                for (var i in attr) {
                    if (attr[i].to) {
                        attribute = attr[i];
                        setter = (i in customAttr && 'set' in customAttr[i]) ?
                                customAttr[i].set : Y.Anim.DEFAULT_SETTER;

                        if (!done) {
                            setter(this, i, attribute.from, attribute.to, t, d, easing, attribute.unit);
                        } else { // ensure final frame value is set
                           // TODO: handle keyframes
                            setter(this, i, attribute.from, attribute.to, lastFrame, d, easing, attribute.unit);
                        }
                    }
                }

                this._actualFrames += 1;
                this._set(ELAPSED_TIME, t);

                this.fire(TWEEN);
                if (done) {
                    this._lastFrame();
                }
            },

            _lastFrame: function() {
                var iter = this.get('iterations'),
                    iterCount = this.get(ITERATION_COUNT);

                iterCount += 1;
                if (iter === 'infinite' || iterCount < iter) {
                    if (this.get('direction') === 'alternate') {
                        this.set(REVERSE, !this.get(REVERSE)); // flip it
                    }
                    /**
                    * @event iteration
                    * @description fires when an animation begins an iteration.
                    * @param {Event} ev The iteration event.
                    * @type Event.Custom
                    */
                    this.fire('iteration');
                } else {
                    iterCount = 0;
                    this._end();
                }

                this._set(START_TIME, new Date());
                this._set(ITERATION_COUNT, iterCount);
            },

            _initAnimAttr: function() {
                var from = this.get('from') || {},
                    to = this.get('to') || {},
                    dur = this.get('duration') * 1000,
                    node = this.get(NODE),
                    easing = this.get('easing') || {},
                    attr = {},
                    customAttr = Y.Anim.behaviors,
                    unit, begin, end;

                Y.each(to, function(val, name) {
                    if (typeof val === 'function') {
                        val = val.call(this, node);
                    }

                    begin = from[name];
                    if (begin === undefined) {
                        begin = (name in customAttr && 'get' in customAttr[name])  ?
                                customAttr[name].get(this, name) : Y.Anim.DEFAULT_GETTER(this, name);
                    } else if (typeof begin === 'function') {
                        begin = begin.call(this, node);
                    }

                    var mFrom = Y.Anim.RE_UNITS.exec(begin);
                    var mTo = Y.Anim.RE_UNITS.exec(val);

                    begin = mFrom ? mFrom[1] : begin;
                    end = mTo ? mTo[1] : val;
                    unit = mTo ? mTo[2] : mFrom ?  mFrom[2] : ''; // one might be zero TODO: mixed units

                    if (!unit && Y.Anim.RE_DEFAULT_UNIT.test(name)) {
                        unit = Y.Anim.DEFAULT_UNIT;
                    }

                    if (!begin || !end) {
                        Y.error('invalid "from" or "to" for "' + name + '"', 'Anim');
                        return;
                    }

                    attr[name] = {
                        from: begin,
                        to: end,
                        unit: unit
                    };

                    attr.duration = dur;
                    attr.easing = easing;

                }, this);

                this._runtimeAttr = attr;
            },

            // TODO: move to computedStyle? (browsers dont agree on default computed offsets)
            _getOffset: function(attr) {
                var node = this._node,
                    val = node.getComputedStyle(attr),
                    get = (attr === 'left') ? 'getX': 'getY',
                    set = (attr === 'left') ? 'setX': 'setY';

                if (val === 'auto') {
                    var position = node.getStyle('position');
                    if (position === 'absolute' || position === 'fixed') {
                        val = node[get]();
                        node[set](val);
                    } else {
                        val = 0;
                    }
                }

                return val;
            }
        };

        Y.extend(Y.Anim, Y.Base, proto);

Copyright © 2009 Yahoo! Inc. All rights reserved.