zoukankan      html  css  js  c++  java
  • 原生JavaScript中动画与特效的实现原理

    现如今,许多页面上均有一些动画效果。适当的动画效果可以在一定程度上提高页面的美观度,具有提示效果的动画可以增强页面的易用性。

    实现页面动画的途径一般有两种。

    • 一种是通过操作JavaScript间接操作CSS样式,每隔一段时间更新一次
    • 一种是直接通过CSS定义动画。第二种方法在CSS3成熟之后被广泛采用。

    我们今天只讲第一种实现方式。

    一、JavaScript中动画原理

    ​ 所谓的动画,就是通过一些列的运动形成的动的画面。在网页中,我们可以通过不断的改变元素的css值,来达到动的效果。

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

    ​ 据说,普通人眼能看到1/24秒,就是说1秒至少24帧,每次移位间隔需要小于1000/24=41.7毫秒,也就说setInterval要每隔至少40毫秒执行一次,一般地,我们采用10毫秒,当然间隔时间越短,客户端执行计算次数就越多,如果你code计算量大则可以适当调长些。

    1.1 setTimeout()和setInterval ()

    1.2 requestAnimationFrame(回调函数)

    ​ 像setTimeout、setInterval一样,requestAnimationFrame是一个全局函数。调用requestAnimationFrame后,它会要求浏览器根据自己的频率进行一次重绘,它接收一个回调函数作为参数,在即将开始的浏览器重绘时,会调用这个函数,并会给这个函数传入调用回调函数时的时间作为参数。由于requestAnimationFrame的功效只是一次性的,所以若想达到动画效果,则必须连续不断的调用requestAnimationFrame,就像我们使用setTimeout来实现动画所做的那样。requestAnimationFrame函数会返回一个资源标识符,可以把它作为参数传入cancelAnimationFrame函数来取消requestAnimationFrame的回调。跟setTimeout的clearTimeout很相似啊。

    ​ 可以这么说,requestAnimationFrame是setTimeout的性能增强版。

    ​ 有一点需要注意的是,requestAnimationFrame不能自行指定函数运行频率,而是有浏览器决定刷新频率。所以这个更能达到浏览器所能达到的最佳动画效果了。

    ​ 这个方法不是所有的浏览器都兼容。

    div{
       100px;
      height: 100px;
      background-color: #f00;
      position: absolute;
    }
    
    <div id="div"></div>
    
    <script type="text/javascript">
      	var id;
    	function step() {
    		var temp = div.offsetLeft + 2;
    		div.style.left = temp + "px";
      		//和setTimeout一样,要手动调用才能实现连续动画。
    		id = window.requestAnimationFrame(step);  //返回值是一个id,可以通过这个id来取消
    	}
    	id = window.requestAnimationFrame(step);
    	//取消回调函数
    	window.cancelAnimationFrame(step);
    </script>
    

    1.3 简单动画的问题

    1.3.1 setTimeout和setInterval深入理解

    ​ 我们知道JavaScript试单线程的产物,两个函数就是利用了插入代码的方式实现了伪异步,和AJAX的原理实际上是一样的。

    console.log("1");
    setTimeout(function(){
      console.log("3")
     },0);
     console.log("2");
    //输出结果是什么?
    //1 2 3
    
    function fn() {
    setTimeout(function(){
    	console.log('can you see me?');
    },1000);
    while(true) {}
    }
    //输出结果是什么?
    

    1.3.2 简单动画的变慢问题

    	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)

    ​ 所以不该采用增量的方式来执行动画,为了更精确的控制动画,更合适的方法是将 动画与时间关联起来

    function startAnimation() {
      var startTime = Date.now();
      requestAnimationFrame(function change() {
        var current = Date.now() - startTime;
        console.log("动画已执行时间" + current);
        requestAnimationFrame(change);
      });
    }
    startAnimation();
    

    动画通常情况下有终止时间,如果是循环动画,我们也可以看做特殊的——当动画达到终止时间之后,重新开始动画。因此,我们可以将动画时间归一(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);
        }
      });
    });
    

    二、常见动画效果实现

    2.1 匀速水平运动

    ​ 用时间来控制进度
    $$
    s = S*p
    $$

    2.2 匀加速(减速)运动

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

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

    // 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);
    		});
    	});
    

    匀减速运动公式
    $$
    s=Sp(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)

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

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

    • 匀速运动:
      $$
      s = S *p
      $$

    • 匀加速运动:

    $$
    s = S *p^2
    $$

    • 匀减速运动:

    $$
    s = Sp(2-p)
    $$

    • 圆周运动x轴:

    $$
    x = Rsin(2PI*p)
    $$

    • 圆周运动y轴:

    $$
    y = Rcos(2PI*p)
    $$

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

    • 匀速算子:

    $$
    e = p
    $$

    • 匀加速算子:

    $$
    e = p^2
    $$

    • 匀减速算子:

    $$
    e = p*(2 - p)
    $$

    • 圆周算子x轴:

    $$
    e = sin(2PIp)
    $$

    • 圆周算子y轴:

    $$
    e = cos(2 * PI * p)
    $$

    一些常用的动画算子

    
    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) (动画的位移相对于动画算子的方程)

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

    <script type="text/javascript">
    	/*
    	  参数1:动画的执行时间
    	  参数2:动画执行的时候的回调函数(动画执行的要干的事情)
    	  参数3:动画算子. 如果没有传入动画算子,则默认使用匀速算子
    	 */
    	function Animator(duration, progress, easing) {
    		this.duration = duration;
    		this.progress = progress;
    		this.easing = easing || function(p) {
    			return p
    		};
    	}
    	Animator.prototype = {
    		/*开始动画的方法, 
    		 参数:一个布尔值
    		 true表示动画不循环执行。	 
    		*/
    		start: function(finished) {
    			/*动画开始时间*/
    			var startTime = Date.now();
    			/*动画执行时间*/
    			var duration = this.duration,
    				self = this;
    			/*定义动画执行函数*/
    			requestAnimationFrame(function step() {
    				/*得到动画执行进度*/
    				var p = (Date.now() - startTime) / duration;
    				/*是否执行下一帧动画*/
    				var next = true;
    				/*判断动画进度是否完成*/
    				if(p < 1.0) {
    					self.progress(self.easing(p), p);   //执行动画回调函数,并传入动画算子的结果和动画进度。
    				} else {
    					if(finished){  //判断是否停止动画。如果是true代表停止动画。
    						next = false;
    					}else{
    						startTime = Date.now();
    					} 
    				}
    				// 如果next是true执行下一帧动画
    				if(next) requestAnimationFrame(step);
    			});
    		}
    	};
    	block.onclick = function  () {
    		var self = this;
    		new Animator(2000, function (p) {
    			self.style.top = 500 * p +"px";
    		},Easing.bounce).start(false);
    	}
    

    五、逐帧动画

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

    <style type="text/css">
    .sprite {display:inline-block; overflow:hidden; background-repeat: no-repeat;background-image:url(http://res.h5jun.com/matrix/8PQEganHkhynPxk-CUyDcJEk.png);}
    
    .bird0 {86px; height:60px; background-position: -178px -2px}
    .bird1 {86px; height:60px; background-position: -90px -2px}
    .bird2 {86px; height:60px; background-position: -2px -2px}
    
     #bird{
       position: absolute;
       left: 100px;
       top: 100px;
       zoom: 0.5;
     }
    </style>
    <div id="bird" class="sprite bird1"></div>
    <script>
    var i = 0;
    setInterval(function(){
      bird.className = "sprite " + "bird" + ((i++) % 3);
    }, 1000/10);
    </script>
    
  • 相关阅读:
    Poj 1742 Coins(多重背包)
    Poj 2350 Above Average(精度控制)
    求二进制数中1的个数
    Poj 1659 Distance on Chessboard(国际象棋的走子规则)
    Poj 2411 Mondriaan's Dream(压缩矩阵DP)
    Poj 2136 Vertical Histogram(打印垂直直方图)
    Poj 1401 Factorial(计算N!尾数0的个数——质因数分解)
    poj 2390 Bank Interest(计算本利和)
    Poj 2533 Longest Ordered Subsequence(LIS)
    Poj 1887 Testing the CATCHER(LIS)
  • 原文地址:https://www.cnblogs.com/xiaoyulive/p/6690999.html
Copyright © 2011-2022 走看看