Controllo fps con requestAnimationFrame?


140

Sembra che requestAnimationFrame sia di fatto il modo di animare le cose ora. Ha funzionato abbastanza bene per me per la maggior parte, ma in questo momento sto provando a fare alcune animazioni su tela e mi chiedevo: c'è un modo per assicurarsi che funzioni a un certo fps? Capisco che lo scopo di rAF è quello di animazioni costantemente fluide, e potrei correre il rischio di rendere la mia animazione instabile, ma in questo momento sembra che funzioni a velocità drasticamente diverse abbastanza arbitrariamente, e mi chiedo se c'è un modo per combattere che in qualche modo.

Vorrei usare setIntervalma voglio le ottimizzazioni offerte da rAF (in particolare l'arresto automatico quando la scheda è attiva).

Nel caso in cui qualcuno voglia guardare il mio codice, è praticamente:

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

Dove Node.drawFlash () è solo un codice che determina il raggio in base a una variabile contatore e quindi disegna un cerchio.


1
La tua animazione è in ritardo? Penso che il più grande vantaggio requestAnimationFramesia (come suggerisce il tipo di nome) di richiedere un frame di animazione solo quando è necessario. Diciamo che mostri una tela nera statica, dovresti ottenere 0 fps perché non è necessaria una nuova cornice. Ma se stai visualizzando un'animazione che richiede 60 fps, dovresti ottenere anche quella. rAFconsente solo di "saltare" i frame inutili e quindi di salvare la CPU.
maxdec,

setInterval non funziona anche nella scheda inattiva.
ViliusL

Questo codice funziona in modo diverso sul display a 90 Hz contro il display a 60 Hz contro il display a 144 Hz.
manthrax,

Risposte:


190

Come limitare requestAnimationFrame a una frequenza di fotogrammi specifica

Demo throttling a 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/

Questo metodo funziona testando il tempo trascorso dall'esecuzione dell'ultimo loop di frame.

Il codice di disegno viene eseguito solo quando è trascorso l'intervallo FPS specificato.

La prima parte del codice imposta alcune variabili utilizzate per calcolare il tempo trascorso.

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

E questo codice è l'effettivo loop requestAnimationFrame che disegna sul tuo FPS specificato.

// 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
Ottima spiegazione ed esempio. Questo dovrebbe essere contrassegnato come risposta accettata
muxcmux,

13
Bella demo - dovrebbe essere accettata. Qui, biforcuto il violino, per dimostrare usando window.performance.now () invece di Date.now (). Questo si adatta bene al timestamp ad alta risoluzione che rAF già riceve, quindi non è necessario chiamare Date.now () all'interno del callback: jsfiddle.net/chicagogrooves/nRpVD/2
Dean Radcliffe,

2
Grazie per il collegamento aggiornato utilizzando la nuova funzione di data / ora rAF. Il nuovo timestamp rAF aggiunge utili infrastruction ed è anche più preciso di Date.now.
segna il

13
Questa è una demo davvero bella, che mi ha ispirato a crearne uno mio ( JSFiddle ). Le differenze principali stanno usando rAF (come la demo di Dean) invece di Date, aggiungendo controlli per regolare dinamicamente il framerate di destinazione, campionando il framerate su un intervallo separato dall'animazione e aggiungendo un grafico dei framerate storici.
Tavnab,

1
Tutto quello che puoi controllare è quando salti un frame. Un monitor da 60 fps disegna sempre a intervalli di 16 ms. Ad esempio, se vuoi che il tuo gioco funzioni a 50 fps, salta ogni 6 ° fotogramma. Controlli se sono trascorsi 20 ms (1000/50) e non lo è (non sono trascorsi solo 16 ms), quindi salti un fotogramma, quindi il fotogramma successivo 32 ms è trascorso da quando hai disegnato, quindi disegni e resetta. Ma poi salterai metà dei fotogrammi ed eseguirai a 30 fps. Quindi quando reimposta ti ricordi di aver aspettato 12ms troppo a lungo l'ultima volta. Quindi passa al fotogramma successivo altri 16 ms ma lo conti come 16 + 12 = 28 ms quindi disegni di nuovo e hai aspettato 8 ms troppo a lungo
Curtis

47

Aggiornamento 2016/6

Il problema a limitare la frequenza dei fotogrammi è che lo schermo ha una frequenza di aggiornamento costante, in genere 60 FPS.

Se vogliamo 24 FPS non avremo mai i veri 24 fps sullo schermo, possiamo cronometrarli come tali ma non mostrarli poiché il monitor può mostrare solo i frame sincronizzati a 15 fps, 30 fps o 60 fps (alcuni monitor anche 120 fps ).

Tuttavia, ai fini della temporizzazione, possiamo calcolare e aggiornare quando possibile.

È possibile creare tutta la logica per il controllo del frame rate incapsulando calcoli e callback in un oggetto:

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

Quindi aggiungere un po 'di controller e codice di configurazione:

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

uso

Diventa molto semplice: ora tutto ciò che dobbiamo fare è creare un'istanza impostando la funzione di callback e la frequenza dei fotogrammi desiderata proprio in questo modo:

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

Quindi avviare (che potrebbe essere il comportamento predefinito, se desiderato):

fc.start();

Tutto qui, tutta la logica è gestita internamente.

dimostrazione

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>

Vecchia risposta

Lo scopo principale di requestAnimationFrameè sincronizzare gli aggiornamenti alla frequenza di aggiornamento del monitor. Ciò richiederà di animare l'FPS del monitor o un suo fattore (ad es. 60, 30, 15 FPS per una frequenza di aggiornamento tipica a 60 Hz).

Se si desidera un FPS più arbitrario, non ha senso utilizzare rAF poiché la frequenza dei fotogrammi non corrisponderà mai alla frequenza di aggiornamento del monitor (solo un fotogramma qua e là) che semplicemente non può darti un'animazione fluida (come con tutti i re-timing dei fotogrammi ) e puoi anche usare setTimeouto setIntervalinvece.

Questo è anche un problema ben noto nel settore dei video professionali quando si desidera riprodurre un video su un FPS diverso da quello sul quale il dispositivo mostra l'aggiornamento. Sono state utilizzate molte tecniche come la fusione dei fotogrammi e il re-timing complesso che ricostruisce fotogrammi intermedi basati su vettori di movimento, ma con la tela queste tecniche non sono disponibili e il risultato sarà sempre un video a scatti.

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

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

    ... code for frame here
}

