JavaScript动画浅析

  原文地址:http://floatyears.info/javascript-animate

  最近研究了一下JavaScript的动画,虽然用那些库的动画方法用起来非常方便,不过真正自己来写就有很多问题了。

Javascript的动画主要是通过setTimeout和setInterval两个函数来进行的。setTimeout可以模拟出setInterval的效果,同时setTimeout还能够改变执行的时间。下面是三个例子:

var n = 0;
          function a(){
               var timeoutId = setTimeout(a,100);
               console.log('n:'+n);
               n++;
          }
          a();
 
var m = 0;
function b(){
               var intervalId = setInterval(function(){
                    console.log('m:'+m);
                    m++;
               },1000)
          }
          b();
 
var t = 100;
function c(){
               var timeoutId = setTimeout(a,t);
               console.log('t:'+t);
               t+=100;
          }
          c();

  

      执行函数a()的效果和执行函数b()的效果是一样的,说明setTimeout可以模拟出于setInterval一样的效果。执行函数c()之后我们发console里面记录的时间间隔越来越长,说明setTimeout可以实时改变间隔时间,而setInterval就没有这样的功能,这就为我们写动画提供了另外的思路,将在讲到。

       那么动画就先选用setInterval函数。setInterval(function(){...},time),每隔一段时间执行一次函数。动画实际上就是一些图像连续的闪过,当达到每秒24帧之后人眼就分不出每个单独的画面了。我们设定每秒执行25次,那么动画的效果就比较连贯了,如果不满意可以设定得更高。比如setInterval(function(){...},40)。js里面,要形成动画,就需要改变一些css属性。我们需要以下的几个函数:

function camlize(prop){
     return prop.replace(/\-(\w)/g,function(strMatch,p1){
          return p1.toUpperCase();
     });
}
function uncamlize(prop,sep){
     return prop.replace(/[A-Z]/g,function(strMatch){
          return ((sep?sep:"-") + strMatch.toLowerCase());
     })
}
 
function getStyle(elem,props,pseudoEl){
     var styles = {};
     if(elem.currentStyle){
          for(var i = 0; i < props.length; i++){
               if(!(styles[props[i]] = elem.currentStyle[props[i]])) continue;
          }
     }
     if(window.getComputedStyle){
          for(var i = 0; i < props.length; i++){
               if(!(styles[props[i]] = window.getComputedStyle(elem,pseudoEl).getPropertyValue(props[i]))) continue;
          }
          
     }
     return styles;
}
 
function setStyleById(elem,styles){
     if(!(elem = $(elem))) return false;
     for(var prop in styles){
          if(!styles.hasOwnProperty(prop)) continue;
          if(elem.style.setProperty){
               elem.style.setProperty(camlize(prop),styles[prop],'important');
          }else{
               elem.style[camelize(prop)] = styles[prop];
          }
     }
}
 
 
function setStyleByClassName(className,styles){
     if(!(elems = getByClassName(className))) return false;
     for(var i = 0; i < elems.length; i++){
          setStyleById(elems[i],styles);
     }
}

  

       这几个函数可以分别获得和设置函数。camlize()是用来将'background-color'转化为'backgroundColor'的,因为在W3C标准中设置style时,必须使用驼峰式的属性名。

       有了控制样式的这几个函数,下面的问题就是改变样式的过程是怎么样的?这就涉及到了缓动效果。考虑最简单的匀速直线运动。每40ms执行一次函数,而我们能够控制的是元素的样式,比如一个50px*50px的正方形,花1s的时间从左到右移动了100px。我们用left的值来表示这个元素的位置,40ms的时候,位置为{left:4px},80ms的时候位置为{left:8px},120ms的时候位置为{left:12px}……

那么用代码可以表示为:

var left = 0;
var lineId = setInterval(function(elem){
     setStyleById(elem,{'left':left+'px'});
     left+=4;
     if(left > 100) clearInterval(lineId);
},40)

查看示例

       接着下一个问题:如何引入其他的缓动效果?上面的例子是直线运动,可以用left = left + 4这样来表示,不过其他的效果就要用到Easing函数了。Easing的具体内容可以参考《JavaScript动画Easing缓动函数》。整个运动过程中,有4个量是已知的。第一个是变化的量changeValue,第二个是持续时间duration,第三个是初始值beginValue,第四个是当前时间currentTime。而我们能够控制的是当前的值,即currentValue,就像上面的left值一样,我们在不同的时间定义currentValue不同的值,就形成了动画,currentValue的变化曲线不同,就会得到不同的缓动效果。上面的例子增加一个二次方缓动的效果,代码如下:

var left = 0, curTime = 0;
var lineId = setInterval(function(elem){
     setStyleById(elem,{'left':left+'px'});
     left = Easing.easeInQuad(currTime,0,100,1);
     currTime += 40;
     if(left > 100) clearInterval(lineId);
},40)

