requestAnimationFrame笔记总结

知乎文章地址:https://zhuanlan.zhihu.com/p/45201506

一.什么是requestAnimationFrame

首先看张图:

相当一部分的浏览器的显示频率是16.5ms,就是上图第一行的节奏,表现就是“我和你一步两步三步四步往前走……”。如果我们火力搞猛一点,例如搞个10ms setTimeout,就会是下面一行的模样——每第三个图形都无法绘制(红色箭头指示),表现就是“我和你一步两步 坑 四步往前走……”。

什么意思呢?

可以这样理解:国庆北京高速,最多每16.7通过一辆车,结果,突然插入一批setTimeout的军车,强行要10s通过。显然,这是超负荷的,要想顺利进行,只能让第三辆车直接消失(正如显示绘制第三帧的丢失)。然,这是不现实的,于是就有了会堵车!

同样的,显示器16.7刷新间隔之前发生了其他绘制请求(setTimeout),导致所有第三帧丢失,继而导致动画断续显示(堵车的感觉),这就是过度绘制带来的问题。不仅如此,这种计时器频率的降低也会对电池使用寿命造成负面影响,并会降低其他应用的性能。

这也是为何setTimeout的定时器推荐最小使用16.7ms的原因(16.7 = 1000/60,每秒60帧)。

还不明白,我们再来看:

假设屏幕是每隔16.7ms刷新一次,而setTimeout每隔10ms设置图像向左移动1px,就会出现如下的绘制过程:

第0ms: 屏幕未刷新,等待中,setTimeout也未执行,等待中;

第10ms:屏幕未刷新,等待中,setTimeout开始执行并设置图像属性left=1px;

第16.7ms:屏幕刷新,屏幕上的图像向左移动了1px,setTimeout未执行,继续等待中;

第20ms:屏幕为刷新,等待中,setTimeout开始执行并设置left=2px;

第30ms: 屏幕未刷新,等待中,setTimeout开始执行并设置left=3px;

第33.4ms: 屏幕开始刷新,屏幕上的图像向左移动了3px, setTimeout未执行,继续等待中;

。。。

可以看到,屏幕并没有展示left=2px的那一帧画面,图像直接从1px的位置跳到了3px的位置,这就是丢帧的现象,这种现象就会引起动画卡顿。

而requestAnimationFrame就是为了解决上述问题出现的:该接口以浏览器的显示频率来作为其动画动作的频率,比如浏览器每10ms刷新一次,动画回调也每10ms调用一次,这样就不会存在过度绘制的问题,动画不会掉帧,自然流畅。

二.requestAnimationFrame的使用

1. 首先,获取requestAnimationFrame接口,由于该接口存在兼容性问题,故而需要做兼容处理:

简单兼容可以这样做:

window.requestAnimationFrame = (function() {
  return window.requestAnimationFrame ||
         window.webkitRequestAnimationFrame ||
         window.mozRequestAnimationFrame ||
         function(callback) {
          window.setTimeout(callback, 1000/60);
         }
})();

但是还是存在问题,并不是所有的设备的绘制时间间隔是1000/60ms,以及上面并没有cancel相关方法,所以,就有了下面这份更全面的兼容方法:

(function() {
  var lastTime = 0;
  var vendors = ['webkit', 'moz'];
 
  //如果window.requestAnimationFrame为undefined先尝试浏览器前缀是否兼容
  for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
    window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
    window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||//webkit中此取消方法的名字变了
                                  window[vendors[x] + 'CancelRequestAnimationFrame'];
  }
 
  //如果仍然不兼容,则使用setTimeOut进行兼容操作
  if(!window.requestAnimationFrame) {
    window.requestAnimationFrame = function(callback, element) {
      var currTime = new Date().getTime();
      var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
      var id = window.setTimeout(function() {
        callback(currTime + timeToCall);
      }, timeToCall);
      lastTime = currTime + timeToCall;
      return id; 
    }
  }
 
  if(!window.cancelAnimationFrame) {
    window.cancelAnimationFrame = function(id) {
      clearTimeout(id);
    }
  }
})();

