首页 > 代码库 > 第三十八课:动画引擎的实现
第三十八课:动画引擎的实现
本课将通过源码分析的形式,来教大家如何实现一个动画引擎的模块。
我们先来看一个使用CSS3实现动画倒带的例子:
.animate { //这个animate类名加在上面的那个方块元素中,这个类名也可以是其他名字,比如:.move,只要设置的是那个方块元素就OK了。
animation-duration:3s;
animation-name:cycle;
animation-iteration-count:2; //动画播放的次数
animation-direction: alternate; //是否应该轮流反向播放动画。如果 animation-direction 值是 "alternate",则动画会在奇数次数(1、3、5 等等)正常播放,而在偶数次数(2、4、6 等等)向后播放。注释:如果把动画设置为只播放一次,则该属性没有效果。
}
@keyframes cycle { //设置这个动画的初始位置和目的位置
from{ width:100px;height:100px; }
to{ width:700px; height: 700px; }
}
此动画先放大,然后再缩回原状。
接下来,我们真正的进入到js实现动画引擎的代码分析:
下面的实现原理:我们搞一个中央队列,其实就是一个数组timeline,只要它里面有元素(动画对象),它就驱动setInterval执行动画,如果动画执行结束,它就会从数组中删除这个动画对象,然后再检测此数组中还有没有元素,没有,就clearInterval,否则,就继续。这种实现原理在YUI,kissy框中使用,而jQuery的实现原理不是这样的,jQuery提供一个queue的参数,目的让作用于同一个元素的动画进行排队,执行完这个后再处理下一个,jQuery的queue是放在元素对应的缓存系统上的,里面有一个Promise对象,Promise对象的状态完成后,就会自动弹出下一个动画对象,所有的动画对象都有自己的setInterval驱动。
$.fn.animate = $.fn.fx = function(props){ //props为元素的样式属性集合,也叫做关键帧,animate方法就是用来添加关键帧的。
var opts = addOptions.apply(null, arguments) , p; //opts就是设置动画的所需时长,缓动公式,结束时执行的回调函数,以及before,after函数的
for(var name in props){ //如果第一个参数不是数字,而是一个对象
p = $.cssName(name) || name; //把样式属性名进行转换
if(name !==p){
props[p] = props[name]; // 比如:$("div").animate({border-top-width:100,float:left});会把props转换成{borderTopWidth:100,cssFloat(styleFloat):left}
delete props[name];
}
}
for(var i=0,node;node = this[i++];){ //$("div").animate({}),当页面有多个div元素时,这里被选择的元素将有多个,我们必须对这几个div进行循环遍历处理
insertFrame(
$.mix({
positive:[], //正向队列
negative:[], //反向队列
node:node, //元素节点
props:props //props是关键帧的样式集合,相当于css3中@keyframes定义的样式规则
},opts); //opts中定义的是动画的基本属性,也就是.animate中定义的动画的变化规则。
); //最后把这些属性值弄成一个json对象,传进insertFrame方法中,进行动画的执行。
}
return this;
}
function addOptions(props){
if(isFinite(props)){ //如果 props 是有限数字(或可转换为有限数字),那么返回 true。否则,如果props是 NaN(非数字),或者是正、负无穷大的数,则返回 false。 比如:$("div").animate(3);
return { duration:props};
}
var opts = {};
for(var i=1;i<arguments.length;i++){ //如果在animate方法中传入了第二个参数,第三个参数....
addOption(opts ,arguments[i]);
}
opts.duration = typeof opts.duration ==="number" ? opts.duration : 400; //如果第二个参数,第三个参数...中有数字类型,那么就返回这个数字类型,如果没有,就把opts.duration = 400;
opts.queue = !!(opts.queue ==null || opts.queue); //这里的opts.queue是undefined,因此这里返回true,也就是默认进行排队操作
opts.easing = $.easing[opts.easing] ? opts.easing : "swing"; //如果第二个参数,第三个参数...中有字符串类型,那么就判断这个字符串是否是缓动公式的名字,如果是就直接返回,如果不是,就设置opts.easing = "swing",默认动画的缓动公式为swing。
return opts;
}
function addOption(opts, p){
switch($.type(p)){ //判断animate方法中第二个参数,第三个参数.....,的类型
case "Object":
addCallback(opts, p , "after");
addCallback(opts, p , "before");
$.mix(opts, p); //把第二个参数,第三个参数....,中的其他属性值赋给opts。
break;
case "Number": //如果是数字,就直接赋给opts的duration属性。
opts.duration = p;
break;
case "String": //如果是字符串,就直接赋给opts的easing属性
opts.easing = p;
break;
case "Function": //如果是函数,就直接赋给opts的complete属性,比如:$("div").animate({},function(){alert(1)});这时,opts = {complete:function(){alert(1)}},当然,第三个参数,以及后面的参数,会覆盖同类型的属性值。比如:$("div").animate({},function(){alert(1)},function(){alert(2)}),complete会变成弹出2的那个函数。
opts.complete = p;
break;
}
}
function addCallback(target, source, name){ //这里的source的类型是Object,name是after或者是before
if(typeof source[name] === "function"){ //查看animate的第二个参数,第三个参数...里面是否有after或者before的函数
var fn = target[name]; //addOptions方法中私有的opts对象中是否有after或before函数
if(fn){ //如果有,就重写opts对象中的同名函数,我们假设$("div").animate({},{before:function(){alert(1)}},{before:function(){alert(2)}}),第一次判断时,opts对象是{},里面没有before函数,因此把opts[before] = function(){alert(1)};,第二次判断时,opts对象是{before:function(){alert(1)}},因此重写opts对象的before方法,此时opts = { before : function(node,fx){ (function(){alert(1)})(node,fx); (function(){alert(2)})(node,fx) ; }}
target[name] = function(node, fx){
fn(node, fx);
source[name](node,fx);
};
}else{
target[name] = source[name];
}
}
delete source[name]; //如果第二个参数,第三个参数....,中的before和after的属性值不是函数,那么就直接删除,如果是函数,赋值给opts后,也删除。
}
var timeline = $.timeline = [];
function insertFrame( frame ){
if(frame.queue){ //在addOptions方法中,默认设置queue为true,也就是动画默认支持队列操作
var gotoQueue = 1;
for(var i= timeline.length,el;el = timeline[--i];){ //timeline默认为空,所以这里第一次不执行
if(el.node === frame.node){ //当对同一个元素节点进行多个动画操作时,只有第一个动画才会马上执行,而其他动画会先保存在此动画对象的positive数组中,只有等第一个动画执行接受,才会取出第二个动画对象进行执行,直到positive数组中的所有动画都执行完
el.positive.push(frame);
gotoQueue = 0;
break;
}
}
if(gotoQueue){
timeline.unshift(frame); //从数组的前面插入此json对象frame。
}
}else{
timeline.push(frame); //如果不用排队,也就是针对一个元素的多个动画对象要同时执行,那么就添加到timeline数组中
}
if(insertFrame.id === null){ //第一次执行时,这里的id为null,因此执行
insertFrame.id = setInterval(deleteFrame , 1000 / $.fps); //fps是刷新率,1000除以fps就代表,多少毫秒需要进行一次帧的切换
}
}
insertFrame.id = null;
function deleteFrame(){
var i = timeline.length; //这里指的是动画的个数
while(--i >= 0){
if(!timeline[i].paused){ //如果动画没有被暂停,正常情况下,这里的paused是undefined
if(!(timeline[i].node && enterFrame(timeline[i],i))){ //这里node就是元素节点,然后执行enterFrame方法,如果此方法返回false,就进入if语句,只要进入了if语句,就会删除数组中的选项,动画就会结束,因此enterFrame方法,只有当动画结束时,才会返回false。
timeline.splice(i,1);
}
}
}
timeline.length || (clearInterval(insertFrame.id), insertFrame.id = null); //如果timeline数组为0,就取消定时器,并且把定时器的id置为null。
}
function enterFrame(fx, index){ //这里的fx其实就是insertFrame中的frame对象
var node = fx.node, now = +new Date(); //node就是元素节点
if(!fx.startTime){ //第一次执行时frame没有这个属性,因此进入if语句
callback(fx, node , "before"); //动画开始时,进行一些准备工作
fx.props && parseFrames(fx.node, fx, index); //这里的props就是调用animate方法时,传入的第一个参数值。parseFrames方法很复杂,这里就不贴出来了,此方法的作用,就是根据animate方法中得到的json对象,生成两个关键帧,存入props属性中。[第一个关键帧,第二个关键帧]
fx.props = fx.props || [];
AnimationPreproccess[fx.method || "noop"](node, fx); //这里的fx没有method属性,因此调用noop方法,这里的fx.method属性值可以是show或hide或toggle等三个属性值。因为在进行show,hide,toggle这三种动画效果时,要对样式进行一些预处理操作。
fx.startTime = now;
}else{ //第二次执行时,fx.startTime已经存在了,因而进入else语句
var per = (now - fx.startTime) / fx.duration; //动画执行的时间除以总时间,得到动画的进度0-1之间的数字
var end = fx.gotoEnd || per >=1; //gotoEnd属性默认为undefined,但是你可以通过stop方法强制让它变成true,这样动画就会马上停止了。当进度>=1时,也意味着动画应该停止了。
var hooks = effect.updateHooks;
if(fx.update){ //这里的update在调用parseFrames方法时,如果样式需要做兼容处理,这里则会赋值为true。
for(var i =0,obj; obj=fx.props[i++];){ //props = [第一个关键帧,第二个关键帧];
(hooks[obj.type] || hooks._default)(node, per, end,obj); //这里的hooks有三个属性,一个是color,一个是scroll,一个是默认值_default,针对每一个关键帧的type类型,进行函数的调用,如果type类型不是color或者scroll,那么就调用默认的_default方法。这里的hooks就是真正实现元素变化的地方,也就是元素出现动画效果的地方。
}
}
if(end){ //如果动画结束,也就是动画的最后一帧
callback(fx, node, "after"); //动画结束后,进行一些收尾工作
callback(fx, node , "complete"); //执行动画完成时的用户回调函数
if(fx.revert && fx.negative.length){ //如果设置了动画倒带操作,并且动画的negative数组存在动画对象,就进入if语句,根据我们的例子,这里不会进入if语句
Array.prototype.unshift.apply(fx.positive, fx.negative.reverse()); //把倒带数组中的动画对象放到正向数组中
fx.negative = []; //清空倒带数组
}
var neo = fx.positive.shift(); //根据我们的例子,这里的positive数组为空,因此取数组的第一项,也是空,如果对此元素有两个或以上的动画操作,这里将返回第二个动画对象,重复第一个动画的操作,执行第二个动画。
if(!neo){
return false; //如果为空,就停止定时器的运转,结束此动画的操作
}
timeline[index] = neo; //如果存在排队的动画,让它继续
neo.positive = fx.positive;
neo.negative = fx.negative;
}else{
callback(fx, node , "step"); //每执行一帧,就执行的回调函数
}
}
return true;
}
function callback(fx, node ,name){ //假设这里的name="before"
if(fx[name]){ //animate的第二个参数,第三个参数...中是否有before的函数,比如,$("#div1").animate({},{"before":function(){}});
fx[name](node,fx); //如果有,就执行这个before函数
}
}
var AnimationPreproccess = {
noop: function(){},
show : function(node, frame){ //node为元素节点
if(node.nodeType ===1 && $.isHidden(node)){ //只有元素节点,并且是隐藏的,才有show操作
这里就是把元素的display改为block,但是对于像li,td,tr,tbody,table这样的元素,它们有默认的display的值,如果强行改成block,布局就会走形,因此需要做兼容处理。根据元素的nodeName设置不同的display。
如果需要对内联元素,比如:span,em等进行缩放操作(设置元素的width或height),我们需要设置内联元素的display为inline-block。但是老版本IE浏览器需要开启hasLayout才能生效。要让老版本IE拥有布局,只需要让元素节点node.style.zoom = 1;就行了。
}
}
},
hide:function(node , frame){
这里就是将显示的元素隐藏起来,由于它对应的动画效果是从大到小(设置元素的width和height),这时进行动画的那个元素的子元素可能会超出父元素的大小。因此我们需要设置元素的overflow:hidden,在动画结束后,还原回来。此外,还原的样式值还有宽,高,边框,透明度等。(除了IE浏览器,如果你改写了overflow-x和overflow-y为同一个值,比如:hidden,那么它的overflow就会变成那个值,比如:hidden,但是IE下overflow不会改变。)
},
toggle:function(){
AnimationPreproccess[$.isHidden(node) ? "show" : "hide"](node,fx);
}
}
effect.updateHooks = {
_default:function(node, per, end, obj){ //node是元素节点,per是动画的进度,end动画是否结束,obj关键帧对象
$.css(node, obj.name , (end? obj.to : obj.from + obj.easing(per) * (obj.to-obj.from)) + obj.unit); //设置元素节点的样式属性obj.name,进度per传入缓存公式中得到它的最终进度,然后乘以总距离,最后加上此帧在整个动画的位置,得到最终值
},
color:function(node, per, end, obj){
var pos = obj.easing(per);
var rgb = end? obj.to : obj.from.map(function(from, i){ //如果是颜色,那么就需要处理三个值,也就是rgb,from是一个数组[r,g,b]
return Math.max( Math.min(parseInt(from+(obj.to[i]-from)*per,10),255),0);
})
node.style[obj.name] = "rgb(" + rgb + ")"; //假设这里的rgb = [33,33,33],当数组与字符串相加时,会把数组转换成字符串,也就是rgb会转换成"33,33,33",
因此node.style.color = "rgb(33,33,33)";设置颜色值。这里把颜色值转换成数组形式的rgb[r,g,b]是在parseFrame中进行的。而parseFrame是调用parseColor实现的。
},
scroll:function(node, per, end, obj){
node[obj.name] = (end? obj.to : obj.from + obj.easing(per) * (obj.to-obj.from));
}
}
var colorMap = {
"black":[0,0,0],
"gray":[128,128,128],
"white":[255,255,255],
"red":[255,0,0],
"green":[0,255,0],
"yellow":[255,255,0],
"blue":[0,0,255]
}
$.parseColor = function(color){
var color = color.toLowerCase();
if(colorMap[color]){ //处理颜色名
return colorMap[color];
}
if(color.indexOf("rgb") == 0){ //如果是rgb格式的,比如:"rgb(33,33,33)"或者"rgb(33%,33,33)"
var match = color.match(/(\d+%?)/g); //match = [33,33,33]或 [33%,33%,33%]
var factor = match[0].indexOf("%") != -1 ? 2.55 :1; //如果是百分数,factor就是2.55,因为这里的百分数已经乘以100了,所以这里只需要乘以2.55就能转化成数字形式了。这里无法处理[33%,33%,33]混合情况。
return colorMap[color]=[ parseInt(match[0]) * factor , parseInt(match[1]) * factor , parseInt(match[2]) * factor ];
}else if(color.chatAt(0) == "#"){ //如果是16进制格式的,比如:"#ffffdd"
if(color.length === 4){ //"#fff",这种情况
color = color.replace(/([^#])/g,"$1$1"); //这个正则的意思就是只要不是"#"就匹配,因此f匹配,被替换成$1$1,而$1代表的是第一个子表达式匹配的元素,也就是f,因此f被替换成ff,最后color = "#ffffff"。
}
var ret = [];
color.replace(/\w{2}/g,function(match){ //这里的match就是ff,每次替换两个字符
ret.push(parseInt(match,16)); //把ff这种16进制的数字,转换成10进制的数字,这里的ff转换成255,然后存入数组ret中,ret最后变成[255,255,255]
});
return colorMap[color] = ret;
}
return colorMap.white; //如果都不匹配,就返回[255,255,2555]
}
此课,内容太多,难度太大,能看懂多少,就看懂多少吧,上面的这个转换颜色的方法,请看懂,大公司社招可能会问。
加油!
第三十八课:动画引擎的实现