原生javascript封装动画库

****转载自自己发表于牛人部落专栏的文章****

一、前言

本文记录了自己利用原生javascript构建自己的动画库的过程,在不断改进的过程中,实现以下动画效果:

针对同一个dom元素上相继发生的动画,针对以下功能,尝试实现方案,(从一个元素向多个元素的拓展并不难,这里不做深入探究):

功能1.知道动画A和动画B的发生顺序(如A先发生,B后发生),能够按照代码撰写顺序实现动画A结束时,动画B调用

功能2.在满足功能1的基础上更进一步,当不知道动画A和动画B的发生顺序(如点击按钮1触发动画A,点击按钮2触发动画B,哪个按钮先点击不确定),能够达到1)两个动画不产生并发干扰;2)可以根据按钮的先后点击顺序,一个动画结束后另一个动画运行,即实现动画序列,以及动画的链式调用。

整个代码实现的过程,是不断改进的过程,包括:

1.利用requestAnimationFrame替代setTimeout来实现动画的平滑效果。

关于requestAnimationFrame的更多资料可参考这篇博客:http://www.zhangxinxu.com/wordpress/2013/09/css3-animation-requestanimationframe-tween-%E5%8A%A8%E7%94%BB%E7%AE%97%E6%B3%95/

2.尝试引入promise

关于promise的介绍可以参考此系列博客:https://github.com/wangfupeng1988/js-async-tutorial

3.尝试引入队列控制

队列结合running标识符来避免并发干扰;

 

二、相关辅助代码

以下是动画库实现的相关辅助代码,动画库的实现依赖于一下js文件,必须优先于动画库引入:

1.tween.js 实现各种缓动效果,具体可参见博客:http://www.zhangxinxu.com/wordpress/2016/12/how-use-tween-js-animation-easing/

代码如下:

 /**
  *Tween 缓动相关
  */
 var tween = {
     Linear: function(t, b, c, d) {
         return c * t / d + b;
     },
     Quad: {
         easeIn: function(t, b, c, d) {
             return c * (t /= d) * t + b;
         },
         easeOut: function(t, b, c, d) {
             return -c * (t /= d) * (t - 2) + b;
         },
         easeInOut: function(t, b, c, d) {
             if ((t /= d / 2) < 1) return c / 2 * t * t + b;
             return -c / 2 * ((--t) * (t - 2) - 1) + b;
         }
     },
     Cubic: {
         easeIn: function(t, b, c, d) {
             return c * (t /= d) * t * t + b;
         },
         easeOut: function(t, b, c, d) {
             return c * ((t = t / d - 1) * t * t + 1) + b;
         },
         easeInOut: function(t, b, c, d) {
             if ((t /= d / 2) < 1) return c / 2 * t * t * t + b;
             return c / 2 * ((t -= 2) * t * t + 2) + b;
         }
     },
     Quart: {
         easeIn: function(t, b, c, d) {
             return c * (t /= d) * t * t * t + b;
         },
         easeOut: function(t, b, c, d) {
             return -c * ((t = t / d - 1) * t * t * t - 1) + b;
         },
         easeInOut: function(t, b, c, d) {
             if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;
             return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
         }
     },
     Quint: {
         easeIn: function(t, b, c, d) {
             return c * (t /= d) * t * t * t * t + b;
         },
         easeOut: function(t, b, c, d) {
             return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
         },
         easeInOut: function(t, b, c, d) {
             if ((t /= d / 2) < 1) return c / 2 * t * t * t * t * t + b;
             return c / 2 * ((t -= 2) * t * t * t * t + 2) + b;
         }
     },
     Sine: {
         easeIn: function(t, b, c, d) {
             return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;
         },
         easeOut: function(t, b, c, d) {
             return c * Math.sin(t / d * (Math.PI / 2)) + b;
         },
         easeInOut: function(t, b, c, d) {
             return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
         }
     },
     Expo: {
         easeIn: function(t, b, c, d) {
             return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
         },
         easeOut: function(t, b, c, d) {
             return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
         },
         easeInOut: function(t, b, c, d) {
             if (t == 0) return b;
             if (t == d) return b + c;
             if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
             return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
         }
     },
     Circ: {
         easeIn: function(t, b, c, d) {
             return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
         },
         easeOut: function(t, b, c, d) {
             return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
         },
         easeInOut: function(t, b, c, d) {
             if ((t /= d / 2) < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
             return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
         }
     },
     Elastic: {
         easeIn: function(t, b, c, d, a, p) {
             if (t == 0) return b;
             if ((t /= d) == 1) return b + c;
             if (!p) p = d * .3;
             if (!a || a < Math.abs(c)) {
                 a = c;
                 var s = p / 4;
             } else var s = p / (2 * Math.PI) * Math.asin(c / a);
             return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
         },
         easeOut: function(t, b, c, d, a, p) {
             if (t == 0) return b;
             if ((t /= d) == 1) return b + c;
             if (!p) p = d * .3;
             if (!a || a < Math.abs(c)) {
                 a = c;
                 var s = p / 4;
             } else var s = p / (2 * Math.PI) * Math.asin(c / a);
             return (a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b);
         },
         easeInOut: function(t, b, c, d, a, p) {
             if (t == 0) return b;
             if ((t /= d / 2) == 2) return b + c;
             if (!p) p = d * (.3 * 1.5);
             if (!a || a < Math.abs(c)) {
                 a = c;
                 var s = p / 4;
             } else var s = p / (2 * Math.PI) * Math.asin(c / a);
             if (t < 1) return -.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
             return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * .5 + c + b;
         }
     },
     Back: {
         easeIn: function(t, b, c, d, s) {
             if (s == undefined) s = 1.70158;
             return c * (t /= d) * t * ((s + 1) * t - s) + b;
         },
         easeOut: function(t, b, c, d, s) {
             if (s == undefined) s = 1.70158;
             return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
         },
         easeInOut: function(t, b, c, d, s) {
             if (s == undefined) s = 1.70158;
             if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;
             return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;
         }
     },
     Bounce: {
         easeIn: function(t, b, c, d) {
             return c - Tween.Bounce.easeOut(d - t, 0, c, d) + b;
         },
         easeOut: function(t, b, c, d) {
             if ((t /= d) < (1 / 2.75)) {
                 return c * (7.5625 * t * t) + b;
             } else if (t < (2 / 2.75)) {
                 return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b;
             } else if (t < (2.5 / 2.75)) {
                 return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b;
             } else {
                 return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b;
             }
         },
         easeInOut: function(t, b, c, d) {
             if (t < d / 2) return Tween.Bounce.easeIn(t * 2, 0, c, d) * .5 + b;
             else return Tween.Bounce.easeOut(t * 2 - d, 0, c, d) * .5 + c * .5 + b;
         }
     }
 };

2.辅助工具util.js,其中包括样式获取和设置的方法,以及requestAnimationFrame,cancelAnimationFrame,获取当前时间戳兼容的方法

//获取元素属性
//元素属性都按照整数计算
var getStyle = function(dom, prop) {
    if (prop === 'opacity' && dom.style.filter) {
        return window.style.filter.match(/(\d+)/)[1];
    }
    var tmp = window.getComputedStyle ? window.getComputedStyle(dom, null)[prop] : dom.currentStyle[prop];
    return prop === 'opacity' ? parseFloat(tmp, 10) : parseInt(tmp, 10);
};
//设置元素属性
var setStyle = function(dom, prop, value) {
    if (prop === 'opacity') {
        dom.style.filter = '(opacity(' + parseFloat(value / 100) + '))';
        dom.style.opacity = value;
        return;
    }
    dom.style[prop] = parseInt(value, 10) + 'px';
};

//requestAnimationFrame的兼容处理
(function() {
    var lastTime = 0;
    var vendors = ['webkit', 'moz'];
    for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||
            window[vendors[x] + 'CancelRequestAnimationFrame'];
    }

    if (!window.requestAnimationFrame) {
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
            var id = window.setTimeout(function() {
                callback(currTime + timeToCall);
            }, timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };
    }
    if (!window.cancelAnimationFrame) {
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
    }
}());

