Pregunta Controlando fps con requestAnimationFrame?


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

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

En caso de que alguien quiera mirar 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.


73
2017-11-04 08:31


origen


Respuestas:


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

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

Este método funciona al probar el tiempo transcurrido desde la ejecución del último ciclo de cuadro.

Su código de dibujo se ejecuta solo cuando ha transcurrido su intervalo de 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 actual 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

    }
}

112
2017-11-04 16:25



Actualización 2016/6

El problema que acelera la velocidad de fotogramas es que la pantalla tiene una tasa de actualización constante, normalmente 60 FPS.

Si queremos 24 FPS nunca obtendremos los 24 fps verdaderos 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 a 120 fps) )

Sin embargo, por motivos de tiempo podemos calcular y actualizar cuando sea posible.

Puede construir toda la lógica para controlar la velocidad de fotogramas encapsulando cálculos y devoluciones de llamadas 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)
    }
}

A continuación, agregue un controlador y un código de 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 de la siguiente manera:

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

Luego inicie (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>

Respuesta anterior

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

Si desea un FPS más arbitrario, no tiene sentido utilizar rAF ya que la velocidad de fotogramas nunca coincidirá con la frecuencia de actualización del monitor (solo un fotograma aquí y allá) que simplemente no le puede dar una animación suave (como con todas las repeticiones de fotogramas) ) y usted también podría usar setTimeout o setInterval en lugar.

Este es también un problema bien conocido en la industria del video profesional cuando desea reproducir un video en un FPS diferente y luego el dispositivo que lo muestra actualizado. Se han utilizado muchas técnicas, como la combinación de cuadros y el complejo cambio de tiempos, 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 cual colocamos setTimeout  primero (y por qué un lugar rAF primero cuando se usa un relleno múltiple) es que esto será más preciso ya que setTimeout pondrá en cola un evento inmediatamente cuando se inicie el ciclo, de modo que no importa cuánto tiempo use el código restante (siempre que no exceda el intervalo de tiempo de espera) la próxima llamada será en el intervalo que represente (para rAF puro esto no es esencial) como rAF intentará saltar al siguiente fotograma en cualquier caso).

También vale la pena tener en cuenta que al colocarlo primero también se corre el riesgo de acumular llamadas como con setInterval. setInterval puede ser un poco más preciso para este uso.

Y puedes usar setInterval en lugar fuera de el ciclo 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 forma puedes reducir el FPS a 1/4, etc.


27
2017-11-04 17:31



Te sugiero que envuelvas tu llamada a requestAnimationFrame en un setTimeout. Si llamas setTimeout desde dentro de la función desde la cual solicitó el cuadro de animación, está derrotando el propósito de requestAnimationFrame. Pero si llamas requestAnimationFrame desde adentro setTimeout funciona sin problemas:

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

18
2017-08-25 02:09



Estas son todas buenas ideas en teoría, hasta que profundizas. El problema es que no puede estrangular un RAF sin des-sincronizarlo, derrotando su propósito de existir.  Así que lo dejas correr a toda velocidad y actualizas tus datos en un bucle separado, o incluso un hilo separado!

Sí, lo dije. Tú poder hacer JavaScript multiproceso en el navegador!

Hay dos métodos que sé que funcionan extremadamente bien sin jank, usando mucho menos jugo y creando menos calor. El resultado neto es una sincronización precisa a escala humana y la eficiencia de la máquina.

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


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

Use un setInterval por separado para actualizar los valores de traducción y rotación, física, colisiones, etc. Mantenga esos valores en un objeto para cada elemento animado. Asigna la cadena de transformación a una variable en el objeto cada setInterval 'frame'. Mantenga estos objetos en una matriz. Configure su intervalo a su fps deseado 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 ciclo requestAnimationFrame, itere a través de su matriz con un bucle for old-school; 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 sus elementos id. Ya debe tener sus elementos 'sprite' unidos a una variable o de fácil acceso por otros medios para que no pierda tiempo 'obteniéndolos' en el RAF. Mantenerlos en un objeto con el nombre de su id. Html funciona bastante bien. Configure esa parte antes incluso de que entre en su SI o RAF.

Usa el RAF para actualizar tus transformaciones solamente, 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 en los objetos de datos y las cadenas de transformación sincronizadas con la tasa de 'frame' 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 el 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. Coloque el SI en un trabajador web. ¡Este es FAAAST y suave!

Igual que el método 1, pero pon el SI en el trabajador web. Se ejecutará en un hilo totalmente independiente, dejando la página para tratar solo con RAF y UI. Pase la matriz de sprites hacia adelante y hacia atrás como un "objeto transferible". Esto es buko rápido. No se necesita tiempo para clonar o serializar, pero no es como pasar por referencia porque 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, ordenarlos de como 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 y cuando verifiquen si no está indefinido para evitar un error. El RAF es RÁPIDO y lo desactivará de inmediato, luego pasará por un montón de cuadros de GPU simplemente comprobando si ya se envió de vuelta. El SI en el trabajador web tendrá la matriz de sprites la mayor parte del tiempo, y actualizará los datos de posición, movimiento y física, así como también la creación de la nueva cadena de transformación, y luego la devolverá al RAF en la página.

Esta es la forma más rápida que conozco para animar elementos a través del guión. Las dos funciones se ejecutarán como dos programas separados, en dos subprocesos separados, aprovechando las CPU multinúcleo de una manera que un solo script js no ejecuta. Animación javascript multiproceso.

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


Resultado:

Cualquiera de estos dos métodos garantizará 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).


5
2018-01-23 23:44



Salto a la comba requestAnimationFrame porque no es suaveanimación (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 por @tavnab.


2
2018-05-16 10:05



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 los juegos de JavaScript Loops and Timing de Isaac Sukin


1
2018-01-06 06:51



Aquí hay una buena explicación que encontré: CreativeJS.com, para envolver un setTimeou) llamada dentro de la función pasada a requestAnimationFrame. Mi preocupación con un requestion "simple" de AnnimationFrame sería, "¿y si solo querer animar tres veces por segundo? "Incluso con requestAnimationFrame (en contraposición a setTimeout) es que todavía desperdicia (algo) 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 sin embargo, muchas veces por segundo, en lugar de solo dos o tres veces por segundo (como podría querer).

La mayoría de las veces ejecuto mis navegadores con JavaScript de manera intencional apagado por solo 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, es decir, 2011).


0
2018-04-02 23:24