利用 requestAnimationFrame API 流畅播放序列帧动画并控制帧率FPS

2,492 阅读3分钟

一、前言

书接前文,前讲如何批量加载图片,本文讲,当图片都加载在完成之后,如何高性能地播放序列帧动画。

二、requestAnimationFrame API

先简单复习一下 requestAnimationFrame API

告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

语法

window.requestAnimationFrame(callback);

参数

callback

下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数)。该回调函数会被传入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻。

返回值

一个 long 整数,请求 ID ,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给 window.cancelAnimationFrame() 以取消回调函数

三、控制帧率

直接使用 requestAnimationFrame() 会有一个各设备动画速率不同的问题,如今各种电子设备屏幕刷新率层出不穷(60hz,90hz,120hz,144hz...),为了用上requestAnimationFrame() 的特性。但是又想保证各种设备上的动画速率一致,我们迫切需要控制动画帧率,下面参考了 creativejs 的文章,封装了一下模块

/**
 * @param {number} [fps] - 帧率
 * @param {function} callback - 动画逻辑
 * @return {function} cancelRFA  - 取消播放
 */

export function rAF(callback, fps = 24) {
  let last = Date.now();
  let delta = last;
  const interval = 1000 / fps;
  let cancel = false;
  function draw() {
    if (cancel) return;
    requestAnimationFrame(draw);
    let now = Date.now();
    delta = now - last;
    if (delta > interval) {
     /* 这里不能简单last = now,否则出现细微时间差问题。例如fps= 10,每帧100ms,
        而现在每16ms(60fps)执行一次draw。16*7=112>100,需要7次才实际绘制一帧。这个情况下,
        实际10帧需要 112*10=1120ms>1000ms 才绘制完成。
        当 now - (delta % interval) 时,会将时间修正 delta % interval = 12ms,
        实际10帧需要 112+(112-12)*9=1012ms 绘制完成。
     */
      last = now - (delta % interval);
      callback();
    }
  }
  draw();

  return function cancelRAF() {
    cancel = true;
  };
}

在线体验地址:rAF - StackBlitz

四、为何使用requestAnimationFrame API,而不是setTimeoutsetInterval

requestAnimationFrame()的回调函数在页面渲染之前同步执行,如果在回调函数中执行了动画逻辑(例如 DOM 位移、旋转等),能确保在屏幕下一帧渲染出来。动画过渡效果更佳流畅和平滑。

setTimeout()setInterval()传入的回调函数会根据传入的第二个时间间隔参数延后执行,浏览器在事件循环中无法保证执行时机的准确性,这可能导致动画丢帧,产生动画物体“瞬移”问题,无法确保动画的流畅性。