Steuern von fps mit requestAnimationFrame?

140

Es scheint, requestAnimationFrameals wäre dies de facto der Weg, Dinge jetzt zu animieren. Es hat größtenteils ziemlich gut für mich funktioniert, aber im Moment versuche ich einige Canvas-Animationen zu machen und habe mich gefragt: Gibt es eine Möglichkeit, um sicherzustellen, dass es mit einer bestimmten Geschwindigkeit läuft? Ich verstehe, dass der Zweck von rAF darin besteht, Animationen durchgehend flüssig zu machen, und ich könnte das Risiko eingehen, dass meine Animation abgehackt wird, aber im Moment scheint sie ziemlich willkürlich mit drastisch unterschiedlichen Geschwindigkeiten zu laufen, und ich frage mich, ob es einen Weg gibt, sie zu bekämpfen das irgendwie.

Ich würde verwenden, setIntervalaber ich möchte die Optimierungen, die rAF bietet (insbesondere das automatische Stoppen, wenn die Registerkarte scharfgestellt ist).

Wenn jemand meinen Code ansehen möchte, ist es so ziemlich:

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

Wobei Node.drawFlash () nur ein Code ist, der den Radius anhand einer Zählervariablen bestimmt und dann einen Kreis zeichnet.

robert.vinluan
quelle
1
Lag Ihre Animation zurück? Ich denke, der größte Vorteil requestAnimationFrameist (wie der Name schon sagt), dass ein Animationsrahmen nur dann angefordert wird, wenn er benötigt wird. Angenommen, Sie zeigen eine statische schwarze Leinwand. Sie sollten 0 fps erhalten, da kein neuer Frame benötigt wird. Wenn Sie jedoch eine Animation anzeigen, die 60 fps erfordert, sollten Sie diese auch erhalten. rAFerlaubt nur, nutzlose Frames zu "überspringen" und dann die CPU zu sparen.
Maxdec
setInterval funktioniert auch nicht in der inaktiven Registerkarte.
ViliusL
Dieser Code läuft auf dem 90-Hz-Display anders als auf dem 60-Hz-Display gegenüber dem 144-Hz-Display.
Manthrax

Antworten:

190

So drosseln Sie requestAnimationFrame auf eine bestimmte Bildrate

Demo-Drosselung mit 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/

Diese Methode testet die seit dem Ausführen der letzten Frame-Schleife verstrichene Zeit.

Ihr Zeichnungscode wird nur ausgeführt, wenn das angegebene FPS-Intervall abgelaufen ist.

Der erste Teil des Codes legt einige Variablen fest, die zur Berechnung der verstrichenen Zeit verwendet werden.

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

Und dieser Code ist die eigentliche requestAnimationFrame-Schleife, die mit Ihrem angegebenen FPS gezeichnet wird.

// 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
quelle
5
Hervorragende Erklärung und Beispiel. Dies sollte als akzeptierte Antwort markiert werden
muxcmux
13
Schöne Demo - es sollte akzeptiert werden. Hier haben Sie Ihre Geige gegabelt, um die Verwendung von window.performance.now () anstelle von Date.now () zu demonstrieren. Dies passt gut zu dem hochauflösenden Zeitstempel, den rAF bereits erhält, sodass Date.now () im Rückruf nicht aufgerufen werden muss
Dean Radcliffe
2
Vielen Dank für den aktualisierten Link mit der neuen Funktion für den rAF-Zeitstempel. Der neue rAF-Zeitstempel bietet eine nützliche Infrastruktur und ist außerdem präziser als Date.now.
Markieren Sie den
13
Dies ist eine wirklich schöne Demo, die mich dazu inspiriert hat, meine eigene zu machen ( JSFiddle ). Die Hauptunterschiede bestehen darin, rAF (wie Deans Demo) anstelle von Date zu verwenden, Steuerelemente hinzuzufügen, um die Zielframerate dynamisch anzupassen, die Framerate in einem von der Animation getrennten Intervall abzutasten und ein Diagramm mit historischen Frameraten hinzuzufügen.
Tavnab
1
Sie können nur steuern, wann Sie einen Frame überspringen. Ein 60-fps-Monitor zeichnet immer in Intervallen von 16 ms. Wenn Sie beispielsweise möchten, dass Ihr Spiel mit 50 fps ausgeführt wird, möchten Sie jeden 6. Frame überspringen. Sie überprüfen, ob 20 ms (1000/50) verstrichen sind und nicht (nur 16 ms sind verstrichen), sodass Sie einen Frame überspringen. Dann sind die nächsten 32 ms seit dem Zeichnen verstrichen, also zeichnen und setzen Sie zurück. Aber dann überspringen Sie die Hälfte der Frames und laufen mit 30 fps. Wenn Sie also zurücksetzen, erinnern Sie sich, dass Sie das letzte Mal 12 ms zu lange gewartet haben. Das nächste Bild vergeht also weitere 16 ms, aber Sie zählen es als 16 + 12 = 28 ms, also zeichnen Sie erneut und haben 8 ms zu lange gewartet
Curtis
47

