Управление фпс с помощью requestAnimationFrame?


140

Похоже, 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
Ваша анимация запаздывает? Я думаю, что самое большое преимущество requestAnimationFrame(как подсказывает название) запрашивает кадр анимации только тогда, когда это необходимо. Допустим, вы показываете статический черный холст, вы должны получить 0 кадров в секунду, потому что новый кадр не требуется. Но если вы отображаете анимацию, которая требует 60 кадров в секунду, вы должны получить это тоже. rAFпозволяет просто «пропустить» ненужные кадры, а затем сохранить процессор.
maxdec

setInterval не работает и в неактивной вкладке.
ViliusL

Этот код работает по-разному на дисплее с частотой 90 Гц и дисплеем с частотой 60 Гц и дисплеем с частотой 144 Гц.
Мантракс

Ответы:


190

Как регулировать requestAnimationFrame для определенной частоты кадров

Демонстрация при 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's interval (16.7ms)
        then = now - (elapsed % fpsInterval);

        // Put your drawing code here

    }
}

5
Отличное объяснение и пример. Это должно быть помечено как принятый ответ
muxcmux

13
Хорошая демонстрация - это должно быть принято. Здесь, разветвленная скрипка, чтобы продемонстрировать использование window.performance.now () вместо Date.now (). Это хорошо сочетается с меткой времени с высоким разрешением, которую уже получает rAF, поэтому нет необходимости вызывать Date.now () внутри обратного вызова: jsfiddle.net/chicagogrooves/nRpVD/2
Дин Рэдклифф,

2
Спасибо за обновленную ссылку, использующую новую функцию отметки времени rAF. Новая временная метка rAF добавляет полезную инфраструктуру и более точна, чем Date.now.
markE

13
Это действительно хорошее демо, которое вдохновило меня на создание собственного ( JSFiddle ). Основными отличиями являются использование rAF (например, демонстрации Дина) вместо даты, добавление элементов управления для динамической настройки целевой частоты кадров, выборка частоты кадров на отдельном интервале от анимации и добавление графика исторических частот кадров.
tavnab

1
Все, что вы можете контролировать, это когда вы собираетесь пропустить кадр. Монитор со скоростью 60 кадров в секунду всегда рисует с интервалом 16 мс. Например, если вы хотите, чтобы ваша игра работала со скоростью 50 кадров в секунду, вы хотите пропустить каждый 6-й кадр. Вы проверяете, прошло ли 20 мс (1000/50), и не прошло (прошло только 16 мс), поэтому вы пропускаете фрейм, затем следующий фрейм 32 мс прошел с момента рисования, поэтому вы рисуете и сбрасываете. Но тогда вы пропустите половину кадров и будете работать со скоростью 30 кадров в секунду. Поэтому, когда вы перезагружаетесь, вы помните, что в последний раз вы ждали 12 мс слишком долго. Таким образом, в следующем кадре проходит еще 16 мс, но вы считаете это 16 + 12 = 28 мс, поэтому вы снова рисуете и ждали 8 мс слишком долго
Кертис

47

Обновление 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- синхронизировать обновления с частотой обновления монитора. Это потребует от вас анимирования на частоте кадров монитора или ее факторе (т. Е. 60, 30, 15 кадров в секунду для типичной частоты обновления при 60 Гц).

Если вам нужен более произвольный 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
}

Причина, по которой мы ставим на 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 и т. Д.


4
В некоторых случаях вы не пытаетесь сопоставить частоту кадров мониторов, а, например, в последовательностях изображений, например, отбрасывать кадры. Отличное объяснение между прочим
Сидональдсон

3
Одной из главных причин регулирования с помощью requestAnimationFrame является выравнивание выполнения некоторого кода с кадром анимации браузера. В конечном итоге все становится намного более плавным, особенно если вы выполняете некоторую логику для данных в каждом кадре, как, например, в музыкальных визуализаторах.
Крис Дельфин