上述的代码是由Opera浏览器的技术师Erik Möller设计的,使得更好得兼容各种浏览器,但基本上他的代码就是判断使用4ms还是16ms的延迟,来最佳匹配60fps。

2.贝塞尔曲线

在具体进行动画效果之前,我们先来了解一下贝塞尔曲线,下面的例子中会用到这个。

相信很多同学都知道“贝塞尔曲线”这个词,我们在很多地方都能经常看到。但是,可能并不是每位同学都清楚地知道,到底什么是“贝塞尔曲线”,又是什么特点让它有这么高的知名度。

贝塞尔曲线的数学基础是早在 1912 年就广为人知的伯恩斯坦多项式。但直到 1959 年,当时就职于雪铁龙的法国数学家 Paul de Casteljau 才开始对它进行图形化应用的尝试,并提出了一种数值稳定的 de Casteljau 算法。然而贝塞尔曲线的得名,却是由于 1962 年另一位就职于雷诺的法国工程师 Pierre Bézier 的广泛宣传。他使用这种只需要很少的控制点就能够生成复杂平滑曲线的方法,来辅助汽车车体的工业设计。

正是因为控制简便却具有极强的描述能力,贝塞尔曲线在工业设计领域迅速得到了广泛的应用。不仅如此,在计算机图形学领域,尤其是矢量图形学,贝塞尔曲线也占有重要的地位。今天我们最常见的一些矢量绘图软件,如 Flash、Illustrator、CorelDraw 等,无一例外都提供了绘制贝塞尔曲线的功能。甚至像 Photoshop 这样的位图编辑软件,也把贝塞尔曲线作为仅有的矢量绘制工具(钢笔工具)包含其中。

贝塞尔曲线在 web 开发领域同样占有一席之地。CSS3 新增了 transition-timing-function 属性,它的取值就可以设置为一个三次贝塞尔曲线方程。在此之前,也有不少 JavaScript 动画库使用贝塞尔曲线来实现美观逼真的缓动效果。

下面我们就通过例子来了解一下如何用 de Casteljau 算法绘制一条贝塞尔曲线。

在平面内任选 3 个不共线的点,依次用线段连接。

在第一条线段上任选一个点 D。计算该点到线段起点的距离 AD,与该线段总长 AB 的比例。

根据上一步得到的比例,从第二条线段上找出对应的点 E,使得 AD:AB= BE:BC。

连接这两点 DE。

从新的线段 DE 上再次找出相同比例的点 F,使得 DF:DE= AD:AB= BE:BC。

到这里,我们就确定了贝塞尔曲线上的一个点 F。接下来,请稍微回想一下中学所学的极限知识,让选取的点 D 在第一条线段上从起点 A 移动到终点 B,找出所有的贝塞尔曲线上的点 F。所有的点找出来之后,我们也得到了这条贝塞尔曲线。

你实在想象不出这个过程,没关系,看动画!

回过头来看这条贝塞尔曲线,为了确定曲线上的一个点,需要进行两轮取点的操作,因此我们称得到的贝塞尔曲线为二次曲线(这样记忆很直观,但曲线的次数其实是由前面提到的伯恩斯坦多项式决定的)。

当控制点个数为 4 时,情况是怎样的?

步骤都是相同的,只不过我们每确定一个贝塞尔曲线上的点,要进行三轮取点操作。如图,AE:AB= BF:BC= CG:CD= EH:EF= FI:FG= HJ:HI,其中点 J 就是最终得到的贝塞尔曲线上的一个点。

这样我们得到的是一条三次贝塞尔曲线。

看过了二次和三次曲线,更高次的贝塞尔曲线大家应该也知道要怎么画了吧。那么比二次曲线更简单的一次(线性)贝塞尔曲线存在吗?长什么样?根据前面的介绍,只要稍作思考,想必你也能猜出来了。哈!就是一条直线~

能画曲线也能画直线,是不是很厉害?要绘制更复杂的曲线,控制点的增加也仅仅是线性的。这一特点使其不光在工业设计领域大展拳脚,就连数学基础不好的人也可以比较容易地掌握,比如大多数平面美术设计师们。