Il motivo per cui posizioniamo per setTimeout primi (e perché si posiziona per rAFprimi quando viene utilizzato un polilucido) è che questo sarà più accurato poiché setTimeoutmetterà immediatamente in coda un evento all'avvio del ciclo in modo che non importa quanto tempo il codice rimanente utilizzerà (a condizione che non superi l'intervallo di timeout) la chiamata successiva sarà nell'intervallo che rappresenta (per puro rAF questo non è essenziale poiché in ogni caso rAF proverà a passare al frame successivo).

Vale anche la pena notare che posizionarlo per primo rischierà anche di accumulare chiamate come con setInterval. setIntervalpotrebbe essere leggermente più preciso per questo uso.

E puoi usare setIntervalinvece al di fuori del ciclo per fare lo stesso.

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

function loop() {

    ... code for frame here
}

E per interrompere il ciclo:

clearInterval(rememberMe);

Per ridurre la frequenza dei fotogrammi quando la scheda viene sfocata, è possibile aggiungere un fattore come questo:

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
}

In questo modo è possibile ridurre l'FPS a 1/4 ecc.


4
In alcuni casi non si sta cercando di far corrispondere la frequenza dei fotogrammi dei monitor ma piuttosto, ad esempio nelle sequenze di immagini, eliminare i fotogrammi. Eccellente spiegazione btw
sidonaldson

3
Uno dei motivi principali per limitare la richiesta con AnimationFrame sarebbe quello di allineare l'esecuzione di un po 'di codice con il frame di animazione del browser. Le cose finiscono per essere molto più fluide, soprattutto se si esegue una logica sui dati in ogni frame, come ad esempio con i visualizzatori di musica.
Chris Dolphin,

4
Ciò è errato perché l'uso principale di requestAnimationFrameè sincronizzare le operazioni DOM (lettura / scrittura), quindi non usarlo danneggerà le prestazioni quando si accede al DOM, poiché le operazioni non verranno messe in coda per essere eseguite insieme e imporranno inutilmente ridipingere il layout.
vsync,

