javascript - stop - setinterval animation
Controlando fps con requestAnimationFrame? (7)
Aquí hay una buena explicación que encontré: CreativeJS.com , para envolver un setTimeou) llame dentro de la función pasada para solicitarAnimationFrame. Mi preocupación con un requestion "simple" de Announcement Frame sería, "¿y si solo quiero animarlo tres veces por segundo?" Incluso con requestAnimationFrame (a diferencia de setTimeout) es que todavía desperdicia (algo) 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 se opone a solo dos o tres veces por segundo (como podría desear).
La mayoría de las veces ejecuto mis navegadores con JavaScript de manera intencionada solo 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, es decir, 2011).
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.
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.
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
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 en un hilo separado!
Sí, lo dije. ¡Puedes 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.
Use RAF para actualizar sus transformadas 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).
La solicitud de omisiónAnimationFrame 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 por @tavnab.
Te sugiero que envuelvas tu llamada para solicitar requestAnimationFrame
en un setTimeout
. Si llama a setTimeout
desde la función desde la que solicitó el cuadro de animación, está derrotando el propósito de requestAnimationFrame
. Pero si llama requestAnimationFrame
desde setTimeout
, funciona sin problemas:
var fps = 25
function animate() {
setTimeout(function() {
requestAnimationFrame(animate);
}, 1000 / fps);
}
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 con 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 también podría usar setTimeout
o setInterval
en setInterval
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 que colocamos setTimeout
primero (y por qué algunos lugares rAF
primero cuando se usa un relleno rAF
) es que esto será más preciso ya que setTimeout
pondrá en cola un evento inmediatamente cuando se inicie el ciclo, por lo que no importa cuánto tiempo el código usará (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 cuadro en cualquier caso).
También vale la pena tener en cuenta que colocarlo primero también pondrá en riesgo las llamadas apiladas como con setInterval
. setInterval
puede ser un poco más preciso para este uso.
Y puede usar setInterval
lugar del loop 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.
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
}
}