• HTML动画 request animation frame


    在网页中,实现动画无外乎两种方式。
    1. CSS3 方式,也就是利用浏览器对CSS3 的原生支持实现动画;
    2. 脚本方式,通过间隔一段时间用JavaScript 来修改页面元素样式来实现动画。
    接下来我们就分别介绍这两种方式的原理,让大家先对这两种方式有一个直观认识,了解各自的优缺点。

    CSS3 的方式下,开发者一般在css 中定义一些包含CSS3 transition 语法的规则。在某些特定情况下,让这些规则发生作用,于是浏览器就会将这些规则应用于指定的DOM元素上,产生动画的效果。这种方式毫无疑问运行效率要比脚本方式高,因为浏览器原生支持,省去了JavaScript 的解释执行负担,有的浏览器(比如Chrome 浏览器)甚至还可以充分利用GPU 加速的优势,进一步增强了动画渲染的性能。不过CSS3 的方式并非完美,也有不少缺点。
    首先, CSS3 Transition 对一个动画规则的定义是基于时间和速度曲线( Speed Curve)的规则。换句话来说,就是CSS3 的动画过程要描述成“在什么时间范围内,以什么样的运动节奏完成动画” 。

    <!DOCTYPE html>
    <html>
      <head>
        <style>
    .sample {
      background: red;
      position: absolute;
      left: 0px;
      width: 100px;
      height: 100px;
      transition-property: left;
      transition-duration: 0.5s;
      transition-timing-function: ease
    }
    .sample:hover {
      left: 420px;
    }
        </style>
      </head>
      <body>
        <div class="sample" />
      </body>
    </html>


    在上面的例子中, sample 类的元素定义了这样的动画属性:“ left 属性会在0.2 秒内以ease 速度曲线完成动画” 。transition 只定义了动画涉及的属性、时间和速度曲线,并不定义需要修改的具体值。sample 类的left 属性默认值为0 ,当鼠标移到sample 类元素上时, left 属性就拥有新的值420px 。这时候transition 定义的规则发生作用,让left 属性以ease 速度曲线在0.2 秒
    的时间完成从0 变成420px 的转化过程,这个过程中,用户看到的就是sample 类元素向右移动420 个像素的动画过程。
            因为CSS3 定义动画的方式是基于时间和速度曲线,可能不利于动画的流畅,因为动画是可能会被中途打断的,在上面的例子中,鼠标移到sample 类元素上的时候开始动画,但是在0.2 秒的动画时间内,用户的鼠标可能会移出这个sample 类元素,这时候CSS3 还会以ease 速度曲线的节奏让sample 类元素回到原位。从用户体验角度来说,中途sample 类元素回到原位的动作,语义上是“取消操作”的含义,但却依然以同样的时间和ease 节奏来完成“取消操作”的动画,这并不合理。

              时间和速度曲线的不合理是CSS3 先天的属性,更让开发者头疼的就是开发CSS3 规则的过程,尤其是对transition-duration 时间很短的动画调试,因为CSS3 的transition 过程总是一闪而过,捕捉不到中间状态,只能一遍一遍用肉眼去检验动画效果,用CSS3做过复杂动画的开发者肯定都深有体会。虽然CSS3 有这样一些缺点,但是因为其无与伦比的性能,用来处理一些简单的动画还是不错的选择。

           相对于CSS3 方式,脚本方式最大的好处就是更强的灵活度,开发者可以任意控制动画的时间长度,也可以控制每个时间点上元素渲染出来的样式,可以更容易做出丰富的动画效果。脚本方式的缺点也很明显,动画过程通过JavaScript 实现,不是浏览器原生支持,消耗的计算资源更多。如果处理不当,动画可能会出现卡顿滞后现象,本来使用动画是为了创造更好的用户体验,如果出现卡顿,反而对用户体验带来不好的影响。最原始的脚本方式就是利用setlnterval 或者setTimeout 来实现,每隔一段时间一个指定的函数被执行来修改界面的内容或者样式,从而达到动画的效果。

    <!DOCTYPE html>
    <html>
      <head>
        <style>
    #sample {
      position: absolute;
      background: red;
      width: 100px;
      height: 100px;
    }
        </style>
      </head>
      <body>
        <div id="sample" />
        <script type="text/javascript">
    var animatedElement = document.getElementById("sample");
    var left = 0;
    var timer;
    var ANIMATION_INTERVAL = 16;
    
    timer = setInterval(function() {
      left += 10;
      animatedElement.style.left = left + "px";
      if ( left >= 400 ) {
        clearInterval(timer);
      }
    }, ANIMATION_INTERVAL);
    
        </script>
      </body>
    </html>


    在上面的例子中,有一个常量ANIMATION INTERVAL 定义为16 , setlnterval 以这个常量为间隔,每16 毫秒计算一次sample 元素的left 值,每次都根据时间推移按比例增加left 的值,直到left 大于400 。为什么要选择16 毫秒呢?因为每秒渲染60 帧(也叫60fps, 60 Frame Per Second)会给用户带来足够流畅的视觉体验,一秒钟有1000 毫秒, 1000 /60 =16 ,也就是说,如果我们做到每16 毫秒去渲染一次画面,就能够达到比较流畅的动画效果。对于简单的动画, setlnterval 方式勉强能够及格,但是对于稍微复杂一些的动画,脚本方式就顶不住了,比如渲染一帧要花去超过32 毫秒的时间,那么还用16 毫秒一个间隔的方式肯定不行。实际上,因为一帧渲染要占用网页线程32 毫秒,会导致setlnterval根本无法以16 毫秒间隔调用渲染函数,这就产生了明显的动画滞后感,原本一秒钟完成的动画现在要花两秒钟完成,所以这种原始的setlnterval 方式是肯定不适合复杂的动画的。
           出现上面问题的本质原因是setlnterval 和setTimeout 并不能保证在指定时间间隔或者延迟的情况下准时调用指定函数。所以可以换一个思路,当指定函数调用的时候,根据逝去的时间计算当前这一帧应该显示成什么样子,这样即使因为浏览器渲染主线程忙碌导致一帧渲染时间超过16 毫秒,在后续帧谊染时至少内容不会因此滞后,即使达不倒60fps 的效果,也能保证动画在指定时间内完成。

    <!DOCTYPE html>
    <html>
      <head>
        <style>
    #sample {
      position: absolute;
      background: red;
      width: 100px;
      height: 100px;
    }
        </style>
      </head>
      <body>
        <div id="sample" />
        <script type="text/javascript">
    
    var lastTimeStamp = new Date().getTime();
    function raf(fn) {
      var currTimeStamp = new Date().getTime();
      var delay  = Math.max(0, 16 - (currTimeStamp - lastTimeStamp));
      var handle = setTimeout(function(){
        fn(currTimeStamp);
      }, delay);
      lastTimeStamp = currTimeStamp;
      return handle;
    }
    
    var left = 0;
    var animatedElement = document.getElementById("sample");
    var startTimestamp = new Date().getTime();
    function render(timestamp) {
      left += (timestamp - startTimestamp) / 16;
      animatedElement.style.left = left + 'px';
      if (left < 400) {
        raf(render);
      }
    }
    
    raf(render);
        </script>
      </body>
    </html>

    在上面定义的raf 中,接受的fn 函数参数是真正的渲染过程, raf 只是协调渲染的节奏。raf 尽量以每隔16 毫秒的速度去调用传递的fn 参数,如果发现上一次被调用时间和这一次被调用时间相差不足16 毫秒,就会保持16 毫秒一次的渲染间隔继续,如果发现
    两次调用时间间隔已经超出了16 毫秒,就会在下一次时钟周期立刻调用fn 。上面的render 函数中根据当前时间和开始动画的时间差来计算sample 元素的left 属性,这样无论render 函数何时被调用,总能够渲染出正确的结果。
    最后,我们将render 作为参数传递给raf ,启动了动画过程

  • 相关阅读:
    c++拷贝构造函数和赋值运算符
    c++运算符定义为成员函数还是非成员函数
    c++重载运算符位置的限制
    为什么operator<<运算符重载一定要为友元函数
    动态规划求一定数量骰子和的概率
    vector之reserve的坑
    c++ decltype和auto对比学习
    asio的前摄器模式
    动态显示当前时间
    js遍历二维数组
  • 原文地址:https://www.cnblogs.com/majiang/p/9691950.html
Copyright © 2020-2023  润新知