rob*_*uan 111 javascript performance animation canvas requestanimationframe
这似乎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();
}
}
Run Code Online (Sandbox Code Playgroud)
其中Node.drawFlash()只是一些代码,它根据计数器变量确定半径,然后绘制一个圆.
mar*_*rkE 154
How to throttle requestAnimationFrame to a specific frame rate
Demo throttling at 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/
This method works by testing the elapsed time since executing the last frame loop.
Your drawing code executes only when your specified FPS interval has elapsed.
The first part of the code sets some variables used to calculate elapsed time.
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();
}
Run Code Online (Sandbox Code Playgroud)
And this code is the actual requestAnimationFrame loop which draws at your specified 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
}
}
Run Code Online (Sandbox Code Playgroud)
小智 38
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)
}
}
Run Code Online (Sandbox Code Playgroud)
然后添加一些控制器和配置代码:
// 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;
}
};
Run Code Online (Sandbox Code Playgroud)
它变得非常简单 - 现在,我们所要做的就是通过设置回调函数和所需的帧速率来创建一个实例,如下所示:
var fc = new FpsCtrl(24, function(e) {
// render each frame here
});
Run Code Online (Sandbox Code Playgroud)
然后开始(如果需要,可以是默认行为):
fc.start();
Run Code Online (Sandbox Code Playgroud)
就是这样,所有逻辑都在内部处理.
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;
}
};
}Run Code Online (Sandbox Code Playgroud)
body {font:16px sans-serif}Run Code Online (Sandbox Code Playgroud)
<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>Run Code Online (Sandbox Code Playgroud)
老答案
主要目的requestAnimationFrame是将更新同步到监视器的刷新率.这将要求您在显示器的FPS或其系数的动画处设置动画(即,对于60 Hz的典型刷新率,为60,30,15 FPS).
如果你想要一个更随意的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
}
Run Code Online (Sandbox Code Playgroud)
我们setTimeout 首先放置的原因(以及为什么某些地方rAF在使用poly-fill时首先放置)的原因是,这将更加准确,因为setTimeout当循环开始时,将立即对事件进行排队,这样无论剩余代码将使用多长时间(假设它没有超过超时间隔)下一次调用将是它所代表的间隔(对于纯rAF,这不是必需的,因为rAF将尝试在任何情况下跳转到下一帧).
另外值得注意的是,将它放在第一位也会冒着堆叠的风险setInterval.setInterval对于这种用途可能稍微准确一些.
你也可以使用setInterval,而不是外循环做同样的.
var FPS = 29.97; /// NTSC
var rememberMe = setInterval(loop, 1000 / FPS);
function loop() {
... code for frame here
}
Run Code Online (Sandbox Code Playgroud)
并停止循环:
clearInterval(rememberMe);
Run Code Online (Sandbox Code Playgroud)
为了在标签变得模糊时降低帧速率,您可以添加如下因素:
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
}
Run Code Online (Sandbox Code Playgroud)
这样你可以将FPS降低到1/4等.
Luk*_*lor 31
我建议把你的电话换成requestAnimationFrame一个setTimeout.如果你setTimeout从你请求动画帧的函数中调用,那么你就失去了目的requestAnimationFrame.但如果你requestAnimationFrame从内部打电话setTimeout顺利工作:
var fps = 25
function animate() {
setTimeout(function() {
requestAnimationFrame(animate);
}, 1000 / fps);
}
Run Code Online (Sandbox Code Playgroud)
jdm*_*eld 10
这些都是理论上的好主意,直到你深入. 问题是你不能在没有去同步的情况下限制RAF,打败它是现有的目的. 所以你让它以全速运行,并在一个单独的循环中更新你的数据,甚至是一个单独的线程!
是的,我说了.您可以在浏览器中执行多线程JavaScript!
我知道有两种方法可以很好地工作,没有粘性,使用更少的果汁和产生更少的热量.精确的人体计时和机器效率是最终结果.
道歉,如果这有点罗嗦,但这里......
方法1:通过setInterval更新数据,通过RAF更新图形.
使用单独的setInterval更新平移和旋转值,物理,碰撞等.将这些值保存在每个动画元素的对象中.将变换字符串分配给对象中的每个setInterval"frame"中的变量.将这些对象保存在一个数组中.将您的间隔设置为所需的fps,单位为ms:ms =(1000/fps).这保持了一个稳定的时钟,允许任何设备上的相同fps,无论RAF速度如何. 不要在这里将变换分配给元素!
在requestAnimationFrame循环中,使用old-school for循环遍历数组 - 不要在这里使用较新的表单,它们很慢!
for(var i=0; i<sprite.length-1; i++){ rafUpdate(sprite[i]); }
Run Code Online (Sandbox Code Playgroud)
在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
Run Code Online (Sandbox Code Playgroud)
这样可以保持对数据对象的更新,并将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]
Run Code Online (Sandbox Code Playgroud)
方法2.将SI放入Web工作者.这个是FAAAST并且顺利!
与方法1相同,但将SI放在web-worker中.它将在一个完全独立的线程上运行,然后让页面只处理RAF和UI.将精灵数组来回传递为"可转移对象".这快速了.克隆或序列化没有时间,但它不像是通过引用传递,因为来自另一方的引用被破坏,所以你需要将双方传递到另一方,并且只在存在时更新它们,排序就像在高中时和女朋友来回传递一张便条一样.
一次只能读写一个.这是好的,只要他们检查是否未定义以避免错误.RAF是快速的,会立即将它踢回来,然后通过一堆GPU帧检查它是否已被发回.Web工作者中的SI将在大多数时间内拥有精灵阵列,并将更新位置,移动和物理数据,以及创建新的变换字符串,然后将其传递回页面中的RAF.
这是我所知道的通过脚本动画元素的最快方式.这两个函数将作为两个独立的程序在两个独立的线程上运行,利用多核CPU的方式,而单个js脚本则不会.多线程javascript动画.
并且它会在没有抖动的情况下顺利地完成,但是在实际指定的帧速率下,几乎没有发散.
结果:
这两种方法中的任何一种都可以确保您的脚本在任何PC,手机,平板电脑等上以相同的速度运行(当然,在设备和浏览器的功能范围内).
小智 8
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();
Run Code Online (Sandbox Code Playgroud)
如何轻松节流到特定的 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);
Run Code Online (Sandbox Code Playgroud)
来源:Isaac Sukin 对 JavaScript 游戏循环和计时的详细解释
解决此问题的一个简单方法是,如果不需要渲染帧,则从渲染循环返回:
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 ...
}
Run Code Online (Sandbox Code Playgroud)
重要的是要知道 requestAnimationFrame 取决于用户监视器刷新率(垂直同步)。因此,如果您在模拟中没有使用单独的计时器机制,那么依靠 requestAnimationFrame 来提高游戏速度将使其无法在 200Hz 显示器上玩。
小智 5
var time = 0;
var time_framerate = 1000; //in milliseconds
function animate(timestamp) {
if(timestamp > time + time_framerate) {
time = timestamp;
//your code
}
window.requestAnimationFrame(animate);
}
Run Code Online (Sandbox Code Playgroud)