codecamp

JavaScript中动画与特效的实现原理

一、JavaScript中的动画原理

动画效果可以通过两种方式来实现,一种是通过JavaScript间接的操作css,每隔几秒执行一次,另外一种是利用纯css实现,该方法在css3成熟后广泛应用.这里主要将js里面的动画:

1. JavaScript动画用的最多的3个api就是setInterval()、setTimeout()和requestAnimationFrame():

1. 1 setTimeout()和setInterval ()主要是自身会执行动画效果,它们在里面放入function和时间参数,然后既可以设置事件;

1. 2requestAnimationFrame(回调函数):像setTimeout、setInterval一样,requestAnimationFrame是一个全局函数。调用requestAnimationFrame后,它会要求浏览器根据自己的频率进行一次重绘,它接收一个回调函数作为参数,在即将开始的浏览器重绘时,会调用这个函数,并会给这个函数传入调用回调函数时的时间作为参数。由于requestAnimationFrame的功效只是一次性的,所以若想达到动画效果,则必须连续不断的调用requestAnimationFrame,就像我们使用setTimeout来实现动画所做的那样。

requestAnimationFrame函数会返回一个资源标识符,可以把它作为参数传入cancelAnimationFrame函数来取消.requestAnimationFrame的回调,跟setTimeout的clearTimeout很相似。 可以这么说,requestAnimationFrame其实就是setTimeout的性能增强版。

        <button  id="btn">清除</button>

        var id;

        var time = new Date();

        requestAnimationFrame(function step(){

            console.log(new Date() - time);

            time = new Date();

            id = requestAnimationFrame(step);

        });

        btn.onclick = function (){

            cancelAnimationFrame(id )

        }

1.3简单动画的问题

1.3.1 setTimeout和setInterval的深入理解

setTimeout():如下面这段代码,输出结果其实是1 2 3,而不是 1 3 2,因为setTimeout()其实在执行的时候会先存储这个结果但不会立即输出(即使时间间隔是0也这样)而是等待页面加载完成后再输出结果

        console.log("1");

        setTimeout(function(){

          console.log("3")

         },0);

         console.log("2");

        //输出结果是什么?

        //1 2 3

1.3.2 简单动画的变慢问题

当setTimeout、setInterval甚至是requestAnimationFrame()在循环里面要做很长的处理时,就会出现动画时间变慢的结果,使它本该在固定时间内结束而结果却是不尽人意的延迟

实例:

        function step() {

            var temp = div.offsetLeft + 2;

            div.style.left = temp + "px";

            window.requestAnimationFrame(step);

            for (var i = 0; i < 50000; i++) {

            console.log("再牛逼的定时器也得等到我执行完才能执行")

            }

        }

        window.requestAnimationFrame(step);

1.4 使用动画的正确姿势

 其实是 “位移”关于“时间”的函数:s=f(t)

  动画变慢的结果其实是采用增量的方式来执行了动画,为了更精确的控制动画,更合适的方法是将动画与时间关联起来

        var box = document.querySelector("div");

        var dis = 1000;

        var duration = 5000;

        var statTime = new Date();

        requestAnimationFrame(function step(){

            var time = new Date() - statTime;

            time = time >= duration ? duration : time;

            box.style.left = dis * (time / duration) + "px";

            if(time >= duration) return;

            requestAnimationFrame(step)

            for(var i = 0; i < 100000; i++) console.log("a");

        })

动画通常情况下有终止时间,如果是循环动画,我们也可以看做特殊的——当动画达到终止时间之后,重新开始动画。因此,我们可以将动画时间归一(Normalize)表示:

        //duration 是动画执行时间   isLoop是否为循环执行。

        function startAnimation(duration, isLoop){

          var startTime = Date.now();

          requestAnimationFrame(function change(){

            // 动画已经用去的时间占总时间的比值

            var p = (Date.now() - startTime) / duration;

            if(p >= 1.0){

              if(isLoop){ // 如果是循环执行,则开启下一个循环周期。并且把开始时间改成上个周期的结束时间

                startTime += duration;

                p -= 1.0; //动画进度初始化

              }else{

                p = 1.0;    //如果不是循环,则把时间进度至为 1.0 表示动画执行结束

              }

            }

            console.log("动画已执行进度", p);

            if(p < 1.0){ //如果小于1.0表示动画还诶有值完毕,继续执行动画。

              requestAnimationFrame(change);

            }

          });

        }

示例1:用时间控制动画周期精确到2s中

        block.addEventListener("click", function() {

          var self = this,

              startTime = Date.now(),

              duration = 2000;

          setInterval(function() {

            var p = (Date.now() - startTime) / duration;

            // 时间已经完成了2000的比例,则360度也是进行了这么个比例。

            self.style.transform = "rotate(" + (360 * p) + "deg)";

          }, 100);

        });

