Contrôle des images par seconde avec requestAnimationFrame?

140

Il semble que requestAnimationFrame c'est la manière de facto d'animer les choses maintenant. Cela a plutôt bien fonctionné pour moi pour la plupart, mais pour le moment, j'essaie de faire des animations de canevas et je me demandais: y a-t-il un moyen de m'assurer qu'il fonctionne à un certain fps? Je comprends que le but de la rAF est d'avoir des animations toujours fluides, et je pourrais courir le risque de rendre mon animation saccadée, mais pour le moment, il semble fonctionner à des vitesses radicalement différentes de manière assez arbitraire, et je me demande s'il existe un moyen de combattre cela en quelque sorte.

J'utiliserais setIntervalmais je veux les optimisations offertes par rAF (en particulier l'arrêt automatique lorsque l'onglet est mis au point).

Au cas où quelqu'un voudrait regarder mon code, c'est à peu près:

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

Où Node.drawFlash () est juste un code qui détermine le rayon en fonction d'une variable de compteur, puis dessine un cercle.

robert.vinluan
la source
1
Votre animation est-elle en retard? Je pense que le plus grand avantage requestAnimationFrameest (comme son nom l'indique) de demander une image d'animation uniquement lorsque cela est nécessaire. Supposons que vous montriez un canevas noir statique, vous devriez obtenir 0 fps car aucun nouveau cadre n'est nécessaire. Mais si vous affichez une animation qui nécessite 60 ips, vous devriez l'obtenir également. rAFpermet juste de "sauter" les trames inutiles, puis de sauvegarder le CPU.
maxdec
setInterval ne fonctionne pas non plus dans l'onglet inactif.
ViliusL
Ce code fonctionne différemment sur un écran 90 Hz par rapport à un écran 60 Hz et un affichage 144 Hz.
manthrax le

Réponses:

190

Comment limiter la demande d'AnimationFrame à une fréquence d'images spécifique

Limitation de démo à 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/

Cette méthode fonctionne en testant le temps écoulé depuis l'exécution de la dernière boucle de trame.

Votre code de dessin s'exécute uniquement lorsque votre intervalle FPS spécifié s'est écoulé.

La première partie du code définit certaines variables utilisées pour calculer le temps écoulé.

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

Et ce code est la boucle requestAnimationFrame réelle qui dessine à votre FPS spécifié.

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

    }
}
marqueE
la source
5
Excellente explication et exemple. Cela devrait être marqué comme la réponse acceptée
muxcmux
13
Belle démo - elle devrait être acceptée. Ici, fourchez votre violon, pour démontrer l'utilisation de window.performance.now () au lieu de Date.now (). Cela va bien avec l'horodatage haute résolution que la RAF reçoit déjà, il n'est donc pas nécessaire d'appeler Date.now () dans le rappel: jsfiddle.net/chicagogrooves/nRpVD/2
Dean Radcliffe
2
Merci pour le lien mis à jour utilisant la nouvelle fonction d'horodatage rAF. Le nouvel horodatage rAF ajoute une infrastructure utile et il est également plus précis que Date.now.
markE
13
C'est une très belle démo, qui m'a inspiré à faire la mienne ( JSFiddle ). Les principales différences sont l'utilisation de rAF (comme la démo de Dean) au lieu de Date, l'ajout de commandes pour ajuster dynamiquement le framerate cible, l'échantillonnage du framerate sur un intervalle séparé de l'animation et l'ajout d'un graphique des framerates historiques.
tavnab
1
Tout ce que vous pouvez contrôler, c'est quand vous allez sauter une image. Un moniteur à 60 ips dessine toujours à des intervalles de 16 ms. Par exemple, si vous voulez que votre jeu tourne à 50 ips, vous voulez sauter toutes les 6 images. Vous vérifiez si 20 ms (1000/50) se sont écoulés, et ce n'est pas le cas (seulement 16 ms se sont écoulés), vous sautez une image, puis l'image suivante 32 ms s'est écoulée depuis que vous avez dessiné, vous dessinez et réinitialisez. Mais alors vous sauterez la moitié des images et fonctionnerez à 30 ips. Ainsi, lorsque vous réinitialisez, vous vous souvenez que vous avez attendu 12 ms de trop la dernière fois. Donc l'image suivante passe encore 16 ms mais vous comptez comme 16 + 12 = 28 ms donc vous dessinez à nouveau et vous avez attendu 8 ms trop longtemps
Curtis
47

