Animatable.js 7.51 KB
import Animator from '../animation/Animator';
import log from '../core/log';
import {
    isString,
    isFunction,
    isObject,
    isArrayLike,
    indexOf
} from '../core/util';

/**
 * @alias modue:zrender/mixin/Animatable
 * @constructor
 */
var Animatable = function () {

    /**
     * @type {Array.<module:zrender/animation/Animator>}
     * @readOnly
     */
    this.animators = [];
};

Animatable.prototype = {

    constructor: Animatable,

    /**
     * 动画
     *
     * @param {string} path The path to fetch value from object, like 'a.b.c'.
     * @param {boolean} [loop] Whether to loop animation.
     * @return {module:zrender/animation/Animator}
     * @example:
     *     el.animate('style', false)
     *         .when(1000, {x: 10} )
     *         .done(function(){ // Animation done })
     *         .start()
     */
    animate: function (path, loop) {
        var target;
        var animatingShape = false;
        var el = this;
        var zr = this.__zr;
        if (path) {
            var pathSplitted = path.split('.');
            var prop = el;
            // If animating shape
            animatingShape = pathSplitted[0] === 'shape';
            for (var i = 0, l = pathSplitted.length; i < l; i++) {
                if (!prop) {
                    continue;
                }
                prop = prop[pathSplitted[i]];
            }
            if (prop) {
                target = prop;
            }
        }
        else {
            target = el;
        }

        if (!target) {
            log(
                'Property "'
                + path
                + '" is not existed in element '
                + el.id
            );
            return;
        }

        var animators = el.animators;

        var animator = new Animator(target, loop);

        animator.during(function (target) {
            el.dirty(animatingShape);
        })
        .done(function () {
            // FIXME Animator will not be removed if use `Animator#stop` to stop animation
            animators.splice(indexOf(animators, animator), 1);
        });

        animators.push(animator);

        // If animate after added to the zrender
        if (zr) {
            zr.animation.addAnimator(animator);
        }

        return animator;
    },

    /**
     * 停止动画
     * @param {boolean} forwardToLast If move to last frame before stop
     */
    stopAnimation: function (forwardToLast) {
        var animators = this.animators;
        var len = animators.length;
        for (var i = 0; i < len; i++) {
            animators[i].stop(forwardToLast);
        }
        animators.length = 0;

        return this;
    },

    /**
     * Caution: this method will stop previous animation.
     * So do not use this method to one element twice before
     * animation starts, unless you know what you are doing.
     * @param {Object} target
     * @param {number} [time=500] Time in ms
     * @param {string} [easing='linear']
     * @param {number} [delay=0]
     * @param {Function} [callback]
     * @param {Function} [forceAnimate] Prevent stop animation and callback
     *        immediently when target values are the same as current values.
     *
     * @example
     *  // Animate position
     *  el.animateTo({
     *      position: [10, 10]
     *  }, function () { // done })
     *
     *  // Animate shape, style and position in 100ms, delayed 100ms, with cubicOut easing
     *  el.animateTo({
     *      shape: {
     *          width: 500
     *      },
     *      style: {
     *          fill: 'red'
     *      }
     *      position: [10, 10]
     *  }, 100, 100, 'cubicOut', function () { // done })
     */
        // TODO Return animation key
    animateTo: function (target, time, delay, easing, callback, forceAnimate) {
        // animateTo(target, time, easing, callback);
        if (isString(delay)) {
            callback = easing;
            easing = delay;
            delay = 0;
        }
        // animateTo(target, time, delay, callback);
        else if (isFunction(easing)) {
            callback = easing;
            easing = 'linear';
            delay = 0;
        }
        // animateTo(target, time, callback);
        else if (isFunction(delay)) {
            callback = delay;
            delay = 0;
        }
        // animateTo(target, callback)
        else if (isFunction(time)) {
            callback = time;
            time = 500;
        }
        // animateTo(target)
        else if (!time) {
            time = 500;
        }
        // Stop all previous animations
        this.stopAnimation();
        this._animateToShallow('', this, target, time, delay);

        // Animators may be removed immediately after start
        // if there is nothing to animate
        var animators = this.animators.slice();
        var count = animators.length;
        function done() {
            count--;
            if (!count) {
                callback && callback();
            }
        }

        // No animators. This should be checked before animators[i].start(),
        // because 'done' may be executed immediately if no need to animate.
        if (!count) {
            callback && callback();
        }
        // Start after all animators created
        // Incase any animator is done immediately when all animation properties are not changed
        for (var i = 0; i < animators.length; i++) {
            animators[i]
                .done(done)
                .start(easing, forceAnimate);
        }
    },

    /**
     * @private
     * @param {string} path=''
     * @param {Object} source=this
     * @param {Object} target
     * @param {number} [time=500]
     * @param {number} [delay=0]
     *
     * @example
     *  // Animate position
     *  el._animateToShallow({
     *      position: [10, 10]
     *  })
     *
     *  // Animate shape, style and position in 100ms, delayed 100ms
     *  el._animateToShallow({
     *      shape: {
     *          width: 500
     *      },
     *      style: {
     *          fill: 'red'
     *      }
     *      position: [10, 10]
     *  }, 100, 100)
     */
    _animateToShallow: function (path, source, target, time, delay) {
        var objShallow = {};
        var propertyCount = 0;
        for (var name in target) {
            if (!target.hasOwnProperty(name)) {
                continue;
            }

            if (source[name] != null) {
                if (isObject(target[name]) && !isArrayLike(target[name])) {
                    this._animateToShallow(
                        path ? path + '.' + name : name,
                        source[name],
                        target[name],
                        time,
                        delay
                    );
                }
                else {
                    objShallow[name] = target[name];
                    propertyCount++;
                }
            }
            else if (target[name] != null) {
                // Attr directly if not has property
                // FIXME, if some property not needed for element ?
                if (!path) {
                    this.attr(name, target[name]);
                }
                else {  // Shape or style
                    var props = {};
                    props[path] = {};
                    props[path][name] = target[name];
                    this.attr(props);
                }
            }
        }

        if (propertyCount > 0) {
            this.animate(path, false)
                .when(time == null ? 500 : time, objShallow)
                .delay(delay || 0);
        }

        return this;
    }
};

export default Animatable;