上面介绍的内容并不足以展示贝塞尔曲线的真正威力。推广到三维空间的贝塞尔曲面,以及更进一步的非均匀有理 B 样条(NURBS),早已成为当今计算机辅助设计(CAD)的行业标准,不论是我们平常用到的各种产品,还是在电影院看到的精彩大片,都少不了它们的功劳。

通过上面的贝塞尔曲线,我们能够很容易模拟动画的快慢。

Web中有很多领域都用到了贝塞尔曲线:CSS3动画、svg、canvas,下面我们再来看一张图:

看看有点吓人吧。。。

别怕,先来了解一下Tween:

  • Linear:无缓动效果
  • Quadratic:二次方的缓动(t^2)
  • Cubic:三次方的缓动(t^3)
  • Quartic:四次方的缓动(t^4)
  • Quintic:五次方的缓动(t^5)
  • Sinusoidal:正弦曲线的缓动(sin(t))
  • Exponential:指数曲线的缓动(2^t)
  • Circular:圆形曲线的缓动(sqrt(1-t^2))
  • Elastic:指数衰减的正弦曲线缓动
  • 超过范围的三次方缓动((s+1)*t^3 – s*t^2)
  • 指数衰减的反弹缓动

每个效果都分三个缓动方式,分别是(可采用后面的邪恶记忆法帮助记忆):

  • easeIn:从0开始加速的缓动,想象OOXX进去,探路要花时间,因此肯定是先慢后快的;
  • easeOut:减速到0的缓动,想象OOXX出来,肯定定先快后慢的,以防掉出来;
  • easeInOut:前半段从0开始加速,后半段减速到0的缓动,想象OOXX进进出出,先慢后快然后再慢。

每周动画效果都有其自身的算法。我们都知道jQuery UI中就有缓动,As脚本也内置了缓动,其中的运动算法都是一致的,下面是相应的算法(之后例子中用到的tween.js文件):