Update 2016/6

Das Problem bei der Drosselung der Bildrate besteht darin, dass der Bildschirm eine konstante Aktualisierungsrate aufweist, normalerweise 60 FPS.

Wenn wir 24 FPS wollen, werden wir nie die wahren 24 fps auf dem Bildschirm sehen, wir können sie als solche zeitlich festlegen, aber nicht anzeigen, da der Monitor nur synchronisierte Frames mit 15 fps, 30 fps oder 60 fps anzeigen kann (einige Monitore auch 120 fps ).

Aus zeitlichen Gründen können wir jedoch nach Möglichkeit berechnen und aktualisieren.

Sie können die gesamte Logik zur Steuerung der Bildrate erstellen, indem Sie Berechnungen und Rückrufe in ein Objekt einkapseln:

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

Fügen Sie dann einen Controller- und Konfigurationscode hinzu:

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

Verwendung

Es wird sehr einfach - jetzt müssen wir nur noch eine Instanz erstellen, indem wir die Rückruffunktion und die gewünschte Bildrate wie folgt einstellen:

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

Starten Sie dann (was bei Bedarf das Standardverhalten sein kann):

fc.start();

Das war's, die gesamte Logik wird intern gehandhabt.

Demo

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>

Alte Antwort

Der Hauptzweck von requestAnimationFramebesteht darin, Aktualisierungen mit der Aktualisierungsrate des Monitors zu synchronisieren. Dazu müssen Sie die FPS des Monitors oder einen Faktor davon animieren (dh 60, 30, 15 FPS für eine typische Bildwiederholfrequenz bei 60 Hz).

Wenn Sie eine willkürlichere FPS wünschen, macht es keinen Sinn, rAF zu verwenden, da die Bildrate sowieso nie mit der Aktualisierungsfrequenz des Monitors übereinstimmt (nur ein Bild hier und da), was einfach keine reibungslose Animation ermöglichen kann (wie bei allen Bild-Timings) ) und Sie können genauso gut setTimeoutoder setIntervalstattdessen verwenden.

Dies ist auch ein bekanntes Problem in der professionellen Videobranche, wenn Sie ein Video mit einer anderen FPS als dem Gerät abspielen möchten, bei dem es aktualisiert wird. Es wurden viele Techniken verwendet, wie z. B. das Überblenden von Bildern und das komplexe Neugestalten von Zwischenbildern basierend auf Bewegungsvektoren. Bei Leinwand sind diese Techniken jedoch nicht verfügbar, und das Ergebnis ist immer ein ruckeliges Video.

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

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

    ... code for frame here
}

Der Grund, warum wir an setTimeout erster Stelle stehen (und warum an rAFerster Stelle, wenn eine Polyfüllung verwendet wird), ist, dass dies genauer ist, da setTimeoutein Ereignis sofort beim Start der Schleife in die Warteschlange gestellt wird, sodass unabhängig davon, wie viel Zeit der verbleibende Code benötigt (vorausgesetzt, das Timeout-Intervall wird nicht überschritten) Der nächste Aufruf erfolgt in dem Intervall, das er darstellt (für reines rAF ist dies nicht unbedingt erforderlich, da rAF in jedem Fall versucht, auf den nächsten Frame zu springen).

Es ist auch erwähnenswert, dass das Platzieren an erster Stelle auch das Risiko birgt, dass sich Anrufe wie bei stapeln setInterval. setIntervalkann für diese Verwendung etwas genauer sein.

