Керування fps у запитіAnimationFrame?


140

Схоже, requestAnimationFrameце фактичний спосіб анімації речей зараз. Здебільшого це працювало для мене здебільшого, але зараз я намагаюся зробити кілька анімаційних полотен, і мені було цікаво: чи є якийсь спосіб переконатися, що він працює на певному кадрі в секунду? Я розумію, що ціль 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.
позначкаE

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

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

47

Оновлення 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();

Ось і все, вся логіка обробляється всередині.

Демо

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 Гц).

Якщо ви хочете отримати більш довільну 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
У деяких випадках ви не намагаєтеся відповідати частоті кадрів моніторів, а, наприклад, у послідовностях зображень, наприклад, опускати кадри. Відмінне пояснення btw
sidonaldson

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

4
Це погано, тому що головне використання - requestAnimationFrameце синхронізація DOM-операцій (читання / запис), тому її використання не призведе до погіршення продуктивності при доступі до DOM, оскільки операції не будуть в черзі виконуватись разом і змушуватимуть перефарбування макета без потреби.
vsync

1
Не існує ризику "складання викликів", оскільки JavaScript запускається однопотоково, і під час роботи коду не запускається подія тайм-ауту. Отже, якщо функція займає більше часу, ніж час очікування, вона просто працює майже будь-який час так швидко, наскільки це можливо, тоді як браузер все-таки буде перемальовувати і запускати інші тайм-аути між дзвінками.
дрон

Я знаю, що ви заявляєте, що оновлення сторінки не може бути оновлено швидше, ніж обмеження кадрів у кадрі на дисплеї. Однак чи можна оновити швидше, запустивши оновлення сторінки? І навпаки, чи можна не помітити декілька оновлень сторінок, якщо вони виконуються швидше, ніж натиснута частота кадрів в секунду?
Travis J

36

Я пропоную завершити ваш дзвінок requestAnimationFrameв setTimeout. Якщо ви телефонуєте setTimeoutв межах функції, від якої ви запитували кадр анімації, ви перемагаєте мету requestAnimationFrame. Але якщо ви телефонуєте requestAnimationFrameзсередини, setTimeoutце працює безперебійно:

var fps = 25
function animate() {
  setTimeout(function() {
    requestAnimationFrame(animate);
  }, 1000 / fps);
}

1
Насправді це, здається, працює в тому, щоб зменшити частоту кадрів, і тому не готувати мій процесор. І це так просто. Ура!
записує

Це приємний, простий спосіб зробити це для легкої анімації. Хоча це дещо не синхронізується, хоча б на деяких пристроях. Я використовував цю техніку на одному з моїх колишніх двигунів. Це працювало добре, поки справи не склалися. Найбільшою проблемою було те, що, якщо підключитися до датчиків орієнтації, воно буде або відставати, або стає стрибковим. Пізніше я знайшов використання окремого набору Інтервалу та спілкування оновлень між датчиками, кадрами 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 'frame'. Зберігайте ці об’єкти в масиві. Встановіть свій інтервал на бажаний кадр в секунду в мс: ms = (1000 / fps). Це зберігає стійкий годинник, який дозволяє однакові fps на будь-якому пристрої, незалежно від швидкості RAF. Не призначайте сюди перетворення елементам!

У циклі requestAnimationFrame, повторіть свій масив зі старою школою для циклу-- не використовуйте тут новіші форми, вони повільні!

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

У функції rafUpdate отримайте рядок перетворення з вашого js-об’єкта в масиві та ідентифікатор його елементів. Ви вже повинні мати свої «спрайти» елементи, приєднані до змінної або легко доступні за допомогою інших засобів, щоб ви не втрачали час, отримуючи їх у RAF. Тримаючи їх в об’єкті, названому на честь їх HTML-коду, працює досить добре. Встановіть цю частину, перш ніж вона навіть перейде у ваш SI або RAF.

Використовуйте RAF оновити перетворення тільки використовувати тільки 3D перетворення (навіть для 2d), а також безліч CSS «волі , зміна: перетворення;» на елементи, які будуть змінюватися. Це дозволяє максимально синхронізувати ваші перетворення з нативним коефіцієнтом оновлення, натискає на 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, але зміни в даних та побудова рядка перетворення є в СІ, тому жодні дженкі, а «час» протікає з потрібною швидкістю кадрів.


Потік:

[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. Помістіть СІ у веб-працівника. Цей - ШВИДКО і гладко!

Те саме, що і метод 1, але покласти СІ у веб-працівника. Він буде запущений на абсолютно окремому потоці, залишаючи сторінку для роботи лише з RAF та інтерфейсом користувача. Передайте масив спрайт вперед і назад як "об'єкт, що передається". Це швидко буко. Клонування або серіалізація не потребує часу, але це не так, як пройти посилання, оскільки посилання з іншого боку знищено, тому вам потрібно буде перейти на іншу сторону і оновити їх лише при наявності, сортувати про те, як передавати записку туди-сюди зі своєю дівчиною у середній школі

Одночасно можна читати і писати лише один. Це добре, якщо вони перевіряють, чи не визначено це, щоб уникнути помилки. RAF - Швидкий і негайно поверне його назад, а потім пройде через купу кадрів GPU, просто перевіривши, чи він ще відправлений назад. SI у веб-робітника буде мати масив спрайтів, і він оновлюватиме дані про позиції, рух та фізику, а також створює нову рядок перетворення, а потім передасть її назад до RAF на сторінці.

Це найшвидший спосіб, яким я знаю, анімувати елементи за допомогою сценарію. Дві функції будуть виконуватись як дві окремі програми, на двох окремих потоках, використовуючи переваги багатоядерних процесорів таким чином, що один сценарій js не робить. Багатопотокова анімація javascript.

І це буде робити безперешкодно, але за фактичної заданої частоти кадрів, з дуже невеликою розбіжністю.


Результат:

Будь-який із цих двох методів забезпечить ваш сценарій працювати з однаковою швидкістю на будь-якому ПК, телефоні, планшеті тощо (звичайно, у межах можливостей пристрою та браузера).


Як бічна примітка - у Способі 1, якщо у вашому setInterval занадто багато активності, це може уповільнити ваш RAF через однопоточну асинхронізацію. Ви можете пом'якшити це розбиття цієї активності більше, ніж на кадрі SI, так що асинхронізація швидше передасть управління назад RAF. Пам'ятайте, що RAF працює з максимальною швидкістю кадрів, але синхронізує графічні зміни з дисплеєм, тому нормально пропустити кілька кадрів RAF-- до тих пір, поки ви не пропустите більше, ніж рамки SI, він не зашкодить.
jdmayfield

Спосіб 2 є більш надійним, оскільки він фактично багатозадачний для двох циклів, не перемикаючись назад і назад через асинхронізацію, але ви все одно хочете уникати, щоб ваш кадр SI зайняв більше часу, ніж бажана частота кадрів, тому розбиття активності SI може все-таки бути бажано, якщо у нього буде багато маніпуляцій з даними, що потребує більше одного кадру SI.
jdmayfield

Я подумав, що варто згадати, як відому цікавість, що запущені парні петлі на зразок цього насправді реєструються в Chromes DevTools, що GPU працює з частотою кадрів, визначеною в циклі 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);

Джерело: Детальне пояснення ігрових циклів і часу Ісаака Сукіна


1
Якщо мій монітор працює зі швидкістю 60 кадрів в секунду, і я хочу, щоб моя гра працювала зі швидкістю 58 кадрів в секунду, я встановив maxFPS = 58, це призведе до запуску в 30 кадрів в секунду, оскільки він буде пропускати кожен другий кадр.
Кертіс

Так, я спробував і цей. Я вирішую фактично не придушувати сам RAF - лише зміни оновлюються setTimeout. Принаймні, у Chrome це призводить до того, що ефективний кадр в секунду запускається у встановленому темпі, відповідно до показань у DevTools. Звичайно, він може оновлювати лише реальні відеокадри зі швидкістю відеокарти та відстежувати частоту оновлення монітора, але цей метод, як видається, працює з найменшими джанками, настільки плавним "очевидним" управлінням кадрів у кадрі, про що я йду.
jdmayfield

Оскільки я відслідковую весь рух в JS-об'єктах окремо від RAF, це зберігає логіку анімації, виявлення зіткнення або все, що вам потрібно, працює з сприйнятливо стійкою швидкістю, незалежно від RAF або setTimeout, з невеликою додатковою математикою.
jdmayfield

2

Пропуск проханняAnimationFrame викликає не плавну (бажану) анімацію на користувальницькій 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.


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. Моє занепокоєння "простою" requestionAnimationFrame було б "що робити, якщо я хочу лише три рази в секунду анімувати?" Навіть з requestAnimationFrame (на відміну від setTimeout) полягає в тому, що він все одно витрачає (деяку) кількість "енергії" (це означає, що код браузера щось робить і, можливо, уповільнює роботу системи) 60 або 120 або скільки разів у секунду, як на противагу лише два-три рази на секунду (як ви хочете).

Більшу частину часу я запускаю браузери з JavaScript навмисно з цієї причини. Але я використовую Yosemite 10.10.3, і я думаю, що з цим є якась проблема з таймером - принаймні у моїй старій системі (відносно стара - це означає 2011 рік).

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.