设计Tween缓动动画基础

课后整理 2020-12-14

缓动动画(Tween)演示 设计缓动动画

运动算法基础

运动可以简单理解为:随着时间的改变,物体的位置不断改变的现象。这里有三个要素:时间、起始位置、终点位置。而且位置和时间之间存在一对一的对应关系。

在JavaScript中,时间的问题可以用setInterval或setTimeOut解决,位置的问题可以通过CSS的style属性控制。

设p(position)为位置,t(time)为时间,那么得到一个函数等式:

p = ƒ(t)

转换为JavaScript函数:

p =  function(t){
    var v = 2;                                          //速度 
    return   t*v;
}

速度 v(velocity) = 距离/时间,那么设距离为c(change positioin),经过时间为d(druation),那么得到一个等式:

p = t *  ( c / d ) 

通过上面这个公式得到物体在(c/d)的速度下移动时间t所在的位置p。注意,这里假设物体的初始位置为0。那么如果设物体的当前位置为b(begining positoin),那么得到一个等式:

p = t *  ( c / d ) + b

将这个公式转换为JavaScript函数:

p =  funciton( t, b, c, d ){
    return t * ( c / d ) + b;
}

格式化:

Math.prototype.linearTween  = function( t, b, c, d){
     return t*c/d + b;
}

这样就得到了最简单的直线运动函数。

为了更加形象的描述问题,引入坐标系,那么直线运动在这里就可以表示如下图所示,这就是最简单的线性表达式:y=x;此时斜率为1。

图1  直线运动

设计一个示例

下面将这个公式引入到JavaScript中,做一个简单的示例看一下实际效果。

<!doctype  html>
<html>
<head>
<title>TweenTest</title>
<meta  charset="utf-8">
<style  type="text/css">
#moveLinear  {
    width: 50px;
    height: 50px;
    background: #ccc;
}
</style>
</head>
<body>
<div  id="moveLinear"></div>
</body>
<script>
//将$设置为获取元素对象的函数,留待以后使用。 
$ =  function( id ){ return typeof id ==  "string"?document.getElementById(id):id }
/*  des:tween算法。 
    t: 动画已经执行的时间(实际上是已经执行了多少次/帧数) 
    b: 起始位置 
    c: 终止位置 
    d: 从起始位置到终止位置的经过时间(实际上总共执行多少次/帧数) 
*/
tween =  {
    linear : function( t, b, c, d){
        return t*c/d + b;
    }
}
/* des: 控制元素移动的动画对象 
    moveType : 元素移动的方式,默认为linear 。 
            参数: 
                mvTp = string 移动方式 
    startMove : 移动元素的函数 
            参数: 
                mvObj = string 被移动的元素的id
                mvTp = string 移动方式 
*/
move={
    //元素移动的方式:默认为linear
    moveType : function(mvTp){return  mvTp && typeof(mvTp)==  "string" && tween[mvTp] ? mvTp: "linear" },
    //移动元素的函数 
    //mvObj:string 被移动的元素的id
    //mvTp:string 移动类型 
    startMove : function( mvObj,mvTp,t,b,c,d ){ 
        //判断传入参数是否正确,如果t存在那么还是t, 
        //否则为t设置默认值,其他的以此类推 
        t ? t : t=0; b ? b : b = 0; c ? c : c  =300; d ? d : d = 50; 
        //判断被移动的元素是否存在相对或者绝对的定位属性, 
        //1 的作用仅仅是为了完成语法格式(若没有该属性,则无法移动元素) 
        $(mvObj).style.position  =="relative" || $(mvObj).style.position =="absolute" 
            ? 1 : $(mvObj).style.position =  "relative";
        //每隔30毫秒重复执行改变元素位置的函数 
        mvTimer = setInterval(function(){
        //判断动画已经执行的时间(次数/帧数)是否小于总时间, 
        //是的话继续执行改变位置的函数,否则的话,清理该interval。 
        //?和:之间的函数是个匿名函数,在匿名函数的后面加上()
        //直接调用该函数,简便写法。 
        //tween[move.moveType(mvTp)]() 先通过对象数组的属性obj["x"]方式 
        //访问tween的某个属性对象,然后加上()执行该函数对象。 
            t <= d 
                ? function(){  $(mvObj).style.left =  parseInt(tween[move.moveType(mvTp)](t,b,c,d))+"px"; t++;}() 
                : clearInterval(mvTimer);
        } ,30 ) 
    }
}
move.startMove("moveLinear");
</script>
</html>

通过上面动画,发现灰色方框可以很顺利的从左边移动到右边。但如果在上面这个例子简单的将一个方框从左边的0px以6px/30ms的速度移动到右边300px的位置,现在看一下动画消耗时间。按照理论,每30ms移动6px,那么300px的动画时间应该是30ms * 50 = 1500ms,但如果再测试一下,就会发现,动画总时间并不是那么准确。

下面示例在程序中间加两个时间戳,来计算动画总共的时常:

startMove  : function( mvObj,mvTp,t,b,c,d ){ 
    t ? t : t=0; b ? b : b = 0; c ? c : c =300;  d ? d : d = 50; 
    $(mvObj).style.position  =="relative" || $(mvObj).style.position =="absolute" 
        ? 1 : $(mvObj).style.position =  "relative";
    //开始时间戳 
    strT = new Date();
    mvTimer = setInterval(function(){
        t <= d 
            ? function(){ $(mvObj).style.left =  parseInt(tween[move.moveType(mvTp)](t,b,c,d))+"px"; t++;}() 
            //结束时间戳,并弹出动画总时长 
            : function(){  clearInterval(mvTimer); alert(new Date() - strT); }()
         } ,30 )
}

如果多次测试,会发现每次的结果可能都不尽相同,这对精确控制动画执行时间绝对是个噩耗。

演示效果

控制时间精度

首先我们必须知道出现时间精度问题的原因。

JavaScript动画在浏览器中运行的过程中会产生时间误差,误差由系统环境、浏览器环境、程序运算时间,以及其他人为原因造成。系统环境包括计算机硬件、计算机操作系统,浏览器环境包括浏览器内核、JavaScript解释引擎等,程序运算时间虽然有时可以忽略,但程序却可能由于JavaScript的单线程机制造成时间延迟。所以如果想把时间精确到毫秒,是很有难度的,jQuery也考虑到了这个问题,所以它采用了消息队列机制来尽量消除这种误差。因为程序本身的时间精度就很难控制,所以如果使用程序本身来控制时间精度,那是无法实现的。所以这里暂且将动画时间精确到秒。

在Flash中,动画时长都是以帧为单位来计算的。每帧时长*帧数就是动画总时间。在JavaScript中,帧时长实际上就是setInterval、setTimeOut的第二个参数。而帧数就是:

Math.prototype.linearTween  = function( t, b, c, d){
    return t*c/d + b;
}

上面linear算法中的d和t(d表示总帧数,t表示已经经过的帧数)。所以如果单纯的认为t是时间,可能会陷入迷茫中,半天不能自拔。

以上面示例代码为例,在这里设计动画的帧数是d=50,经过帧数为t,当t<=d时,继续执行动画,每帧的时间间隔是30毫秒,该元素将从距离窗口左边距为0的位置移动到距离窗口左边距为300的位置,这里忽略body的默认margin。

线性运动

在现实生活中,物体的运动都存在加速度或者缓冲,物体不可能从一个速度突然蹦到另外一个速度。如果用符合自然现实的方式实现动画,那么动画看起来才会更加自然、生动、流畅。那么下面来了解一下缓动。缓动包含3种类型。

图2  缓入运动

图3  缓出运动

图4  缓入缓出运动

下面结合二次方曲线,具体分析缓动曲线及公式。实际上就是二次方曲线(抛物线),p(t) = t ^ 2,如下图所示。

图5  二次方曲线

动画函数:

p =  function( t, b, c, d){
    return (t/=d)*t*c + b;
}

JavaScript实例:

tween =  {
    quad : function( t, b, c, d){
        return (t/=d)*t*c + b;
    }
}

这与直线算法(p = t/d*c+b)一样,也是以t为变量,求p,只不过这里计算的t变量的平方。在直线算法中,t*c/d+b表示当前时间量乘以不变的速度(c/d)再加上起始位置。但缓入算法中因为每个时间帧的速度不一致,所以不能直接用c/d。

既然是二次方抛物线,可以直接写成p = t * t /d * c + b,不过如果尝试一下,就会发现,这个抛物线会直接把物体抛到一个非常神秘非常遥远的地方。实际上这里的抛物线是有限制的:0 <= p <= c,0 <= t <= d。整理该公式:

0 <=  p <= c
0 <=  t * c / d + b <= c
//b为常数,假设起始位置为0,则: 
0 <=  t * c / d <= c
//两边除以c,则: 
0 <=  t / d <= 1
这时候,公式从p = f(t)变成了p = f(t^2),即0 <= p <=c,0 <= t^2 <= d。那么得到: 
0 <=  (t/d)^2 <= 1

这时可以看到,t与d的斜率为1,匀速运动;斜率从0到1变化,缓入运动;从1到0变化,缓出运动。缓入缓出运动实际上就是两者结合,时间各为二分之一。下面给出二次方曲线算法。

Quad: {
     //缓入函数:斜率从0到1。 
      easeIn: function(t,b,c,d){
          return c*(t/=d)*t + b;
     },
     //缓出函数:将c系数变为负值使图形翻转, 
     //然后对t减2,使图形向右平移至正确位置 
      easeOut: function(t,b,c,d){
          return -c *(t/=d)*(t-2) + b;
     },
     ///缓入缓出 
    easeInOut: function(t,b,c,d){
        //判断当前时间是否在总时间的一半以内, 
        //如果是,则执行缓入函数,否则执行缓出函数 
        if  ((t/=d/2) < 1) return c/2*t*t + b;
        //将总长度设置为一半,并且时间从当前开始递减, 
        return  -c/2 * ((--t)*(t-2) - 1) + b;
    }
}

其它缓动算法与此类似,本节就不再详细拓展。