Und Sie können setIntervalstattdessen außerhalb der Schleife verwenden, um dasselbe zu tun.

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

function loop() {

    ... code for frame here
}

Und um die Schleife zu stoppen:

clearInterval(rememberMe);

Um die Bildrate zu verringern, wenn die Registerkarte unscharf wird, können Sie einen Faktor wie den folgenden hinzufügen:

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
}

Auf diese Weise können Sie die FPS auf 1/4 usw. reduzieren.


quelle
4
In einigen Fällen versuchen Sie nicht, die Bildrate des Monitors anzupassen, sondern in Bildsequenzen beispielsweise Frames zu löschen. Ausgezeichnete Erklärung übrigens
Sidsonson
3
Einer der Hauptgründe für die Drosselung mit requestAnimationFrame besteht darin, die Ausführung von Code mit dem Animationsrahmen des Browsers abzustimmen. Die Dinge laufen am Ende viel reibungsloser, besonders wenn Sie in jedem Frame eine Logik für Daten ausführen, wie zum Beispiel bei Musikvisualisierern.
Chris Dolphin
4
Dies ist schlecht, da die Hauptverwendung darin requestAnimationFramebesteht, DOM-Vorgänge (Lesen / Schreiben) zu synchronisieren. Wenn Sie sie nicht verwenden, wird die Leistung beim Zugriff auf das DOM beeinträchtigt, da Vorgänge nicht in die Warteschlange gestellt werden, um zusammen ausgeführt zu werden, und das unnötige Neulackieren des Layouts erzwingen.
vsync
1
Es besteht kein Risiko, dass sich "Anrufe stapeln", da JavaScript Single-Threaded ausführt und kein Timeout-Ereignis ausgelöst wird, während Ihr Code ausgeführt wird. Wenn die Funktion also länger als das Zeitlimit dauert, wird sie fast immer so schnell wie möglich ausgeführt, während der Browser zwischen den Aufrufen immer noch Neuzeichnungen durchführt und andere Zeitlimits auslöst.
Dronus
Ich weiß, dass Sie angeben, dass die Seitenaktualisierung nicht schneller als die fps-Grenze auf dem Display aktualisiert werden kann. Ist es jedoch möglich, schneller zu aktualisieren, indem ein Seitenreflow ausgelöst wird? Ist es umgekehrt möglich, mehrere Seitenrückflüsse nicht zu bemerken, wenn sie schneller als die native fps-Rate ausgeführt werden?
Travis J
36

Ich schlage vor, Ihren Anruf requestAnimationFramein a setTimeout. Wenn Sie setTimeoutinnerhalb der Funktion aufrufen, von der Sie den Animationsrahmen angefordert haben, verlieren Sie den Zweck von requestAnimationFrame. Aber wenn Sie requestAnimationFramevon innen anrufen setTimeout, funktioniert es reibungslos:

var fps = 25
function animate() {
  setTimeout(function() {
    requestAnimationFrame(animate);
  }, 1000 / fps);
}
Luke Taylor
quelle
1
Dies scheint tatsächlich zu funktionieren, um die Framerate niedrig zu halten und meine CPU nicht zu kochen. Und es ist so einfach. Prost!
Phocks
Dies ist eine schöne, einfache Möglichkeit, dies für leichte Animationen zu tun. Zumindest auf einigen Geräten ist die Synchronisation jedoch etwas unpassend. Ich habe diese Technik bei einem meiner früheren Motoren angewendet. Es hat gut funktioniert, bis die Dinge komplex wurden. Das größte Problem war, wenn es an Orientierungssensoren angeschlossen wurde, dass es entweder zurückbleibt oder nervös wird. Später stellte ich fest, dass die Verwendung eines separaten setInterval und die Kommunikation von Aktualisierungen zwischen Sensoren, setInterval-Frames und RAF-Frames über Objekteigenschaften es den Sensoren und der RAF ermöglichten, in Echtzeit zu arbeiten, während die Animationszeit über Eigenschaftsaktualisierungen von setInterval gesteuert werden konnte.
jdmayfield
Beste Antwort ! Danke;)
538ROMEO
Mein Monitor hat 60 FPS. Wenn ich var fps = 60 setze, erhalte ich mit diesem Code nur etwa 50 FPS. Ich möchte es auf 60 verlangsamen, weil einige Leute 120 FPS-Monitore haben, aber ich möchte nicht alle anderen beeinflussen. Das ist überraschend schwierig.
Curtis
Der Grund, warum Sie niedrigere FPS als erwartet erhalten, ist, dass setTimeout den Rückruf nach mehr als der angegebenen Verzögerung ausführen kann. Dafür gibt es eine Reihe möglicher Gründe. Und in jeder Schleife dauert es einige Zeit, einen neuen Timer einzustellen und Code auszuführen, bevor das neue Timeout eingestellt wird. Sie haben keine Möglichkeit, damit genau zu sein. Sie sollten immer ein langsameres als erwartetes Ergebnis in Betracht ziehen. Solange Sie jedoch nicht wissen, wie viel langsamer es sein wird, wäre der Versuch, die Verzögerung zu verringern, ebenfalls ungenau. JS in Browsern soll nicht so genau sein.
pdepmcp
17

