用 requestAnimationFrame 控制 fps ?

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()只是一些基于计数器变量确定半径的代码,然后绘制一个圆。

182640 次浏览

如何节流requestAnimationFrame到特定的帧率

演示5 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帧/秒。

如果我们想要24帧/秒,我们永远不会在屏幕上得到真正的24帧/秒,我们可以这样计时,但不显示它,因为显示器只能显示15帧/秒,30帧/秒或60帧/秒的同步帧(有些显示器也只能显示120帧/秒)。

但是,为了计时,我们可以在可能的时候计算和更新。

你可以通过将计算和回调封装到一个对象中来构建控制帧速率的所有逻辑:

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 sans-serif";


// update canvas with some information and animation
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)
})


// start the loop
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;


function loop(timestamp) {
if (time === null) time = timestamp;
var seg = Math.floor((timestamp - time) / delay);
if (seg > frame) {
frame = seg;
callback({
time: timestamp,
frame: frame
})
}
tref = requestAnimationFrame(loop)
}


this.isPlaying = false;
	

this.frameRate = function(newfps) {
if (!arguments.length) return fps;
fps = newfps;
delay = 1000 / fps;
frame = -1;
time = null;
};
	

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;
}
};
}
body {font:16px sans-serif}
<label>Framerate: <select id=sFPS>
<option>12</option>
<option>15</option>
<option>24</option>
<option>25</option>
<option>29.97</option>
<option>30</option>
<option>60</option>
</select></label><br>
<canvas id=c height=60></canvas><br>
<button id=bState>Start/Stop</button>

旧的答案

requestAnimationFrame的主要目的是同步更新到监视器的刷新率。这将需要你动画在显示器的FPS或它的一个因素(即。60,30,15 FPS的典型刷新率@ 60hz)。

如果你想要一个更任意的FPS,那么使用rAF是没有意义的,因为帧率永远不会匹配监视器的更新频率,无论如何(只是一个帧在这里和那里),这根本不能给你一个流畅的动画(与所有帧重新计时),你不妨使用setTimeoutsetInterval代替。

这也是专业视频行业中一个众所周知的问题,当你想要以不同的FPS播放视频,然后设备显示它刷新。许多技术已经被使用,如帧混合和基于运动向量的复杂的重新计时重建中间帧,但对于画布,这些技术是不可用的,结果将始终是颠簸的视频。

var FPS = 24;  /// "silver screen"
var isPlaying = true;


function loop() {
if (isPlaying) setTimeout(loop, 1000 / FPS);


... code for frame here
}

为什么我们setTimeout 第一个(以及为什么有些地方rAF第一poly-fill时使用),这将是更准确setTimeout将队列事件立即在循环开始时,无论多少时间剩下的代码将使用(不超过提供超时间隔)将在下一次调用它所代表的间隔(纯rAF这不是重要的皇家空军将试图跳到下一个框架在任何情况下)。

同样值得注意的是,将它放在前面也会有调用堆积的风险,就像setInterval一样。setInterval可能更准确一点。

你可以使用setInterval代替循环来做同样的事情。

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等等。

这是我发现的一个很好的解释:CreativeJS.com,以包装一个setTimeou)调用传递给requestAnimationFrame的函数内部。我对“普通”requestionAnimationFrame的关注是,“如果我只想要它来每秒动画三次呢?”即使使用requestAnimationFrame(而不是setTimeout),它仍然也会浪费(一些)“能量”(意味着浏览器代码正在做某事,可能会使系统变慢)60或120或任何每秒多次,而不是每秒只有2或3次(正如你可能想要的)。

大多数时候,我故意用JavaScript 来运行浏览器就是因为这个原因。但是,我使用的是Yosemite 10.10.3,我认为它有某种定时器问题-至少在我的旧系统上(相对较旧-意思是2011年)。

跳过requestAnimationFrame会导致自定义fps的不光滑的(所需)动画。

.
// Input/output DOM elements
var $results = $("#results");
var $fps = $("#fps");
var $period = $("#period");


// Array of FPS samples for graphing


// Animation state/parameters
var fpsInterval, lastDrawTime, frameCount_timed, frameCount, lastSampleTime,
currentFps=0, currentFps_timed=0;
var intervalID, requestID;


// Setup canvas being animated
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");




// Setup input event handlers


$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);
}
});




function 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();
lastSampleTime = lastDrawTime;
frameCount = 0;
frameCount_timed = 0;
animate();
    

intervalID = setInterval(sampleFps, sampleFreq);
animate_timed()
}