4
Это плохо, потому что основное использование requestAnimationFrame- это синхронизация операций DOM (чтение / запись), поэтому, если вы не используете его, это снизит производительность при доступе к DOM, поскольку операции не будут поставлены в очередь для одновременного выполнения и вынудят перерисовку макета без необходимости.
vsync

1
Нет риска «сложения вызовов», так как JavaScript выполняется однопоточным, и во время выполнения вашего кода событие тайм-аута не вызывается. Поэтому, если функция занимает больше времени, чем тайм-аут, она просто запускается практически в любое время с такой скоростью, с какой может, в то время как браузер все равно выполняет перерисовку и запускает другие таймауты между вызовами.
Дронус

Я знаю, что вы заявляете, что обновление страницы не может быть обновлено быстрее, чем ограничение fps на дисплее. Однако можно ли быстрее обновлять, вызывая переформатирование страницы? И наоборот, возможно ли не заметить многократное переворачивание страниц, если они выполняются быстрее, чем собственная частота кадров в секунду?
Трэвис Дж

37

Я предлагаю заключить ваш звонок requestAnimationFrameв setTimeout:

const fps = 25;
function animate() {
  // perform some animation task here

  setTimeout(() => {
    requestAnimationFrame(animate);
  }, 1000 / fps);
}
animate();

Вам нужно звонить requestAnimationFrameизнутри setTimeout, а не наоборот, потому что requestAnimationFrameвы планируете запускать вашу функцию прямо перед следующей перерисовкой, и если вы отложите обновление дальше, setTimeoutвы пропустите это временное окно. Однако делать обратное - это разумно, поскольку вы просто ждете некоторое время, прежде чем сделать запрос.


1
На самом деле, похоже, что это помогает снизить частоту кадров и не загружать мой процессор. И это так просто. Ура!
Фокс

Это хороший, простой способ сделать это для легкой анимации. Это немного не синхронизировано, по крайней мере, на некоторых устройствах. Я использовал эту технику на одном из моих бывших двигателей. Это работало хорошо, пока все не стало сложным. Самая большая проблема заключалась в том, что при подключении к датчикам ориентации он либо отставал, либо становился нервным. Позже я обнаружил, что использование отдельного setInterval и передача обновлений между датчиками, кадрами setInterval и кадрами RAF через свойства объекта позволяют датчикам и RAF работать в режиме реального времени, в то время как временем анимации можно управлять с помощью обновлений свойств из setInterval.
Jdmayfield

Лучший ответ ! Спасибо;)
538ROMEO

Мой монитор 60 FPS, если я установлю var fps = 60, я получу только около 50 FPS, используя этот код. Я хочу уменьшить его до 60, потому что у некоторых людей есть 120 FPS мониторов, но я не хочу влиять на всех остальных. Это на удивление сложно.
Кертис

Причина, по которой вы получаете более низкий FPS, чем ожидалось, заключается в том, что setTimeout может выполнить обратный вызов после превышения указанной задержки. Существует ряд возможных причин для этого. И каждый цикл требует времени, чтобы установить новый таймер и выполнить некоторый код перед установкой нового тайм-аута. У вас нет способа быть точным с этим, вы всегда должны учитывать более медленный, чем ожидалось, результат, но если вы не знаете, насколько медленным он будет, попытка уменьшить задержку также будет неточной. JS в браузерах не должен быть таким точным.
pdepmcp

17

Это все хорошие идеи в теории, пока вы не углубитесь. Проблема в том, что вы не можете ограничить RAF, не отменив синхронизацию, не победив его цель для существования. Таким образом , вы дайте ему поработать на полной скорости, и обновлять данные в отдельном цикле , или даже отдельный поток!

Да, я сказал это. Вы можете сделать многопоточный JavaScript в браузере!

Я знаю, что есть два метода, которые работают очень хорошо без рывков, используют гораздо меньше сока и создают меньше тепла. Точное человеческое масштабирование и эффективность машины - чистый результат.

Извинения, если это немного многословно, но здесь идет ...


Способ 1: обновить данные через setInterval и графику через RAF.