//时间戳获取的兼容处理
function nowtime() {
    if (typeof performance !== 'undefined' && performance.now) {
        return performance.now();
    }
    return Date.now ? Date.now() : (new Date()).getTime();
}

3.为了便于测试,布局html文件如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>测试动画库</title>
    <style>
    .mydiv {
        width: 300px;
        height: 200px;
        background-color: pink;
        position: absolute;
        top: 100px;
        left: 100px;
    }
    </style>
</head>

<body>
    <div class="mydiv" id="mydiv"></div>
</body>
</html>

 

三、动画库animation的具体实现

1.仅考虑实现功能1:即

知道动画A和动画B的发生顺序(如A先发生,B后发生),能够按照代码撰写顺序实现动画A结束时,动画B调用

方法一:利用动画结束时,执行回调的思路,代码如下:

//实现动画库(暂不使用promise)
var Animate = {
    init: function(el) {
        this.el = typeof el === 'string' ? document.querySelector(el) : el;
        this.timer = null;
        return this;
    },
    initAnim: function(props, option) {
        this.propChange = {};
        this.duration = (option && option.duration) || 1000;
        this.easing = (option && option.easing) || tween.Linear;
        for (var prop in props) {
            this.propChange[prop] = {};
            this.propChange[prop]['to'] = props[prop];
            this.propChange[prop]['from'] = getStyle(this.el, prop);
        }
        return this;
    },
    stop: function() {
        clearTimeout(this.timer);
        this.timer = null;
        return this;
    },
    play: function(callback) {
        var startTime = 0;
        var self = this;
        if (this.timer) {
            this.stop();
        }

        function step() {
            if (!startTime) {
                startTime = nowtime();
            }
            var passedTime = Math.min(nowtime() - startTime, self.duration);
            console.log('passedTime:' + passedTime + ',duration:' + self.duration);
            for (var prop in self.propChange) {
                var target = self.easing(passedTime, self.propChange[prop]['from'], self.propChange[prop]['to'] - self.propChange[prop]['from'], self.duration);
                setStyle(self.el, prop, target);
            }
            if (passedTime >= self.duration) {
                self.stop();
                if (callback) {
                    callback.call(self);
                }
            } else {
                this.timer = setTimeout(step, 1000 / 50);
            }
        }
        this.timer = setTimeout(step, 1000 / 50);
    },
    runAnim: function(props, option, callback) {
        this.initAnim(props, option);
        this.play(callback);
    }
};

调用代码如下:

    <script type="text/javascript">
    //测试animate.js
    //利用回调来实现顺序调用
    var div = document.getElementById('mydiv');
    var anim = Object.create(Animate);
    anim.init(div);
    anim.runAnim({
        width: 500
    }, {
        duration: 400
    }, function() {
        anim.runAnim({
            height: 500
        }, {
            duration: 400
        });
    });

经过测试,上述代码能够实现,长度变为500之后,高度再变为500.即实现了功能1.

但是,如果两个动画发生的先后顺序实现并不知道,如点击按钮1使得长度变为500,紧接着点击按钮2使得高度变为500,后者反过来。总之哪个按钮先按下并不知情。这种情况,上面的方法就不适用了。程序永远只执行最后一个动画事件,因为一旦进入动画执行函数play,就首先将上一个函数的timer进行了清空。

 

方法二:如果只是单纯的实现功能,除了动画完成执行回调的思路外,自然而然可以考虑到将回调的写法改进为promise的写法,此外下面的代码还使用requestAnimation替代了setTimeout.具体如下:

 

//实现动画库
//1.使用requestAnimationFrame
//2.引入promise
var Animate = {
    init: function(el) {
        this.el = typeof el === 'string' ? document.querySelector(el) : el;
        this.reqId = null;
        return this;
    },
    initAnim: function(props, option) {
        this.propChange = {};
        this.duration = (option && option.duration) || 1000;
        this.easing = (option && option.easing) || tween.Linear;
        for (var prop in props) {
            this.propChange[prop] = {};
            this.propChange[prop]['to'] = props[prop];
            this.propChange[prop]['from'] = getStyle(this.el, prop);
        }
        return this;
    },
    stop: function() {
        if (this.reqId) {
            cancelAnimationFrame(this.reqId);
        }
        this.reqId = null;
        return this;
    },
    play: function() {
        console.log('进入动画:');
        var startTime = 0;
        var self = this;
        if (this.reqId) {
            this.stop();
        }
        return new Promise((resolve, reject) => {
            function step(timestamp) {
                if (!startTime) {
                    startTime = timestamp;
                }
                var passedTime = Math.min(timestamp - startTime, self.duration);
                console.log('passedTime:' + passedTime + ',duration:' + self.duration);
                for (var prop in self.propChange) {
                    var target = self.easing(passedTime, self.propChange[prop]['from'], self.propChange[prop]['to'] - self.propChange[prop]['from'], self.duration);
                    setStyle(self.el, prop, target);
                }
                if (passedTime >= self.duration) {
                    self.stop();
                    resolve();
                } else {
                    this.reqId = requestAnimationFrame(step);
                }
            }
            this.reqId = requestAnimationFrame(step);
            this.cancel = function() {
                self.stop();
                reject('cancel');
            };
        });

    },
    runAnim: function(props, option) {
        this.initAnim(props, option);
        return this.play();
    }
};

 

调用方法如下:

1.可以使用promise的then方法:

var div = document.getElementById('mydiv');
    var anim = Object.create(Animate);
    anim.init(div);
    anim.runAnim({width:500},{duration:600}).then(function(){
       return anim.runAnim({height:400},{duration:400});
    }).then(function(){
        console.log('end');
    });

2.当然也可以使用ES7新引入的async,await方法(目前chrome浏览器已经支持)

    var div = document.getElementById('mydiv');
    var anim = Object.create(Animate);
    anim.init(div);

    async function  run() {
        var a = await anim.runAnim({
            width: 500,
            opacity: .4
        }, {
            duration: 600
        });
        var b = await anim.runAnim({
            height: 400
        }, {
            duration: 400
        });
    }
    run();

这种方法同样存在一样的弊端,即只适用于动画顺序实现知道的情形。

 

2.考虑功能2的情形,即动画发生顺序实现无法预知的情况下,在一个动画进行过程中触发另一个不会引发冲突,而是根据触发顺序依次执行。

实现思路:既然是依次,就容易想到队列,同时需要设置标志位running,保证在动画进行过程中,不会触发出队事件。

具体如下:

//实现动画库
//改进:利用requestAnimationFrame替代setTimeout
var Animate = {
    init: function(el) {
        this.el = typeof el === 'string' ? document.querySelector(el) : el;
        this.queue = [];
        this.running = false;
        this.reqId = null;
        return this;
    },
    initAnim: function(props, option) {
        this.propChange = {};
        this.duration = (option && option.duration) || 1000;
        this.easing = (option && option.easing) || tween.Linear;
        for (var prop in props) {
            this.propChange[prop] = {};
            this.propChange[prop]['to'] = props[prop];
            this.propChange[prop]['from'] = getStyle(this.el, prop);
        }
        return this;
    },
    stop: function() {
        this.running = false;
        if (this.reqId) {
            cancelAnimationFrame(this.reqId);
        }
        this.reqId = null;
        return this;
    },
    play: function() {
        this.running = true;
        console.log('进入动画:' + this.running);
        var startTime = 0;
        var self = this;
        if (this.reqId) {
            this.stop();
        }

        function step(timestamp) {
            if (!startTime) {
                startTime = timestamp;
            }
            var passedTime = Math.min(timestamp - startTime, self.duration);
            console.log('passedTime:' + passedTime + ',duration:' + self.duration);
            for (var prop in self.propChange) {
                var target = self.easing(passedTime, self.propChange[prop]['from'], self.propChange[prop]['to'] - self.propChange[prop]['from'], self.duration);
                setStyle(self.el, prop, target);
            }
            if (passedTime >= self.duration) {
                self.stop();
                //播放队列当中的下一组动画
                self.dequeue();
            } else {
                this.reqId = requestAnimationFrame(step, 1000 / 50);
            }
        }
        this.reqId = requestAnimationFrame(step, 1000 / 50);
    },
    enqueue: function(props, option) {
        this.queue.push(() => {
            this.initAnim.call(this, props, option);
            this.play.call(this);
        });
        return this;
    },
    hasNext: function() {
        return this.queue.length > 0;
    },
    dequeue: function(props) {
        //console.log('length', this.queue.length);
        if (!this.running && this.hasNext()) {
            if (props) {
                for (var prop in props) {
                    console.log(prop + '出队成功');
                }
            }
            //console.log('length',this.queue.length);
            this.queue.shift().call(this);
        }
        return this;
    },
    runAnim: function(props, option) {
        this.enqueue(props, option);
        //传入参数props仅仅是为了调试打印,即使不传也不影响功能
        this.dequeue(props);
        //setTimeout(this.dequeue.bind(this), 0);
    }
};

