Управление 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();
}
}
Где Узел.drawFlash () - это просто код, который определяет радиус на основе переменной счетчика, а затем рисует круг.
8 ответов
как дросселировать requestAnimationFrame до определенной частоты кадров
демо дросселированием на 5 ФПС: http://jsfiddle.net/m1erickson/CtsY3/
этот метод работает путем тестирования прошедшего времени с момента выполнения последнего цикла кадров.
код чертежа выполняется только по истечении указанного интервала кадров в секунду.
первая часть кода устанавливает некоторые переменные, используемые для вычисления прошедших время.
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 для типичного обновленный тариф @ 60 Hz).
если вы хотите более произвольный FPS, то нет смысла использовать rAF, так как частота кадров никогда не будет соответствовать частоте обновления монитора в любом случае (просто кадр здесь и там), который просто не может дать вам плавную анимацию (как и со всеми кадровыми повторными таймингами), и вы можете также использовать setTimeout
или setInterval
вместо.
это также хорошо известная проблема в профессиональной видеоиндустрии, когда вы хотите воспроизвести видео на другом FPS, а затем устройство, показывающее его обновление. Много методов имеет были использованы такие как смешивание кадров и сложное перестроение промежуточных кадров на основе векторов движения, но с canvas эти методы недоступны, и результат всегда будет отрывистым видео.
var FPS = 24; /// "silver screen"
var isPlaying = true;
function loop() {
if (isPlaying) setTimeout(loop, 1000 / FPS);
... code for frame here
}
причина, почему мы размещаем setTimeout
первый (а почему какое-то место rAF
во-первых, когда используется Поли-заполнение) является то, что это будет более точным, как setTimeout
будет стоять в очереди событие сразу же при запуске цикла, так что независимо от того, сколько времени оставшийся код будет использовать (при условии, что он не превышает интервал тайм-аута) следующий вызов будет на интервале, который он представляет (для чистого rAF это не важно, так как 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 и т. д.
Я предлагаю обернуть ваш звонок в requestAnimationFrame
на setTimeout
. Если вы позвоните setTimeout
из функции, из которой Вы запросили кадр анимации, вы побеждаете цель requestAnimationFrame
. Но если вы позвоните requestAnimationFrame
внутри setTimeout
это работает:
var fps = 25
function animate() {
setTimeout(function() {
requestAnimationFrame(animate);
}, 1000 / fps);
}
Это все хорошие идеи в теории, пока не углублялся. проблема в том, что вы не можете дросселировать RAF без его де-синхронизации, победив его, это очень важно для существования. таким образом, вы позволяете ему работать на полной скорости и обновлять свои данные в отдельном цикле, или даже отдельный поток!
Да, я это сказал. Вы можете сделайте многопоточный JavaScript в браузере!
есть два метода, которые я знаю, что работа очень хорошо без jank, используя гораздо меньше сока и создавая меньше тепла. Точное время людск-маштаба и эффективность машины чистый результат.
извиняюсь, если это немного многословно, но здесь идет...
метод 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 в массиве и его id элементов. У вас уже должны быть элементы "sprite", прикрепленные к переменной или легко доступные другими средствами, чтобы вы не теряли время на их "получение" в 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. Это FAAAST и гладкий!
то же самое, что и метод 1, но поместите SI в web-worker. Он будет работать на полностью отдельный поток, оставляя страницу для работы только с RAF и UI. Передайте массив sprite взад и вперед как "передаваемый объект". Это Буко фаст. Это не займет времени, чтобы клонировать или сериализовать, но это не похоже на передачу по ссылке в том, что ссылка с другой стороны уничтожена, поэтому вам нужно будет иметь обе стороны, чтобы перейти на другую сторону, и только обновлять их, когда они присутствуют, вроде как передавая записку взад и вперед с вашей подругой в средняя школа.
только один может читать и писать одновременно. Это нормально, пока они проверяют, не определено ли это, чтобы избежать ошибки. RAF быстро и сразу же отбросит его, а затем пройдет через кучу кадров GPU, просто проверяя, был ли он отправлен обратно. SI в веб-работнике будет иметь массив спрайтов большую часть времени и будет обновлять данные о положении, движении и физике, а также создавать новую строку преобразования, а затем передавать ее обратно в RAF на странице.
Это самый быстрый способ, который я знаю, чтобы оживить элементы с помощью скрипта. Две функции будут работать как две отдельные программы, в двух отдельных потоках, используя преимущества многоядерных процессоров таким образом, что один сценарий js этого не делает. Многопоточная анимация javascript.
и он будет делать это плавно без jank, но с фактической заданной частотой кадров, с очень небольшим расхождением.
результат:
любой из этих двух методов убедитесь, что ваш скрипт будет работать с одинаковой скоростью на любом ПК, телефоне, планшете и т. д. (В пределах возможностей устройства и браузера, конечно).
пропуск 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.
Как легко дросселировать до определенного 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 и времени Исаака сукина
Я всегда делаю это очень простым способом, не трогая метки:
var 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
fucntion frame() {
if (frameCount == eachNthFrame) {
frameCount = 0;
animate();
}
frameCount++;
requestAnimationFrame(frame);
}
вот хорошее объяснение, которое я нашел:CreativeJS.com, чтобы обернуть вызов setTimeou) внутри функции, переданной requestAnimationFrame. Моя забота о "простом" requestionAnimationFrame будет: "что, если я только хочу это анимировать три раза в секунду?"Даже с requestAnimationFrame (в отличие от setTimeout) является то, что это еще отходы (некоторое) количество " энергии "(что означает, что код браузера что-то делает и, возможно, замедляет система вниз) 60 или 120 или сколько раз в секунду, а не только два или три раза в секунду (как вы хотите).
большую часть времени я запускаю свои браузеры с JavaScript intentially выкл именно по этой причине. Но я использую Yosemite 10.10.3, и я думаю, что с ним есть какая - то проблема с таймером - по крайней мере, в моей старой системе (относительно старый-2011).