平滑流畅的 JavaScript动画
的秘密!
如果您使用JavaScript创建动画,很可能会使用到 setTimeout
或 setInterval
函数。
典型的例子:
function draw() {
// Drawing code goes here
}
setInterval(draw, 100);
这段代码将每 100ms 调用一次 draw 函数,直到某个时候调用 clearInterval
函数终止。此代码的替代方法可以改为在 setTimeout
函数内部使用 draw 函数:
function draw() {
setTimeout(draw, 100);
// Drawing code goes here
}
draw();
对该 draw 函数的单次调用将启动动画循环,从此以后,它将每 100ms 重复调用一次。
帧率和setInterval
动画的平滑度取决于动画的帧频。帧速率以每秒帧数(fps)为单位。电影通常以24fps的速度运行,视频通常以30fps的速度运行。该数字越高,动画看起来就越平滑。更多的帧意味着需要消耗更多的资源,通常很可能会导致停顿和跳过。这就是为什么要丢弃帧的原因。由于大多数屏幕的刷新率均为60Hz,因此您应追求的最快帧速率为60fps。是时候来点数学了!
/**
* 1s = 1000ms (remember that setInterval and setTimeout run on milliseconds)
* 1000ms / 60(fps) = 16.7ms (we'll round this to 17)
*/
// Lights, camera…function!
setInterval(function() {
animateEverything();
}, 17);
setTimeout和setInterval有什么问题?
首先,setTimeout
不考虑浏览器中正在发生的其他事情。该页面可能隐藏在选项卡的后面,不需要时会占用您的CPU,或者动画本身可以从页面上滚动下来,从而再次不需要进行更新调用。Chrome 浏览器确实会在隐藏的标签页中进行调节setInterval
并 setTimeout
达到 1fps,但这并不是所有浏览器都会这样做。
其次,setTimeout
仅在需要时更新屏幕,而不在计算机能够更新时更新屏幕。这意味着性能不良的浏览器必须在重新绘制整个屏幕的同时努力重新绘制动画,并且如果您的动画帧速率与屏幕的重新绘制不同步,则可能占用更多的处理能力。这意味着更高的CPU使用率。
另一个考虑因素是同时播放多个元素的动画。一种解决方法是,将所有动画逻辑放在一个间隔中,即使特定元素可能不需要当前帧的任何动画,动画调用也可能正在运行。替代方法是使用单独的间隔。这种方法的问题在于,每次在屏幕上移动某些内容时,浏览器都必须重新绘制屏幕。这很浪费!
requestAnimationFrame 可以解救!
为了克服这些效率问题,Mozilla(Firefox的制造商)提出了该 requestAnimationFrame
功能,后来由 WebKit 团队(Chrome和Safari)采用并对其进行了改进。它提供了本机 API,可在浏览器中运行任何类型的动画,无论使用DOM元素,CSS,canvas,WebGL或其他任何方式。
使用方法如下:
function draw() {
requestAnimationFrame(draw);
// Drawing code goes here
}
draw();
对!它与 setTimeout
版本完全一样,只是用了 requestAnimationFrame
替代。你也可以将一个参数传递给正在调用的函数,例如,将当前元素动画化,如下所示:requestAnimationFrame(draw, element)
;
您可能已经注意到,尽管您未指定间隔速率。那么该 draw 函数多久调用一次?所有这些都取决于浏览器和计算机的帧速率,但通常为60fps(因为计算机的显示屏通常以60Hz的速率刷新)。此处的主要区别在于,您是在要求浏览器在下一个可用机会(而不是以预定间隔)绘制动画。还暗示浏览器可以使用 requestAnimationFrame
根据负载,元素可见性(滚动到视图之外)和电池状态来选择优化性能。
另一个好处是,requestAnimationFrame
它将所有动画组合到一个浏览器重绘中。这样可以节省CPU周期。
因此,如果使用 requestAnimationFrame
动画,则动画应变得柔滑流畅,并与GPU同步并减少CPU占用。而且,如果您浏览了新的标签页,浏览器会将动画限制执行,从而防止它在您忙碌时还占用计算机资源。
浏览器兼容
由于这是一个新API,因此当前仅在浏览器中通过供应商前缀可用,例如 Safari 使用 webkitRequestAnimationFrameChrome
以及 mozRequestAnimationFrameFirefox
。总体而言,浏览器支持还不错,甚至 Microsoft Explorer 10 也将支持 msRequestAnimationFrameInternet
。
为了获得各种支持,EricMöller(Opera),Paul Irish(Google)和 Tino Zijdel(Tweakers.net)创建了一个 polyfill,以使其易于使用:
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller
// fixes from Paul Irish and Tino Zijdel
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
|| window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (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);
};
}());
将这段代码拖放到你的代码中,可以作为 requestAnimationFrame
的替代。
如果在不支持 requestAnimationFrame
的情况下可以用一个 setTimeout
作为备用。如果将调用放置到requestAnimationFrame
循环的开始而不是结束,也可以。否则,正如Opera的ErikMöller解释的那样,动画时间可能变得无法预测。因此,最好还是坚持使用上面的用法:)
如果要设置帧频怎么办?
剩下的一个明显问题是:requestAnimationFrame
如果无法指定帧频,如何控制动画的时间?游戏通常需要为其动画设置特定的帧频。
再回到之前 setInterval
,可以使用以下技术:
var fps = 15;
function draw() {
setTimeout(function() {
requestAnimationFrame(draw);
// Drawing code goes here
}, 1000 / fps);
}
通过将其包裹起来 requestAnimationFrame
,setTimeout 您就可以吃蛋糕了。您的代码可以节省效率,并且您可以指定最高60fps的帧速率。
一种更复杂的技术是检查自上次绘制调用以来经过的毫秒数,并根据时间差更新动画的位置。例如:
var time;
function draw() {
requestAnimationFrame(draw);
var now = new Date().getTime(),
dt = now - (time || now);
time = now;
// Drawing code goes here... for example updating an 'x' position:
this.x += 10 * dt; // Increase 'x' by 10 units per millisecond
}