示例2:让滑块在2秒内向右匀速移动600px

        block.addEventListener("click", function(){

          var self = this,startTime = Date.now(),

              distance = 600,duration = 2000;

          requestAnimationFrame(function step(){

            var p = Math.min(1.0, (Date.now() - startTime) / duration);

            self.style.transform = "translateX(" + (distance * p) +"px)";

            if(p < 1.0) {

              requestAnimationFrame(step);

            }

          });

        });

![](//atts.w3cschool.cn/attachments/image/20180130/1517307390797799.png)

二、常见动画效果实现

 2.1 匀速水平运动

  用时间来控制进度   s=S∗p

2.2 匀加速(减速)运动

1)加速度恒定,速度从0开始随时间增加而均匀增加。

2)匀加速公式:大写S:要移动的总距离 p:归一化的时间进度    s=S∗p*p

// 2s中内匀加速运动2000px

block.addEventListener("click", function() {

var self = this,

startTime = Date.now(),

distance = 1000,

duration = 2000;

requestAnimationFrame(function step() {

var p = Math.min(1.0, (Date.now() - startTime) / duration);

self.style.transform = "translateX(" + (distance * p * p) + "px)";

if(p < 1.0) requestAnimationFrame(step);

});

});

3)匀减速运动公式:s=S∗p∗(2−p)

//2s中使用速度从最大匀减速到0运动1000px

block.addEventListener("click", function(){

  var self = this, startTime = Date.now(),

  distance = 1000, duration = 2000;

  requestAnimationFrame(function step(){

var p = Math.min(1.0, (Date.now() - startTime) / duration);

self.style.transform = "translateX("+ (distance * p * (2-p)) +"px)";

if(p < 1.0) requestAnimationFrame(step);

  });

});

2.3 水平抛物运动

匀速水平运动和自由落体运动的组合。

block.addEventListener("click", function(){

  var self = this, startTime = Date.now(),

  disX = 1000, disY = 1000, 

  duration = Math.sqrt(2 * disY / 10 / 9.8) * 1000;   // 落到地面需要的时间  单位ms

//假设10px是1米,disY = 100米

  requestAnimationFrame(function step(){

var p = Math.min(1.0, (Date.now() - startTime) / duration);

var tx = disX * p;  //水平方向是匀速运动

var ty = disY * p * p;  //垂直方向是匀加速运动

self.style.transform = "translate("+ tx + "px" + "," + ty +"px)";

if(p < 1.0) requestAnimationFrame(step);

  });

});

2.4 正弦曲线运动

正弦运动:x方向匀速,垂直方向是时间t的正弦函数

block.addEventListener("click", function(){

  var self = this, startTime = Date.now(),

  distance = 800,

  duration = 2000;

  requestAnimationFrame(function step(){

var p = Math.min(1.0, (Date.now() - startTime) / duration);

var ty = distance * Math.sin(2 * Math.PI * p);

var tx = 2 * distance * p;

self.style.transform = "translate("+ tx + "px," + ty + "px)";

if(p < 1.0) requestAnimationFrame(step);

  });

});

2.5 圆周运动

圆周运动公式:x=R.sin(2∗π∗p),y=R.cos(2∗π∗p)

block.addEventListener("click", function() {

  var self = this,

  startTime = Date.now(),

  r = 100,

  duration = 2000;

  requestAnimationFrame(function step() {

var p = Math.min(1.0, (Date.now() - startTime) / duration);

var tx = r * Math.sin(2 * Math.PI * p),

ty = -r * Math.cos(2 * Math.PI * p);

self.style.transform = "translate(" +

  tx + "px," + ty + "px)";

requestAnimationFrame(step);

  });

});

三、动画算子(easing)

 对于一些比较复杂的变化,算法也比较复杂,就要用到动画算子。动画算子 是一个函数,可以把进度转化成另外一个值。其实也就是一种算法。

我们总结一下上面的各类动画,发现它们是非常相似的,匀速运动、匀加速运动、匀减速运动、圆周运动唯一的区别仅仅在于位移方程:

1.匀速运动:s=S∗p

2.匀加速运动:s=S∗p2

3.匀减速运动:s=S∗p∗(2−p)

4.圆周运动x轴:x=R∗sin(2∗PI∗p)

5.圆周运动y轴:y=R∗cos(2∗PI∗p)

我们把共同的部分 S 或R 去掉,得到一个关于 p 的方程 ,这个方程我们称为**动画的算子(easing)**,它决定了动画的性质。

1.匀速算子:e=p

2.匀加速算子:e = p^2

3.匀减速算子:e=p∗(2−p)

4.圆周算子x轴:e=sin(2∗PI∗p)

5.圆周算子y轴:e=cos(2∗PI∗p)

一些常用的动画算子

//easing.js库封装

var pow = Math.pow,

BACK_CONST = 1.70158;

// t指的的是动画进度 归一化的时间  前面的p