测试方法如下:

    //测试animate2.js
    //使用requeustAnimationFrame代替settimeout实现动画库
    var div = document.getElementById('mydiv');
    var anim = Object.create(Animate);
    anim.init(div);
    anim.runAnim({
        width: 500,
        opacity: .4
    }, {
        duration: 600
    });
    anim.runAnim({
        height: 500
    }, {
        duration: 600
    });

 

2,考虑能否将promise与队列结合起来,于是有了下面的代码:

//实现动画库
//1.使用requestAnimationFrame
//2.引入promise
var Animate = {
    init: function(el) {
        this.el = typeof el === 'string' ? document.querySelector(el) : el;
        this.reqId = null;
        this.queue = [];
        this.running = false;
        return this;
    },
    initAnim: function(props, option) {
        this.propChange = {};
        this.duration = (option && option.duration) || 1000;
        this.easing = (option && option.easing) || tween.Linear;
        for (var prop in props) {
            this.propChange[prop] = {};
            this.propChange[prop]['to'] = props[prop];
            this.propChange[prop]['from'] = getStyle(this.el, prop);
        }
        return this;
    },
    stop: function() {
        if (this.reqId) {
            cancelAnimationFrame(this.reqId);
        }
        this.running = false;
        this.reqId = null;
        return this;
    },
    play: function() {
        this.running = true;
        console.log('进入动画:' + this.running);
        var startTime = 0;
        var self = this;
        if (this.reqId) {
            this.stop();
        }
        return new Promise((resolve, reject) => {
            function step(timestamp) {
                if (!startTime) {
                    startTime = timestamp;
                }
                var passedTime = Math.min(timestamp - startTime, self.duration);
                console.log('passedTime:' + passedTime + ',duration:' + self.duration);
                for (var prop in self.propChange) {
                    var target = self.easing(passedTime, self.propChange[prop]['from'], self.propChange[prop]['to'] - self.propChange[prop]['from'], self.duration);
                    setStyle(self.el, prop, target);
                }
                if (passedTime >= self.duration) {
                    self.stop();                    
                    self.dequeue();
                    resolve();
                    
                } else {
                    this.reqId = requestAnimationFrame(step);
                }
            }
            this.reqId = requestAnimationFrame(step);
            this.cancel = function() {
                self.stop();
                reject('cancel');
            };
        });

    },
    hasNext: function() {
        return this.queue.length > 0;
    },
    enqueue: function(props, option) {
        this.queue.push(() => {
            this.initAnim(props, option);
            return this.play();
        });
    },
    dequeue: function(callback) {
        var prom;
        if (!this.running && this.hasNext()) {
            prom = this.queue.shift().call(this);
        }
        if (callback) {
            return prom.then(() => {
                callback.call(this);
            });
        } else {
            return prom;
        }
    },
    runAnim(props, option, callback) {
        this.enqueue(props, option);
        this.dequeue(callback);
    }
};

不过感觉这么做意义不是特别大。动画队列中的每一个元素是个函数,该函数返回一个promise,貌似看起来是为给动画队列中每一个动画结束的时候添加回调增加了可能,经过如下测试:

var div = document.getElementById('mydiv');
    var anim = Object.create(Animate);
    anim.init(div);
    anim.runAnim({
        width: 500
    }, {
        duration: 600
    }, function() {
        console.log(1);
    });
    anim.runAnim({
        height: 500
    }, {
        

如果回调是个同步代码,如上面的console.log(1),那么该打印语句在宽度变为500动画结束后立即执行。

但如果回调是个异步代码,如下:

 var div = document.getElementById('mydiv');
    var anim = Object.create(Animate);
    anim.init(div);
    anim.runAnim({
        width: 500
    }, {
        duration: 600
    }, function() {
        anim.runAnim({
            opacity: .4
        });
    });
    anim.runAnim({
        height: 500
    }, {
        duration: 400
    });

发现透明度的变化,实在长度变为500,并且高度变为500的动画结束之后,才执行。

 

总结:

1.回调与promise的关系无需多说,通过上面的代码发现二者和队列貌似也有某种联系。转念一想,貌似jquery中的defer,promise就是回调和队列结合实现的

2.上面的代码库远不完善,很多因素没有考虑,诸如多元素动画,css3动画等等。希望后续有时间能够多多优化。

 

二、封装javascript动画库2

参照jQuery队列设计方法,不是通过变量running判定动画是否正在执行,而是通过队列队首元素run来控制,此外还支持:

1)预定义动画序列;

2)直接到达动画最后一帧;

3)动画反转;

4)预定义动画效果。

工具类util.js

 

//获取元素属性
//返回元素对应的属性值(不包含单位)
//考虑的特殊情况包括:
//1.透明度,值为小数,如0.2
//2.颜色,值的表示法有rgb,16进制表示法(缩写,不缩写。两种形式)
//3.transform属性,包括 [ "translateZ", "scale", "scaleX", "scaleY", "translateX", "translateY", "scaleZ", "skewX", "skewY", "rotateX", "rotateY", "rotateZ" ]
//transfrom属性中,不考虑matrix,translate(30,40),translate3d等复合写法
// 上面的功能尚未实现,等有时间补上
(function(window) {
    var transformPropNames = ["translateZ", "scale", "scaleX", "scaleY", "translateX", "translateY", "scaleZ", "skewX", "skewY", "rotateX", "rotateY", "rotateZ"];

    window.getStyle = function(dom, prop) {
        var tmp = window.getComputedStyle ? window.getComputedStyle(dom, null)[prop] : dom.currentStyle[prop];
        return prop === 'opacity' ? parseFloat(tmp, 10) : parseInt(tmp, 10);
    };
    //设置元素属性
    window.setStyle = function(dom, prop, value) {
        if (prop === 'opacity') {
            dom.style.filter = '(opacity(' + parseFloat(value * 100) + '))';
            dom.style.opacity = value;
            return;
        }
        dom.style[prop] = parseInt(value, 10) + 'px';
    };
})(window);


//requestAnimationFrame的兼容处理
(function() {
    var lastTime = 0;
    var vendors = ['webkit', 'moz'];
    for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||
            window[vendors[x] + 'CancelRequestAnimationFrame'];
    }

    if (!window.requestAnimationFrame) {
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
            var id = window.setTimeout(function() {
                callback(currTime + timeToCall);
            }, timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };
    }
    if (!window.cancelAnimationFrame) {
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
    }
}());

//时间戳获取的兼容处理
function nowtime() {
    if (typeof performance !== 'undefined' && performance.now) {
        return performance.now();
    }
    return Date.now ? Date.now() : (new Date()).getTime();
}
util.js

 

缓动效果:tween.js

 /**
  *Tween 缓动相关
  */
 var tween = {
     Linear: function(t, b, c, d) {
         return c * t / d + b;
     },
     Quad: {
         easeIn: function(t, b, c, d) {
             return c * (t /= d) * t + b;
         },
         easeOut: function(t, b, c, d) {
             return -c * (t /= d) * (t - 2) + b;
         },
         easeInOut: function(t, b, c, d) {
             if ((t /= d / 2) < 1) return c / 2 * t * t + b;
             return -c / 2 * ((--t) * (t - 2) - 1) + b;
         }
     },
     Cubic: {
         easeIn: function(t, b, c, d) {
             return c * (t /= d) * t * t + b;
         },
         easeOut: function(t, b, c, d) {
             return c * ((t = t / d - 1) * t * t + 1) + b;
         },
         easeInOut: function(t, b, c, d) {
             if ((t /= d / 2) < 1) return c / 2 * t * t * t + b;
             return c / 2 * ((t -= 2) * t * t + 2) + b;
         }
     },
     Quart: {
         easeIn: function(t, b, c, d) {
             return c * (t /= d) * t * t * t + b;
         },
         easeOut: function(t, b, c, d) {
             return -c * ((t = t / d - 1) * t * t * t - 1) + b;
         },
         easeInOut: function(t, b, c, d) {
             if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;
             return -c / 2 * ((t -= 2) * t * t * t - 2) + b;
         }
     },
     Quint: {
         easeIn: function(t, b, c, d) {
             return c * (t /= d) * t * t * t * t + b;
         },
         easeOut: function(t, b, c, d) {
             return c * ((t = t / d - 1) * t * t * t * t + 1) + b;
         },
         easeInOut: function(t, b, c, d) {
             if ((t /= d / 2) < 1) return c / 2 * t * t * t * t * t + b;
             return c / 2 * ((t -= 2) * t * t * t * t + 2) + b;
         }
     },
     Sine: {
         easeIn: function(t, b, c, d) {
             return -c * Math.cos(t / d * (Math.PI / 2)) + c + b;
         },
         easeOut: function(t, b, c, d) {
             return c * Math.sin(t / d * (Math.PI / 2)) + b;
         },
         easeInOut: function(t, b, c, d) {
             return -c / 2 * (Math.cos(Math.PI * t / d) - 1) + b;
         }
     },
     Expo: {
         easeIn: function(t, b, c, d) {
             return (t == 0) ? b : c * Math.pow(2, 10 * (t / d - 1)) + b;
         },
         easeOut: function(t, b, c, d) {
             return (t == d) ? b + c : c * (-Math.pow(2, -10 * t / d) + 1) + b;
         },
         easeInOut: function(t, b, c, d) {
             if (t == 0) return b;
             if (t == d) return b + c;
             if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
             return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
         }
     },
     Circ: {
         easeIn: function(t, b, c, d) {
             return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
         },
         easeOut: function(t, b, c, d) {
             return c * Math.sqrt(1 - (t = t / d - 1) * t) + b;
         },
         easeInOut: function(t, b, c, d) {
             if ((t /= d / 2) < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
             return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
         }
     },
     Elastic: {
         easeIn: function(t, b, c, d, a, p) {
             if (t == 0) return b;
             if ((t /= d) == 1) return b + c;
             if (!p) p = d * .3;
             if (!a || a < Math.abs(c)) {
                 a = c;
                 var s = p / 4;
             } else var s = p / (2 * Math.PI) * Math.asin(c / a);
             return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
         },
         easeOut: function(t, b, c, d, a, p) {
             if (t == 0) return b;
             if ((t /= d) == 1) return b + c;
             if (!p) p = d * .3;
             if (!a || a < Math.abs(c)) {
                 a = c;
                 var s = p / 4;
             } else var s = p / (2 * Math.PI) * Math.asin(c / a);
             return (a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b);
         },
         easeInOut: function(t, b, c, d, a, p) {
             if (t == 0) return b;
             if ((t /= d / 2) == 2) return b + c;
             if (!p) p = d * (.3 * 1.5);
             if (!a || a < Math.abs(c)) {
                 a = c;
                 var s = p / 4;
             } else var s = p / (2 * Math.PI) * Math.asin(c / a);
             if (t < 1) return -.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
             return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p) * .5 + c + b;
         }
     },
     Back: {
         easeIn: function(t, b, c, d, s) {
             if (s == undefined) s = 1.70158;
             return c * (t /= d) * t * ((s + 1) * t - s) + b;
         },
         easeOut: function(t, b, c, d, s) {
             if (s == undefined) s = 1.70158;
             return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
         },
         easeInOut: function(t, b, c, d, s) {
             if (s == undefined) s = 1.70158;
             if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;
             return c / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;
         }
     },
     Bounce: {
         easeIn: function(t, b, c, d) {
             return c - Tween.Bounce.easeOut(d - t, 0, c, d) + b;
         },
         easeOut: function(t, b, c, d) {
             if ((t /= d) < (1 / 2.75)) {
                 return c * (7.5625 * t * t) + b;
             } else if (t < (2 / 2.75)) {
                 return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b;
             } else if (t < (2.5 / 2.75)) {
                 return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b;
             } else {
                 return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b;
             }
         },
         easeInOut: function(t, b, c, d) {
             if (t < d / 2) return Tween.Bounce.easeIn(t * 2, 0, c, d) * .5 + b;
             else return Tween.Bounce.easeOut(t * 2 - d, 0, c, d) * .5 + c * .5 + b;
         }
     }
 };
tween.js

具体实现animation.js

var Animate = {
    init: function(el) {
        this.dom = typeof el === 'string' ? document.querySelector(el) : el;
        // console.log(this.dom);
        this.queue = [];
        this.isRuning = false;
        this.reqId = null;
        this.toEnd = false;
    },
    initAnim: function(props, opts) {
        this.propchanges = {};
        this.duration = (opts && opts.duration) || 1000;
        this.easing = (opts && opts.easing) || tween.Linear;
        //为了实现reverse,需要initProps来记录变化之前的数值
        this.initprops = {};
        // 可以使用数组同时指定开始值和结束值,也可以仅仅指定结束值
        for (var prop in props) {
            this.propchanges[prop] = {};
            if (Array.isArray(props[prop])) {
                this.propchanges[prop]['from'] = this.initprops[prop] = props[prop][0];
                this.propchanges[prop]['to'] = props[prop][1];
            } else {
                this.propchanges[prop]['from'] = this.initprops[prop] = getStyle(this.dom, prop);
                this.propchanges[prop]['to'] = props[prop];
            }
        }
        return this;
    },
    stop: function() {
        this.isRuning = false;
        if (this.reqId) {
            cancelAnimationFrame(this.reqId);
            this.reqId = null;
        }
        return this;
    },
    play: function(opts) {
        console.log('opts', opts);
        this.isRuning = true;
        var self = this;
        var startTime;

        function tick(timestamp) {
            var curTime = timestamp || nowtime();
            if (!startTime) {
                startTime = curTime;
            }
            // console.log('passedTime', curTime - startTime);
            var passedTime = Math.min(curTime - startTime, self.duration);
            // 实现finish功能,直接到达动画最终状态
            if (self.toEnd) {
                passedTime = self.duration;
            }
            for (var prop in self.propchanges) {
                var curValue = self.easing(passedTime, self.propchanges[prop]['from'], self.propchanges[prop]['to'] - self.propchanges[prop]['from'], self.duration);
                console.log(prop + ':' + passedTime, curValue);
                setStyle(self.dom, prop, curValue);
            }
            if (passedTime >= self.duration) {
                //动画停止
                self.stop(); //在stop中将isRunning置为了false
                // startTime = 0;
                //下一个动画出队
                self.dequeue();
                if (opts.next) {
                    opts.next.call(null);
                }
            } else if (self.isRuning) {
                self.reqId = requestAnimationFrame(tick);
            }

            //必须将判断放在else里面
            //否则经过试验,链式调用时,除了第一个动画外,其他动画会出现问题
            //这是因为,虽然stop中将isRunning置为了false
            //但是接下来的dequeue执行play,又马上将isRunning置为了true
            // if (self.isRuning) {
            //     self.reqId = requestAnimationFrame(tick);
            // }
        }
        tick();
        return this;
    },
    // 如果当前有动画正在执行,那么动画队列的首个元素一定是'run'
    // 动画函数出队之后,开始执行前,立即在队列头部添加一个'run'元素,代表动画函数正在执行
    // 只有当对应动画函数执行完之后,才会调用出队操作,原队首的'run'元素才可以出队
    // 如果动画函数执行完毕,调用出队操作之后,动画队列中还有下一个动画函数,下一个动画函数出队后,执行之前,依旧将队列头部置为'run',重复上述操作
    // 如果动画函数执行完毕,调用出队操作之后,动画队列中没有其他动画函数,那么队首的‘run’元素出队之后,队列为空
    // 首次入队时,动画队列的首个元素不是'run',动画立即出队执行
    // 
    enqueue: function(fn) {
        this.queue.push(fn);
        if (this.queue[0] !== 'run') {
            this.dequeue();
        }
    },
    //上一个版本使用isRuning来控制出队执行的时机,这里运用队首的'run'来控制,isRunning的一一貌似不大
    dequeue: function() {
        while (this.queue.length) {
            var curItem = this.queue.shift();
            if (typeof curItem === 'function') {
                curItem.call(this); //这是个异步操作
                this.queue.unshift('run');
                break;
            }
        }
    },
    // 对外接口:开始动画的入口函数
    animate: function(props, opts) {
        // console.log(typeof this.queue);
        this.enqueue(() => {
            this.initAnim(props, opts);
            this.play(opts);
        });
        return this;
    },

    // 对外接口,直接到达动画的最终状态
    finish: function() {
        this.toEnd = true;
        return this;
    },
    // 对外接口:恢复到最初状态
    reverse: function() {
        if (!this.initprops) {
            alert('尚未调用任何动画,不能反转!');
        }
        this.animate(this.initprops);
        return this;
    },
    //
    runsequence: function(sequence) {
        let reSequence = sequence.reverse();
        reSequence.forEach((curItem, index) => {
            if (index >= 1) {
                prevItem = reSequence[index - 1];
                curItem.o.next = function() {
                    var anim = Object.create(Animate);
                    anim.init(prevItem.e);
                    anim.animate(prevItem.p, prevItem.o);
                };
            }
        });
        var firstItem = reSequence[reSequence.length - 1];
        var firstAnim = Object.create(Animate);
        firstAnim.init(firstItem.e);
        firstAnim.animate(firstItem.p, firstItem.o);
    },
};
myanimation.js

