Control de fps con requestAnimationFrame?

140

Parece que requestAnimationFrame es la forma de facto de animar las cosas ahora. Funcionó bastante bien para mí en su mayor parte, pero en este momento estoy tratando de hacer algunas animaciones de lienzo y me preguntaba: ¿hay alguna manera de asegurarme de que funcione a ciertos fps? Entiendo que el propósito de rAF es para animaciones consistentemente suaves, y podría correr el riesgo de hacer que mi animación sea entrecortada, pero en este momento parece funcionar a velocidades drásticamente diferentes de manera bastante arbitraria, y me pregunto si hay una manera de combatir que de alguna manera

Lo usaría setIntervalpero quiero las optimizaciones que ofrece rAF (especialmente parando automáticamente cuando la pestaña está enfocada).

En caso de que alguien quiera ver mi código, es más o menos:

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

Donde Node.drawFlash () es solo un código que determina el radio basado en una variable de contador y luego dibuja un círculo.

robert.vinluan
fuente
1
¿Tu animación se retrasa? Creo que la mayor ventaja de requestAnimationFramees (como su nombre indica) solicitar un cuadro de animación solo cuando es necesario. Digamos que muestra un lienzo negro estático, debería obtener 0 fps porque no se necesita un nuevo marco. Pero si está mostrando una animación que requiere 60 fps, también debería obtenerla. rAFsimplemente permite "omitir" tramas inútiles y luego guardar la CPU.
maxdec
setInterval tampoco funciona en la pestaña inactiva.
ViliusL
Este código se ejecuta de manera diferente en pantallas de 90 Hz frente a pantallas de 60 Hz frente a pantallas de 144 Hz.
Manthrax

Respuestas:

190

Cómo estrangular requestAnimationFrame a una velocidad de fotogramas específica

Aceleración de demostración a 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/

Este método funciona probando el tiempo transcurrido desde la ejecución del último bucle de cuadro.

Su código de dibujo se ejecuta solo cuando ha transcurrido el intervalo FPS especificado.

La primera parte del código establece algunas variables utilizadas para calcular el tiempo transcurrido.

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

Y este código es el bucle requestAnimationFrame real que se basa en su FPS especificado.

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

    }
}
markE
fuente
55
Excelente explicación y ejemplo. Esto debe marcarse como la respuesta aceptada
muxcmux
13
Buena demostración: debe ser aceptada. Aquí, bifurqué su violín, para demostrar el uso de window.performance.now () en lugar de Date.now (). Esto va muy bien con la marca de tiempo de alta resolución que rAF ya recibe, por lo que no es necesario llamar a Date.now () dentro de la devolución de llamada: jsfiddle.net/chicagogrooves/nRpVD/2
Dean Radcliffe
2
Gracias por el enlace actualizado que usa la nueva función de marca de tiempo rAF. La nueva marca de tiempo rAF agrega una infraestructura útil y también es más precisa que Date.now.
markE
13
Esta es una demostración realmente agradable, que me inspiró a hacer la mía ( JSFiddle ). Las principales diferencias son usar rAF (como la demostración de Dean) en lugar de Fecha, agregar controles para ajustar dinámicamente la velocidad de fotogramas objetivo, muestrear la velocidad de fotogramas en un intervalo separado de la animación y agregar un gráfico de velocidades de fotogramas históricas.
tavnab
1
Todo lo que puedes controlar es cuando vas a saltar un cuadro. Un monitor de 60 fps siempre dibuja a intervalos de 16 ms. Por ejemplo, si desea que su juego se ejecute a 50 fps, desea omitir cada 6to cuadro. Comprueba si han transcurrido 20 ms (1000/50) y no ha transcurrido (solo han transcurrido 16 ms), por lo que omite un fotograma, luego ha transcurrido el siguiente fotograma de 32 ms desde que dibujó, por lo que dibuja y restablece. Pero luego saltará la mitad de los cuadros y correrá a 30 fps. Entonces, cuando reinicia, recuerda que esperó 12 ms demasiado tiempo la última vez. Entonces, en el siguiente fotograma, pases otros 16 ms, pero lo cuentas como 16 + 12 = 28 ms, así que vuelves a dibujar y esperaste 8 ms demasiado tiempo
Curtis
47

