chaojidan

导航

第三十八课:动画引擎的实现

本课将通过源码分析的形式,来教大家如何实现一个动画引擎的模块。

我们先来看一个使用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]

}

此课,内容太多,难度太大,能看懂多少,就看懂多少吧,上面的这个转换颜色的方法,请看懂,大公司社招可能会问。

 

 

 

加油!

posted on 2015-01-07 18:26  chaojidan  阅读(1173)  评论(1编辑  收藏  举报