预定义动画和预定义动画序列

// 实现一些自定义动画
;
(function(window) {
    const Animate = window.Animate;
    if (!Animate) {
        console.log('请首先引入myanimate.js');
        return;
    }
    const effects = {
        "transition.slideUpIn": {
            defaultDuration: 900,
            calls: [
                [{ opacity: [1, 0], translateY: [0, 20] }]
            ]
        },
        "transition.slideUpOut": {
            defaultDuration: 900,
            calls: [
                [{ opacity: [0, 1], translateY: -20 }]
            ],
            reset: { translateY: 0 }
        },
        "transition.slideDownIn": {
            defaultDuration: 900,
            calls: [
                [{ opacity: [1, 0], translateY: [0, -20] }]
            ]
        },
        "transition.slideDownOut": {
            defaultDuration: 900,
            calls: [
                [{ opacity: [0, 1], translateY: 20 }]
            ],
            reset: { translateY: 0 }
        },
        "transition.slideLeftIn": {
            defaultDuration: 1000,
            calls: [
                [{ opacity: [1, 0], translateX: [0, -20] }]
            ]
        },
        "transition.slideLeftOut": {
            defaultDuration: 1050,
            calls: [
                [{ opacity: [0, 1], translateX: -20 }]
            ],
            reset: { translateX: 0 }
        },
        "transition.slideRightIn": {
            defaultDuration: 1000,
            calls: [
                [{ opacity: [1, 0], translateX: [0, 20] }]
            ]
        },
        "transition.slideRightOut": {
            defaultDuration: 1050,
            calls: [
                [{ opacity: [0, 1], translateX: 20, translateZ: 0 }]
            ],
            reset: { translateX: 0 }
        },
        "callout.pulse": {
            defaultDuration: 900,
            calls: [
                [{ scaleX: 1.1 }, 0.50],
                [{ scaleX: 1 }, 0.50]
            ]
        },
        'test': {
            defaultDuration: 2000,
            calls: [
                [{ left: 200, opacity: 0.1 }, 0.5],
                [{ opacity: 1 }, 0.5]
            ]
        }
    };
    Animate.runEffect = function(effectName) {
        let curEffect = effects[effectName];
        if (!curEffect) {
            return;
        }
        let sequence = [];
        let defaultDuration = curEffect.defaultDuration;
        curEffect.calls.forEach((item, index) => {
            let propMap = item[0];
            let duration = item[1] ? item[1] * defaultDuration : defaultDuration;
            let options = item[2] || {};
            options.duration = duration;
            sequence.push({
                e: this.dom,
                p: propMap,
                o: options
            });
        });
        Animate.runsequence(sequence);
    };

})(window);
myanimation.effect.js

测试代码:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>测试动画库</title>
    <style type="text/css">
    .main {
        padding: 50px;
        position: relative;
    }
    
    .btn-wrapper {
        padding: 15px 0;
    }
    
    .mydiv {
        margin: 20px 0;
        width: 300px;
        height: 200px;
        background-color: pink;
        position: relative;
        top: 0;
        left: 0;
    }
    </style>
</head>

<body>
    <div class="main">
        <div class="mydiv" id="mydiv"></div>
        <div id="btn-wrapper">
            <button id="chainBtn">链式调用</button>
        </div>
        <div class="mydiv" id="mydiv-reverse"></div>
        <div id="btn-wrapper">
            <button id="reverseBtn">reverse调用</button>
        </div>
        <div class="mydiv" id="mydiv-predefine1"></div>
        <div class="mydiv" id="mydiv-predefine2"></div>
        <div id="btn-wrapper">
            <button id="predefineBtn">预定义动画队列</button>
        </div>
        <div class="mydiv" id="mydiv-effect"></div>
        <div id="btn-wrapper">
            <button id="effectBtn">预定义动画</button>
        </div>
    </div>
    <script src="./util.js"></script>
    <script src="./tween.js"></script>
    <script src="./myanimate.js"></script>
    <script src="./myanimate.effect.js"></script>
    <script type="text/javascript">
    // 链式调用
    document.querySelector('#chainBtn').addEventListener('click', function(e) {
        var div = document.getElementById('mydiv');
        var anim = Object.create(Animate);
        anim.init(div);
        anim.animate({
            opacity: 0.2
        }).animate({
            left: 200
        });
        //测试停止动画,stop函数
        // setTimeout(function() {
        //     anim.stop();
        // }, 500);
        //测试直接到达动画的最终状态,finish函数
        //如果是链式调用,到达所有动画的最终状态
        //如果只想到达当前动画的最终状态,只需要稍微修改,在stop中重置toEnd=false即可
        // setTimeout(function() {
        //     anim.finish();
        // }, 500);
    });

    //reverse调用
    document.querySelector('#reverseBtn').addEventListener('click', function(e) {
        var div = document.getElementById('mydiv-reverse');
        var anim = Object.create(Animate);
        anim.init(div);
        anim.animate({
            left: 200
        }).reverse();
    });

    //预定义动画测试
    document.querySelector('#predefineBtn').addEventListener('click', function(e) {
        var anims = [{
            e: '#mydiv-predefine1',
            p: {
                left: 300
            },
            o: {
                duration: 500
            }
        }, {
            e: '#mydiv-predefine2',
            p: {
                left: 200,
                opacity: 0.3
            },
            o: {
                duration: 1000
            }
        }];
        //不需要新建一个实例,直接在Animate上调用即可
        Animate.runsequence(anims);
    });
    //预定义动画测试
    document.querySelector('#effectBtn').addEventListener('click', function(e) {
        var anim = Object.create(Animate);
        anim.init('#mydiv-effect');
        anim.runEffect('test');
    });
    </script>
</body>

</html>
index.html

 

 

 附上一份jQuery动画部分的源代码

