在这之前,大家应该了解了缓动函数(Easing Functions)的概念:
动画的每一帧需要计算一次元素样式,如果样式改变则需要重绘屏幕。细一点讲,当我们每调用一次计时器函数,需要通过向缓动函数传入一些动画上下文变量,从而获取到元素的某个样式在当前帧合理的值。
我所了解的缓动函数实现方式有两种,一种是tbcd方式(Robert Penner's Easing Functons)
function(t,b,c,d){ return c*t/d + b; }
t: timestamp 以毫秒(ms)为单位,指从第一帧到当前帧所经历的时间
b: beginning position,变量初始值
c: change 变量改变量(即在整个动画过程中,变量将从 b 变到 b + c)
d: duration 动画时间
另一种是mootools的单参数方式,由于我没了解过,这里就不说了,这里主要说一下第一种方式。
整个动画模块为Animation,其接受多个参数(元素, 动画样式, 持续时间[, 缓动函数名][, 回调函数]),是一个构造函数,调用方式为:
var animation = new Animation(test, {width: {value: "500px"}, 500, "sin", function(){ console.log("complete"); });
animation.stop();
其中,每个样式属性可单独指定持续时间与缓动函数名,但回调函数必须等到所有动画结束才调用。
Animaion模块定义如下:
1 var Animation = function(){ 2 3 var debug = false, //如果debug,遇到异常将抛出 4 unit = {}, //样式存取函数,详见下方each函数 5 fx = { //缓动函数 6 linear: function(currentTime, initialDistance, totalDistance, duration){ //自带一个线性缓动函数 7 return initialDistance + (currentTime / duration * totalDistance); 8 } 9 }, 10 getTime = function(){ //获取当前时间(ms或更精确) 11 return window.performance.now && performance.now() || new Date().getTime(); 12 }, 13 executorCanceler = window.cancelAnimationFrame, //取消帧函数 14 executor = window.requestAnimationFrame //帧执行函数 15 || window.webkitRequestAnimationFrame 16 || window.msRequestAnimationFrame 17 || window.mozRequestAnimationFrame 18 || window.oRequestAnimationFrame 19 || function(){ 20 var callbacks = []; 21 22 !function frame(){ 23 var oldTime = getTime(), 24 tmp = callbacks; 25 26 callbacks = []; 27 28 for(var i = 0, length = tmp.length; i < length; i++){ 29 tmp[i].callback(oldTime); 30 } 31 32 var currentTime = getTime(), 33 delayTime = Math.max(16.66 - currentTime + oldTime, 0); 34 35 setTimeout(frame, delayTime); 36 }(); 37 38 executorCanceler = function(id){ 39 for(var i = 0, length = callbacks.length; i < length; i++){ 40 if(callbacks[i].id === id) callbacks.splice(i, 1); 41 } 42 } 43 44 return function(callback){ 45 var context = {callback: callback, id: Math.random()}; 46 callbacks.push(context); 47 return context.id; 48 } 49 }(), 50 /* 51 * 为每个属性运行此函数,类似于启动一个线程(虽然不是真正的线程) 52 */ 53 animate = function(element, attribute, distances, duration, timingFunction, completeCallback){ 54 var oldTime = getTime(), 55 animationPassedTime = 0, 56 executorReference = executor(function anonymous(currentTimeStamp){ 57 animationPassedTime = currentTimeStamp - oldTime; 58 59 var computedValues = []; //computedValues为缓动函数计算值,可能返回数值或者数组(按动画属性不同,比如rgb) 60 61 if(animationPassedTime >= duration){ 62 if(distances.length > 1){ 63 for(var j = 0, length = distances.length; j < length; j++){ 64 computedValues.push(distances[j][0] + distances[j][1]); 65 } 66 } else { 67 computedValues = distances[0][0] + distances[0][1]; 68 } 69 70 stop(); 71 } else { 72 if(distances.length > 1){ 73 for(var i = 0, length = distances.length; i < length; i++){ 74 computedValues.push(fx[timingFunction](animationPassedTime, distances[i][0], distances[i][1], duration)); 75 } 76 } else { 77 computedValues = fx[timingFunction](animationPassedTime, distances[0][0], distances[0][1], duration); 78 } 79 80 animationPassedTime = getTime() - oldTime; 81 executorReference = executor(anonymous); 82 } 83 unit[attribute].setter(element, computedValues); 84 }, Math.random()), 85 completed = false, 86 stop = function(){ 87 executorCanceler(executorReference); 88 completeCallback(); //执行回调函数 89 }; 90 91 return { 92 stop: stop 93 } 94 }, 95 /* 96 * Animation 引用的函数,此函数返回一个包含所有动画属性的控制对象(如停止操作),因此可以采取函数调用或者new的方式创建一个动画对象 97 */ 98 init = function(element, animationVars, duration, timingFunction, callback){ 99 100 var animateQueue = {}, animationCount = 0, animationCompletedCount = 0, completeCallback = function(){ 101 return function(){ //每个animate完成后调用此函数,当计数器满调用callback 102 animationCompletedCount ++; 103 104 if(animationCount === animationCompletedCount){ 105 typeof timingFunction === "function" ? timingFunction() : callback && callback(); 106 } 107 } 108 }(); 109 110 if(!element.nodeType){ 111 if(debug) 112 throw "an htmlElement is required"; 113 return; 114 } 115 116 for(var attribute in animationVars){ 117 if(!(attribute in unit)){ 118 if(debug){ 119 throw "no attribute handler"; 120 } 121 122 return; 123 } 124 125 try { 126 var initialDistance = unit[attribute].getter(element), 127 finalDistance = unit[attribute].getter(animationVars[attribute].value || animationVars[attribute]), 128 distances = []; 129 130 if(typeof initialDistance === "number"){ 131 distances.push([initialDistance, finalDistance - initialDistance]); 132 } else { 133 for(var i = 0, length = initialDistance.length; i < length; i++){ 134 distances.push([initialDistance[i], finalDistance[i] - initialDistance[i]]); 135 } 136 } 137 /* 138 * 可以为每个属性指定缓动函数与时间 139 */ 140 animateQueue[attribute] = animate(element, attribute, distances, animationVars[attribute].duration || duration, animationVars[attribute].timingFunction || (typeof timingFunction === "string" ? timingFunction : false) || "linear", completeCallback); 141 } catch (e) { 142 if(debug) { 143 throw "an error occurred: " + e.stack; 144 } 145 146 return; 147 } 148 149 animationCount ++; 150 } 151 152 animateQueue.stop = function() { 153 for(var attribute in animateQueue) { 154 animateQueue[attribute].stop && animateQueue[attribute].stop(); 155 } 156 } 157 158 return animateQueue; 159 }; 160 161 init.config = function(configVars){ 162 if(configVars){ 163 if(configVars.fx) { 164 for(var fxName in configVars.fx){ 165 if(typeof configVars.fx[fxName] === "function"){ 166 fx[fxName] = configVars.fx[fxName]; 167 } 168 } 169 } 170 171 if(configVars.unit) { 172 for(var unitName in configVars.unit){ 173 if(typeof configVars.unit[unitName] === "object"){ 174 unit[unitName] = configVars.unit[unitName]; 175 } 176 } 177 } 178 179 if(configVars.debug) { 180 debug = configVars.debug || false; 181 } 182 } 183 }; 184 185 init.each = function(array, handler){ 186 if(typeof handler === "function"){ 187 for(var i = 0, length = array.length; i < length; i++){ 188 handler.call(array[i], i, array); 189 } 190 } 191 }; 192 193 /* 194 * 赠送几个单位存取函数(暂时实现行内样式读取,单位px -。-) 195 */ 196 init.each("width, height, left, right, top, bottom, marginLeft, marginTop".split(/\s*,\s*/), function(index, array){ 197 var attributeName = this; 198 unit[attributeName] = { 199 getter: function(element){ 200 return parseInt((element.nodeType && element.style[attributeName] || element)["match"](/\d+/)[0]); 201 }, 202 setter: function(element, value){ 203 element.style[attributeName] = value + "px"; 204 } 205 } 206 }); 207 208 return init; 209 210 }();
测试如下(需引入Animation):
详见:http://runjs.cn/code/lgrfeykn
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title></title> 5 <script type="text/javascript"> 6 7 8 function init(){ 9 10 Animation.config({ //可以在这里设置或扩充功能 11 debug: true, 12 fps: 60, 13 fx: { 14 easeOutElastic: function(t,b,c,d){ 15 var s=1.70158;var p=0;var a=c; 16 if (t==0) return b; if ((t/=d)==1) return b+c; if (!p) p=d*.3; 17 if (a < Math.abs(c)) { a=c; var s=p/4; } 18 else var s = p/(2*Math.PI) * Math.asin (c/a); 19 return a*Math.pow(2,-10*t) * Math.sin( (t*d-s)*(2*Math.PI)/p ) + c + b; 20 } 21 }, 22 unit: { 23 backgroundColor: { // 24 getter: function(element){ 25 var backgroundColor = (element.nodeType && element.style.backgroundColor || element)["match"](/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); 26 return [parseInt(backgroundColor[1]), parseInt(backgroundColor[2]), parseInt(backgroundColor[3])]; 27 }, 28 setter: function(element, value){ 29 element.style.backgroundColor = "rgb(" + parseInt(value[0]) + ", " + parseInt(value[1]) + ", " + parseInt(value[2]) + ")"; 30 } 31 } 32 } 33 }); 34 35 36 37 var animation = new Animation(test, {width: {value: "100px"}, height: {value: "100px"}, marginLeft: {value: "50px"}, marginTop: {value: "50px"}, backgroundColor: {value: "rgb(203,215,255)"}}, 1000, "easeOutElastic", function(){ 38 console.log("complete"); 39 }); 40 41 } 42 43 </script> 44 </head> 45 <body onload="init();"> 46 47 <div id="test" style="width: 200px; height: 200px; background: rgb(255,104,228);margin-left: 0; margin-top: 0"></div> 48 49 </body> 50 </html>