看起来 requestAnimationFrame
是事实上的动画方式。它在大多数情况下对我来说效果很好,但现在我正在尝试做一些画布动画,我想知道:有没有办法确保它以特定的 fps 运行?我知道 rAF 的目的是为了始终流畅的动画,我可能会冒着让我的动画断断续续的风险,但现在它似乎以非常任意的速度运行,我想知道是否有办法对抗不知何故。
我会使用 setInterval
但我想要 rAF 提供的优化(尤其是在标签处于焦点时自动停止)。
如果有人想查看我的代码,这几乎是:
animateFlash: function() {
ctx_fg.clearRect(0,0,canvasWidth,canvasHeight);
ctx_fg.fillStyle = 'rgba(177,39,116,1)';
ctx_fg.strokeStyle = 'none';
ctx_fg.beginPath();
for(var i in nodes) {
nodes[i].drawFlash();
}
ctx_fg.fill();
ctx_fg.closePath();
var instance = this;
var rafID = requestAnimationFrame(function(){
instance.animateFlash();
})
var unfinishedNodes = nodes.filter(function(elem){
return elem.timer < timerMax;
});
if(unfinishedNodes.length === 0) {
console.log("done");
cancelAnimationFrame(rafID);
instance.animate();
}
}
其中 Node.drawFlash() 只是一些根据计数器变量确定半径然后绘制圆的代码。
requestAnimationFrame
的最大优势是(顾名思义)仅在需要时才请求动画帧。假设你展示了一个静态的黑色画布,你应该得到 0 fps,因为不需要新的帧。但是,如果您正在显示需要 60fps 的动画,您也应该得到它。 rAF
只允许“跳过”无用的帧,然后节省 CPU。
如何将 requestAnimationFrame 限制为特定的帧速率
FPS 的演示节流:http://jsfiddle.net/m1erickson/CtsY3/
此方法通过测试自执行最后一帧循环以来经过的时间来工作。
您的绘图代码仅在您指定的 FPS 间隔已过时执行。
代码的第一部分设置了一些用于计算经过时间的变量。
var stop = false;
var frameCount = 0;
var $results = $("#results");
var fps, fpsInterval, startTime, now, then, elapsed;
// initialize the timer variables and start the animation
function startAnimating(fps) {
fpsInterval = 1000 / fps;
then = Date.now();
startTime = then;
animate();
}
这段代码是实际的 requestAnimationFrame 循环,它以您指定的 FPS 进行绘制。
// the animation loop calculates time elapsed since the last loop
// and only draws if your specified fps interval is achieved
function animate() {
// request another frame
requestAnimationFrame(animate);
// calc elapsed time since last loop
now = Date.now();
elapsed = now - then;
// if enough time has elapsed, draw the next frame
if (elapsed > fpsInterval) {
// Get ready for next frame by setting then=now, but also adjust for your
// specified fpsInterval not being a multiple of RAF's interval (16.7ms)
then = now - (elapsed % fpsInterval);
// Put your drawing code here
}
}
2016/6 更新
限制帧速率的问题是屏幕具有恒定的更新速率,通常为 60 FPS。
如果我们想要 24 fps,我们将永远无法在屏幕上获得真正的 24 fps,我们可以这样计时但不显示它,因为显示器只能以 15 fps、30 fps 或 60 fps 显示同步帧(某些显示器也 120 fps )。
但是,出于计时目的,我们可以在可能的情况下计算和更新。
您可以通过将计算和回调封装到一个对象中来构建用于控制帧速率的所有逻辑:
function FpsCtrl(fps, callback) {
var delay = 1000 / fps, // calc. time per frame
time = null, // start time
frame = -1, // frame count
tref; // rAF time reference
function loop(timestamp) {
if (time === null) time = timestamp; // init start time
var seg = Math.floor((timestamp - time) / delay); // calc frame no.
if (seg > frame) { // moved to next frame?
frame = seg; // update
callback({ // callback function
time: timestamp,
frame: frame
})
}
tref = requestAnimationFrame(loop)
}
}
然后添加一些控制器和配置代码:
// play status
this.isPlaying = false;
// set frame-rate
this.frameRate = function(newfps) {
if (!arguments.length) return fps;
fps = newfps;
delay = 1000 / fps;
frame = -1;
time = null;
};
// enable starting/pausing of the object
this.start = function() {
if (!this.isPlaying) {
this.isPlaying = true;
tref = requestAnimationFrame(loop);
}
};
this.pause = function() {
if (this.isPlaying) {
cancelAnimationFrame(tref);
this.isPlaying = false;
time = null;
frame = -1;
}
};
用法
它变得非常简单 - 现在,我们所要做的就是通过设置回调函数和所需的帧速率来创建一个实例,就像这样:
var fc = new FpsCtrl(24, function(e) {
// render each frame here
});
然后开始(如果需要,这可能是默认行为):
fc.start();
就是这样,所有的逻辑都在内部处理。
演示
var ctx = c.getContext("2d"), pTime = 0, mTime = 0, x = 0; ctx.font = "20px 无衬线"; // 用一些信息和动画更新画布 var fps = new FpsCtrl(12, function(e) { ctx.clearRect(0, 0, c.width, c.height); ctx.fillText("FPS: " + fps. frameRate() + " Frame: " + e.frame + " Time: " + (e.time - pTime).toFixed(1), 4, 30); pTime = e.time; var x = (pTime - mTime) * 0.1; if (x > c.width) mTime = pTime; ctx.fillRect(x, 50, 10, 10) }) // 开始循环 fps.start(); // UI bState.onclick = function() { fps.isPlaying ? fps.pause() : fps.start(); }; sFPS.onchange = function() { fps.frameRate(+this.value) }; function FpsCtrl(fps, callback) { var delay = 1000 / fps, time = null, frame = -1, tref;函数循环(时间戳){如果(时间===空)时间=时间戳; var seg = Math.floor((时间戳 - 时间) / 延迟); if (seg > frame) { frame = seg;回调({时间:时间戳,帧:帧})}tref = requestAnimationFrame(循环)} this.isPlaying = false; this.frameRate = function(newfps) { if (!arguments.length) return fps;帧数 = 新帧数;延迟 = 1000 / fps;帧 = -1;时间=空; }; this.start = function() { if (!this.isPlaying) { this.isPlaying = true; tref = requestAnimationFrame(循环); } }; this.pause = function() { if (this.isPlaying) { cancelAnimationFrame(tref); this.isPlaying = false;时间=空;帧 = -1; } }; } body {font:16px sans-serif}
旧答案
requestAnimationFrame
的主要目的是将更新同步到显示器的刷新率。这将要求您以显示器的 FPS 或它的一个因素(即 60、30、15 FPS 的典型刷新率 @ 60 Hz)进行动画处理。
如果您想要更随意的 FPS,那么使用 rAF 毫无意义,因为帧速率无论如何都不会匹配显示器的更新频率(只是这里和那里的一帧),这根本无法为您提供流畅的动画(与所有帧重新计时一样) ),您也可以改用 setTimeout
或 setInterval
。
当您想要以不同的 FPS 播放视频,然后显示它的设备刷新时,这也是专业视频行业的一个众所周知的问题。已经使用了许多技术,例如帧混合和基于运动矢量的复杂重新定时重建中间帧,但是对于画布,这些技术不可用,结果将始终是生涩的视频。
var FPS = 24; /// "silver screen"
var isPlaying = true;
function loop() {
if (isPlaying) setTimeout(loop, 1000 / FPS);
... code for frame here
}
我们将 setTimeout
first 放置的原因(以及为什么在使用 poly-fill 时将某些位置 rAF
先放置)是因为这样会更准确,因为 setTimeout
会立即将事件排队当循环开始时,无论剩余代码将使用多少时间(只要它不超过超时间隔),下一次调用都将在它表示的间隔内(对于纯 rAF,这不是必需的,因为 rAF 会尝试在任何情况下都跳到下一帧)。
还值得注意的是,将它放在首位也会像 setInterval
一样冒着调用堆叠的风险。 setInterval
对于这种用途可能会稍微准确一些。
您可以使用 setInterval
而不是 outside 循环来执行相同的操作。
var FPS = 29.97; /// NTSC
var rememberMe = setInterval(loop, 1000 / FPS);
function loop() {
... code for frame here
}
并停止循环:
clearInterval(rememberMe);
为了在选项卡变得模糊时降低帧速率,您可以添加如下因素:
var isFocus = 1;
var FPS = 25;
function loop() {
setTimeout(loop, 1000 / (isFocus * FPS)); /// note the change here
... code for frame here
}
window.onblur = function() {
isFocus = 0.5; /// reduce FPS to half
}
window.onfocus = function() {
isFocus = 1; /// full FPS
}
这样您可以将 FPS 降低到 1/4 等。
requestAnimationFrame
的主要用途是同步 DOM 操作(读/写),因此不使用它会在访问 DOM 时损害性能,因为操作不会排队一起执行,并且会强制重新绘制不必要的布局。
我建议将您对 requestAnimationFrame
的调用包装在 setTimeout
中:
const fps = 25;
function animate() {
// perform some animation task here
setTimeout(() => {
requestAnimationFrame(animate);
}, 1000 / fps);
}
animate();
您需要在 setTimeout
中调用 requestAnimationFrame
,而不是相反,因为 requestAnimationFrame
将您的函数安排在下一次重绘之前运行,如果您使用 setTimeout
进一步延迟更新,您将错过那个时间窗口。但是,反向操作是合理的,因为您只是在发出请求之前等待一段时间。
这些都是理论上的好主意,直到你深入为止。问题是你不能在不去同步的情况下限制 RAF,破坏它的存在目的。所以你让它全速运行,并在一个单独的循环甚至一个单独的线程中更新你的数据!
是的,我说过。您可以在浏览器中执行多线程 JavaScript!
我知道有两种方法在没有卡顿的情况下效果非常好,使用更少的果汁并产生更少的热量。准确的人工计时和机器效率是最终结果。
抱歉,如果这有点罗嗦,但这里有......
方法一:通过setInterval更新数据,通过RAF更新图形。
使用单独的 setInterval 更新平移和旋转值、物理、碰撞等。将这些值保存在每个动画元素的对象中。将转换字符串分配给每个 setInterval 'frame' 对象中的一个变量。将这些对象保存在一个数组中。以毫秒为单位将间隔设置为所需的 fps:ms=(1000/fps)。这可以保持稳定的时钟,在任何设备上都允许相同的 fps,无论 RAF 速度如何。不要将变换分配给这里的元素!
在 requestAnimationFrame 循环中,使用老式的 for 循环遍历您的数组——不要在这里使用较新的形式,它们很慢!
for(var i=0; i<sprite.length-1; i++){ rafUpdate(sprite[i]); }
在您的 rafUpdate 函数中,从数组中的 js 对象获取转换字符串及其元素 id。您应该已经将您的“精灵”元素附加到变量或通过其他方式轻松访问,这样您就不会浪费时间在 RAF 中“获取”它们。将它们保存在以它们的 html id 命名的对象中效果很好。在它进入您的 SI 或 RAF 之前设置该部分。
仅使用 RAF 更新您的变换,仅使用 3D 变换(即使是 2d),并设置 css "will-change: transform;"关于会改变的元素。这使您的转换尽可能地与本机刷新率同步,启动 GPU,并告诉浏览器最集中的位置。
所以你应该有这样的伪代码......
// refs to elements to be transformed, kept in an array
var element = [
mario: document.getElementById('mario'),
luigi: document.getElementById('luigi')
//...etc.
]
var sprite = [ // read/write this with SI. read-only from RAF
mario: { id: mario ....physics data, id, and updated transform string (from SI) here },
luigi: { id: luigi .....same }
//...and so forth
] // also kept in an array (for efficient iteration)
//update one sprite js object
//data manipulation, CPU tasks for each sprite object
//(physics, collisions, and transform-string updates here.)
//pass the object (by reference).
var SIupdate = function(object){
// get pos/rot and update with movement
object.pos.x += object.mov.pos.x; // example, motion along x axis
// and so on for y and z movement
// and xyz rotational motion, scripted scaling etc
// build transform string ie
object.transform =
'translate3d('+
object.pos.x+','+
object.pos.y+','+
object.pos.z+
') '+
// assign rotations, order depends on purpose and set-up.
'rotationZ('+object.rot.z+') '+
'rotationY('+object.rot.y+') '+
'rotationX('+object.rot.x+') '+
'scale3d('.... if desired
; //...etc. include
}
var fps = 30; //desired controlled frame-rate
// CPU TASKS - SI psuedo-frame data manipulation
setInterval(function(){
// update each objects data
for(var i=0; i<sprite.length-1; i++){ SIupdate(sprite[i]); }
},1000/fps); // note ms = 1000/fps
// GPU TASKS - RAF callback, real frame graphics updates only
var rAf = function(){
// update each objects graphics
for(var i=0; i<sprite.length-1; i++){ rAF.update(sprite[i]) }
window.requestAnimationFrame(rAF); // loop
}
// assign new transform to sprite's element, only if it's transform has changed.
rAF.update = function(object){
if(object.old_transform !== object.transform){
element[object.id].style.transform = transform;
object.old_transform = object.transform;
}
}
window.requestAnimationFrame(rAF); // begin RAF
这使您对数据对象和变换字符串的更新保持同步到 SI 中所需的“帧”速率,并且 RAF 中的实际变换分配同步到 GPU 刷新率。因此,实际的图形更新仅在 RAF 中,但对数据的更改和构建转换字符串在 SI 中,因此没有 jankies,而是“时间”以所需的帧速率流动。
流动:
[setup js sprite objects and html element object references]
[setup RAF and SI single-object update functions]
[start SI at percieved/ideal frame-rate]
[iterate through js objects, update data transform string for each]
[loop back to SI]
[start RAF loop]
[iterate through js objects, read object's transform string and assign it to it's html element]
[loop back to RAF]
方法 2. 将 SI 放在 web-worker 中。这一款非常流畅!
与方法 1 相同,但将 SI 放在 web-worker 中。然后它将在一个完全独立的线程上运行,让页面只处理 RAF 和 UI。将精灵数组作为“可转移对象”来回传递。这是 buko 快。克隆或序列化不需要时间,但它不像通过引用传递,因为来自另一端的引用被破坏了,所以你需要让双方都传递到另一端,并且只在存在时更新它们,排序就像在高中时和你的女朋友来回传递一张纸条。
一次只有一个人可以读写。这很好,只要他们检查它是否未定义以避免错误。 RAF 速度很快,会立即将其踢回,然后通过一堆 GPU 帧检查它是否已被发回。 web-worker 中的 SI 将大部分时间拥有 sprite 数组,并将更新位置、运动和物理数据,以及创建新的转换字符串,然后将其传递回页面中的 RAF。
这是我所知道的通过脚本为元素设置动画的最快方式。这两个函数将作为两个单独的程序在两个单独的线程上运行,以单个 js 脚本所不具备的方式利用多核 CPU。多线程javascript动画。
并且它会在没有卡顿的情况下顺利进行,但在实际指定的帧速率下,几乎没有分歧。
结果:
这两种方法中的任何一种都可以确保您的脚本在任何 PC、手机、平板电脑等上以相同的速度运行(当然,在设备和浏览器的能力范围内)。
visibilitychange
事件。
如何轻松限制到特定的 FPS:
// timestamps are ms passed since document creation.
// lastTimestamp can be initialized to 0, if main loop is executed immediately
var lastTimestamp = 0,
maxFPS = 30,
timestep = 1000 / maxFPS; // ms for each frame
function main(timestamp) {
window.requestAnimationFrame(main);
// skip if timestep ms hasn't passed since last frame
if (timestamp - lastTimestamp < timestep) return;
lastTimestamp = timestamp;
// draw frame here
}
window.requestAnimationFrame(main);
来源:A Detailed Explanation of JavaScript Game Loops and Timing by Isaac Sukin
var time = 0;
var time_framerate = 1000; //in milliseconds
function animate(timestamp) {
if(timestamp > time + time_framerate) {
time = timestamp;
//your code
}
window.requestAnimationFrame(animate);
}
最简单的方法
note
:它在具有不同帧速率的不同屏幕上可能表现不同。
const FPS = 30;
let lastTimestamp = 0;
function update(timestamp) {
requestAnimationFrame(update);
if (timestamp - lastTimestamp < 1000 / FPS) return;
/* <<< PUT YOUR CODE HERE >>> */
lastTimestamp = timestamp;
}
update();
这个问题的一个简单解决方案是在不需要渲染帧的情况下从渲染循环返回:
const FPS = 60;
let prevTick = 0;
function render()
{
requestAnimationFrame(render);
// clamp to fixed framerate
let now = Math.round(FPS * Date.now() / 1000);
if (now == prevTick) return;
prevTick = now;
// otherwise, do your stuff ...
}
重要的是要知道 requestAnimationFrame 取决于用户监视器刷新率 (vsync)。因此,如果您没有在模拟中使用单独的计时器机制,例如依靠 requestAnimationFrame 来获得游戏速度将使其无法在 200Hz 显示器上播放。
跳过 requestAnimationFrame 会导致自定义 fps 的动画不流畅(所需的)。
// 输入/输出 DOM 元素 var $results = $("#results"); var $fps = $("#fps"); var $period = $("#period"); // 用于绘图的 FPS 样本数组 // 动画状态/参数 var fpsInterval, lastDrawTime, frameCount_timed, frameCount, lastSampleTime, currentFps=0, currentFps_timed=0; var 间隔 ID,请求 ID; // 设置画布动画 var canvas = document.getElementById("c"); var canvas_timed = document.getElementById("c2"); canvas_timed.width = canvas.width = 300; canvas_timed.height = canvas.height = 300; var ctx = canvas.getContext("2d"); var ctx2 = canvas_timed.getContext("2d"); // 设置输入事件处理程序 $fps.on('click change keyup', function() { if (this.value > 0) { fpsInterval = 1000 / +this.value; } }); $period.on('click change keyup', function() { if (this.value > 0) { if (intervalID) { clearInterval(intervalID); } intervalID = setInterval(sampleFps, +this.value); } }) ;函数 startAnimating(fps, sampleFreq) { ctx.fillStyle = ctx2.fillStyle = "#000"; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx2.fillRect(0, 0, canvas.width, canvas.height); ctx2.font = ctx.font = "32px sans"; fpsInterval = 1000 / fps; lastDrawTime = performance.now();最后采样时间 = 最后绘制时间;帧数 = 0; frameCount_timed = 0;动画();间隔ID = setInterval(sampleFps, sampleFreq); animate_timed() } function sampleFps() { // 采样 FPS var now = performance.now(); if (frameCount > 0) { currentFps = (frameCount / (now - lastSampleTime) * 1000).toFixed(2); currentFps_timed = (frameCount_timed / (now - lastSampleTime) * 1000).toFixed(2); $results.text(currentFps + " | " + currentFps_timed);帧数 = 0; frameCount_timed = 0; } lastSampleTime = 现在; } function drawNextFrame(now, canvas, ctx, fpsCount) { // 只画一个摆动的秒针 var length = Math.min(canvas.width, canvas.height) / 2.1;变量步= 15000; var theta = (现在 % step) / step * 2 * Math.PI; var xCenter = canvas.width / 2; var yCenter = canvas.height / 2; var x = xCenter + 长度 * Math.cos(theta); var y = yCenter + 长度 * Math.sin(theta); ctx.beginPath(); ctx.moveTo(xCenter, yCenter); ctx.lineTo(x, y); ctx.fillStyle = ctx.strokeStyle = '白色'; ctx.stroke(); var theta2 = theta + 3.14/6; ctx.beginPath(); ctx.moveTo(xCenter, yCenter); ctx.lineTo(x, y); ctx.arc(xCenter, yCenter, 长度*2, theta, theta2); ctx.fillStyle = "rgba(0,0,0,.1)" ctx.fill(); ctx.fillStyle = "#000"; ctx.fillRect(0,0,100,30); ctx.fillStyle = "#080"; ctx.fillText(fpsCount,10,30); } // 每个 fpsInterval (1000/fps) 重画第二个画布 function animate_timed() { frameCount_timed++; drawNextFrame(performance.now(), canvas_timed, ctx2, currentFps_timed); setTimeout(animate_timed, fpsInterval); } function animate(now) { // 请求另一个帧 requestAnimationFrame(animate); // 计算自上次循环以来经过的时间 var elapsed = now - lastDrawTime; // 如果足够的时间已经过去,则绘制下一帧 if (elapsed > fpsInterval) { // 通过设置 lastDrawTime=now 为下一帧做好准备,但是... // 另外,调整 fpsInterval 不是 16.67 的倍数 lastDrawTime =现在 - (经过的 % fpsInterval);帧数++; drawNextFrame(现在,画布,ctx,currentFps); } } startAnimating(+$fps.val(), +$period.val());输入{宽度:100px; } #tvs{ 颜色:红色;填充:0px 25px; } H3{ 字体粗细:400; }
@tavnab 的原始代码。
我总是以非常简单的方式做到这一点,而不会弄乱时间戳:
let fps, eachNthFrame, frameCount;
fps = 30;
//This variable specifies how many frames should be skipped.
//If it is 1 then no frames are skipped. If it is 2, one frame
//is skipped so "eachSecondFrame" is renderd.
eachNthFrame = Math.round((1000 / fps) / 16.66);
//This variable is the number of the current frame. It is set to eachNthFrame so that the
//first frame will be renderd.
frameCount = eachNthFrame;
requestAnimationFrame(frame);
//I think the rest is self-explanatory
function frame() {
if (frameCount === eachNthFrame) {
frameCount = 0;
animate();
}
frameCount++;
requestAnimationFrame(frame);
}
如需将 FPS 限制为任何值,请参阅 jdmayfields answer。但是,对于将帧速率减半的非常快速简便的解决方案,您可以通过以下方式仅每隔 2 帧进行一次计算:
requestAnimationFrame(render);
function render() {
// ... computations ...
requestAnimationFrame(skipFrame);
}
function skipFrame() { requestAnimationFrame(render); }
同样,您始终可以调用 render
但使用变量来控制这次是否进行计算,从而允许您也将 FPS 减少到三分之一或四分之一(在我的情况下,对于示意图 webgl-animation 20fps 仍然足够,而大大降低了客户端的计算负载)
我尝试了针对此问题提供的多种解决方案。尽管解决方案按预期工作,但它们导致的输出并不那么专业。
根据我的个人经验,我强烈建议不要在浏览器端控制 FPS,尤其是使用 requestAnimationFrame。因为,当你这样做时,它会使帧渲染体验非常不稳定,用户会清楚地看到帧跳跃,最后看起来一点也不真实或专业。
因此,我的建议是在发送自身时从服务器端控制 FPS,并在浏览器端收到帧后立即渲染帧。
注意:如果您仍想在客户端进行控制,请尽量避免在控制 fps 的逻辑中使用 setTimeout 或 Date 对象。因为,当 FPS 很高时,它们会在事件循环或对象创建方面引入它们自己的延迟。
这是达到所需fps的想法:
检测浏览器的animationFrameRate(通常为60fps)构建一个bitSet,根据animationFrameRate和你的disiredFrameRate(比如24fps)查找bitSet并有条件地“继续”动画帧循环
它使用 requestAnimationFrame
,因此实际帧速率不会大于 animationFrameRate
。您可以根据animationFrameRate
调整disiredFrameRate
。
我写了一个迷你库和一个画布动画演示。
函数 filterNums(nums, jitter = 0.2, downJitter = 1 - 1 / (1 + jitter)) { let len = nums.length;让 mid = Math.floor(len % 2 === 0 ? len / 2 : (len - 1) / 2), low = mid, high = mid;让低=真,高=真;让 sum = nums[mid], count = 1; for (let i = 1, j, num; i <= mid; i += 1) { if (higher) { j = mid + i;如果 (j === len) 中断;数 = 数[j]; if (num < (sum / count) * (1 + jitter)) { sum += num;计数 += 1;高 = j; } 其他 { 更高 = 假; } } if (lower) { j = mid - i;数 = 数[j]; if (num > (sum / count) * (1 - downJitter)) { sum += num;计数 += 1;低 = j; } 其他 { 较低 = 假; } } } 返回 nums.slice(low, high + 1); } function snapToOrRound(n, values, distance = 3) { for (let i = 0, v; i < values.length; i += 1) { v = values[i]; if (n >= v - 距离 && n <= v + 距离) { 返回 v; } } 返回 Math.round(n); } function detectAnimationFrameRate(numIntervals = 6) { if (typeof numIntervals !== 'number' || !isFinite(numIntervals) || numIntervals < 2) { throw new RangeError('Argument numIntervals 应该是一个不小于 2 的数字') ; } return new Promise((resolve) => { 让 num = Math.floor(numIntervals); 让 numFrames = num + 1; 让最后; 让间隔 = []; 让 i = 0; 让滴答 = () => { let now = performance.now(); i += 1; if (i < numFrames) { requestAnimationFrame(tick); } if (i === 1) { last = now; } else { interval.push(now - last ); last = now; if (i === numFrames) { let compareFn = (a, b) => a < b ? -1 : a > b ? 1 : 0; let sortedIntervals = interval.slice().sort (compareFn); 让 selectedIntervals = filterNums(sortedIntervals, 0.2, 0.1); 让 selectedDuration = selectedIntervals.reduce((s, n) => s + n, 0); 让 seletedFrameRate = 1000 / (selectedDuration / selectedIntervals.length);让 finalFrameRate = snapToOrRound(seletedFrameRate, [60, 120, 90, 30], 5); 解决(finalFrameRate); } } }; requestAnimationFrame(() => { requestAnimationFrame(tick); }); }); } 函数 buildFrameBitSet(animationFrameRate, desiredFrameRate){ 让 bitSet = new Uint8Array(animationFrameRate);让比率=期望帧率/动画帧率; if(ratio >= 1) 返回 bitSet.fill(1); for(let i = 0, prev = -1, curr; i < animationFrameRate; i += 1, prev = curr){ curr = Math.floor(i * ratio); bitSet[i] = (curr !== prev) ? 1:0; } 返回位集; } let $ = (s, c = document) => c.querySelector(s);让 $$ = (s, c = document) => Array.prototype.slice.call(c.querySelectorAll(s));异步函数 main(){ let canvas = $('#digitalClock');让 context2d = canvas.getContext('2d'); await new Promise((resolve) => { if(window.requestIdleCallback){ requestIdleCallback(resolve, {timeout:3000}); }else{ setTimeout(resolve, 0, {didTimeout: false}); } }); let animationFrameRate = await detectAnimationFrameRate(10); // 1. 检测动画帧率 let desiredFrameRate = 24;让 frameBits = buildFrameBitSet(animationFrameRate, desiredFrameRate); // 2. 建立一个位集让句柄;让我 = 0;让 count = 0,那么,actualFrameRate = $('#actualFrameRate'); // 仅调试 let draw = () => { if(++i >= animationFrameRate){ // 应该使用 === 如果 frameBits 不动态改变 i = 0; /* 仅调试 */ let now = performance.now();让 deltaT = 现在 - 然后;让 fps = 1000 / (deltaT / count);实际帧率.textContent = fps;那么=现在;计数 = 0; } if(frameBits[i] === 0){ // 3. 查找位集 handle = requestAnimationFrame(draw);返回; } 计数 += 1; // 仅调试 let d = new Date();让 text = d.getHours().toString().padStart(2, '0') + ':' + d.getMinutes().toString().padStart(2, '0') + ':' + d .getSeconds().toString().padStart(2, '0') + '.' + (d.getMilliseconds() / 10).toFixed(0).padStart(2, '0'); context2d.fillStyle = '#000000'; context2d.fillRect(0, 0, canvas.width, canvas.height); context2d.font = '36px 等宽'; context2d.fillStyle = '#ffffff'; context2d.fillText(文本, 0, 36);句柄 = requestAnimationFrame(draw); }; handle = requestAnimationFrame(() => { then = performance.now(); handle = requestAnimationFrame(draw); }); /* 仅调试 */ $('#animationFrameRate').textContent = animationFrameRate;让 frameRateInput = $('#frameRateInput');让 frameRateOutput = $('#frameRateOutput'); frameRateInput.addEventListener('input', (e) => { frameRateOutput.value = e.target.value; }); frameRateInput.max = 动画帧率; frameRateOutput.value = frameRateOutput.value = desiredFrameRate; frameRateInput.addEventListener('change', (e) => { desiredFrameRate = +e.target.value; frameBits = buildFrameBitSet(animationFrameRate, desiredFrameRate); }); } document.addEventListener('DOMContentLoaded', main);
对先前答案的简化解释。至少如果您想要实时、准确的节流而不会出现卡顿,或者像炸弹一样丢帧。 GPU和CPU友好。
setInterval 和 setTimeout 都是面向 CPU 的,而不是 GPU。
requestAnimationFrame 纯粹是面向 GPU 的。
分别运行它们。这很简单而且不笨拙。在您的 setInterval 中,更新您的数学并在字符串中创建一个小的 CSS 脚本。使用您的 RAF 循环,仅使用该脚本来更新元素的新坐标。不要在 RAF 循环中执行任何其他操作。
RAF 本质上与 GPU 相关联。只要脚本没有改变(即因为 SI 运行速度慢了无数倍),基于 Chromium 的浏览器就知道它们不需要做任何事情,因为没有任何改变。因此,动态脚本创建每个“帧”,例如每秒 60 次,对于 1000 个 RAF GPU 帧仍然相同,但它知道什么都没有改变,最终结果是它不会在这方面浪费任何能量。如果您检查 DevTools,您将看到您的 GPU 帧速率寄存器以 setInterval 描述的速率。
真的,就是这么简单。分开他们,他们就会合作。
没有詹基斯。
这是我找到的一个很好的解释:CreativeJS.com,在传递给 requestAnimationFrame 的函数内包装 setTimeou) 调用。我对“普通” requestionAnimationFrame 的担忧是,“如果我只想要它每秒动画三次怎么办?”即使使用 requestAnimationFrame(相对于 setTimeout),它仍然浪费(一些)“能量”(意味着浏览器代码正在做某事,并可能减慢系统速度)60 或 120 或但是每秒多次,而不是每秒只有两次或三次(如您所愿)。
大多数时候,出于这个原因,我故意使用 JavaScript 来运行我的浏览器。但是,我使用的是 Yosemite 10.10.3,我认为它存在某种计时器问题 - 至少在我的旧系统上(相对较旧 - 意思是 2011 年)。
不定期副业成功案例分享