var fxNow, timerId,
    rfxtypes = /^(?:toggle|show|hide)$/,
    rfxnum = new RegExp( "^(?:([+-])=|)(" + core_pnum + ")([a-z%]*)$", "i" ),
    rrun = /queueHooks$/,
    animationPrefilters = [ defaultPrefilter ],
    tweeners = {
        "*": [function( prop, value ) {
            var tween = this.createTween( prop, value ),
                target = tween.cur(),
                parts = rfxnum.exec( value ),
                unit = parts && parts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ),

                // Starting value computation is required for potential unit mismatches
                start = ( jQuery.cssNumber[ prop ] || unit !== "px" && +target ) &&
                    rfxnum.exec( jQuery.css( tween.elem, prop ) ),
                scale = 1,
                maxIterations = 20;

            if ( start && start[ 3 ] !== unit ) {
                // Trust units reported by jQuery.css
                unit = unit || start[ 3 ];

                // Make sure we update the tween properties later on
                parts = parts || [];

                // Iteratively approximate from a nonzero starting point
                start = +target || 1;

                do {
                    // If previous iteration zeroed out, double until we get *something*
                    // Use a string for doubling factor so we don't accidentally see scale as unchanged below
                    scale = scale || ".5";

                    // Adjust and apply
                    start = start / scale;
                    jQuery.style( tween.elem, prop, start + unit );

                // Update scale, tolerating zero or NaN from tween.cur()
                // And breaking the loop if scale is unchanged or perfect, or if we've just had enough
                } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );
            }

            // Update tween properties
            if ( parts ) {
                start = tween.start = +start || +target || 0;
                tween.unit = unit;
                // If a +=/-= token was provided, we're doing a relative animation
                tween.end = parts[ 1 ] ?
                    start + ( parts[ 1 ] + 1 ) * parts[ 2 ] :
                    +parts[ 2 ];
            }

            return tween;
        }]
    };

// Animations created synchronously will run synchronously
function createFxNow() {
    setTimeout(function() {
        fxNow = undefined;
    });
    return ( fxNow = jQuery.now() );
}

function createTween( value, prop, animation ) {
    var tween,
        collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ),
        index = 0,
        length = collection.length;
    for ( ; index < length; index++ ) {
        if ( (tween = collection[ index ].call( animation, prop, value )) ) {

            // we're done with this property
            return tween;
        }
    }
}

function Animation( elem, properties, options ) {
    var result,
        stopped,
        index = 0,
        length = animationPrefilters.length,
        deferred = jQuery.Deferred().always( function() {
            // don't match elem in the :animated selector
            delete tick.elem;
        }),
        tick = function() {
            if ( stopped ) {
                return false;
            }
            var currentTime = fxNow || createFxNow(),
                remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
                // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)
                temp = remaining / animation.duration || 0,
                percent = 1 - temp,
                index = 0,
                length = animation.tweens.length;

            for ( ; index < length ; index++ ) {
                animation.tweens[ index ].run( percent );
            }

            deferred.notifyWith( elem, [ animation, percent, remaining ]);

            if ( percent < 1 && length ) {
                return remaining;
            } else {
                deferred.resolveWith( elem, [ animation ] );
                return false;
            }
        },
        animation = deferred.promise({
            elem: elem,
            props: jQuery.extend( {}, properties ),
            opts: jQuery.extend( true, { specialEasing: {} }, options ),
            originalProperties: properties,
            originalOptions: options,
            startTime: fxNow || createFxNow(),
            duration: options.duration,
            tweens: [],
            createTween: function( prop, end ) {
                var tween = jQuery.Tween( elem, animation.opts, prop, end,
                        animation.opts.specialEasing[ prop ] || animation.opts.easing );
                animation.tweens.push( tween );
                return tween;
            },
            stop: function( gotoEnd ) {
                var index = 0,
                    // if we are going to the end, we want to run all the tweens
                    // otherwise we skip this part
                    length = gotoEnd ? animation.tweens.length : 0;
                if ( stopped ) {
                    return this;
                }
                stopped = true;
                for ( ; index < length ; index++ ) {
                    animation.tweens[ index ].run( 1 );
                }

                // resolve when we played the last frame
                // otherwise, reject
                if ( gotoEnd ) {
                    deferred.resolveWith( elem, [ animation, gotoEnd ] );
                } else {
                    deferred.rejectWith( elem, [ animation, gotoEnd ] );
                }
                return this;
            }
        }),
        props = animation.props;

    propFilter( props, animation.opts.specialEasing );

    for ( ; index < length ; index++ ) {
        result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );
        if ( result ) {
            return result;
        }
    }

    jQuery.map( props, createTween, animation );

    if ( jQuery.isFunction( animation.opts.start ) ) {
        animation.opts.start.call( elem, animation );
    }

    jQuery.fx.timer(
        jQuery.extend( tick, {
            elem: elem,
            anim: animation,
            queue: animation.opts.queue
        })
    );

    // attach callbacks from options
    return animation.progress( animation.opts.progress )
        .done( animation.opts.done, animation.opts.complete )
        .fail( animation.opts.fail )
        .always( animation.opts.always );
}

function propFilter( props, specialEasing ) {
    var index, name, easing, value, hooks;

    // camelCase, specialEasing and expand cssHook pass
    for ( index in props ) {
        name = jQuery.camelCase( index );
        easing = specialEasing[ name ];
        value = props[ index ];
        if ( jQuery.isArray( value ) ) {
            easing = value[ 1 ];
            value = props[ index ] = value[ 0 ];
        }

        if ( index !== name ) {
            props[ name ] = value;
            delete props[ index ];
        }

        hooks = jQuery.cssHooks[ name ];
        if ( hooks && "expand" in hooks ) {
            value = hooks.expand( value );
            delete props[ name ];

            // not quite $.extend, this wont overwrite keys already present.
            // also - reusing 'index' from above because we have the correct "name"
            for ( index in value ) {
                if ( !( index in props ) ) {
                    props[ index ] = value[ index ];
                    specialEasing[ index ] = easing;
                }
            }
        } else {
            specialEasing[ name ] = easing;
        }
    }
}

jQuery.Animation = jQuery.extend( Animation, {

    tweener: function( props, callback ) {
        if ( jQuery.isFunction( props ) ) {
            callback = props;
            props = [ "*" ];
        } else {
            props = props.split(" ");
        }

        var prop,
            index = 0,
            length = props.length;

        for ( ; index < length ; index++ ) {
            prop = props[ index ];
            tweeners[ prop ] = tweeners[ prop ] || [];
            tweeners[ prop ].unshift( callback );
        }
    },

    prefilter: function( callback, prepend ) {
        if ( prepend ) {
            animationPrefilters.unshift( callback );
        } else {
            animationPrefilters.push( callback );
        }
    }
});

function defaultPrefilter( elem, props, opts ) {
    /* jshint validthis: true */
    var prop, value, toggle, tween, hooks, oldfire,
        anim = this,
        orig = {},
        style = elem.style,
        hidden = elem.nodeType && isHidden( elem ),
        dataShow = data_priv.get( elem, "fxshow" );

    // handle queue: false promises
    if ( !opts.queue ) {
        hooks = jQuery._queueHooks( elem, "fx" );
        if ( hooks.unqueued == null ) {
            hooks.unqueued = 0;
            oldfire = hooks.empty.fire;
            hooks.empty.fire = function() {
                if ( !hooks.unqueued ) {
                    oldfire();
                }
            };
        }
        hooks.unqueued++;

        anim.always(function() {
            // doing this makes sure that the complete handler will be called
            // before this completes
            anim.always(function() {
                hooks.unqueued--;
                if ( !jQuery.queue( elem, "fx" ).length ) {
                    hooks.empty.fire();
                }
            });
        });
    }

    // height/width overflow pass
    if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
        // Make sure that nothing sneaks out
        // Record all 3 overflow attributes because IE9-10 do not
        // change the overflow attribute when overflowX and
        // overflowY are set to the same value
        opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];

        // Set display property to inline-block for height/width
        // animations on inline elements that are having width/height animated
        if ( jQuery.css( elem, "display" ) === "inline" &&
                jQuery.css( elem, "float" ) === "none" ) {

            style.display = "inline-block";
        }
    }

    if ( opts.overflow ) {
        style.overflow = "hidden";
        anim.always(function() {
            style.overflow = opts.overflow[ 0 ];
            style.overflowX = opts.overflow[ 1 ];
            style.overflowY = opts.overflow[ 2 ];
        });
    }


    // show/hide pass
    for ( prop in props ) {
        value = props[ prop ];
        if ( rfxtypes.exec( value ) ) {
            delete props[ prop ];
            toggle = toggle || value === "toggle";
            if ( value === ( hidden ? "hide" : "show" ) ) {

                // If there is dataShow left over from a stopped hide or show and we are going to proceed with show, we should pretend to be hidden
                if ( value === "show" && dataShow && dataShow[ prop ] !== undefined ) {
                    hidden = true;
                } else {
                    continue;
                }
            }
            orig[ prop ] = dataShow && dataShow[ prop ] || jQuery.style( elem, prop );
        }
    }

    if ( !jQuery.isEmptyObject( orig ) ) {
        if ( dataShow ) {
            if ( "hidden" in dataShow ) {
                hidden = dataShow.hidden;
            }
        } else {
            dataShow = data_priv.access( elem, "fxshow", {} );
        }

        // store state if its toggle - enables .stop().toggle() to "reverse"
        if ( toggle ) {
            dataShow.hidden = !hidden;
        }
        if ( hidden ) {
            jQuery( elem ).show();
        } else {
            anim.done(function() {
                jQuery( elem ).hide();
            });
        }
        anim.done(function() {
            var prop;

            data_priv.remove( elem, "fxshow" );
            for ( prop in orig ) {
                jQuery.style( elem, prop, orig[ prop ] );
            }
        });
        for ( prop in orig ) {
            tween = createTween( hidden ? dataShow[ prop ] : 0, prop, anim );

            if ( !( prop in dataShow ) ) {
                dataShow[ prop ] = tween.start;
                if ( hidden ) {
                    tween.end = tween.start;
                    tween.start = prop === "width" || prop === "height" ? 1 : 0;
                }
            }
        }
    }
}

