欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

HTML动画 request animation frame

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

在网页中,实现动画无外乎两种方式。
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 ,启动了动画过程