/*
 * Tween.js
 * t: current time(当前时间)
 * b: beginning value(初始值)
 * c: change in value(变化量)
 * d: duration(持续时间)
*/
var Tween = {
    Linear: function(t, b, c, d) { return c*t/d + b; },
    Quad: {
        easeIn: function(t, b, c, d) {
            return c * (t /= d) * t + b;
        },
        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;
        }
    },
    Cubic: {
        easeIn: function(t, b, c, d) {
            return c * (t /= d) * t * t + b;
        },
        easeOut: function(t, b, c, d) {
            return c * ((t = t/d - 1) * t * t + 1) + b;
        },
        easeInOut: function(t, b, c, d) {
            if ((t /= d / 2) < 1) return c / 2 * t * t*t + b;
            return c / 2*((t -= 2) * t * t + 2) + b;
        }
    },
    Quart: {
        easeIn: function(t, b, c, d) {
            return c * (t /= d) * t * t*t + b;
        },
        easeOut: function(t, b, c, d) {
            return -c * ((t = t/d - 1) * t * t*t - 1) + b;
        },
        easeInOut: function(t, b, c, d) {
            if ((t /= d / 2) < 1) return c / 2 * t * t * t * t + b;
            return -c / 2 * ((t -= 2) * t * t*t - 2) + b;
        }
    },
    Quint: {
        easeIn: function(t, b, c, d) {
            return c * (t /= d) * t * t * t * t + b;
        },
        easeOut: function(t, b, c, d) {
            return c * ((t = t/d - 1) * t * t * t * t + 1) + b;
        },
        easeInOut: function(t, b, c, d) {
            if ((t /= d / 2) < 1) return c / 2 * t * t * t * t * t + b;
            return c / 2*((t -= 2) * t * t * t * t + 2) + b;
        }
    },
    Sine: {
        easeIn: function(t, b, c, d) {
            return -c * Math.cos(t/d * (Math.PI/2)) + c + b;
        },
        easeOut: function(t, b, c, d) {
            return c * Math.sin(t/d * (Math.PI/2)) + b;
        },
        easeInOut: function(t, b, c, d) {
            return -c / 2 * (Math.cos(Math.PI * t/d) - 1) + b;
        }
    },
    Expo: {
        easeIn: function(t, b, c, d) {
            return (t==0) ? b : c * Math.pow(2, 10 * (t/d - 1)) + b;
        },
        easeOut: function(t, b, c, d) {
            return (t==d) ? b + c : c * (-Math.pow(2, -10 * t/d) + 1) + b;
        },
        easeInOut: function(t, b, c, d) {
            if (t==0) return b;
            if (t==d) return b+c;
            if ((t /= d / 2) < 1) return c / 2 * Math.pow(2, 10 * (t - 1)) + b;
            return c / 2 * (-Math.pow(2, -10 * --t) + 2) + b;
        }
    },
    Circ: {
        easeIn: function(t, b, c, d) {
            return -c * (Math.sqrt(1 - (t /= d) * t) - 1) + b;
        },
        easeOut: function(t, b, c, d) {
            return c * Math.sqrt(1 - (t = t/d - 1) * t) + b;
        },
        easeInOut: function(t, b, c, d) {
            if ((t /= d / 2) < 1) return -c / 2 * (Math.sqrt(1 - t * t) - 1) + b;
            return c / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1) + b;
        }
    },
    Elastic: {
        easeIn: function(t, b, c, d, a, p) {
            var s;
            if (t==0) return b;
            if ((t /= d) == 1) return b + c;
            if (typeof p == "undefined") p = d * .3;
            if (!a || a < Math.abs(c)) {
                s = p / 4;
                a = c;
            } else {
                s = p / (2 * Math.PI) * Math.asin(c / a);
            }
            return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
        },
        easeOut: function(t, b, c, d, a, p) {
            var s;
            if (t==0) return b;
            if ((t /= d) == 1) return b + c;
            if (typeof p == "undefined") p = d * .3;
            if (!a || a < Math.abs(c)) {
                a = c; 
                s = p / 4;
            } else {
                s = p/(2*Math.PI) * Math.asin(c/a);
            }
            return (a * Math.pow(2, -10 * t) * Math.sin((t * d - s) * (2 * Math.PI) / p) + c + b);
        },
        easeInOut: function(t, b, c, d, a, p) {
            var s;
            if (t==0) return b;
            if ((t /= d / 2) == 2) return b+c;
            if (typeof p == "undefined") p = d * (.3 * 1.5);
            if (!a || a < Math.abs(c)) {
                a = c; 
                s = p / 4;
            } else {
                s = p / (2  *Math.PI) * Math.asin(c / a);
            }
            if (t < 1) return -.5 * (a * Math.pow(2, 10* (t -=1 )) * Math.sin((t * d - s) * (2 * Math.PI) / p)) + b;
            return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * d - s) * (2 * Math.PI) / p ) * .5 + c + b;
        }
    },
    Back: {
        easeIn: function(t, b, c, d, s) {
            if (typeof s == "undefined") s = 1.70158;
            return c * (t /= d) * t * ((s + 1) * t - s) + b;
        },
        easeOut: function(t, b, c, d, s) {
            if (typeof s == "undefined") s = 1.70158;
            return c * ((t = t/d - 1) * t * ((s + 1) * t + s) + 1) + b;
        },
        easeInOut: function(t, b, c, d, s) {
            if (typeof s == "undefined") s = 1.70158; 
            if ((t /= d / 2) < 1) return c / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)) + b;
            return c / 2*((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2) + b;
        }
    },
    Bounce: {
        easeIn: function(t, b, c, d) {
            return c - Tween.Bounce.easeOut(d-t, 0, c, d) + b;
        },
        easeOut: function(t, b, c, d) {
            if ((t /= d) < (1 / 2.75)) {
                return c * (7.5625 * t * t) + b;
            } else if (t < (2 / 2.75)) {
                return c * (7.5625 * (t -= (1.5 / 2.75)) * t + .75) + b;
            } else if (t < (2.5 / 2.75)) {
                return c * (7.5625 * (t -= (2.25 / 2.75)) * t + .9375) + b;
            } else {
                return c * (7.5625 * (t -= (2.625 / 2.75)) * t + .984375) + b;
            }
        },
        easeInOut: function(t, b, c, d) {
            if (t < d / 2) {
                return Tween.Bounce.easeIn(t * 2, 0, c, d) * .5 + b;
            } else {
                return Tween.Bounce.easeOut(t * 2 - d, 0, c, d) * .5 + c * .5 + b;
            }
        }
    }
}
Math.tween = Tween;