function sampleFps() {
// sample 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);
        

frameCount = 0;
frameCount_timed = 0;
}
lastSampleTime = now;
}


function drawNextFrame(now, canvas, ctx, fpsCount) {
// Just draw an oscillating seconds-hand
    

var length = Math.min(canvas.width, canvas.height) / 2.1;
var step = 15000;
var theta = (now % step) / step * 2 * Math.PI;


var xCenter = canvas.width / 2;
var yCenter = canvas.height / 2;
    

var x = xCenter + length * Math.cos(theta);
var y = yCenter + length * Math.sin(theta);
    

ctx.beginPath();
ctx.moveTo(xCenter, yCenter);
ctx.lineTo(x, y);
ctx.fillStyle = ctx.strokeStyle = 'white';
ctx.stroke();
    

var theta2 = theta + 3.14/6;
    

ctx.beginPath();
ctx.moveTo(xCenter, yCenter);
ctx.lineTo(x, y);
ctx.arc(xCenter, yCenter, length*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);
}


// redraw second canvas each fpsInterval (1000/fps)
function animate_timed() {
frameCount_timed++;
drawNextFrame( performance.now(), canvas_timed, ctx2, currentFps_timed);
    

setTimeout(animate_timed, fpsInterval);
}


function animate(now) {
// request another frame
requestAnimationFrame(animate);
    

// calc elapsed time since last loop
var elapsed = now - lastDrawTime;


// if enough time has elapsed, draw the next frame
if (elapsed > fpsInterval) {
// Get ready for next frame by setting lastDrawTime=now, but...
// Also, adjust for fpsInterval not being multiple of 16.67
lastDrawTime = now - (elapsed % fpsInterval);


frameCount++;
drawNextFrame(now, canvas, ctx, currentFps);
}
}
startAnimating(+$fps.val(), +$period.val());
input{
width:100px;
}
#tvs{
color:red;
padding:0px 25px;
}
H3{
font-weight:400;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h3>requestAnimationFrame skipping <span id="tvs">vs.</span> setTimeout() redraw</h3>
<div>
<input id="fps" type="number" value="33"/> FPS:
<span id="results"></span>
</div>
<div>
<input id="period" type="number" value="1000"/> Sample period (fps, ms)
</div>
<canvas id="c"></canvas><canvas id="c2"></canvas>

原始代码由@tavnab。

我建议将你对requestAnimationFrame的调用包装在setTimeout中:

const fps = 25;
function animate() {
// perform some animation task here


setTimeout(() => {
requestAnimationFrame(animate);
}, 1000 / fps);
}
animate();

你需要从setTimeout内部调用requestAnimationFrame,而不是相反,因为requestAnimationFrame将你的函数安排在下一次重绘之前运行,如果你使用setTimeout进一步延迟更新,你就会错过这个时间窗口。然而,相反的做法是合理的,因为您只是在发出请求之前等待一段时间。

如何轻松节流到特定的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);

来源:关于JavaScript游戏循环和计时的详细解释

这些在理论上都是不错的想法,直到你深入研究。__abc0 __abc1, __abc2

是的,我说了。你可以在浏览器中做多线程JavaScript !

我知道有两种方法效果非常好,不用太多果汁,产生的热量也少得多。精确的人类尺度的时间和机器的效率是最终的结果。

如果这有点啰嗦,我很抱歉,但是……


方法1:通过setInterval更新数据,通过RAF更新图形。

使用单独的setInterval来更新平移和旋转值,物理,碰撞等。将这些值保存在每个动画元素的对象中。将转换字符串分配给对象中的一个变量setInterval 'frame'。将这些对象保存在一个数组中。设置你的间隔到你想要的fps在ms: ms=(1000/fps)。这保持了一个稳定的时钟,允许相同的fps在任何设备上,不管RAF速度。不要将转换分配给这里的元素!

在requestAnimationFrame循环中,用一个老派的for循环遍历你的数组——不要在这里使用更新的形式,它们很慢!

for(var i=0; i<sprite.length-1; i++){  rafUpdate(sprite[i]);  }

在rafUpdate函数中,从数组中的js对象中获取transform字符串,以及它的elements 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放到网络工作者中。这是FAAAST和平滑!

与方法1相同,但将SI放在web-worker中。它将在一个完全独立的线程上运行,只留下页面处理RAF和UI。将精灵数组作为“可转移对象”来回传递。这太快了。它不需要时间来克隆或序列化,但它不像通过引用传递,因为来自另一端的引用被破坏了,所以你需要让双方都传递到另一端,并且只在出现时更新它们,有点像在高中和你的女朋友来回传递一张纸条。