1
Non vi è alcun rischio di "impilare le chiamate", poiché JavaScript esegue un thread singolo e nessun evento di timeout viene attivato mentre il codice è in esecuzione. Quindi, se la funzione richiede più tempo del timeout, viene eseguita quasi sempre il più velocemente possibile, mentre il browser eseguirà comunque i ridisegni e innescherebbe altri timeout tra le chiamate.
dronus,

So che affermi che l'aggiornamento della pagina non può essere aggiornato più velocemente del limite fps sul display. Tuttavia, è possibile eseguire l'aggiornamento più velocemente attivando il ridisposizione della pagina? Viceversa, è possibile non notare il riflusso di più pagine se vengono eseguite più velocemente della frequenza fps nativa?
Travis J,

37

Suggerisco di racchiudere la chiamata requestAnimationFramein un setTimeout:

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

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

Devi chiamare requestAnimationFramedall'interno setTimeout, piuttosto che viceversa, perché requestAnimationFramepianifica l'esecuzione della tua funzione proprio prima della riverniciatura successiva e se ritardi il tuo ulteriore aggiornamento utilizzando setTimeout, perderai quella finestra temporale. Tuttavia, fare il contrario è corretto, poiché stai semplicemente aspettando un periodo di tempo prima di effettuare la richiesta.


1
Questo in realtà sembra funzionare nel mantenere il framerate basso e quindi non cuocere la mia CPU. Ed è così semplice. Saluti!
phocks

Questo è un modo semplice e carino per farlo per animazioni leggere. Tuttavia, è un po 'fuori sincrono, almeno su alcuni dispositivi. Ho usato questa tecnica su uno dei miei precedenti motori. Ha funzionato bene fino a quando le cose non sono diventate complesse. Il problema più grande era quando collegato ai sensori di orientamento, sarebbe rimasto indietro o sarebbe saltato. Successivamente ho scoperto che usando un setInterval separato e comunicando gli aggiornamenti tra sensori, frame setInterval e frame RAF tramite le proprietà degli oggetti ho permesso ai sensori e al RAF di andare in tempo reale, mentre il tempo di animazione poteva essere controllato tramite gli aggiornamenti delle proprietà da setInterval.
jdmayfield,

Migliore risposta ! Grazie;)
538ROMEO

Il mio monitor è 60 FPS, se imposto var fps = 60, ottengo solo 50 FPS usando questo codice. Voglio rallentarlo a 60 perché alcune persone hanno 120 monitor FPS, ma non voglio influenzare tutti gli altri. Questo è sorprendentemente difficile.
Curtis,

Il motivo per cui si ottiene un FPS inferiore al previsto è perché setTimeout può eseguire il callback dopo un periodo di tempo superiore a quello specificato. C'è una serie di possibili ragioni per questo. E ogni ciclo richiede il tempo per impostare un nuovo timer ed eseguire del codice prima di impostare il nuovo timeout. Non hai modo di essere accurato con questo, dovresti sempre considerare un risultato più lento del previsto, ma finché non sai quanto sarà più lento, anche il tentativo di ridurre il ritardo sarebbe inaccurato. JS nei browser non è pensato per essere così preciso.
pdepmcp,

17

Queste sono tutte buone idee in teoria, fino a quando non vai in profondità. Il problema è che non è possibile limitare un RAF senza de-sincronizzarlo, sconfiggendo il suo scopo per esistere. Quindi si lascia funzionare a piena velocità, e aggiornare i dati in un ciclo separato , o anche un thread separato!

Sì, l'ho detto. È possibile fare multi-threaded JavaScript nel browser!

Ci sono due metodi che conosco che funzionano estremamente bene senza jank, usando molto meno succo e creando meno calore. Il risultato netto è un accurato timing su scala umana ed efficienza della macchina.

Mi scuso se questo è un po 'prolisso, ma qui va ...


Metodo 1: aggiornamento dei dati tramite setInterval e grafica tramite RAF.