Используйте отдельный setInterval для обновления значений перемещения и поворота, физики, столкновений и т. Д. Сохраняйте эти значения в объекте для каждого анимированного элемента. Присвойте строку преобразования переменной в объекте каждый setInterval 'frame'. Храните эти объекты в массиве. Установите ваш интервал на желаемый fps в мс: ms = (1000 / fps). Это сохраняет постоянные часы, которые позволяют одинаковые кадры в секунду на любом устройстве, независимо от скорости RAF. Не назначайте преобразования для элементов здесь!

В цикле requestAnimationFrame итерируйте свой массив с циклом старой школы for - не используйте новые формы здесь, они медленные!

for(var i=0; i<sprite.length-1; i++){  rafUpdate(sprite[i]);  }

В вашей функции rafUpdate получите строку преобразования из вашего объекта js в массиве и идентификатор его элементов. У вас уже должны быть элементы «спрайта», прикрепленные к переменной или легкодоступные с помощью других средств, чтобы вы не теряли время на «получение» их в RAF. Хранить их в объекте, названном в честь их html-идентификаторов, довольно хорошо. Настройте эту часть еще до того, как она попадет в ваш SI или RAF.

Используйте RAF для обновления только ваших преобразований , используйте только 3D-преобразования (даже для 2d) и установите css "will-change: transform;" на элементы, которые будут меняться. Это обеспечивает максимально возможную синхронизацию ваших преобразований с собственной частотой обновления, запускает графический процессор и сообщает браузеру, где сосредоточиться больше всего.

Таким образом, вы должны иметь что-то вроде этого псевдокода ...

// 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, таким образом, нет никаких затей, а «время» течет с желаемой частотой кадров.


Поток:

[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. Поместите СИ в веб-работника. Этот FAAAST и гладкий!

То же, что и в методе 1, но поместите SI в web-работника. Тогда он будет работать в совершенно отдельном потоке, оставляя страницу для работы только с RAF и пользовательским интерфейсом. Передайте массив спрайтов назад и вперед как «передаваемый объект». Это быстро. Клонирование или сериализация не требуют времени, но это не похоже на передачу по ссылке, поскольку ссылка с другой стороны уничтожается, поэтому вам нужно будет передать обе стороны на другую сторону и обновлять их только при наличии, сортировать как передавать записку туда и обратно со своей девушкой в ​​старшей школе.

Только один может читать и писать одновременно. Это нормально, если они проверяют, не является ли оно неопределенным, чтобы избежать ошибки. RAF является БЫСТРЫМ и немедленно отбросит его, а затем пройдёт через кучу кадров GPU, просто проверяя, отправлено ли оно ещё. SI в web-работнике будет большую часть времени иметь массив спрайтов, обновлять данные о положении, движении и физике, а также создавать новую строку преобразования, а затем передавать ее обратно в RAF на странице.

Это самый быстрый способ, которым я знаю, для анимации элементов с помощью скрипта. Эти две функции будут работать как две отдельные программы в двух отдельных потоках, используя преимущества многоядерных процессоров так, как этого не делает один js-скрипт. Многопоточная анимация JavaScript.

И это будет происходить плавно без рывков, но с фактической заданной частотой кадров, с очень небольшим расхождением.


Результат:

Любой из этих двух способов гарантирует, что ваш скрипт будет работать с одинаковой скоростью на любом ПК, телефоне, планшете и т. Д. (Конечно, в пределах возможностей устройства и браузера).


Как примечание: в методе 1, если в вашем setInterval слишком много активности, это может замедлить ваш RAF из-за однопоточной асинхронности. Вы можете смягчить это разделение этой активности более чем на SI-кадре, поэтому async быстрее передаст управление RAF. Помните, что RAF работает с максимальной частотой кадров, но синхронизирует графические изменения с дисплеем, поэтому можно пропустить несколько кадров RAF - при условии, что вы не пропустите больше кадров SI, они не будут отображаться.
Jdmayfield

Метод 2 является более надежным, поскольку он фактически выполняет многозадачность в двух циклах, не переключаясь назад и вперед через асинхронный режим, но вы все равно хотите избежать, чтобы ваш кадр SI занимал больше времени, чем желаемая частота кадров, поэтому разделение активности SI все еще может быть желательно, если в нем много манипуляций с данными, для завершения которых требуется более одного кадра SI.
Jdmayfield

Я подумал, что стоит отметить в качестве примечания, что подобные парные циклы на самом деле регистрируются в Chromes DevTools, что графический процессор работает с частотой кадров, указанной в цикле setInterval! Похоже, только кадры RAF, в которых происходят графические изменения, подсчитываются как кадры измерителем FPS. Таким образом, кадры RAF, в которых только неграфическая работа, или просто пустые циклы, не учитываются применительно к GPU. Я нахожу это интересным в качестве отправной точки для дальнейших исследований.
Jdmayfield

Я полагаю, что у этого решения есть проблема, что оно продолжает работать, когда rAF приостанавливается, например, потому что пользователь переключился на другую вкладку.
N4ppeL

1
PS Я немного читал, и, похоже, большинство браузеров в любом случае ограничивают синхронизируемые события раз в секунду в фоновых вкладках (которые, вероятно, также должны обрабатываться каким-то образом). Если вы все еще хотите решить проблему и сделать паузу, когда она не видна, похоже, это visibilitychangeсобытие.
N4ppeL

3

Как легко дросселировать до определенного 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 от Исаака Сукина


1
Если мой монитор работает на скорости 60 кадров в секунду и я хочу, чтобы моя игра работала на скорости 58 кадров в секунду, я установил maxFPS = 58, это заставит его работать со скоростью 30 кадров в секунду, потому что он будет пропускать каждый второй кадр.
Кертис

Да, я тоже это попробовал. Я предпочитаю не регулировать сам RAF - только изменения обновляются setTimeout. По крайней мере, в Chrome это заставляет эффективные fps работать с темпом setTimeouts, согласно показаниям DevTools. Конечно, он может обновлять только реальные видеокадры со скоростью видеокарты и частотой обновления монитора, но этот метод, по-видимому, работает с наименьшими затратами, таким как наиболее плавное «кажущееся» управление частотой кадров, что я и собираюсь использовать.
Jdmayfield

Так как я отслеживаю все движения в объектах JS отдельно от RAF, это сохраняет логику анимации, обнаружение столкновений или все, что вам нужно, с постоянной воспринимаемой скоростью, независимо от RAF или setTimeout, с небольшой дополнительной математикой.
Jdmayfield

2

Пропуск 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>

Оригинальный код от @tavnab.


2
var time = 0;
var time_framerate = 1000; //in milliseconds

function animate(timestamp) {
  if(timestamp > time + time_framerate) {
    time = timestamp;    

    //your code
  }

  window.requestAnimationFrame(animate);
}

Пожалуйста, добавьте несколько предложений, чтобы объяснить, что делает ваш код, чтобы вы могли получить больше голосов за ваш ответ.
Нечеткий анализ

1

Я всегда делаю это очень простым способом, не путаясь с метками времени:

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

1
Это будет работать слишком быстро, если ваш монитор имеет 120 кадров в секунду.
Кертис

0

Вот хорошее объяснение, которое я нашел: CreativeJS.com , чтобы обернуть вызов setTimeou) внутри функции, переданной в requestAnimationFrame. Моя проблема с «простым» requestInimationFrame будет: «Что если я хочу, чтобы он анимировался только три раза в секунду?» Даже с requestAnimationFrame (в отличие от setTimeout) он по- прежнему тратит (некоторое) количество «энергии» (что означает, что код браузера что-то делает, и, возможно, замедляет работу системы) 60 или 120 или столько раз в секунду, как в противоположность только два или три раза в секунду (как вы могли бы хотеть).

Большую часть времени я запускаю свои браузеры с отключенным JavaScript именно по этой причине. Но я использую Yosemite 10.10.3, и я думаю, что с ним есть какая-то проблема с таймером - по крайней мере, на моей старой системе (относительно старой - имеется в виду 2011).

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.