同一时间只有一个人可以读和写。这很好,只要他们检查它是否没有定义,以避免错误。RAF是FAST的,它会立即把它踢回来,然后通过一堆GPU帧来检查它是否已经被发回。web worker中的SI在大多数情况下拥有精灵数组,并将更新位置、移动和物理数据,以及创建新的转换字符串,然后将其传递回页面中的RAF。

这是我所知道的通过脚本制作动画元素的最快方法。这两个函数将作为两个独立的程序,在两个独立的线程上运行,以一种单一js脚本无法利用的方式利用多核CPU。多线程javascript动画。

而且它会平稳地这样做,没有jank,但在实际指定的帧速率下,发散很小。


结果:

这两种方法中的任何一种都可以确保您的脚本在任何PC、手机、平板电脑等上以相同的速度运行(当然,在设备和浏览器的能力范围内)。

我总是用这种非常简单的方法来做这件事,而不会弄乱时间戳:

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);
}
var time = 0;
var time_framerate = 1000; //in milliseconds


function animate(timestamp) {
if(timestamp > time + time_framerate) {
time = timestamp;


//your code
}


window.requestAnimationFrame(animate);
}

这个问题的一个简单解决方案是,如果帧不需要渲染,则从渲染循环返回:

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的显示器上播放。

要将FPS调到任何值,请参见jdmayfields回答。 然而,对于一个非常简单快捷的解决方案来减半你的帧率,你可以简单地通过

每2帧进行计算
requestAnimationFrame(render);
function render() {
// ... computations ...
requestAnimationFrame(skipFrame);
}
function skipFrame() { requestAnimationFrame(render); }

类似地,你总是可以调用render,但使用一个变量来控制你这次是否进行计算,允许你也将FPS削减到第三或第四(在我的情况下,对于一个原理图webgl动画20fps仍然足够,同时大大降低客户机的计算负载)

最简单的方法

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();


我尝试了针对这个问题提供的多种解决方案。即使解决方案如预期的那样工作,它们也会导致不那么专业的输出。

根据我的个人经验,我强烈建议不要在浏览器端控制FPS,特别是使用requestAnimationFrame。因为,当你这样做的时候,它会使帧渲染体验非常不稳定,用户会清楚地看到帧跳跃,最后,它看起来一点也不真实或专业。

所以,我的建议是在发送的时候从服务器端控制FPS,并在浏览器端接收到帧后立即渲染它们。

注意:如果您仍然想在客户端控制,请尝试避免 在控制fps的逻辑中使用setTimeout或Date对象。 因为,当FPS很高的时候,这些会引入自己的延迟 事件循环或对象创建的术语

这里有一个想法,以达到理想的fps:

  1. 检测浏览器的animationFrameRate(通常为60fps)
  2. 根据animationFrameRate和你的disiredFrameRate(比如24fps)构建一个bitSet
  3. 查找bitSet和有条件的“继续”;动画帧循环

它使用requestAnimationFrame,因此实际帧速率不会大于animationFrameRate。你可以根据animationFrameRate调整disiredFrameRate

我写了一个迷你库,和一个画布动画演示。

function filterNums(nums, jitter = 0.2, downJitter = 1 - 1 / (1 + jitter)) {
let len = nums.length;
let mid = Math.floor(len % 2 === 0 ? len / 2 : (len - 1) / 2), low = mid, high = mid;
let lower = true, higher = true;
let sum = nums[mid], count = 1;
for (let i = 1, j, num; i <= mid; i += 1) {
if (higher) {
j = mid + i;
if (j === len)
break;
num = nums[j];
if (num < (sum / count) * (1 + jitter)) {
sum += num;
count += 1;
high = j;
} else {
higher = false;
}
}
if (lower) {
j = mid - i;
num = nums[j];
if (num > (sum / count) * (1 - downJitter)) {
sum += num;
count += 1;
low = j;
} else {
lower = false;
}
}
}
return 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 - distance && n <= v + distance) {
return v;
}
}
return Math.round(n);
}


