Kontrolujesz fps za pomocą requestAnimationFrame?


140

To wygląda jak requestAnimationFrame to, że jest to de facto sposób na animowanie rzeczy teraz. W większości przypadków działało to całkiem dobrze, ale teraz próbuję zrobić kilka animacji na płótnie i zastanawiałem się: czy jest jakiś sposób, aby upewnić się, że działa z określoną liczbą klatek na sekundę? Rozumiem, że celem RAF jest konsekwentnie płynne animacje i mogę ryzykować, że moja animacja będzie się zacinać, ale w tej chwili wydaje się, że działa z drastycznie różnymi prędkościami dość arbitralnie i zastanawiam się, czy istnieje sposób na walkę że jakoś.

Używałbym, setIntervalale chcę optymalizacji, które oferuje rAF (szczególnie automatyczne zatrzymywanie, gdy zakładka jest aktywna).

Na wypadek, gdyby ktoś chciał spojrzeć na mój kod, jest to mniej więcej:

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

Gdzie Node.drawFlash () to tylko jakiś kod, który określa promień na podstawie zmiennej licznika, a następnie rysuje okrąg.


1
Czy twoja animacja się opóźnia? Myślę, że największą zaletą requestAnimationFramejest (jak sugeruje nazwa) żądanie ramki animacji tylko wtedy, gdy jest to potrzebne. Powiedzmy, że pokazujesz statyczne czarne płótno, powinieneś uzyskać 0 fps, ponieważ nie jest potrzebna nowa klatka. Ale jeśli wyświetlasz animację wymagającą 60 klatek na sekundę, to też powinieneś otrzymać. rAFpozwala po prostu „pomijać” bezużyteczne ramki i oszczędzać procesor.
maxdec

setInterval nie działa również w nieaktywnej karcie.
ViliusL

Ten kod działa inaczej na wyświetlaczu 90 Hz w porównaniu z wyświetlaczem 60 Hz w porównaniu z wyświetlaczem 144 Hz.
mantrax

Odpowiedzi:


190

Jak ograniczyć requestAnimationFrame do określonej liczby klatek na sekundę

Demo dławienie przy 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/

Ta metoda działa poprzez testowanie czasu, który upłynął od wykonania pętli ostatniej ramki.

Twój kod rysowania jest wykonywany tylko wtedy, gdy upłynął określony interwał FPS.

Pierwsza część kodu ustawia niektóre zmienne używane do obliczania czasu, który upłynął.

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

Ten kod jest rzeczywistą pętlą requestAnimationFrame, która rysuje z określoną liczbą klatek na sekundę.

// 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
Doskonałe wyjaśnienie i przykład. To powinno być oznaczone jako zaakceptowana odpowiedź
muxcmux

13
Niezłe demo - powinno zostać zaakceptowane. Tutaj rozwidlono twoje skrzypce, aby zademonstrować użycie window.performance.now () zamiast Date.now (). Świetnie pasuje to do znacznika czasu w wysokiej rozdzielczości, który rAF już otrzymuje, więc nie ma potrzeby wywoływania Date.now () wewnątrz wywołania zwrotnego: jsfiddle.net/chicagogrooves/nRpVD/2
Dean Radcliffe

2
Dzięki za zaktualizowany link wykorzystujący nową funkcję sygnatury czasowej RAF. Nowy znacznik czasu RAF dodaje użyteczną infrastrukturę i jest również bardziej precyzyjny niż Date.now.
markE

13
To naprawdę fajne demo, które zainspirowało mnie do stworzenia własnego ( JSFiddle ). Główne różnice polegają na używaniu rAF (jak demo Deana) zamiast Date, dodaniu elementów sterujących do dynamicznego dostosowywania docelowej liczby klatek na sekundę, próbkowaniu liczby klatek na sekundę w innym przedziale niż animacja oraz dodaniu wykresu historycznych klatek na sekundę.
tavnab