Mise à jour 2016/6

Le problème de la limitation de la fréquence d'images est que l'écran a un taux de mise à jour constant, généralement 60 FPS.

Si nous voulons 24 FPS, nous n'obtiendrons jamais les véritables 24 fps à l'écran, nous pouvons le chronométrer en tant que tel mais ne pas le montrer car le moniteur ne peut afficher que des images synchronisées à 15 fps, 30 fps ou 60 fps (certains moniteurs également 120 fps ).

Cependant, à des fins de synchronisation, nous pouvons calculer et mettre à jour lorsque cela est possible.

Vous pouvez créer toute la logique de contrôle de la fréquence d'images en encapsulant les calculs et les rappels dans un objet:

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

Ensuite, ajoutez un contrôleur et un code de configuration:

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

Usage

Cela devient très simple - maintenant, tout ce que nous avons à faire est de créer une instance en définissant la fonction de rappel et la fréquence d'images souhaitée comme ceci:

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

Puis démarrez (ce qui pourrait être le comportement par défaut si vous le souhaitez):

fc.start();

Voilà, toute la logique est gérée en interne.

Démo

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>

Ancienne réponse

L'objectif principal de requestAnimationFrame est de synchroniser les mises à jour avec la fréquence de rafraîchissement du moniteur. Cela vous obligera à animer au FPS du moniteur ou à un facteur de celui-ci (c'est-à-dire 60, 30, 15 FPS pour un taux de rafraîchissement typique à 60 Hz).

Si vous voulez un FPS plus arbitraire, il est inutile d'utiliser rAF car la fréquence d'images ne correspondra jamais à la fréquence de mise à jour du moniteur de toute façon (juste une image ici et là) qui ne peut tout simplement pas vous donner une animation fluide (comme avec tous les resynchronisations d'image ) et vous pouvez aussi bien utiliser setTimeoutou à la setIntervalplace.

Il s'agit également d'un problème bien connu dans l'industrie de la vidéo professionnelle lorsque vous souhaitez lire une vidéo à un FPS différent de celui de l'appareil qui l'affiche. De nombreuses techniques ont été utilisées, telles que le mélange d'images et la recréation complexe d'images intermédiaires basées sur des vecteurs de mouvement, mais avec le canevas, ces techniques ne sont pas disponibles et le résultat sera toujours une vidéo saccadée.

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

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

    ... code for frame here
}