Das sind alles gute Ideen in der Theorie, bis Sie tief gehen. Das Problem ist, dass Sie eine RAF nicht drosseln können, ohne sie zu de-synchronisieren, wodurch ihr eigentlicher Zweck für die Existenz zunichte gemacht wird. So können Sie es bei voller Geschwindigkeit laufen lassen und Ihre Daten in einer separaten Schleife aktualisieren , oder sogar einem separaten Thread!

Ja, ich habe es gesagt. Sie können im Browser Multithread-JavaScript erstellen!

Ich kenne zwei Methoden, die ohne Ruck sehr gut funktionieren, viel weniger Saft verbrauchen und weniger Wärme erzeugen. Genaue Zeitmessung im menschlichen Maßstab und Maschineneffizienz sind das Nettoergebnis.

Entschuldigung, wenn dies ein wenig wortreich ist, aber hier geht ...


Methode 1: Aktualisieren Sie Daten über setInterval und Grafiken über RAF.

Verwenden Sie ein separates setInterval zum Aktualisieren von Übersetzungs- und Rotationswerten, Physik, Kollisionen usw. Behalten Sie diese Werte für jedes animierte Element in einem Objekt. Weisen Sie die Transformationszeichenfolge einer Variablen im Objekt jedes setInterval-Frames zu. Halten Sie diese Objekte in einem Array. Stellen Sie Ihr Intervall in ms auf die gewünschten fps ein: ms = (1000 / fps). Dadurch bleibt eine konstante Uhr erhalten, die auf jedem Gerät unabhängig von der RAF-Geschwindigkeit die gleichen Bilder pro Sekunde zulässt. Ordnen Sie die Transformationen hier nicht den Elementen zu!

Durchlaufen Sie in einer requestAnimationFrame-Schleife Ihr Array mit einer for-Schleife der alten Schule - verwenden Sie hier nicht die neueren Formulare, sie sind langsam!

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

Rufen Sie in Ihrer Funktion rafUpdate die Transformationszeichenfolge von Ihrem js-Objekt im Array und dessen Element-ID ab. Sie sollten Ihre Sprite-Elemente bereits an eine Variable angehängt haben oder auf andere Weise leicht zugänglich sein, damit Sie keine Zeit verlieren, sie in der RAF abzurufen. Es funktioniert ziemlich gut, sie in einem Objekt zu behalten, das nach der HTML-ID benannt ist. Richten Sie diesen Teil ein, bevor er überhaupt in Ihre SI oder RAF gelangt.

Verwenden Sie die RAF Ihre Transformationen aktualisieren nur , verwenden Sie nur 3D - Transformationen (auch für 2d) und Set css „will-change: verwandeln;“ auf Elemente, die sich ändern werden. Auf diese Weise werden Ihre Transformationen so weit wie möglich mit der nativen Aktualisierungsrate synchronisiert, die GPU aktiviert und dem Browser mitgeteilt, wo er sich am meisten konzentrieren soll.

Sie sollten also so etwas wie diesen Pseudocode haben ...

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