function detectAnimationFrameRate(numIntervals = 6) {
if (typeof numIntervals !== 'number' || !isFinite(numIntervals) || numIntervals < 2) {
throw new RangeError('Argument numIntervals should be a number not less than 2');
}
return new Promise((resolve) => {
let num = Math.floor(numIntervals);
let numFrames = num + 1;
let last;
let intervals = [];
let i = 0;
let tick = () => {
let now = performance.now();
i += 1;
if (i < numFrames) {
requestAnimationFrame(tick);
}
if (i === 1) {
last = now;
} else {
intervals.push(now - last);
last = now;
if (i === numFrames) {
let compareFn = (a, b) => a < b ? -1 : a > b ? 1 : 0;
let sortedIntervals = intervals.slice().sort(compareFn);
let selectedIntervals = filterNums(sortedIntervals, 0.2, 0.1);
let selectedDuration = selectedIntervals.reduce((s, n) => s + n, 0);
let seletedFrameRate = 1000 / (selectedDuration / selectedIntervals.length);
let finalFrameRate = snapToOrRound(seletedFrameRate, [60, 120, 90, 30], 5);
resolve(finalFrameRate);
}
}
};
requestAnimationFrame(() => {
requestAnimationFrame(tick);
});
});
}
function buildFrameBitSet(animationFrameRate, desiredFrameRate){
let bitSet = new Uint8Array(animationFrameRate);
let ratio = desiredFrameRate / animationFrameRate;
if(ratio >= 1)
return 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;
}
return bitSet;
}






let $ = (s, c = document) => c.querySelector(s);
let $$ = (s, c = document) => Array.prototype.slice.call(c.querySelectorAll(s));


async function main(){
let canvas = $('#digitalClock');
let 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. detect animation frame rate
let desiredFrameRate = 24;
let frameBits = buildFrameBitSet(animationFrameRate, desiredFrameRate); // 2. build a bit set
let handle;
let i = 0;
let count = 0, then, actualFrameRate = $('#actualFrameRate'); // debug-only


let draw = () => {
if(++i >= animationFrameRate){ // shoud use === if frameBits don't change dynamically
i = 0;
/* debug-only */
let now = performance.now();
let deltaT = now - then;
let fps = 1000 / (deltaT / count);
actualFrameRate.textContent = fps;
then = now;
count = 0;
}
if(frameBits[i] === 0){ // 3. lookup the bit set
handle = requestAnimationFrame(draw);
return;
}
count += 1; // debug-only
let d = new Date();
let 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 monospace';
context2d.fillStyle = '#ffffff';
context2d.fillText(text, 0, 36);
handle = requestAnimationFrame(draw);
};
handle = requestAnimationFrame(() => {
then = performance.now();
handle = requestAnimationFrame(draw);
});


/* debug-only */
$('#animationFrameRate').textContent = animationFrameRate;
let frameRateInput = $('#frameRateInput');
let frameRateOutput = $('#frameRateOutput');
frameRateInput.addEventListener('input', (e) => {
frameRateOutput.value = e.target.value;
});
frameRateInput.max = animationFrameRate;
frameRateOutput.value = frameRateOutput.value = desiredFrameRate;
frameRateInput.addEventListener('change', (e) => {
desiredFrameRate = +e.target.value;
frameBits = buildFrameBitSet(animationFrameRate, desiredFrameRate);
});
}


document.addEventListener('DOMContentLoaded', main);
<div>
Animation Frame Rate: <span id="animationFrameRate">--</span>
</div>
<div>
Desired Frame Rate: <input id="frameRateInput" type="range" min="1" max="60" step="1" list="frameRates" />
<output id="frameRateOutput"></output>
<datalist id="frameRates">
<option>15</option>
<option>24</option>
<option>30</option>
<option>48</option>
<option>60</option>
</datalist>
</div>
<div>
Actual Frame Rate: <span id="actualFrameRate">--</span>
</div>


<canvas id="digitalClock" width="240" height="48"></canvas>

对前面答案的简化解释。至少如果你想要实时,准确的节流,没有janks,或像炸弹一样丢帧。GPU和CPU友好。

setInterval和setTimeout都是面向cpu的,而不是GPU的。

requestAnimationFrame是纯粹面向gpu的。

分别运行它们。这很简单,也不蹩脚。在setInterval中,更新您的数学并在字符串中创建一个小的CSS脚本。对于您的RAF循环,只使用该脚本更新元素的新坐标。不要在RAF循环中做任何其他事情。

RAF本质上与GPU绑定在一起。只要脚本没有改变(例如,因为SI运行速度慢了很多倍),基于chromium的浏览器就知道他们不需要做任何事情,因为没有变化。如果你签入< >强DevTools < / >强,你会看到你的GPU帧速率寄存器的速率由setInterval

真的,就是这么简单。分开他们,他们就会合作。

没有jankies。