Actualización 2016/6

El problema que limita la velocidad de fotogramas es que la pantalla tiene una velocidad de actualización constante, típicamente 60 FPS.

Si queremos 24 FPS, nunca obtendremos los verdaderos 24 fps en la pantalla, podemos cronometrarlo como tal, pero no mostrarlo, ya que el monitor solo puede mostrar cuadros sincronizados a 15 fps, 30 fps o 60 fps (algunos monitores también 120 fps )

Sin embargo, para fines de tiempo podemos calcular y actualizar cuando sea posible.

Puede construir toda la lógica para controlar la velocidad de cuadros encapsulando cálculos y devoluciones de llamada en un objeto:

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

Luego agregue un código de controlador y configuración:

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

Se vuelve muy simple: ahora, todo lo que tenemos que hacer es crear una instancia configurando la función de devolución de llamada y la velocidad de fotogramas deseada así:

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

Luego, comience (que podría ser el comportamiento predeterminado si lo desea):

fc.start();

Eso es todo, toda la lógica se maneja internamente.

Manifestación

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>

Vieja respuesta

El objetivo principal de requestAnimationFramees sincronizar las actualizaciones a la frecuencia de actualización del monitor. Esto requerirá que se anime en el FPS del monitor o en un factor del mismo (es decir, 60, 30, 15 FPS para una frecuencia de actualización típica a 60 Hz).

Si desea un FPS más arbitrario, entonces no tiene sentido usar rAF ya que la frecuencia de cuadros nunca coincidirá con la frecuencia de actualización del monitor de todos modos (solo un cuadro aquí y allá) que simplemente no puede proporcionarle una animación uniforme (como con todas las repeticiones de cuadros) ) y también puede usar setTimeouto en su setIntervallugar.

Este también es un problema bien conocido en la industria del video profesional cuando desea reproducir un video en un FPS diferente del dispositivo que muestra la actualización en. Se han utilizado muchas técnicas, como la combinación de cuadros y la re-sincronización compleja, la reconstrucción de cuadros intermedios basados ​​en vectores de movimiento, pero con el lienzo estas técnicas no están disponibles y el resultado siempre será un video desigual.

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

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

    ... code for frame here
}

La razón por la que colocamos setTimeout primero (y por qué algún lugar rAFprimero cuando se usa un relleno de polietileno) es que esto será más preciso ya que setTimeoutpondrá en cola un evento inmediatamente cuando se inicie el ciclo, de modo que no importa cuánto tiempo usará el código restante (siempre que no exceda el intervalo de tiempo de espera) la próxima llamada será en el intervalo que representa (para rAF puro esto no es esencial ya que rAF intentará saltar al siguiente marco en cualquier caso).

También vale la pena señalar que colocarlo primero también correrá el riesgo de que las llamadas se acumulen como con setInterval. setIntervalpuede ser un poco más preciso para este uso.

Y puede usar setIntervalen su lugar fuera del bucle para hacer lo mismo.

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

function loop() {

    ... code for frame here
}

Y para detener el ciclo:

clearInterval(rememberMe);

Para reducir la velocidad de fotogramas cuando la pestaña se vuelve borrosa, puede agregar un factor como este:

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
}

De esta manera, puede reducir el FPS a 1/4, etc.


fuente
44
En algunos casos, no está tratando de igualar la velocidad de fotogramas del monitor, sino que, en secuencias de imágenes, por ejemplo, suelta fotogramas. Excelente explicación por cierto
sidonaldson
3
Una de las principales razones para acelerar con requestAnimationFrame sería alinear la ejecución de algún código con el marco de animación del navegador. Las cosas terminan funcionando mucho mejor, especialmente si estás ejecutando algo de lógica en los datos en cada cuadro, como con visualizadores de música, por ejemplo.
Chris Dolphin
44
Esto es malo porque el uso principal requestAnimationFramees sincronizar las operaciones del DOM (lectura / escritura) para que no usarlo perjudique el rendimiento al acceder al DOM, ya que las operaciones no se pondrán en cola para que se realicen juntas y forzarán el repintado del diseño innecesariamente.
vsync
1
No hay riesgo de "acumulación de llamadas", ya que JavaScript se ejecuta con un solo subproceso y no se activa ningún evento de tiempo de espera mientras se ejecuta el código. Entonces, si la función tarda más que el tiempo de espera, se ejecuta casi en cualquier momento lo más rápido que puede, mientras que el navegador aún redibuja y activa otros tiempos de espera entre las llamadas.
dronus
Sé que usted dice que la actualización de la página no se puede actualizar más rápido que el límite de fps en la pantalla. Sin embargo, ¿es posible actualizar más rápido activando un reflujo de página? Por el contrario, ¿es posible no notar los reflujos de varias páginas si se realizan más rápido que la tasa fps nativa?
Travis J
37

