Canvas 动画循环的学习笔记(requestAnimationFrame)

想做会动的画面,一开始我想当然地用 setInterval 每隔几十毫秒画一次,结果画面一顿一顿的。查了资料才知道,浏览器专门为动画提供了一个方法:requestAnimationFrame

最简单的动画循环

function draw() {
  // 1. 清屏:把上一帧的画面擦掉
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 2. 画这一帧的内容
  ctx.fillRect(x, 100, 20, 20);

  // 3. 改一下位置
  x += 2;

  // 4. 请求浏览器下一次刷新的时候再画
  requestAnimationFrame(draw);
}

draw();

它和 setInterval 最大的区别是:它跟着屏幕的刷新节奏走(一般每秒 60 次),而且浏览器标签页切到后台时会自动暂停,不浪费电。

清屏很重要

我一开始忘了清屏,方块就变成了拖着长尾巴的一条线——因为上一帧画的还在。每帧开头用 clearRect 把画布擦干净,画面就干净了。

deltaTime 是干嘛的

不同设备的刷新率不一样,有的屏幕一秒 60 帧,有的是 120 帧。如果每帧固定加 2 像素,在 120 帧的屏幕上速度就快了一倍。解决办法是用两帧之间的真实时间差:

let lastTime = 0;

function draw(now) {
  // now 是浏览器传进来的当前时间(毫秒)
  const deltaTime = now - lastTime;
  lastTime = now;

  // 按 60 帧为基准折算,速度快慢就和设备无关了
  const speed = 2 * (deltaTime / (1000 / 60));
  x += speed;

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillRect(x, 100, 20, 20);
  requestAnimationFrame(draw);
}

理解了这个以后再看别人的游戏代码,那些 dt 参数一下子就懂了。

小结

这一篇是我觉得收获最大的一课。小练习里的星空动画就是照这个思路写的:一堆小星星,每帧移动一点点,点击的时候加一颗流星。