Управление fps с помощью 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();
}
}
Где Node.drawFlash() - это всего лишь некоторый код, который определяет радиус, основанный на переменной счетчика, а затем рисует круг.
Спасибо!
Ответы
Ответ 1
Как отключить запросAnimationFrame к определенной частоте кадров
Демонстрация дросселирования при 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 interval (16.7ms)
then = now - (elapsed % fpsInterval);
// Put your drawing code here
}
}
Ответ 2
Обновление 2016/6
Проблема, регулирующая частоту кадров, заключается в том, что экран имеет постоянную скорость обновления, обычно 60 FPS.
Если мы хотим 24 FPS, мы никогда не получим истинные 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();
Что он, вся логика обрабатывается внутренне.
Demo
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>
Ответ 3
Я предлагаю переносить ваш вызов на requestAnimationFrame
в setTimeout
. Если вы вызываете setTimeout
из функции, из которой вы запросили рамку анимации, вы побеждаете цель requestAnimationFrame
. Но если вы вызываете requestAnimationFrame
из setTimeout
, он работает плавно:
var fps = 25
function animate() {
setTimeout(function() {
requestAnimationFrame(animate);
}, 1000 / fps);
}
Ответ 4
Пропуск requestAnimationFrame вызывает не гладкую (желаемую) анимацию на пользовательских кадрах.
// 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>
Ответ 5
Вот хорошее объяснение, которое я нашел: CreativeJS.com, чтобы обернуть вызов setTimeou) внутри функции, переданной requestAnimationFrame. Моя забота о "равнине" requestionAnimationFrame была бы "что, если я только хочу, чтобы она анимировалась три раза в секунду?" Даже с requestAnimationFrame (в отличие от setTimeout) заключается в том, что он по-прежнему тратит (часть) количество "энергии" (что означает, что код браузера что-то делает, и, возможно, замедляет работу системы) 60 или 120 или даже много раз в секунду, против двух или трех раз в секунду (как вам может захотеть).
В большинстве случаев я запускаю свои браузеры с встроенным JavaScript по этой причине. Но я использую Yosemite 10.10.3, и я думаю, что с ним связана какая-то проблема с таймером - по крайней мере, на моей старой системе (относительно старой - значит 2011).