Sugiero finalizar su llamada requestAnimationFrameen un setTimeout:

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

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

Debe llamar requestAnimationFramedesde dentro setTimeout, en lugar de al revés, porque requestAnimationFrameprograma su función para que se ejecute justo antes del próximo repintado, y si retrasa su actualización aún más setTimeout, habrá perdido esa ventana de tiempo. Sin embargo, hacer lo contrario es correcto, ya que simplemente está esperando un período de tiempo antes de realizar la solicitud.

Luke Taylor
fuente
1
Esto en realidad parece funcionar para mantener baja la velocidad de cuadros y no cocinar mi CPU. Y es muy simple. ¡Salud!
mete el
Esta es una forma agradable y sencilla de hacerlo para animaciones ligeras. Sin embargo, se desincroniza un poco, al menos en algunos dispositivos. Utilicé esta técnica en uno de mis motores anteriores. Funcionó bien hasta que las cosas se complicaron. El mayor problema era cuando se conectaba a los sensores de orientación, ya que se quedaba atrás o se ponía nervioso. Más tarde descubrí que el uso de setInterval separado y la comunicación de actualizaciones entre sensores, marcos setInterval y marcos RAF a través de propiedades de objeto permitieron que los sensores y RAF fueran en tiempo real, mientras que el tiempo de animación se podía controlar a través de actualizaciones de propiedades de setInterval.
jdmayfield
La mejor respuesta ! Gracias;)
538ROMEO
Mi monitor es de 60 FPS, si configuro var fps = 60, solo obtengo alrededor de 50 FPS usando este código. Quiero reducirlo a 60 porque algunas personas tienen 120 monitores FPS, pero no quiero afectar a los demás. Esto es sorprendentemente difícil.
Curtis
La razón por la que obtiene un FPS más bajo de lo esperado es porque setTimeout puede ejecutar la devolución de llamada después de un retraso mayor al especificado. Hay varias razones posibles para esto. Y cada ciclo toma tiempo para configurar un nuevo temporizador y ejecutar un código antes de configurar el nuevo tiempo de espera. No tiene forma de ser exacto con esto, siempre debe considerar un resultado más lento de lo esperado, pero mientras no sepa cuánto más lento será, tratar de reducir el retraso también sería incorrecto. JS en los navegadores no pretende ser tan preciso.
pdepmcp
17

Todas estas son buenas ideas en teoría, hasta que profundices. El problema es que no se puede estrangular un RAF sin desincronizarlo, lo que frustra su propósito de existir. ¡Entonces lo deja correr a toda velocidad y actualiza sus datos en un bucle separado , o incluso en un hilo separado!

Sí lo dije. Usted puede hacer de múltiples hebras de JavaScript en el navegador!

Sé que hay dos métodos que funcionan extremadamente bien sin jank, usando mucho menos jugo y creando menos calor. El resultado neto es el tiempo preciso a escala humana y la eficiencia de la máquina.

Disculpas si esto es un poco prolijo, pero aquí va ...


Método 1: Actualice los datos a través de setInterval y los gráficos a través de RAF.

Use un setInterval separado para actualizar los valores de traslación y rotación, física, colisiones, etc. Mantenga esos valores en un objeto para cada elemento animado. Asigne la cadena de transformación a una variable en el objeto cada 'marco' setInterval. Mantenga estos objetos en una matriz. Establezca su intervalo a sus fps deseados en ms: ms = (1000 / fps). Esto mantiene un reloj constante que permite los mismos fps en cualquier dispositivo, independientemente de la velocidad RAF. ¡No asigne las transformaciones a los elementos aquí!

En un bucle requestAnimationFrame, recorra su matriz con un bucle for de la vieja escuela; no use los formularios más nuevos aquí, ¡son lentos!

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

En su función rafUpdate, obtenga la cadena de transformación de su objeto js en la matriz y su id de elementos. Ya debería tener sus elementos 'sprite' unidos a una variable o de fácil acceso a través de otros medios para que no pierda tiempo 'metiéndolos' en la RAF. Mantenerlos en un objeto con el nombre de su identificación html funciona bastante bien. Configure esa parte incluso antes de que entre en su SI o RAF.

Use la RAF para actualizar solo sus transformaciones , use solo transformaciones 3D (incluso para 2d) y configure css "will-change: transform;" en elementos que cambiarán. Esto mantiene sus transformaciones sincronizadas con la frecuencia de actualización nativa tanto como sea posible, activa la GPU y le dice al navegador dónde concentrarse más.

Entonces deberías tener algo como este pseudocódigo ...

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

Esto mantiene sus actualizaciones de los objetos de datos y las cadenas de transformación sincronizadas con la frecuencia de 'fotograma' deseada en el SI, y las asignaciones de transformación reales en la RAF sincronizadas con la frecuencia de actualización de la GPU. Por lo tanto, las actualizaciones de gráficos reales solo están en la RAF, pero los cambios en los datos y la construcción de la cadena de transformación están en el SI, por lo tanto, no hay jankies sino que el 'tiempo' fluye a la velocidad de fotogramas deseada.


Fluir:

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

Método 2. Ponga el SI en un trabajador web. ¡Este es FAAAST y suave!

Igual que el método 1, pero coloca el SI en web-worker. Se ejecutará en un hilo totalmente separado, dejando la página para tratar solo con la RAF y la interfaz de usuario. Pase el conjunto de sprites de ida y vuelta como un 'objeto transferible'. Esto es muy rápido. No toma tiempo clonar o serializar, pero no es como pasar por referencia, ya que la referencia del otro lado se destruye, por lo que deberá hacer que ambos lados pasen al otro lado y solo actualizarlos cuando estén presentes, ordenar de pasar una nota de ida y vuelta con tu novia en la escuela secundaria.

Solo uno puede leer y escribir a la vez. Esto está bien siempre que verifiquen si no está indefinido para evitar un error. El RAF es RÁPIDO y lo devolverá de inmediato, luego pasará por un montón de marcos de GPU solo para verificar si ya se ha enviado de vuelta. El SI en el web-worker tendrá la matriz de sprites la mayor parte del tiempo y actualizará los datos de posición, movimiento y física, así como creará la nueva cadena de transformación y luego la devolverá a la RAF en la página.

Esta es la forma más rápida que conozco para animar elementos mediante un script. Las dos funciones se ejecutarán como dos programas separados, en dos subprocesos separados, aprovechando las CPU de varios núcleos de una manera que un solo script js no lo hace. Animación javascript multihilo.

Y lo hará sin problemas sin jank, pero a la velocidad de cuadro especificada real, con muy poca divergencia.


Resultado:

Cualquiera de estos dos métodos asegurará que su script se ejecute a la misma velocidad en cualquier PC, teléfono, tableta, etc. (dentro de las capacidades del dispositivo y el navegador, por supuesto).

