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


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

    我们先来看一个使用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{  100px;height:100px;  }

      to{    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-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]

    }

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

    加油!

  • 相关阅读:
    反流技术之IE插件技术研究第一部分
    c# post和接收的实现
    C# post提交表单的例程
    用C#搭建IE BHO勾子, 取表单密码
    复杂的 DataBinding 接受 IList 或 IListSource 作为数据源" 错误原来是自己的笔误
    C#判断ContextMenuStrip右键菜单的来源(从哪个控件弹出来的)
    练习5.1
    示例:实用函数(Utilities)
    闭包
    一个错误
  • 原文地址:https://www.cnblogs.com/chaojidan/p/4205559.html
Copyright © 2020-2023  润新知