Dadurch bleiben Ihre Aktualisierungen der Datenobjekte und Transformationszeichenfolgen mit der gewünschten Framerate im SI synchronisiert, und die tatsächlichen Transformationszuweisungen in der RAF werden mit der GPU-Aktualisierungsrate synchronisiert. Die eigentlichen Grafikaktualisierungen befinden sich also nur in der RAF, aber die Änderungen an den Daten und das Erstellen der Transformationszeichenfolge befinden sich in der SI, sodass keine Ruckler, sondern "Zeit" mit der gewünschten Bildrate fließt.


Fließen:

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

Methode 2. Legen Sie den SI in einen Web-Worker. Dieser ist FAAAST und glatt!

Wie Methode 1, jedoch den SI in Web-Worker einfügen. Es wird dann in einem völlig separaten Thread ausgeführt, sodass die Seite nur für RAF und Benutzeroberfläche verwendet werden kann. Übergeben Sie das Sprite-Array als übertragbares Objekt hin und her. Das ist buko schnell. Das Klonen oder Serialisieren dauert nicht lange, aber es ist nicht so, als würde eine Referenz übergeben, da die Referenz von der anderen Seite zerstört wird. Sie müssen also beide Seiten auf die andere Seite übergeben und sie nur aktualisieren, wenn sie vorhanden sind, sortieren als würde man mit seiner Freundin in der High School eine Notiz hin und her geben.

Es kann immer nur einer lesen und schreiben. Dies ist in Ordnung, solange sie prüfen, ob es nicht undefiniert ist, um einen Fehler zu vermeiden. Die RAF ist SCHNELL und wird sie sofort zurückwerfen und dann eine Reihe von GPU-Frames durchlaufen, um zu überprüfen, ob sie bereits zurückgesendet wurde. Der SI im Web-Worker verfügt die meiste Zeit über das Sprite-Array und aktualisiert Positions-, Bewegungs- und Physikdaten sowie die neue Transformationszeichenfolge und gibt sie dann an die RAF auf der Seite zurück.

Dies ist der schnellste Weg, um Elemente per Skript zu animieren. Die beiden Funktionen werden als zwei separate Programme auf zwei separaten Threads ausgeführt, wobei Multi-Core-CPUs auf eine Weise genutzt werden, die ein einzelnes js-Skript nicht bietet. Multithread-Javascript-Animation.

Und das ohne Ruck, aber mit der tatsächlich angegebenen Bildrate und mit sehr geringer Abweichung.


Ergebnis:

Mit beiden Methoden wird sichergestellt, dass Ihr Skript auf jedem PC, Telefon, Tablet usw. mit der gleichen Geschwindigkeit ausgeführt wird (natürlich im Rahmen der Funktionen des Geräts und des Browsers).

jdmayfield
quelle
Als Randnotiz: Wenn in Methode 1 zu viel Aktivität in Ihrem setInterval vorhanden ist, kann dies Ihre RAF aufgrund von Single-Threaded-Async verlangsamen. Sie können dieses Aufbrechen dieser Aktivität über mehr als einen SI-Frame hinweg abmildern, sodass Async die Kontrolle schneller an RAF zurückgibt. Denken Sie daran, dass RAF mit maximaler Bildrate arbeitet, aber grafische Änderungen mit der Anzeige synchronisiert. Es ist also in Ordnung, einige RAF-Bilder zu überspringen - solange Sie nicht mehr als SI-Bilder überspringen, wird es nicht ruckeln.
jdmayfield
Methode 2 ist robuster, da die beiden Schleifen tatsächlich multitaskingfähig sind und nicht asynchron hin- und hergeschaltet werden. Sie möchten jedoch vermeiden, dass Ihr SI-Frame länger als die gewünschte Framerate dauert, sodass die SI-Aktivität möglicherweise immer noch aufgeteilt wird Wünschenswert, wenn viele Datenmanipulationen durchgeführt werden, für deren Fertigstellung mehr als ein SI-Frame erforderlich wäre.
jdmayfield
Ich fand es erwähnenswert, dass das Ausführen von gepaarten Schleifen wie dieser in Chromes DevTools tatsächlich registriert, dass die GPU mit der in der setInterval-Schleife angegebenen Bildrate ausgeführt wird! Es werden nur RAF-Frames angezeigt, in denen grafische Änderungen auftreten. Diese werden vom FPS-Messgerät als Frames gezählt. RAF-Frames, in denen nur nicht grafische Arbeiten oder auch nur leere Schleifen verwendet werden, zählen für die GPU nicht. Ich finde dies als Ausgangspunkt für weitere Forschungen interessant.
jdmayfield
Ich glaube, diese Lösung hat das Problem, dass sie weiter ausgeführt wird, wenn rAF angehalten wird, z. B. weil der Benutzer zu einer anderen Registerkarte gewechselt hat.
N4ppeL
1
PS Ich habe etwas gelesen und es scheint, dass die meisten Browser zeitgesteuerte Ereignisse in Hintergrundregistern sowieso auf einmal pro Sekunde beschränken (was wahrscheinlich auch auf irgendeine Weise behandelt werden sollte). Wenn Sie das Problem weiterhin beheben und eine Pause einlegen möchten, wenn es nicht sichtbar ist, scheint das visibilitychangeEreignis aufgetreten zu sein .
N4ppeL
3