1
Jedyne, co możesz kontrolować, to pomijanie klatki. Monitor 60 kl./s zawsze rysuje w odstępach 16 ms. Na przykład, jeśli chcesz, aby Twoja gra działała z prędkością 50 klatek na sekundę, chcesz pomijać co szóstą klatkę. Sprawdzasz, czy upłynęło 20 ms (1000/50), a nie (minęło tylko 16 ms), więc pomijasz klatkę, a następna klatka 32 ms minęła od narysowania, więc rysujesz i resetujesz. Ale wtedy pominiesz połowę klatek i będziesz działać z prędkością 30 klatek na sekundę. Więc kiedy resetujesz, pamiętasz, że ostatnio czekałeś 12 ms za długo. Więc następna klatka mija kolejne 16 ms, ale liczysz to jako 16 + 12 = 28 ms, więc rysujesz ponownie i czekałeś 8 ms za długo
Curtis

47

Aktualizacja 2016/6

Problem ograniczający liczbę klatek na sekundę polega na tym, że ekran ma stałą częstotliwość odświeżania, zwykle 60 klatek na sekundę.

Jeśli chcemy 24 kl./s, nigdy nie uzyskamy prawdziwych 24 kl./s na ekranie, możemy to tak ustawić, ale nie pokazać, ponieważ monitor może wyświetlać tylko zsynchronizowane klatki przy 15 fps, 30 fps lub 60 fps (niektóre monitory również 120 fps ).

Jednak ze względu na czas możemy obliczyć i zaktualizować, gdy to możliwe.

Możesz zbudować całą logikę kontrolowania liczby klatek na sekundę poprzez hermetyzację obliczeń i wywołań zwrotnych w obiekcie:

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

Następnie dodaj kontroler i kod konfiguracyjny:

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

Stosowanie

Staje się to bardzo proste - teraz wszystko, co musimy zrobić, to utworzyć instancję, ustawiając funkcję zwrotną i żądaną liczbę klatek na sekundę, tak jak poniżej:

var fc = new FpsCtrl(24, function(e) {
     // render each frame here
  });

Następnie uruchom (co może być zachowaniem domyślnym w razie potrzeby):

fc.start();

To wszystko, cała logika jest obsługiwana wewnętrznie.

Próbny

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>

Stara odpowiedź

Głównym celem requestAnimationFrame jest synchronizacja aktualizacji z częstotliwością odświeżania monitora. Będzie to wymagało animacji przy FPS monitora lub jej współczynniku (tj. 60, 30, 15 FPS dla typowej częstotliwości odświeżania przy 60 Hz).

Jeśli chcesz mieć bardziej arbitralny FPS, nie ma sensu używać rAF, ponieważ częstotliwość klatek i tak nigdy nie będzie pasować do częstotliwości aktualizacji monitora (tylko klatka tu i tam), co po prostu nie może zapewnić płynnej animacji (jak w przypadku wszystkich zmian czasu klatek) ) i równie dobrze możesz użyć setTimeoutlub setIntervalzamiast tego.

Jest to również dobrze znany problem w branży profesjonalnego wideo, gdy chcesz odtwarzać wideo z inną liczbą klatek na sekundę niż urządzenie wyświetlające je odświeża się. Zastosowano wiele technik, takich jak łączenie klatek i złożone ponowne tworzenie klatek pośrednich w oparciu o wektory ruchu, ale w przypadku płótna te techniki nie są dostępne, a rezultatem będzie zawsze urywany obraz.

var FPS = 24;  /// "silver screen"
var isPlaying = true;

function loop() {
    if (isPlaying) setTimeout(loop, 1000 / FPS);

    ... code for frame here
}

Powodem, dla którego umieszczamy setTimeout pierwsze miejsce (i dlaczego jakieś miejsce na rAFpierwszym miejscu, gdy jest używane wypełnienie poly), jest to, że będzie to dokładniejsze, ponieważ setTimeoutustawia zdarzenie w kolejce natychmiast po rozpoczęciu pętli, więc bez względu na to, ile czasu zajmie pozostały kod (pod warunkiem, że nie przekracza limitu czasu), następne wywołanie będzie odbywać się w przedziale, który reprezentuje (dla czystego rAF nie jest to konieczne, ponieważ rAF będzie próbował przeskoczyć do następnej klatki w każdym przypadku).