查看示例

       在上面的例子中,我们会发现如果在运动过程中多次触发了运动函数(mouseover即触发函数),运动会出现抖动的情况。出现样的效果是因为前面一个动画没有结束,后面又触发了一个动画,两个动画叠加导致的。为了防止这种情况发生,我引入一个动画队列机制,但是发现有点困难。队列中存储要存储什么呢?开始的想法是用闭包存储setInterval的id值,但是动画是在一个具体的元素上面进行的,我们并不知道哪些id值是属于这个元素。接下来,容易想到在这个元素上面绑定一些信息,例如将动画的setInterval id值绑定在上面,动画执行完,清空id,如果检测到有id值不为空,则将后面的动画延迟。看了一下jQuery的queue()函数,发现机制跟上面类似。queue函数里面,用data()函数将相关的动画信息绑定在元素上。调用dequeue()则是用unshift方法把队列前端的动画取出并执行。

       而另外一个办法,面向对象的办法,将信息保存在实例中,这样,调用前就能知道有没有动画在某个元素上面进行了。

       后来发现JavaScript里面,对象元素是可以互相比较的。例如alert(document.getElementsByTagName('ul')[0] == document.getElementsByTagName('ul')[1]);返回值为false。这样就可以在闭包里面存储对应的元素了,而将元素动画的id值保存在与这个元素中相关的对象中。通过比较元素对象,我们可以得知当前触发的动画所在的元素上面是否正在进行动画。代码如下:

var Queue = (function(){
//var queues = [];
var queue = [];
return {
addQueue:function(elem,animateId,dur,delay){
if(queue.length > 0){
for(var i = 0, len = queue.length; i < len; i++){
if(queue[i].element == elem){
queue[i].animateId.push(animateId);
var a = queue[i].animateId;
clearInterval(a[a.length-2]);
return;
}
}
queue.push({element:elem,animateId:[animateId],duration:dur});
}else{
queue.push({element:elem,animateId:[animateId],duration:dur});
}
},
deQueue:function(elem,animateId){
for(var i = 0, len = queue.length; i < len; i++){
if(queue[i] && queue[i].elem == elem){
clearInterval(animateId);
queue[i].animateId.splice(i,1);
console.log(Queue.queue);
}
}
//queue[elem] = null;
}
}
})();

  队列机制有了之后,animate()函数的问题就基本解决了。下面是animate代码:

function animate(elem,from,to,dur,fx,func,fps){
     fps = fps || 25;
     var sep = parseInt(1000/fps),curTime = 0, times = parseInt(fps*dur), changes = {}, curVal = {},dur = dur*1000;
     for(var prop in to){
          var props = [];
          props.push(prop);
          from[prop] = parseInt((from[prop]?from[prop]:getStyle(elem,props)[prop]).replace(/\D*/g,''));
          to[prop] = parseInt(to[prop].replace(/\D*/g,''));
          changes[prop] = parseInt(to[prop] - from[prop]);
     }
     //if(getStyle(elem,['display'])['display'] == 'none'){setStyleById(elem,{'display':'block'});}
     var animateId = setInterval(function(){
          if(curTime > dur) {
               clearAnimate(elem,animateId)
               console.log('animate was dequeue');
          }
          for(var prop in from){
               curVal[prop] = fx(curTime,from[prop],changes[prop],dur) + 'px';
          }
          setStyleById(elem,curVal);
          curTime += sep;
     },sep);
     Queue.addQueue(elem,animateId,dur);
}

       用animate()函数写了一个完整的弹出菜单例子出来,查看示例。完整的文件可以查看Github:https://github.com/floatyears/Javascript-Library

       测试发现IE9下面有问题,Firefox下面开始也没有效果,但是后来发现style.setProperty()这个函数在FF下面必须要三个参数才行,例如:setProperty('background','black',''important'),'important'不能省略,不然会提示'not enough arguments'。修改后发现仍然有点问题,估计是style的操作的兼容性问题吧。而IE9下面也是style的兼容问题,因为这里主要讨论动画,所有这个兼容问题以后再讨论。

       前面提到过用setTimeout来写动画,代码如下:

var left = 0, change = 2,sep;
function animateSec(){
left += change;
sep = change/(2*Math.sqrt(left))*70;
console.log(sep);
if(left > 300){return;}
var animateSecId = setTimeout(animateSec,sep);
setStyleById(elem,{'left':left+'px'});

  

查看示例

       简单说一下这个函数。利用setTimeout递归地调用动画函数animateSec(),我们让每次的移动距离一样,而每次移动的时间逐渐变化,这就形成了一个缓动。重要的是这个时间如何变化?上面的例子中,运动的时间间隔是用sep表示,change表示每次移动的距离,left值是到左端的距离。因为每次移动的距离是已知的,所以当前的left值也是已知的。然后利用sep = change/(2*Math.sqrt(left))*70(最后70这个系数是单独乘上去的,因为setTimeout是以ms为单位,不乘以一个系数速度的变化就不明显),可以求得每一次的移动时间间隔。这里求解利用了一点数学方法(高中数学的东西,很多都忘了,还专门问了一下别人,汗颜),有兴趣的同学可以找我相互探讨。

       因为后面使用setTimeout引用前面的函数时,是直接引用的函数名,参数不知道如何传递,就直接使用了全局变量,如果要写成一个较为完善的动画函数,这是一个需要解决的问题。不过这仅是动画实现的另一种思路,就忽略它了。

posted on 2012-06-12 14:49  刀光建影  阅读(319)  评论(0编辑  收藏  举报

导航