jdmayfield
fuente
Como nota al margen: en el Método 1, si hay demasiada actividad en su setInterval, puede ralentizar su RAF debido a la sincronización asincrónica. Puede mitigar esta ruptura de esa actividad en más de un marco SI, por lo que async pasará el control a RAF más rápido. Recuerde, RAF tiene una velocidad de fotogramas máxima, pero sincroniza los cambios gráficos con la pantalla, por lo que está bien omitir algunos fotogramas RAF, siempre que no omita más que los fotogramas SI, no se disparará.
jdmayfield
El método 2 es más robusto, ya que en realidad es multitarea en los dos bucles, no cambia de un lado a otro a través de asíncrono, pero aún así desea evitar que su cuadro SI tome más tiempo que su frecuencia de cuadro deseada, por lo que la división de la actividad SI aún puede ser deseable si tiene mucha manipulación de datos en curso que requeriría más de un marco SI para completarse.
jdmayfield
Pensé que valía la pena mencionar, como una nota de interés, que ejecutar bucles emparejados como este en realidad registra en Chromes DevTools que la GPU se ejecuta a la velocidad de fotogramas especificada en el bucle setInterval. Parece que solo los cuadros RAF en los que se producen cambios gráficos se cuentan como cuadros por el medidor FPS. Por lo tanto, los marcos RAF en los que solo el trabajo no gráfico, o incluso solo bucles en blanco, no cuentan en lo que respecta a la GPU. Esto me parece interesante como punto de partida para futuras investigaciones.
jdmayfield
Creo que esta solución tiene el problema de que sigue ejecutándose cuando se suspende rAF, por ejemplo, porque el usuario cambió a otra pestaña.
N4ppeL
1
PD: Leí un poco y parece que la mayoría de los navegadores limitan los eventos cronometrados a una vez por segundo en pestañas de fondo de todos modos (lo que probablemente también debería manejarse de alguna manera). Si aún desea abordar el problema y hacer una pausa completa cuando no está visible, parece ser el visibilitychangeevento.
N4ppeL
3

Cómo acelerar fácilmente a un FPS específico:

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

Fuente: Una explicación detallada de JavaScript Game Loops and Timing por Isaac Sukin

Rustem Kakimov
fuente
1
Si mi monitor funciona a 60 FPS y quiero que mi juego se ejecute a 58 FPS, configuro maxFPS = 58, esto hará que se ejecute a 30 FPS porque se saltará cada segundo fotograma.
Curtis
Sí, probé este también. Elijo no estrangular la RAF en sí misma; solo los cambios son actualizados por setTimeout. Al menos en Chrome, esto hace que los fps efectivos se ejecuten al ritmo setTimeouts, según las lecturas de DevTools. Por supuesto, solo puede actualizar cuadros de video reales a la velocidad de la tarjeta de video y la frecuencia de actualización del monitor, pero este método parece funcionar con la menor cantidad de jankies, por lo que el control fps "aparente" más suave, que es lo que estoy buscando.
jdmayfield
Dado que realizo un seguimiento de todo el movimiento en los objetos JS por separado de la RAF, esto mantiene la lógica de animación, la detección de colisiones o lo que sea que necesite, ejecutándose a una velocidad perceptualmente consistente, independientemente de la RAF o el setTimeout, con un poco de matemática adicional.
jdmayfield
2

Saltar requestAnimationFrame causa una animación no uniforme (deseada) en fps personalizados.

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

Código original de @tavnab.

befzz
fuente
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);
}
luismsf
fuente
Agregue algunas oraciones para explicar lo que está haciendo su código, para que pueda obtener más votos a favor para su respuesta.
Fuzzy Analysis
1

Siempre lo hago de esta manera muy simple sin meterme con las marcas de tiempo:

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);
}
Samer Alkhabbaz
fuente
1
Esto funcionará demasiado rápido si su monitor es de 120 fps.
Curtis
0

Aquí hay una buena explicación que encontré: CreativeJS.com , para envolver una llamada setTimeou) dentro de la función pasada a requestAnimationFrame. Mi preocupación con una solicitud "simple" Marco de animación sería: "¿y si solo quiero que se anime tres veces por segundo?" Incluso con requestAnimationFrame (en oposición a setTimeout) es que todavía desperdicia (alguna) cantidad de "energía" (lo que significa que el código del navegador está haciendo algo, y posiblemente ralentizando el sistema) 60 o 120 o tantas veces por segundo, como en oposición a solo dos o tres veces por segundo (como desee).

La mayoría de las veces ejecuto mis navegadores con JavaScript desactivado por esta razón. Pero estoy usando Yosemite 10.10.3, y creo que hay algún tipo de problema con el temporizador, al menos en mi sistema anterior (relativamente antiguo, lo que significa 2011).

Jim Witte
fuente