Warto również zauważyć, że umieszczenie go na pierwszym miejscu może również spowodować kumulowanie się połączeń, tak jak w przypadku setInterval. setIntervalmoże być nieco dokładniejsze w tym zastosowaniu.

setIntervalZamiast tego możesz użyć poza pętlą, aby zrobić to samo.

var FPS = 29.97;   /// NTSC
var rememberMe = setInterval(loop, 1000 / FPS);

function loop() {

    ... code for frame here
}

Aby zatrzymać pętlę:

clearInterval(rememberMe);

Aby zmniejszyć liczbę klatek na sekundę, gdy karta jest rozmyta, możesz dodać taki czynnik:

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
}

W ten sposób możesz zmniejszyć FPS do 1/4 itd.


4
W niektórych przypadkach nie próbujesz dopasować liczby klatek na sekundę monitora, ale raczej w sekwencjach obrazów, na przykład, pomijaj klatki. Doskonałe wyjaśnienie przy okazji
Sidonaldson

3
Jednym z największych powodów, dla których warto ograniczać ruch za pomocą requestAnimationFrame, byłoby wyrównanie wykonywania kodu z ramką animacji przeglądarki. Sprawy kończą się znacznie płynniej, zwłaszcza jeśli używasz logiki na danych w każdej klatce, na przykład w przypadku wizualizatorów muzyki.
Chris Dolphin

4
Jest to złe, ponieważ głównym zastosowaniem programu requestAnimationFramejest synchronizacja operacji DOM (odczyt / zapis), więc nieużywanie go obniży wydajność podczas uzyskiwania dostępu do DOM, ponieważ operacje nie będą ustawiane w kolejce do wykonania razem i wymuszą niepotrzebne odświeżenie układu.
vsync

1
Nie ma ryzyka „kumulacji wywołań”, ponieważ JavaScript działa w trybie jednowątkowym, a podczas działania kodu nie jest wyzwalane żadne zdarzenie przekroczenia limitu czasu. Jeśli więc funkcja trwa dłużej niż limit czasu, działa prawie w dowolnym momencie tak szybko, jak to możliwe, podczas gdy przeglądarka nadal będzie przerysowywać i wyzwalać inne przekroczenia czasu między wywołaniami.
dronus

Wiem, że twierdzisz, że odświeżanie strony nie może być aktualizowane szybciej niż limit fps na wyświetlaczu. Czy jednak możliwe jest szybsze odświeżanie, uruchamiając ponowne wlanie strony? I odwrotnie, czy można nie zauważyć wielokrotnych przepływów stron, jeśli są one wykonywane szybciej niż natywna szybkość klatek na sekundę?
Travis J

36

Proponuję zawrzeć połączenie do requestAnimationFramepliku setTimeout. Jeśli wywołujesz setTimeoutz funkcji, z której zażądałeś ramki animacji, pokonujesz cel requestAnimationFrame. Ale jeśli dzwonisz requestAnimationFramez wewnątrz setTimeout, działa to płynnie:

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

1
Wydaje się, że to faktycznie działa, zmniejszając liczbę klatek na sekundę, a więc nie gotując mojego procesora. To takie proste. Twoje zdrowie!
phocks

To przyjemny, prosty sposób na zrobienie tego w przypadku lekkich animacji. Jednak trochę się nie synchronizuje, przynajmniej na niektórych urządzeniach. Użyłem tej techniki na jednym z moich poprzednich silników. Działało dobrze, dopóki sprawy się nie skomplikowały. Największym problemem było to, że po podłączeniu do czujników orientacji albo pozostawał w tyle, albo stawał się niespokojny. Później odkryłem, że używanie oddzielnego setInterval i przesyłanie aktualizacji między czujnikami, ramkami setInterval i ramkami RAF za pośrednictwem właściwości obiektów pozwoliło czujnikom i RAF przejść w czasie rzeczywistym, podczas gdy czas animacji można kontrolować za pomocą aktualizacji właściwości z setInterval.
jdmayfield,

