使用 requestAnimationFrame 控制 fps?

这似乎是现在动画化事物的事实方式。它在大多数情况下对我来说效果很好,但现在我正在尝试做一些画布动画,我想知道:有没有办法确保它以一定的fps运行?我知道rAF的目的是为了始终如一地制作流畅的动画,我可能会冒着使我的动画断断续续的风险,但现在它似乎以截然不同的速度运行,非常随意,我想知道是否有一种方法可以以某种方式解决这个问题。requestAnimationFrame

我会使用,但我想要rAF提供的优化(特别是当选项卡处于焦点时自动停止)。setInterval

如果有人想看我的代码,它几乎是:

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


答案 1

如何将请求动画帧限制为特定帧速率

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

此代码是实际的请求动画帧循环,它以指定的 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

    }
}

答案 2

更新 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 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>

旧答案

的主要目的是将更新同步到监视器的刷新率。这将要求您以监视器的FPS或其系数(即60,30,15 FPS,典型刷新率为60 Hz)进行动画处理。requestAnimationFrame

如果你想要一个更任意的FPS,那么使用rAF是没有意义的,因为帧速率无论如何都不会与显示器的更新频率相匹配(只是这里和那里的一帧),这根本无法给你一个平滑的动画(就像所有帧重新计时一样),你也可以使用或代替。setTimeoutsetInterval

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

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

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

    ... code for frame here
}

我们放在第一位(以及为什么在使用多边形填充时将某个位置放在第一位)的原因是,这将更准确,因为当循环开始时,它将立即对事件进行排队,因此无论剩余代码将使用多少时间(前提是它不超过超时间隔),下一个调用都将位于它所代表的间隔(对于纯rAF,这不是必需的,因为rAF将尝试在任何情况下都跳到下一帧)。setTimeoutrAFsetTimeout

另外值得注意的是,将其放在第一位也会冒着调用堆积的风险。 对于此用途可能稍微更准确。setIntervalsetInterval

你可以在循环之外使用来做同样的事情。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等。