我们可以通过调用上面的方法获得动画中某个变化的参数,以模拟动画的演变。

3.接下来,我们就可以去使用它了,以下是一个弹性球的例子:

demo地址:

https://github.com/yaodebian/ballBounce

准备好以上这些目录:

index.html:

<!DOCTYPE html>
<html lang="en">
 
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=
  , initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>ballBounce</title>
  <link rel="stylesheet" href="index.css">
  <script src="jquery.min.js"></script>
  <script src="requestAnimation.js"></script>
  <script src="tween.js"></script>
  <script src="index.js"></script>
</head>
 
<body>
  <div id="main">
    <h1 class="headerTit">requestAnimationFrame实现皮球落地的缓动效果实例页面</h1>
    <div class="demo">
      <h4 class="demoTit">向上拖动小球然后放下</h4>
      <ball></ball>
      <shadow></shadow>
    </div>
  </div>
</body>
 
</html>

index.css:

.headerTit {
  text-align: center;
}
.demo {
  height: 450px;
  position: relative;
}
.demoTit {
  text-align: center;
  font-size: 110%;
}
ball {
  width: 100px; 
  height: 100px; 
  border-radius: 100px;
  position: absolute; 
  left: 50%; 
  top: 335px;
  margin-left: -50px;
  background-color: #34538b;
  background-image: -webkit-radial-gradient(100px 100px at 50px 20px, #a0b3d6, #34538b);
  background-image:    -moz-radial-gradient(100px 100px at 50px 20px, #a0b3d6, #34538b);
  background-image:         radial-gradient(100px 100px at 50px 20px, #a0b3d6, #34538b);
  cursor: pointer;
  z-index: 1;
}
shadow {
  position: absolute;
  width: 100px; 
  height: 30px;
  position: absolute; 
  left: 50%; 
  bottom: 5px;
  margin-left: -50px;
  background-image: -webkit-radial-gradient(ellipse closest-side, rgba(0,0,0,.75), rgba(0,0,0,0));
  background-image:    -moz-radial-gradient(ellipse closest-side, rgba(0,0,0,.75), rgba(0,0,0,0));
  background-image:         radial-gradient(ellipse closest-side, rgba(0,0,0,.75), rgba(0,0,0,0));
}

requestAnimation.js:即刚刚我们做的requestAnimationFrame兼容处理

tween.js:上面贝塞尔曲线中涉及到的tween.js

截图如下:

三.RequestAnimationFrame相对于setTimeout的优点

1. 使得动画更加流畅,防止动画失帧情况;

2. Cpu节能:

使用setTimeout实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费CPU资源。而requestAnimationFrame则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的requestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。

3.函数节流:在高频率事件(resize,scroll等)中,为了防止在一个刷新间隔内发生多次函数执行,使用requestAnimationFrame可保证每个刷新间隔内,函数只被执行一次(这个可以自己测试一下,在每次回调函数中打印出当前的毫秒数:getTime()),这样既能保证流畅性,也能更好的节省函数执行的开销。一个刷新间隔内函数执行多次是没有意义的,因为显示器每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来。

参考文章:

https://www.zhangxinxu.com/wordpress/2013/09/css3-animation-requestanimationframe-tween-%E5%8A%A8%E7%94%BB%E7%AE%97%E6%B3%95/

https://www.zhangxinxu.com/wordpress/2016/12/how-use-tween-js-animation-easing/

http://web.jobbole.com/91578/

http://www.webhek.com/post/requestanimationframe.html

https://blog.csdn.net/themagickeyjianan/article/details/54861945/

猜你喜欢

转载自blog.csdn.net/YaoDeBiAn/article/details/82823574