Najlepsza odpowiedź ! Dzięki;)
538ROMEO

Mój monitor ma 60 FPS, jeśli ustawię var fps = 60, uzyskam tylko około 50 FPS używając tego kodu. Chcę spowolnić to do 60, ponieważ niektórzy mają monitory 120 FPS, ale nie chcę wpływać na wszystkich innych. To jest zaskakująco trudne.
Curtis

Powodem, dla którego uzyskujesz mniejszą liczbę klatek na sekundę niż oczekiwano, jest to, że setTimeout może wykonać wywołanie zwrotne po dłuższym niż określone opóźnienie. Istnieje wiele możliwych przyczyn takiego stanu rzeczy. W każdej pętli ustawienie nowego licznika czasu i wykonanie kodu przed ustawieniem nowego limitu czasu zajmuje trochę czasu. Nie masz sposobu, aby być dokładnym, zawsze powinieneś rozważyć wolniejszy niż oczekiwany wynik, ale dopóki nie wiesz, o ile wolniejszy będzie, próba zmniejszenia opóźnienia również byłaby niedokładna. JS w przeglądarkach nie ma być tak dokładny.
pdepmcp

17

To wszystko są dobre pomysły w teorii, dopóki nie wejdziesz głęboko. Problem polega na tym, że nie można dławić RAF bez dez-synchronizacji, pokonując jego cel istnienia. Więc pozwalasz mu działać z pełną prędkością i aktualizujesz dane w osobnej pętli , a nawet w osobnym wątku!

Tak, powiedziałem to. W przeglądarce możesz wykonywać wielowątkowy JavaScript!

Wiem, że są dwie metody, które działają wyjątkowo dobrze bez szarpania, zużywając znacznie mniej soku i wytwarzając mniej ciepła. Dokładne wyczucie czasu na skalę ludzką i wydajność maszyny to wynik netto.

Przepraszamy, jeśli jest to trochę rozwlekłe, ale proszę bardzo ...


Metoda 1: Zaktualizuj dane za pomocą setInterval i grafiki za pomocą RAF.

Użyj osobnego setInterval do aktualizacji wartości przesunięcia i obrotu, fizyki, kolizji itp. Zachowaj te wartości w obiekcie dla każdego animowanego elementu. Przypisz ciąg transformacji do zmiennej w obiekcie w każdej „ramce” setInterval. Zachowaj te obiekty w tablicy. Ustaw interwał na żądane fps w ms: ms = (1000 / fps). Utrzymuje to stały zegar, który pozwala na taką samą liczbę klatek na sekundę na dowolnym urządzeniu, niezależnie od prędkości RAF. Nie przypisuj tutaj transformacji do elementów!

W pętli requestAnimationFrame przeprowadź iterację przez tablicę za pomocą starej pętli for - nie używaj tutaj nowszych formularzy, są one powolne!

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

W swojej funkcji rafUpdate pobierz łańcuch transformacji z obiektu js w tablicy oraz identyfikator jego elementów. Powinieneś już mieć swoje elementy „sprite” dołączone do zmiennej lub łatwo dostępne w inny sposób, aby nie tracić czasu na „zdobywanie” ich w RAF. Przechowywanie ich w obiekcie nazwanym na podstawie ich identyfikatorów HTML działa całkiem nieźle. Ustaw tę część, zanim trafi do twojego SI lub RAF.

Użyj RAF zaktualizować przekształca tylko używać wyłącznie 3D przekształca (nawet dla 2d) i ustaw css „will change: transform”; na elementach, które się zmienią. Dzięki temu Twoje transformacje są zsynchronizowane z natywną częstotliwością odświeżania tak bardzo, jak to możliwe, uruchamia GPU i informuje przeglądarkę, na czym się najbardziej skoncentrować.

Więc powinieneś mieć coś takiego jak ten pseudokod ...

// 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