function Tween( elem, options, prop, end, easing ) {
    return new Tween.prototype.init( elem, options, prop, end, easing );
}
jQuery.Tween = Tween;

Tween.prototype = {
    constructor: Tween,
    init: function( elem, options, prop, end, easing, unit ) {
        this.elem = elem;
        this.prop = prop;
        this.easing = easing || "swing";
        this.options = options;
        this.start = this.now = this.cur();
        this.end = end;
        this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
    },
    cur: function() {
        var hooks = Tween.propHooks[ this.prop ];

        return hooks && hooks.get ?
            hooks.get( this ) :
            Tween.propHooks._default.get( this );
    },
    run: function( percent ) {
        var eased,
            hooks = Tween.propHooks[ this.prop ];

        if ( this.options.duration ) {
            this.pos = eased = jQuery.easing[ this.easing ](
                percent, this.options.duration * percent, 0, 1, this.options.duration
            );
        } else {
            this.pos = eased = percent;
        }
        this.now = ( this.end - this.start ) * eased + this.start;

        if ( this.options.step ) {
            this.options.step.call( this.elem, this.now, this );
        }

        if ( hooks && hooks.set ) {
            hooks.set( this );
        } else {
            Tween.propHooks._default.set( this );
        }
        return this;
    }
};

Tween.prototype.init.prototype = Tween.prototype;

Tween.propHooks = {
    _default: {
        get: function( tween ) {
            var result;

            if ( tween.elem[ tween.prop ] != null &&
                (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {
                return tween.elem[ tween.prop ];
            }

            // passing an empty string as a 3rd parameter to .css will automatically
            // attempt a parseFloat and fallback to a string if the parse fails
            // so, simple values such as "10px" are parsed to Float.
            // complex values such as "rotate(1rad)" are returned as is.
            result = jQuery.css( tween.elem, tween.prop, "" );
            // Empty strings, null, undefined and "auto" are converted to 0.
            return !result || result === "auto" ? 0 : result;
        },
        set: function( tween ) {
            // use step hook for back compat - use cssHook if its there - use .style if its
            // available and use plain properties where available
            if ( jQuery.fx.step[ tween.prop ] ) {
                jQuery.fx.step[ tween.prop ]( tween );
            } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {
                jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
            } else {
                tween.elem[ tween.prop ] = tween.now;
            }
        }
    }
};

// Support: IE9
// Panic based approach to setting things on disconnected nodes

Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
    set: function( tween ) {
        if ( tween.elem.nodeType && tween.elem.parentNode ) {
            tween.elem[ tween.prop ] = tween.now;
        }
    }
};

jQuery.each([ "toggle", "show", "hide" ], function( i, name ) {
    var cssFn = jQuery.fn[ name ];
    jQuery.fn[ name ] = function( speed, easing, callback ) {
        return speed == null || typeof speed === "boolean" ?
            cssFn.apply( this, arguments ) :
            this.animate( genFx( name, true ), speed, easing, callback );
    };
});

jQuery.fn.extend({
    fadeTo: function( speed, to, easing, callback ) {

        // show any hidden elements after setting opacity to 0
        return this.filter( isHidden ).css( "opacity", 0 ).show()

            // animate to the value specified
            .end().animate({ opacity: to }, speed, easing, callback );
    },
    animate: function( prop, speed, easing, callback ) {
        var empty = jQuery.isEmptyObject( prop ),
            optall = jQuery.speed( speed, easing, callback ),
            doAnimation = function() {
                // Operate on a copy of prop so per-property easing won't be lost
                var anim = Animation( this, jQuery.extend( {}, prop ), optall );

                // Empty animations, or finishing resolves immediately
                if ( empty || data_priv.get( this, "finish" ) ) {
                    anim.stop( true );
                }
            };
            doAnimation.finish = doAnimation;

        return empty || optall.queue === false ?
            this.each( doAnimation ) :
            this.queue( optall.queue, doAnimation );
    },
    stop: function( type, clearQueue, gotoEnd ) {
        var stopQueue = function( hooks ) {
            var stop = hooks.stop;
            delete hooks.stop;
            stop( gotoEnd );
        };

        if ( typeof type !== "string" ) {
            gotoEnd = clearQueue;
            clearQueue = type;
            type = undefined;
        }
        if ( clearQueue && type !== false ) {
            this.queue( type || "fx", [] );
        }

        return this.each(function() {
            var dequeue = true,
                index = type != null && type + "queueHooks",
                timers = jQuery.timers,
                data = data_priv.get( this );

            if ( index ) {
                if ( data[ index ] && data[ index ].stop ) {
                    stopQueue( data[ index ] );
                }
            } else {
                for ( index in data ) {
                    if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
                        stopQueue( data[ index ] );
                    }
                }
            }

            for ( index = timers.length; index--; ) {
                if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
                    timers[ index ].anim.stop( gotoEnd );
                    dequeue = false;
                    timers.splice( index, 1 );
                }
            }

            // start the next in the queue if the last step wasn't forced
            // timers currently will call their complete callbacks, which will dequeue
            // but only if they were gotoEnd
            if ( dequeue || !gotoEnd ) {
                jQuery.dequeue( this, type );
            }
        });
    },
    finish: function( type ) {
        if ( type !== false ) {
            type = type || "fx";
        }
        return this.each(function() {
            var index,
                data = data_priv.get( this ),
                queue = data[ type + "queue" ],
                hooks = data[ type + "queueHooks" ],
                timers = jQuery.timers,
                length = queue ? queue.length : 0;

            // enable finishing flag on private data
            data.finish = true;

            // empty the queue first
            jQuery.queue( this, type, [] );

            if ( hooks && hooks.stop ) {
                hooks.stop.call( this, true );
            }

            // look for any active animations, and finish them
            for ( index = timers.length; index--; ) {
                if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
                    timers[ index ].anim.stop( true );
                    timers.splice( index, 1 );
                }
            }

            // look for any animations in the old queue and finish them
            for ( index = 0; index < length; index++ ) {
                if ( queue[ index ] && queue[ index ].finish ) {
                    queue[ index ].finish.call( this );
                }
            }

            // turn off finishing flag
            delete data.finish;
        });
    }
});

// Generate parameters to create a standard animation
function genFx( type, includeWidth ) {
    var which,
        attrs = { height: type },
        i = 0;

    // if we include width, step value is 1 to do all cssExpand values,
    // if we don't include width, step value is 2 to skip over Left and Right
    includeWidth = includeWidth? 1 : 0;
    for( ; i < 4 ; i += 2 - includeWidth ) {
        which = cssExpand[ i ];
        attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
    }

    if ( includeWidth ) {
        attrs.opacity = attrs.width = type;
    }

    return attrs;
}

