原生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(); }
缓动效果: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; } } };
具体实现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); }, };
预定义动画和预定义动画序列
// 实现一些自定义动画 ; (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);
测试代码:
<!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>
附上一份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 ); }; }); });