Dzięki temu aktualizacje obiektów danych i ciągi transformacji są zsynchronizowane z żądaną częstotliwością klatek w SI, a rzeczywiste przypisania transformacji w RAF są zsynchronizowane z częstotliwością odświeżania GPU. Tak więc rzeczywiste aktualizacje grafiki są tylko w RAF, ale zmiany danych i tworzenie ciągu transformacji są w SI, więc nie ma jankie, ale „czas” płynie z pożądaną liczbą klatek na sekundę.


Pływ:

[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]

Metoda 2. Umieść SI w pracowniku sieciowym. Ten jest FAAAST i gładki!

To samo co metoda 1, ale umieść SI w programie roboczym sieci. Będzie wtedy działać w zupełnie osobnym wątku, pozostawiając stronę tylko do obsługi RAF i interfejsu użytkownika. Przekazuj tablicę duszków w tę iz powrotem jako „przenoszony obiekt”. To jest szybkie. Klonowanie lub serializacja nie zajmuje czasu, ale to nie jest tak, jak przekazywanie przez referencję, ponieważ referencja z drugiej strony jest niszczona, więc musisz mieć obie strony przejść na drugą stronę i zaktualizować je tylko wtedy, gdy są obecne, posortuj jak przekazywanie sobie notatki z dziewczyną w liceum.

Jednocześnie tylko jeden może czytać i pisać. Jest to w porządku, o ile sprawdzają, czy nie jest to nieokreślone, aby uniknąć błędu. RAF jest SZYBKI i natychmiast go odkopie, a następnie przejdzie przez kilka ramek GPU, sprawdzając tylko, czy został jeszcze odesłany. SI w programie roboczym sieciowym będzie przez większość czasu mieć tablicę duszków i będzie aktualizować dane dotyczące pozycji, ruchu i fizyki, a także tworzyć nowy ciąg transformacji, a następnie przekazywać go z powrotem do RAF na stronie.

Jest to najszybszy znany mi sposób animowania elementów za pomocą skryptu. Te dwie funkcje będą działały jako dwa oddzielne programy, w dwóch oddzielnych wątkach, wykorzystując wielordzeniowe procesory w sposób, w jaki nie robi tego pojedynczy skrypt js. Wielowątkowa animacja javascript.

I zrobi to płynnie bez szarpnięć, ale przy rzeczywistej określonej liczbie klatek na sekundę, z bardzo małą rozbieżnością.


Wynik:

Każda z tych dwóch metod zapewni, że skrypt będzie działał z taką samą prędkością na dowolnym komputerze, telefonie, tablecie itp. (Oczywiście w ramach możliwości urządzenia i przeglądarki).


Na marginesie - w metodzie 1, jeśli w setInterval jest zbyt dużo aktywności, może to spowolnić RAF z powodu asynchronicznej jednowątkowej. Możesz złagodzić to zerwanie tej aktywności w większym stopniu niż na ramce SI, więc asynchronizacja szybciej przekaże kontrolę z powrotem do RAF. Pamiętaj, że RAF działa z maksymalną liczbą klatek na sekundę, ale synchronizuje zmiany graficzne z wyświetlaczem, więc można pominąć kilka klatek RAF - o ile nie pominiesz więcej niż klatek SI, nie będzie szarpać.
jdmayfield

Metoda 2 jest bardziej niezawodna, ponieważ w rzeczywistości polega na wielozadaniowości dwóch pętli, bez przełączania się w przód iw tył w trybie asynchronicznym, ale nadal chcesz, aby ramka SI trwała dłużej niż żądana liczba klatek na sekundę, więc dzielenie aktywności SI może nadal być pożądane, jeśli ma miejsce dużo manipulacji danymi, które zajęłyby więcej niż jedną ramkę SI.
jdmayfield