So drosseln Sie einfach auf einen bestimmten FPS:

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

Quelle: Eine detaillierte Erklärung der JavaScript-Spielschleifen und des Timings von Isaac Sukin

Rustem Kakimov
quelle
1
Wenn mein Monitor mit 60 FPS läuft und ich möchte, dass mein Spiel mit 58 FPS läuft, setze ich maxFPS = 58, wodurch es mit 30 FPS läuft, da jedes zweite Bild übersprungen wird.
Curtis
Ja, ich habe es auch versucht. Ich beschließe, die RAF selbst nicht zu drosseln - nur die Änderungen werden vom setTimeout aktualisiert. Zumindest in Chrome führt dies dazu, dass die effektiven fps gemäß den Messwerten in DevTools im setTimeouts-Tempo ausgeführt werden. Natürlich kann es nur echte Videobilder mit der Geschwindigkeit der Grafikkarte aktualisieren und die Bildwiederholfrequenz überwachen, aber diese Methode scheint mit den geringsten Ruckeln zu funktionieren, also mit der reibungslosesten "scheinbaren" fps-Steuerung, was ich anstrebe.
jdmayfield
Da ich alle Bewegungen in JS-Objekten getrennt von der RAF verfolge, wird die Animationslogik, die Kollisionserkennung oder was auch immer Sie benötigen, unabhängig von der RAF oder dem setTimeout mit ein wenig zusätzlicher Mathematik mit einer wahrnehmungskonsistenten Geschwindigkeit ausgeführt.
jdmayfield
2

Das Überspringen von requestAnimationFrame führt zu einer nicht glatten (gewünschten) Animation bei benutzerdefinierten 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>

Originalcode von @tavnab.

befzz
quelle
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
quelle
Bitte fügen Sie ein paar Sätze hinzu, um zu erklären, was Ihr Code tut, damit Sie mehr positive Stimmen für Ihre Antwort erhalten.
Fuzzy-Analyse
1

Ich mache es immer so einfach, ohne mit Zeitstempeln zu spielen:

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
quelle
1
Dies läuft zu schnell, wenn Ihr Monitor 120 fps hat.
Curtis
0

Hier ist eine gute Erklärung, die ich gefunden habe: CreativeJS.com , um einen setTimeou) -Aufruf in die an requestAnimationFrame übergebene Funktion zu verpacken. Mein Anliegen bei einem "einfachen" requestionAnimationFrame wäre: "Was ist, wenn ich möchte, dass es nur dreimal pro Sekunde animiert wird?" Selbst bei requestAnimationFrame (im Gegensatz zu setTimeout) wird immer noch (etwas) "Energie" verschwendet (was bedeutet, dass der Browser-Code etwas tut und möglicherweise das System verlangsamt) 60 oder 120 oder wie oft auch immer pro Sekunde im Gegensatz zu nur zwei oder drei Mal pro Sekunde (wie Sie möchten).

Die meiste Zeit starte ich meine Browser mit JavaScript aus diesem Grund. Aber ich verwende Yosemite 10.10.3 und ich denke, es gibt eine Art Timer-Problem damit - zumindest auf meinem alten System (relativ alt - dh 2011).

Jim Witte
quelle