Easing = {

// 匀速运动

linear: function (t){

return t;

},

// 匀加速运动

easeIn: function (t){

return t * t;

},

// 减速运动

easeOut: function (t){

return (2 - t) * t;

},

//先加速后减速

easeBoth: function (t){

return (t *= 2) < 1 ? .5 * t * t : .5 * (1 - (--t) * (t - 2));

},

// 4次方加速

easeInStrong: function (t){

return t * t * t * t;

},

// 4次方法的减速

easeOutStrong: function (t){

return 1 - (--t) * t * t * t;

},

// 先加速后减速,加速和减速的都比较剧烈

easeBothStrong: function (t){

return (t *= 2) < 1 ? .5 * t * t * t * t : .5 * (2 - (t -= 2) * t * t * t);

},

easeOutQuart: function (t){

return -(Math.pow((t - 1), 4) - 1)

},

// 指数变化 加减速

easeInOutExpo: function (t){

if (t === 0) return 0;

if (t === 1) return 1;

if ((t /= 0.5) < 1) return 0.5 * Math.pow(2, 10 * (t - 1));

return 0.5 * (-Math.pow(2, -10 * --t) + 2);

},

//指数式减速

easeOutExpo: function (t){

return (t === 1) ? 1 : -Math.pow(2, -10 * t) + 1;

},

// 先回弹,再加速

swingFrom: function (t){

return t * t * ((BACK_CONST + 1) * t - BACK_CONST);

},

// 多走一段,再慢慢的回弹

swingTo: function (t){

return (t -= 1) * t * ((BACK_CONST + 1) * t + BACK_CONST) + 1;

},

//弹跳

bounce: function (t){

var s = 7.5625,

r;

if (t < (1 / 2.75)){

r = s * t * t;

}else if (t < (2 / 2.75)){

r = s * (t -= (1.5 / 2.75)) * t + .75;

}else if (t < (2.5 / 2.75)){

r = s * (t -= (2.25 / 2.75)) * t + .9375;

}else{

r = s * (t -= (2.625 / 2.75)) * t + .984375;

}

return r;

}

};

四、使用面向对象封装动画

为了实现更加复杂的动画,我们可以将动画进行 简易 的封装,要进行封装,我们先要抽象出动画相关的要素:

动画时长:T = duration

动画进程:p = t/T

easing: e = f(p) (动画算子:p的函数 )

动画方程: x = g(e) y = g(e) (动画的位移相对于动画算子的方程)

动画生命周期:开始、进程中、结束

/*my_animator01.js框架

  参数1:动画的执行时间

  参数2:动画算子. 如果没有传入动画算子,则默认使用匀速算子

  参数3:动画执行的时候的回调函数(动画执行的要干的事情)

 */

function Animator(duration,  easing,doSomething) {

 this.duration = duration;

     this.easing = easing;

     this.doSomething = doSomething;

}

Animator.prototype = {

/*开始动画的方法,

 参数:一个布尔值

 true表示动画不循环执行。

*/

start: function (count){ // 参数表示动画播放的周期的个数

        if(count  0){//如果还有周期未结束则继续执行

                    startTime = new Date();

                }else{

                    return;//完成,动画结束

                }

            }

            self.id = requestAnimationFrame(step);//执行下一帧动画

        });

    },

stop:function () {//动画结束

cancelAnimationFrame(this.id);

}

}

五、逐帧动画

有时候,我们不但要支持元素的运动,还需要改变元素的外观,比如飞翔的小鸟需要扇动翅膀,这类动画我们可以用逐帧动画来实现:

Title

div{

width: 250px;

height: 142px;

overflow: hidden;

}

<div><img src="bird.png" alt=""></div>

var img=document.querySelector("img");

var x=0;//初始化img位置

var y=0;

//原地的移动

setInterval(function () {

img.style.transform="translate("+(-244*(x%4))+"px,"+(-146*(y%2))+"px)";

x++;//先从上面图片走,走完在从下面回来

if(x%4==0){

y++;

}

},100);

/*

var animator=new Animator(5000,Easing.linear,function (e) {

img.parentNode.style.transform="translate("+document.body.offsetWidth*e+"px,"+(-142*(y%2))+"px)";

})

animator.start(Number.POSITIVE_INFINITY);*/

//水平垂直的移动

new Animator([{

duration:5000,

easing:Easing.linear,

callback:function (e) {

img.parentNode.style.transform=

"translate("+(document.body.offsetWidth-200)*e+"px,"+600*e+"px)" +

" rotate(0deg)";

}

},

{

duration:5000,

easing:Easing.linear,

callback:function (e) {

img.parentNode.style.transform=

"translate("+(document.body.offsetWidth-200)*(1-e)+"px,"+600*(1-e)+"px)" +

" rotateY(180deg)";//到达右下角反转

}

}]).start(Number.POSITIVE_INFINITY)

/*var x=0;

setInterval(function () {

x+=10;

img.parentNode.style.transform="translate("+document.body.offsetWidth*e+"px,"+(-142*(y%2))+"px)";

},20)*/


原型
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }