Управление 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).