La raison pour laquelle nous plaçons en setTimeout premier (et pourquoi une place en rAFpremier lorsqu'un poly-fill est utilisé) est que ce sera plus précis que lesetTimeout mettra en file d'attente un événement immédiatement lorsque la boucle démarre, de sorte que peu importe combien de temps le code restant utilisera (à condition qu'il ne dépasse pas l'intervalle de temporisation) le prochain appel sera à l'intervalle qu'il représente (pour la rAF pure, ce n'est pas essentiel car rAF essaiera de sauter sur la trame suivante dans tous les cas).

Il convient également de noter que le placer en premier risque également de risquer que les appels s'empilent comme avec setInterval.setIntervalpeut être légèrement plus précis pour cette utilisation.

Et vous pouvez utiliser à la setIntervalplace en dehors de la boucle pour faire de même.

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

function loop() {

    ... code for frame here
}

Et pour arrêter la boucle:

clearInterval(rememberMe);

Afin de réduire la fréquence d'images lorsque l'onglet devient flou, vous pouvez ajouter un facteur comme celui-ci:

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 cette façon, vous pouvez réduire le FPS à 1/4, etc.


la source
4
Dans certains cas, vous n'essayez pas de faire correspondre la fréquence d'images des moniteurs, mais plutôt, dans les séquences d'images par exemple, de supprimer des images. Excellente explication btw
Sidonaldson
3
L'une des principales raisons de ralentir avec requestAnimationFrame serait d'aligner l'exécution de certains codes avec le cadre d'animation du navigateur. Les choses finissent par être beaucoup plus fluides, surtout si vous exécutez une logique sur les données à chaque image, comme avec les visualiseurs de musique par exemple.
Chris Dolphin
4
Ceci est mauvais car l'utilisation principale de requestAnimationFrameest de synchroniser les opérations DOM (lecture / écriture) donc ne pas l'utiliser nuira aux performances lors de l'accès au DOM, car les opérations ne seront pas mises en file d'attente pour être exécutées ensemble et forceront inutilement à repeindre la mise en page.
vsync
1
Il n'y a aucun risque "d'accumulation d'appels", car JavaScript exécute un seul thread et aucun événement de délai d'attente n'est déclenché pendant l'exécution de votre code. Donc, si la fonction prend plus de temps que le délai d'expiration, elle s'exécute presque à tout moment aussi vite que possible, tandis que le navigateur continue de faire des redessins et déclenche d'autres délais entre les appels.
dronus
Je sais que vous déclarez que l'actualisation de la page ne peut pas être mise à jour plus rapidement que la limite de fps sur l'affichage. Cependant, est-il possible d'actualiser plus rapidement en déclenchant une redistribution de page? À l'inverse, est-il possible de ne pas remarquer plusieurs redistributions de pages si elles sont effectuées plus rapidement que le taux de fps natif?
Travis J
37

Je suggère d'envelopper votre appel requestAnimationFramedans un setTimeout:

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

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

Vous devez appeler requestAnimationFramede l'intérieur setTimeout, plutôt que l'inverse, car requestAnimationFramevous programmez votre fonction pour qu'elle s'exécute juste avant le prochain repeint, et si vous retardez votre mise à jour, setTimeoutvous aurez manqué cette fenêtre de temps. Cependant, faire l'inverse est judicieux, car vous attendez simplement un certain temps avant de faire la demande.

Luke Taylor
la source
1
Cela semble en fait fonctionner pour maintenir le framerate bas et donc ne pas cuire mon processeur. Et c'est tellement simple. À votre santé!
phocks
C'est une manière simple et agréable de le faire pour des animations légères. Cependant, il est un peu désynchronisé, du moins sur certains appareils. J'ai utilisé cette technique sur l'un de mes anciens moteurs. Cela a bien fonctionné jusqu'à ce que les choses se compliquent. Le plus gros problème était lorsqu'il était connecté à des capteurs d'orientation, il serait soit à la traîne, soit nerveux. Plus tard, j'ai découvert que l'utilisation d'un setInterval séparé et la communication des mises à jour entre les capteurs, les images setInterval et les images RAF via les propriétés d'objet permettaient aux capteurs et à la RAF de passer en temps réel, tandis que le temps d'animation pouvait être contrôlé via les mises à jour des propriétés de setInterval.
jdmayfield
Meilleure réponse ! Merci;)
538ROMEO
Mon moniteur est à 60 FPS, si je règle var fps = 60, je n'obtiens qu'environ 50 FPS en utilisant ce code. Je veux le ralentir à 60 parce que certaines personnes ont des moniteurs à 120 FPS, mais je ne veux pas affecter tout le monde. C'est étonnamment difficile.
Curtis
La raison pour laquelle vous obtenez un FPS plus bas que prévu est que setTimeout peut exécuter le rappel après plus que le délai spécifié. Il y a un certain nombre de raisons possibles à cela. Et à chaque boucle, il faut le temps de définir une nouvelle minuterie et d'exécuter du code avant de définir le nouveau délai. Vous n'avez aucun moyen d'être précis avec cela, vous devriez toujours envisager un résultat plus lent que prévu, mais tant que vous ne savez pas à quel point il sera plus lent, essayer de réduire le délai serait également inexact. JS dans les navigateurs n'est pas censé être aussi précis.
pdepmcp
17

Ce sont toutes de bonnes idées en théorie, jusqu'à ce que vous approfondissiez. Le problème est que vous ne pouvez pas étrangler une RAF sans la désynchroniser, ce qui va à l'encontre de son objectif même d'exister. Vous le laissez donc fonctionner à pleine vitesse et mettez à jour vos données dans une boucle séparée , ou même un thread séparé!

Oui, je l'ai dit. Vous pouvez faire du JavaScript multi-thread dans le navigateur!

Il y a deux méthodes que je connais qui fonctionnent extrêmement bien sans jank, en utilisant beaucoup moins de jus et en créant moins de chaleur. Une synchronisation précise à l'échelle humaine et une efficacité de la machine en sont le résultat.

Toutes mes excuses si c'est un peu verbeux, mais voilà ...


Méthode 1: mettre à jour les données via setInterval et les graphiques via RAF.

Utilisez un setInterval distinct pour mettre à jour les valeurs de translation et de rotation, la physique, les collisions, etc. Conservez ces valeurs dans un objet pour chaque élément animé. Affectez la chaîne de transformation à une variable de l'objet à chaque setInterval 'frame'. Conservez ces objets dans un tableau. Réglez votre intervalle sur vos fps souhaités en ms: ms = (1000 / fps). Cela permet de maintenir une horloge constante qui permet les mêmes fps sur n'importe quel appareil, quelle que soit la vitesse de la RAF. N'affectez pas les transformations aux éléments ici!

Dans une boucle requestAnimationFrame, parcourez votre tableau avec une boucle for old-school - n'utilisez pas les nouveaux formulaires ici, ils sont lents!

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

Dans votre fonction rafUpdate, récupérez la chaîne de transformation de votre objet js dans le tableau, et son identifiant d'éléments. Vous devriez déjà avoir vos éléments «sprite» attachés à une variable ou facilement accessibles par d'autres moyens pour ne pas perdre de temps à les «obtenir» dans le RAF. Les garder dans un objet nommé d'après leur identifiant html fonctionne plutôt bien. Configurez cette partie avant même qu'elle n'entre dans votre SI ou RAF.

Utilisez la RAF pour mettre à jour vos transformations uniquement , utilisez uniquement des transformations 3D (même pour 2d) et définissez css "will-change: transform;" sur des éléments qui vont changer. Cela maintient autant que possible vos transformations synchronisées avec le taux de rafraîchissement natif, active le GPU et indique au navigateur où se concentrer le plus.

Donc, vous devriez avoir quelque chose comme ce pseudocode ...

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

Cela permet de synchroniser vos mises à jour des objets de données et des chaînes de transformation à la fréquence d'images souhaitée dans le SI, et les affectations de transformation réelles dans le RAF synchronisées avec la fréquence de rafraîchissement du GPU. Ainsi, les mises à jour graphiques réelles ne sont que dans le RAF, mais les modifications apportées aux données et la construction de la chaîne de transformation sont dans le SI, donc pas de jankies mais le «temps» s'écoule à la fréquence d'images souhaitée.


Couler:

[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éthode 2. Mettez le SI dans un web-worker. Celui-ci est FAAAST et lisse!

Identique à la méthode 1, mais placez le SI dans Web-worker. Il fonctionnera alors sur un thread totalement séparé, laissant la page uniquement pour la RAF et l'interface utilisateur. Passez le tableau de sprites d'avant en arrière en tant qu '«objet transférable». C'est buko rapide. Le clonage ou la sérialisation ne prend pas de temps, mais ce n'est pas comme passer par référence dans la mesure où la référence de l'autre côté est détruite, vous devrez donc faire passer les deux côtés de l'autre côté, et ne les mettre à jour que lorsqu'elles sont présentes, trier comme passer une note dans les deux sens avec votre petite amie au lycée.

Un seul peut lire et écrire à la fois. C'est très bien tant qu'ils vérifient si ce n'est pas indéfini pour éviter une erreur. La RAF est RAPIDE et la réactivera immédiatement, puis passera par un tas de trames GPU en vérifiant simplement si elle a déjà été renvoyée. Le SI dans le web-worker aura le tableau de sprites la plupart du temps, et mettra à jour les données de position, de mouvement et de physique, ainsi que la création de la nouvelle chaîne de transformation, puis la transmettra au RAF dans la page.

C'est le moyen le plus rapide que je connaisse pour animer des éléments via un script. Les deux fonctions seront exécutées comme deux programmes séparés, sur deux threads séparés, profitant des processeurs multicœurs d'une manière qu'un seul script js ne le fait pas. Animation javascript multi-thread.

Et il le fera sans à-coups, mais à la fréquence d'images réelle spécifiée, avec très peu de divergence.


Résultat:

L'une ou l'autre de ces deux méthodes garantira que votre script s'exécutera à la même vitesse sur n'importe quel PC, téléphone, tablette, etc. (dans les limites des capacités de l'appareil et du navigateur, bien sûr).

jdmayfield
la source
En remarque: dans la méthode 1, s'il y a trop d'activité dans votre setInterval, cela peut ralentir votre RAF en raison d'une asynchrone monothread. Vous pouvez atténuer cette rupture de cette activité sur plus que sur une trame SI, de sorte que l'async transmettra le contrôle à RAF plus rapidement. Rappelez-vous, RAF fonctionne à la fréquence d'images maximale, mais synchronise les changements graphiques avec l'affichage, il est donc normal de sauter quelques images RAF - tant que vous ne sautez pas plus que des images SI, il ne sautera pas.
jdmayfield
La méthode 2 est plus robuste, car elle multitâche en fait les deux boucles, sans basculer d'avant en arrière via async, mais vous voulez toujours éviter que votre trame SI prenne plus de temps que la fréquence d'images souhaitée, donc la division de l'activité SI peut toujours être souhaitable s'il y a beaucoup de manipulation de données en cours qui prendrait plus d'une trame SI pour terminer.
jdmayfield
J'ai pensé qu'il valait la peine de mentionner, comme note d'intérêt, que l'exécution de boucles appariées comme celle-ci enregistre dans Chromes DevTools que le GPU fonctionne à la fréquence d'images spécifiée dans la boucle setInterval! Il apparaît que seules les images RAF dans lesquelles des changements graphiques se produisent sont comptées comme des images par le compteur FPS. Ainsi, les cadres RAF dans lesquels seul un travail non graphique, ou même simplement des boucles vides, ne comptent pas en ce qui concerne le GPU. Je trouve cela intéressant comme point de départ pour d'autres recherches.
jdmayfield
Je crois que cette solution a le problème qu'elle continue de fonctionner lorsque rAF est suspendu, par exemple parce que l'utilisateur est passé à un autre onglet.
N4ppeL du
1
PS J'ai fait quelques lectures et il semble que la plupart des navigateurs limitent de toute façon les événements chronométrés à une fois par seconde dans les onglets d'arrière-plan (ce qui devrait probablement également être géré d'une manière ou d'une autre). Si vous souhaitez toujours résoudre le problème et faire une pause complète lorsqu'il n'est pas visible, il semble y avoir l' visibilitychangeévénement.
N4ppeL
3

Comment accélérer facilement à un FPS spécifique:

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

Source: Une explication détaillée des boucles et du timing du jeu JavaScript par Isaac Sukin

Rustem Kakimov
la source
1
Si mon moniteur fonctionne à 60 FPS et que je veux que mon jeu tourne à 58 FPS, je règle maxFPS = 58, cela le fera fonctionner à 30 FPS car il sautera toutes les 2 images.
Curtis
Oui, j'ai également essayé celui-ci. Je choisis de ne pas ralentir réellement la RAF elle-même - seuls les changements sont mis à jour par setTimeout. Dans Chrome au moins, cela fait que les fps efficaces s'exécutent au rythme de setTimeouts, selon les lectures de DevTools. Bien sûr, il ne peut mettre à jour que des images vidéo réelles à la vitesse de la carte vidéo et au taux de rafraîchissement du moniteur, mais cette méthode semble fonctionner avec les moindres jankies, donc le contrôle des fps "apparent" le plus fluide, ce que je recherche.
jdmayfield
Puisque je garde une trace de tous les mouvements dans les objets JS séparément de la RAF, cela maintient la logique d'animation, la détection de collision ou tout ce dont vous avez besoin, à un rythme perceptuellement cohérent, quel que soit le RAF ou le setTimeout, avec un peu de maths supplémentaire.
jdmayfield
2

Saut à la corde requestAnimationFrame la cause pas lisse (souhaitée) à l' animation personnalisée fps.

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

Code original par @tavnab.

befzz
la source
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
la source
Veuillez ajouter quelques phrases pour expliquer ce que fait votre code, afin que vous puissiez obtenir plus de votes positifs pour votre réponse.
Analyse floue le
1

Je le fais toujours de cette manière très simple sans jouer avec les horodatages:

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
la source
1
Cela fonctionnera trop vite si votre moniteur est à 120 ips.
Curtis
0

Voici une bonne explication que j'ai trouvée: CreativeJS.com , pour encapsuler un appel setTimeou) dans la fonction transmise à requestAnimationFrame. Mon souci avec une requestionAnimationFrame "simple" serait, "Et si je ne voulais que l'animation trois fois par seconde?" Même avec requestAnimationFrame (par opposition à setTimeout) est qu'il reste gaspille (une certaine) quantité d '"énergie" (ce qui signifie que le code du navigateur fait quelque chose et ralentit peut-être le système) 60 ou 120 ou autant de fois par seconde, comme par opposition à seulement deux ou trois fois par seconde (comme vous voudrez peut-être).

La plupart du temps, j'exécute mes navigateurs avec JavaScript désactivé pour cette raison. Mais j'utilise Yosemite 10.10.3, et je pense qu'il y a une sorte de problème de minuterie - du moins sur mon ancien système (relativement vieux - c'est-à-dire 2011).

Jim Witte
la source