Utilizzare un setInterval separato per l'aggiornamento di valori di traduzione e rotazione, fisica, collisioni, ecc. Conservare tali valori in un oggetto per ciascun elemento animato. Assegna la stringa di trasformazione a una variabile nell'oggetto ogni setInterval 'frame'. Conservare questi oggetti in un array. Imposta l'intervallo sui fps desiderati in ms: ms = (1000 / fps). Ciò mantiene un clock costante che consente gli stessi fps su qualsiasi dispositivo, indipendentemente dalla velocità RAF. Non assegnare qui le trasformazioni agli elementi!

In un ciclo requestAnimationFrame, scorrere all'interno dell'array con un ciclo di vecchia scuola per-- non usare qui i moduli più recenti, sono lenti!

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

Nella tua funzione rafUpdate, ottieni la stringa di trasformazione dall'oggetto js nell'array e il suo ID elementi. Dovresti già avere i tuoi elementi "sprite" collegati a una variabile o facilmente accessibili con altri mezzi in modo da non perdere tempo a "recuperarli" nella RAF. Mantenerli in un oggetto che prende il nome dal loro ID HTML funziona abbastanza bene. Imposta quella parte prima ancora che entri nel tuo SI o RAF.

Utilizzare RAF per aggiornare solo le trasformazioni , usa solo trasformazioni 3D (anche per 2d) e imposta css "will-change: transform;" su elementi che cambieranno. Ciò mantiene quanto più possibile sincronizzato le tue trasformazioni con la frequenza di aggiornamento nativa, attiva la GPU e indica al browser dove concentrarsi maggiormente.

Quindi dovresti avere qualcosa di simile a questo pseudocodice ...

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

Ciò mantiene gli aggiornamenti degli oggetti dati e trasforma le stringhe sincronizzate con la frequenza di "frame" desiderata nel SI e le assegnazioni di trasformazione effettive nel RAF sincronizzate con la frequenza di aggiornamento della GPU. Quindi gli attuali aggiornamenti grafici sono solo nel RAF, ma le modifiche ai dati e la costruzione della stringa di trasformazione sono nel SI, quindi non ci sono jankies ma il "tempo" scorre al frame rate desiderato.


Flusso:

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

Metodo 2. Inserisci l'IS in un web-worker. Questo è FAAAST e liscio!

Come il metodo 1, ma inserisci l'IS nel web-worker. Verrà quindi eseguito su un thread completamente separato, lasciando la pagina per occuparsi solo di RAF e UI. Passa l'array sprite avanti e indietro come "oggetto trasferibile". Questo è veloce. Non ci vuole tempo per clonare o serializzare, ma non è come passare per riferimento in quanto il riferimento dall'altra parte viene distrutto, quindi dovrai far passare entrambi i lati dall'altra parte e aggiornarli solo quando presenti, ordina di come passare una nota avanti e indietro con la tua ragazza al liceo.

Solo uno può leggere e scrivere alla volta. Questo va bene finché controllano se non è indefinito per evitare un errore. Il RAF è VELOCE e lo riavvierà immediatamente, quindi passerà attraverso un sacco di frame GPU solo controllando se è stato rispedito ancora. L'IS nel web-worker avrà l'array sprite per la maggior parte del tempo e aggiornerà i dati di posizione, movimento e fisica, oltre a creare la nuova stringa di trasformazione, quindi passarla al RAF nella pagina.

Questo è il modo più veloce che conosco per animare elementi tramite script. Le due funzioni verranno eseguite come due programmi separati, su due thread separati, sfruttando le CPU multi-core in un modo in cui un singolo script js non lo fa. Animazione javascript multi-thread.

E lo farà senza intoppi, ma al frame rate specificato, con una divergenza minima.


Risultato:

Uno di questi due metodi garantirà che lo script venga eseguito alla stessa velocità su qualsiasi PC, telefono, tablet, ecc. (Ovviamente all'interno delle funzionalità del dispositivo e del browser).


Come nota a margine: nel Metodo 1, se c'è troppa attività nel tuo setInterval, potrebbe rallentare il tuo RAF a causa dell'asincrono a thread singolo. È possibile mitigare questa interruzione di tale attività oltre il frame SI, in modo che l'asincrono restituisca il controllo alla RAF più rapidamente. Ricorda, RAF va al frame rate massimo, ma sincronizza i cambiamenti grafici con il display, quindi va bene saltare alcuni frame RAF - fintanto che non salti più dei frame SI non si strappa.
jdmayfield,

Il metodo 2 è più robusto, poiché in realtà è multi-tasking dei due loop, non passa avanti e indietro attraverso l'asincronizzazione, ma si desidera comunque evitare che il frame SI impieghi più a lungo del frame rate desiderato, quindi la divisione dell'attività SI potrebbe essere ancora auspicabile se ha in corso molta manipolazione dei dati che richiederebbe il completamento di più di un frame SI.
jdmayfield,

Ho pensato che valesse la pena ricordare, come nota di interesse, che l'esecuzione di loop accoppiati come questo in realtà registra in Chromes DevTools che la GPU sta funzionando al frame rate specificato nel loop setInterval! Appare solo i frame RAF in cui si verificano cambiamenti grafici vengono contati come frame dal misuratore FPS. Quindi i frame RAF in cui funzionano solo elementi non grafici, o anche solo loop vuoti, non contano per quanto riguarda la GPU. Lo trovo interessante come punto di partenza per ulteriori ricerche.
jdmayfield,

Credo che questa soluzione abbia il problema che continua a funzionare quando rAF viene sospeso, ad esempio perché l'utente è passato a un'altra scheda.
N4ppeL

1
PS Ho letto un po 'e sembra che la maggior parte dei browser limiti gli eventi a tempo a una volta al secondo nelle schede in background (che probabilmente dovrebbe essere gestito in qualche modo). Se vuoi ancora risolvere il problema e mettere in pausa completamente quando non è visibile, sembra che ci sia l' visibilitychangeevento.
N4ppeL

3

Come regolare facilmente un FPS specifico:

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

Fonte: una spiegazione dettagliata dei cicli di gioco JavaScript e dei tempi di Isaac Sukin


1
Se il mio monitor funziona a 60 FPS e voglio che il mio gioco funzioni a 58 FPS, ho impostato maxFPS = 58, questo lo farà funzionare a 30 FPS perché salterà ogni 2 fotogrammi.
Curtis,

Sì, ho provato anche questo. Ho scelto di non limitare effettivamente il RAF stesso: solo le modifiche vengono aggiornate da setTimeout. Almeno in Chrome, ciò fa sì che i fps effettivi vengano eseguiti al ritmo di setTimeouts, secondo le letture di DevTools. Ovviamente può solo aggiornare i fotogrammi video reali alla velocità della scheda video e monitorare la frequenza di aggiornamento, ma questo metodo sembra funzionare con il controllo fps "apparente" meno fluido, quindi quello che sto cercando.
jdmayfield,

Dal momento che tengo traccia di tutti i movimenti degli oggetti JS separatamente dal RAF, ciò mantiene la logica di animazione, il rilevamento delle collisioni o qualsiasi altra cosa di cui abbiate bisogno, correndo a un ritmo percettivamente coerente, indipendentemente dal RAF o dal setTimeout, con un po 'di matematica in più.
jdmayfield,

2

Saltare la richiesta AnimationFrame causa l' animazione non uniforme (desiderata) a fps personalizzati.

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

Codice originale di @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);
}

Aggiungi alcune frasi per spiegare cosa sta facendo il tuo codice, in modo da poter ottenere più voti per la tua risposta.
Analisi fuzzy

1

Lo faccio sempre in un modo molto semplice senza fare casini con i timestamp:

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
Funzionerà troppo velocemente se il tuo monitor è a 120 fps.
Curtis,

0

Ecco una buona spiegazione che ho trovato: CreativeJS.com , per concludere una chiamata setTimeou) all'interno della funzione passata a requestAnimationFrame. La mia preoccupazione per una "semplice" richiestaAnimationFrame sarebbe "E se volessi che si animasse solo tre volte al secondo?" Anche con requestAnimationFrame (al contrario di setTimeout) è che spreca ancora (un po ') di "energia" (il che significa che il codice del browser sta facendo qualcosa, e forse rallentando il sistema) 60 o 120 o comunque molte volte al secondo, come al contrario di solo due o tre volte al secondo (come si potrebbe desiderare).

Il più delle volte eseguo il mio browser con JavaScript intenzionalmente disattivato proprio per questo motivo. Ma sto usando Yosemite 10.10.3 e penso che ci sia una specie di problema con il timer - almeno sul mio vecchio sistema (relativamente vecchio - che significa 2011).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.