Pomyślałem, że warto wspomnieć, jako ciekawostkę, że uruchamianie sparowanych pętli, takich jak ta, faktycznie rejestruje w Chromes DevTools, że GPU działa z częstotliwością klatek określoną w pętli setInterval! Wydaje się, że tylko klatki RAF, w których zachodzą zmiany graficzne, są liczone jako klatki przez miernik FPS. Tak więc ramki RAF, w których działają tylko nie graficzne lub tylko puste pętle, nie liczą się w przypadku GPU. Uważam to za interesujące jako punkt wyjścia do dalszych badań.
jdmayfield,

Wydaje mi się, że to rozwiązanie ma problem z tym, że działa dalej, gdy rAF zostaje zawieszony, np. Z powodu przełączenia użytkownika na inną kartę.
N4ppeL

1
PS Czytałem trochę i wygląda na to, że większość przeglądarek i tak ogranicza zdarzenia czasowe do jednego na sekundę w kartach w tle (co prawdopodobnie powinno być w jakiś sposób obsługiwane). Jeśli nadal chcesz rozwiązać problem i całkowicie zatrzymać się, gdy nie jest widoczny, wydaje się, że jest to visibilitychangewydarzenie.
N4ppeL,

3

Jak łatwo ustawić przepustnicę na konkretny 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);

Źródło: Izaak Sukin - szczegółowe wyjaśnienie pętli gier JavaScript i ich synchronizacji


1
Jeśli mój monitor działa z szybkością 60 kl./s i chcę, aby moja gra działała z szybkością 58 kl./s, ustawiam maxFPS = 58, dzięki temu będzie działał z szybkością 30 kl./s, ponieważ będzie pomijał co drugą klatkę.
Curtis

Tak, próbowałem też tego. Decyduję się nie dławić samego RAF-a - tylko zmiany są aktualizowane przez setTimeout. Przynajmniej w Chrome powoduje to, że efektywne fps działają w tempie setTimeouts, zgodnie z odczytami w DevTools. Oczywiście może aktualizować rzeczywiste klatki wideo tylko z prędkością karty graficznej i częstotliwością odświeżania monitora, ale ta metoda wydaje się działać przy najmniejszej liczbie ćpunów, a więc najbardziej płynnej, „pozornej” kontroli liczby klatek na sekundę, do czego zmierzam.
jdmayfield,

Ponieważ śledzę wszystkie ruchy w obiektach JS niezależnie od RAF, to utrzymuje logikę animacji, wykrywanie kolizji lub cokolwiek potrzebujesz, działając w percepcyjnie spójnym tempie, niezależnie od RAF lub setTimeout, z odrobiną dodatkowej matematyki.
jdmayfield,

2

Pominięcie requestAnimationFrame powoduje, że animacja nie jest płynna (pożądana) przy niestandardowej liczbie klatek na sekundę.

// 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>

Oryginalny kod @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);
}

Dodaj kilka zdań, aby wyjaśnić, co robi Twój kod, aby uzyskać więcej głosów pozytywnych za swoją odpowiedź.
Fuzzy Analysis

1

Zawsze robię to w bardzo prosty sposób bez majstrowania przy sygnaturach czasowych:

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
Będzie to działać zbyt szybko, jeśli twój monitor ma 120 fps.
Curtis,

0

Oto dobre wyjaśnienie, które znalazłem: CreativeJS.com , aby opakować wywołanie setTimeou) wewnątrz funkcji przekazanej do requestAnimationFrame. Moim zmartwieniem w przypadku „zwykłego” requestionAnimationFrame byłoby: „a co, jeśli chcę , aby animowała się tylko trzy razy na sekundę?” Nawet w przypadku requestAnimationFrame (w przeciwieństwie do setTimeout) nadal marnuje (pewną) ilość „energii” (co oznacza, że ​​kod przeglądarki coś robi i prawdopodobnie spowalnia system) 60 lub 120, a nawet wiele razy na sekundę, w przeciwieństwie do dwóch lub trzech razy na sekundę (jak chcesz).

Przez większość czasu używam przeglądarek z wyłączonym JavaScriptem właśnie z tego powodu. Ale używam Yosemite 10.10.3 i myślę, że jest z nim jakiś problem z zegarem - przynajmniej w moim starym systemie (stosunkowo starym - czyli 2011).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.