// Generate shortcuts for custom animations
jQuery.each({
    slideDown: genFx("show"),
    slideUp: genFx("hide"),
    slideToggle: genFx("toggle"),
    fadeIn: { opacity: "show" },
    fadeOut: { opacity: "hide" },
    fadeToggle: { opacity: "toggle" }
}, function( name, props ) {
    jQuery.fn[ name ] = function( speed, easing, callback ) {
        return this.animate( props, speed, easing, callback );
    };
});

jQuery.speed = function( speed, easing, fn ) {
    var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
        complete: fn || !fn && easing ||
            jQuery.isFunction( speed ) && speed,
        duration: speed,
        easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
    };

    opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
        opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;

    // normalize opt.queue - true/undefined/null -> "fx"
    if ( opt.queue == null || opt.queue === true ) {
        opt.queue = "fx";
    }

    // Queueing
    opt.old = opt.complete;

    opt.complete = function() {
        if ( jQuery.isFunction( opt.old ) ) {
            opt.old.call( this );
        }

        if ( opt.queue ) {
            jQuery.dequeue( this, opt.queue );
        }
    };

    return opt;
};

jQuery.easing = {
    linear: function( p ) {
        return p;
    },
    swing: function( p ) {
        return 0.5 - Math.cos( p*Math.PI ) / 2;
    }
};

jQuery.timers = [];
jQuery.fx = Tween.prototype.init;
jQuery.fx.tick = function() {
    var timer,
        timers = jQuery.timers,
        i = 0;

    fxNow = jQuery.now();

    for ( ; i < timers.length; i++ ) {
        timer = timers[ i ];
        // Checks the timer has not already been removed
        if ( !timer() && timers[ i ] === timer ) {
            timers.splice( i--, 1 );
        }
    }

    if ( !timers.length ) {
        jQuery.fx.stop();
    }
    fxNow = undefined;
};

jQuery.fx.timer = function( timer ) {
    if ( timer() && jQuery.timers.push( timer ) ) {
        jQuery.fx.start();
    }
};

jQuery.fx.interval = 13;

jQuery.fx.start = function() {
    if ( !timerId ) {
        timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
    }
};

jQuery.fx.stop = function() {
    clearInterval( timerId );
    timerId = null;
};

jQuery.fx.speeds = {
    slow: 600,
    fast: 200,
    // Default speed
    _default: 400
};

// Back Compat <1.8 extension point
jQuery.fx.step = {};

if ( jQuery.expr && jQuery.expr.filters ) {
    jQuery.expr.filters.animated = function( elem ) {
        return jQuery.grep(jQuery.timers, function( fn ) {
            return elem === fn.elem;
        }).length;
    };
}
jQuery.fn.offset = function( options ) {
    if ( arguments.length ) {
        return options === undefined ?
            this :
            this.each(function( i ) {
                jQuery.offset.setOffset( this, options, i );
            });
    }

    var docElem, win,
        elem = this[ 0 ],
        box = { top: 0, left: 0 },
        doc = elem && elem.ownerDocument;

    if ( !doc ) {
        return;
    }

    docElem = doc.documentElement;

    // Make sure it's not a disconnected DOM node
    if ( !jQuery.contains( docElem, elem ) ) {
        return box;
    }

    // If we don't have gBCR, just use 0,0 rather than error
    // BlackBerry 5, iOS 3 (original iPhone)
    if ( typeof elem.getBoundingClientRect !== core_strundefined ) {
        box = elem.getBoundingClientRect();
    }
    win = getWindow( doc );
    return {
        top: box.top + win.pageYOffset - docElem.clientTop,
        left: box.left + win.pageXOffset - docElem.clientLeft
    };
};

jQuery.offset = {

    setOffset: function( elem, options, i ) {
        var curPosition, curLeft, curCSSTop, curTop, curOffset, curCSSLeft, calculatePosition,
            position = jQuery.css( elem, "position" ),
            curElem = jQuery( elem ),
            props = {};

        // Set position first, in-case top/left are set even on static elem
        if ( position === "static" ) {
            elem.style.position = "relative";
        }

        curOffset = curElem.offset();
        curCSSTop = jQuery.css( elem, "top" );
        curCSSLeft = jQuery.css( elem, "left" );
        calculatePosition = ( position === "absolute" || position === "fixed" ) && ( curCSSTop + curCSSLeft ).indexOf("auto") > -1;

        // Need to be able to calculate position if either top or left is auto and position is either absolute or fixed
        if ( calculatePosition ) {
            curPosition = curElem.position();
            curTop = curPosition.top;
            curLeft = curPosition.left;

        } else {
            curTop = parseFloat( curCSSTop ) || 0;
            curLeft = parseFloat( curCSSLeft ) || 0;
        }

        if ( jQuery.isFunction( options ) ) {
            options = options.call( elem, i, curOffset );
        }

        if ( options.top != null ) {
            props.top = ( options.top - curOffset.top ) + curTop;
        }
        if ( options.left != null ) {
            props.left = ( options.left - curOffset.left ) + curLeft;
        }

        if ( "using" in options ) {
            options.using.call( elem, props );

        } else {
            curElem.css( props );
        }
    }
};


jQuery.fn.extend({

    position: function() {
        if ( !this[ 0 ] ) {
            return;
        }

        var offsetParent, offset,
            elem = this[ 0 ],
            parentOffset = { top: 0, left: 0 };

        // Fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is it's only offset parent
        if ( jQuery.css( elem, "position" ) === "fixed" ) {
            // We assume that getBoundingClientRect is available when computed position is fixed
            offset = elem.getBoundingClientRect();

        } else {
            // Get *real* offsetParent
            offsetParent = this.offsetParent();

            // Get correct offsets
            offset = this.offset();
            if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
                parentOffset = offsetParent.offset();
            }

            // Add offsetParent borders
            parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true );
            parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true );
        }

        // Subtract parent offsets and element margins
        return {
            top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
            left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true )
        };
    },

    offsetParent: function() {
        return this.map(function() {
            var offsetParent = this.offsetParent || docElem;

            while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position") === "static" ) ) {
                offsetParent = offsetParent.offsetParent;
            }

            return offsetParent || docElem;
        });
    }
});


// Create scrollLeft and scrollTop methods
jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) {
    var top = "pageYOffset" === prop;

    jQuery.fn[ method ] = function( val ) {
        return jQuery.access( this, function( elem, method, val ) {
            var win = getWindow( elem );

            if ( val === undefined ) {
                return win ? win[ prop ] : elem[ method ];
            }

            if ( win ) {
                win.scrollTo(
                    !top ? val : window.pageXOffset,
                    top ? val : window.pageYOffset
                );

            } else {
                elem[ method ] = val;
            }
        }, method, val, arguments.length, null );
    };
});

function getWindow( elem ) {
    return jQuery.isWindow( elem ) ? elem : elem.nodeType === 9 && elem.defaultView;
}
// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
    jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) {
        // margin is only for outerHeight, outerWidth
        jQuery.fn[ funcName ] = function( margin, value ) {
            var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
                extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );

            return jQuery.access( this, function( elem, type, value ) {
                var doc;

                if ( jQuery.isWindow( elem ) ) {
                    // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
                    // isn't a whole lot we can do. See pull request at this URL for discussion:
                    // https://github.com/jquery/jquery/pull/764
                    return elem.document.documentElement[ "client" + name ];
                }

                // Get document width or height
                if ( elem.nodeType === 9 ) {
                    doc = elem.documentElement;

                    // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height],
                    // whichever is greatest
                    return Math.max(
                        elem.body[ "scroll" + name ], doc[ "scroll" + name ],
                        elem.body[ "offset" + name ], doc[ "offset" + name ],
                        doc[ "client" + name ]
                    );
                }

                return value === undefined ?
                    // Get width or height on the element, requesting but not forcing parseFloat
                    jQuery.css( elem, type, extra ) :

                    // Set width or height on the element
                    jQuery.style( elem, type, value, extra );
            }, type, chainable ? margin : undefined, chainable, null );
        };
    });
});
jQuery动画部分源码

 

 

 

 

 

 

posted @ 2017-04-19 23:00  bobo的学习笔记  阅读(2211)  评论(0编辑  收藏  举报