Fps'yi requestAnimationFrame ile kontrol etme?


140

Görünüşe göre requestAnimationFrameşimdi olayları canlandırmanın fiili yolu. Çoğunlukla benim için oldukça iyi çalıştı, ama şu anda bazı tuval animasyonları yapmaya çalışıyorum ve merak ediyordum: Belirli bir fps'de çalıştığından emin olmanın herhangi bir yolu var mı? RAF'ın amacının sürekli olarak pürüzsüz animasyonlar olduğunu anlıyorum ve animasyonumu dalgalı hale getirme riski taşıyabilirim, ancak şu anda oldukça farklı hızlarda keyfi olarak çalışıyor gibi görünüyor ve savaşmanın bir yolu olup olmadığını merak ediyorum bir şekilde.

Ben kullanmak istiyorum setIntervalama rAF (özellikle sekme odak olduğunda otomatik olarak durdurma) sunduğu optimizasyonlar istiyorum.

Birisi koduma bakmak isterse, hemen hemen:

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

Burada Node.drawFlash () işlevi, bir sayaç değişkenine dayalı yarıçapı belirleyen ve daha sonra bir daire çizen bir koddur.


1
Animasyonunuz gecikiyor mu? Bence en büyük avantajı requestAnimationFrame(adından da anlaşılacağı gibi) sadece gerektiğinde bir animasyon karesi istemektir. Diyelim ki statik bir siyah kanvas gösterdiğinizde, yeni kare gerekmediğinden 0 fps almalısınız. Ancak 60 fps gerektiren bir animasyon görüntülüyorsanız, bunu da almalısınız. rAFsadece işe yaramaz çerçeveleri "atlamak" ve daha sonra CPU kaydetmek için izin verir.
maxdec

setInterval etkin olmayan sekmede de çalışmaz.
ViliusL

Bu kod 90hz ekranda vs 60hz ekranda vs 144hz ekranda farklı çalışır.
manthrax

Yanıtlar:


190

İstek nasıl kısılır? AnimasyonFrame belirli bir kare hızına

5 FPS'de demo kısıtlama: http://jsfiddle.net/m1erickson/CtsY3/

Bu yöntem, son kare döngüsünü yürüttükten sonra geçen süreyi test ederek çalışır.

Çizim kodunuz yalnızca belirttiğiniz FPS aralığınız dolduğunda yürütülür.

Kodun ilk kısmı, geçen süreyi hesaplamak için kullanılan bazı değişkenleri ayarlar.

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

Ve bu kod, belirttiğiniz FPS'de çizilen gerçek requestAnimationFrame döngüsüdür.

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

    }
}

5
Mükemmel açıklama ve örnek. Bu kabul edilen cevap olarak işaretlenmelidir
muxcmux

13
Güzel demo - kabul edilmelidir. Burada, kemanınızı Date.now () yerine window.performance.now () kullanarak göstermeleri için çatalladı. Bu, rAF'ın zaten aldığı yüksek çözünürlüklü zaman damgası ile iyi gider, bu nedenle geri arama içinde Date.now () öğesini çağırmaya gerek yoktur: jsfiddle.net/chicagogrooves/nRpVD/2
Dean Radcliffe

2
Yeni rAF zaman damgası özelliğini kullanan güncellenmiş bağlantı için teşekkür ederiz. Yeni rAF zaman damgası kullanışlı altyapı ekler ve ayrıca Date.now'dan daha hassastır.
markE

13
Bu gerçekten güzel bir demo, bana kendiminkini yapmam için ilham verdi ( JSFiddle ). Temel farklar Tarih yerine rAF (Dean'in demosu gibi) kullanmak, hedef kare hızını dinamik olarak ayarlamak için kontroller eklemek, kare hızını animasyondan ayrı bir aralıkta örneklemek ve geçmiş kare hızları grafiği eklemek.
tavnab

1
Kontrol edebileceğiniz tek şey bir kareyi atlayacağınız zamandır. 60 fps monitör her zaman 16 ms aralıklarla çekilir. Örneğin, oyununuzun 50 fps'de çalışmasını istiyorsanız, her 6. kareyi atlamak istersiniz. 20ms (1000/50) süresinin dolup dolmadığını kontrol ettiniz ve geçmedi (sadece 16ms geçti), bu yüzden bir kareyi atlıyorsunuz, sonra çizdiğinizden sonraki 32ms karesi geçtiyse, çiziyor ve sıfırlıyorsunuz. Ama sonra karelerin yarısını atlayacak ve 30 fps'de çalışacaksınız. Sıfırladığınız zaman geçen sefer 12 ms çok uzun beklediğinizi hatırlarsınız. Bir sonraki kare 16 ms geçiyor ancak 16 + 12 = 28 ms olarak sayıyorsunuz, böylece tekrar çiziyorsunuz ve 8 ms çok uzun beklediniz
Curtis

47

2016/6 Güncellemesi

Kare hızını kısıtlayan sorun, ekranın sabit bir güncelleme hızına sahip olması, genellikle 60 FPS olmasıdır.

24 FPS istiyorsak, ekranda gerçek 24 fps'yi asla elde edemeyiz, bunu zamanlayabiliriz, ancak monitör sadece 15 fps, 30 fps veya 60 fps'de senkronize edilmiş kareleri gösterebileceğinden gösteremeyiz (bazı monitörler ayrıca 120 fps ).

Ancak, zamanlama amacıyla, mümkün olduğunda hesaplayabilir ve güncelleyebiliriz.

Kare hızını kontrol etmek için tüm mantığı, hesaplamaları ve geri çağrıları bir nesneye kapsülleyerek oluşturabilirsiniz:

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

Ardından bazı denetleyici ve yapılandırma kodu ekleyin:

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

kullanım

Çok basit hale geliyor - şimdi tek yapmamız gereken, geri arama işlevini ve istenen kare hızını aşağıdaki gibi ayarlayarak bir örnek oluşturmaktır:

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

Sonra başlayın (istenirse varsayılan davranış bu olabilir):

fc.start();

Hepsi bu, tüm mantık dahili olarak ele alınır.

gösteri

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>

Eski cevap

Temel amacı, requestAnimationFramegüncellemeleri monitörün yenileme hızıyla senkronize etmektir. Bu, monitörün FPS'sinde veya bir faktöründe canlandırmanızı gerektirir (örn. 60 Hz'de tipik bir yenileme hızı için 60, 30, 15 FPS).

Daha keyfi bir FPS istiyorsanız, rAF'ı kullanmanın bir anlamı yoktur, çünkü kare hızı hiçbir zaman monitörün güncelleme frekansıyla hiçbir zaman eşleşmeyecektir (sadece burada ve orada bir kare), size basit bir animasyon (tüm kare yeniden zamanlamalarında olduğu gibi) veremez. ) ve siz de kullanabilir olabilir setTimeoutveya setIntervalonun yerine.

Bu, profesyonel bir video endüstrisinde farklı bir FPS'de videoyu oynatmak istediğinizde, ardından cihazın yenilendiğini gösteren iyi bilinen bir sorundur. Hareket vektörlerine dayalı olarak çerçeve karıştırma ve karmaşık yeniden zamanlama yeniden çerçevelerinin yeniden oluşturulması gibi birçok teknik kullanılmıştır, ancak tuval ile bu teknikler mevcut değildir ve sonuç her zaman sarsıntılı bir video olacaktır.

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

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

    ... code for frame here
}

setTimeout İlk sırada yer almamızın nedeni (ve rAFbir çoklu dolgu kullanıldığında ilk olarak bir yer almamızın nedeni ), setTimeoutdöngü başladığında hemen bir olayı kuyruğa alacağı için daha doğru olacağıdır, böylece kalan kod ne kadar sürecek olursa olsun (zaman aşımı aralığını aşmaması koşuluyla) bir sonraki çağrı temsil ettiği aralıkta olacaktır (saf rAF için rAF her durumda bir sonraki kareye atlamaya çalışacağı için bu gerekli değildir).

Ayrıca, ilk yerleştirmenin çağrıların istifleme riskini de artıracağını unutmayın setInterval. setIntervalbu kullanım için biraz daha doğru olabilir.

Ve setIntervalbunun yerine döngü dışında kullanabilirsiniz .

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

function loop() {

    ... code for frame here
}

Ve döngüyü durdurmak için:

clearInterval(rememberMe);

Sekme bulanıklaştığında kare hızını azaltmak için şöyle bir faktör ekleyebilirsiniz:

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
}

Bu şekilde FPS'yi 1/4'e düşürebilirsiniz.


4
Bazı durumlarda, monitörlerin kare hızıyla eşleşmeye çalışmıyorsunuz, daha çok görüntü dizilerinde, örneğin alt kareler. Mükemmel açıklama btw
sidonaldson

3
RequestAnimationFrame ile kısıtlamanın en büyük nedenlerinden biri, bazı kodların yürütülmesini tarayıcıların animasyon karesiyle sıralamak olacaktır. Özellikle müzik görüntüleyicilerde olduğu gibi her karede veriler üzerinde biraz mantık çalıştırıyorsanız, işler çok daha düzgün çalışır.
Chris Dolphin

4
Bu kötü bir durumdur çünkü ana kullanımı requestAnimationFrameDOM işlemlerini senkronize etmektir (okuma / yazma), bu nedenle kullanılmaması DOM'ya erişirken performansa zarar verecektir, çünkü işlemler birlikte gerçekleştirilmek için sıraya alınmayacak ve düzen yeniden boyamaya gereksiz yere zorlanacaktır.
vsync

1
JavaScript tek iş parçacıklı çalıştığından ve kodunuz çalışırken hiçbir zaman aşımı olayı tetiklenmediğinden "çağrıların yığınlanması" riski yoktur. Bu nedenle, işlev zaman aşımından daha uzun sürerse, tarayıcı olabildiğince hızlı çalışır, tarayıcı yine de yeniden çizer ve aramalar arasında diğer zaman aşımlarını tetikler.
dronus

Sayfa yenilemesinin ekrandaki fps sınırından daha hızlı güncellenemeyeceğini belirttiğinizi biliyorum. Ancak, sayfa yeniden akışını tetikleyerek daha hızlı yenileme yapmak mümkün müdür? Tersine, yerel fps oranından daha hızlı yapılırsa birden çok sayfa yeniden akışının fark edilmemesi mümkün müdür?
Travis J

38

Çağrınızı yanıtlamanızı öneririm requestAnimationFrame bir de setTimeout:

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

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

İşlevinizi bir sonraki boyamadan hemen önce çalışacak şekilde zamanladığından ve diğer güncellemenizi daha fazla geciktirirseniz, o zaman penceresini kaçırmış olacağınızdan , başka bir yol yerine requestAnimationFrameiçeriden aramanız gerekir . Ancak, tersini yapmak sağlamdır, çünkü isteği yapmadan önce belirli bir süre beklersiniz.setTimeoutrequestAnimationFramesetTimeout


1
Bu aslında kare hızını düşük tutmak ve böylece CPU'umu pişirmemek için çalışıyor gibi görünüyor. Ve bu çok basit. Şerefe!
phocks

Bu, hafif animasyonlar için bunu yapmanın güzel ve basit bir yoludur. En azından bazı cihazlarda senkronizasyondan biraz uzaklaşıyor. Bu tekniği eski motorlarımdan birinde kullandım. İşler karmaşık olana kadar iyi çalıştı. En büyük sorun, yönlendirme sensörlerine bağlandığında ya geride kalıyor ya da gerginleşiyordu. Daha sonra ayrı bir setInterval kullandığımı ve sensörler, setInterval çerçeveleri ve RAF çerçeveleri arasında nesne özellikleri aracılığıyla iletişim kurarak güncellemeler bulunduğunu fark ettim.
jdmayfield

En iyi cevap ! Thanks;)
538ROMEO

Monitörüm 60 FPS, var fps = 60 ayarladıysam, bu kodu kullanarak sadece yaklaşık 50 FPS alıyorum. 60'a yavaşlatmak istiyorum çünkü bazı kişilerin 120 FPS monitörü var, ancak diğer herkesi etkilemek istemiyorum. Bu şaşırtıcı derecede zor.
Curtis

Beklenenden daha düşük FPS almanızın nedeni, setTimeout'un belirtilen gecikme süresinden sonra geri aramayı yürütebilmesidir. Bunun birkaç olası nedeni vardır. Ve her döngü yeni bir zamanlayıcı ayarlamak ve yeni zaman aşımını ayarlamadan önce bir kod çalıştırmak zaman alır. Bununla doğru olmanın hiçbir yolu yok, her zaman beklenenden daha yavaş bir sonuç düşünmelisiniz, ancak ne kadar yavaş olacağını bilmediğiniz sürece, gecikmeyi azaltmaya çalışmak da yanlış olacaktır. Tarayıcılardaki JS'nin bu kadar doğru olması amaçlanmamıştır.
pdepmcp

17

Bunların hepsi, siz derinlere inene kadar teoride iyi fikirlerdir. Sorun şu ki, bir RAF'ı senkronize etmeden kısıtlayamazsınız, var olanın amacını yenersiniz. Böylece tam hızda çalışmasına izin verin ve verilerinizi ayrı bir döngüde , hatta ayrı bir iş parçacığında güncelleyin!

Evet, söyledim. Sen edebilirsiniz tarayıcıda çok dişli JavaScript yap!

Jank olmadan son derece iyi çalıştığını, çok daha az meyve suyu kullandığını ve daha az ısı ürettiğini bildiğim iki yöntem var. Doğru insan ölçeği zamanlaması ve makine verimliliği net sonuçtur.

Bu biraz garip olsa özür dileriz, ama işte gidiyor ...


Yöntem 1: setInterval yoluyla verileri ve RAF aracılığıyla grafikleri güncelleyin.

Çeviri ve döndürme değerlerini, fizik, çarpışma, vb. Güncellemek için ayrı bir setInterval kullanın. Bu değerleri her animasyonlu öğe için bir nesnede tutun. Dönüştürme dizesini her setInterval 'frame' nesnesindeki bir değişkene atayın. Bu nesneleri bir dizide saklayın. Aralıkınızı istediğiniz fps değerine ms: ms = (1000 / fps) olarak ayarlayın. Bu, RAF hızından bağımsız olarak herhangi bir cihazda aynı fps'ye izin veren sabit bir saat tutar. Buradaki öğelere dönüşümleri atamayın!

Bir requestAnimationFrame döngüsünde, döngü için eski bir okulla dizinizi yineleyin - burada daha yeni formları kullanmayın, yavaşlar!

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

RafUpdate işlevinizde, dizedeki js nesnenizden ve dönüştürme öğelerinden id dönüştürme dizesini alın. 'Sprite' öğelerinizi bir değişkene iliştirilmiş veya başka yollarla kolayca erişilebilir hale getirmelisiniz, böylece bunları RAF'a 'almak' için zaman kaybetmezsiniz. Onları html kimlikleri adlı bir nesnede tutmak oldukça iyi çalışıyor. SI veya RAF'nıza girmeden önce bu parçayı yerleştirin.

RAF'ı yalnızca dönüşümlerinizi güncellemek için kullanın , yalnızca 3B dönüşümleri (2d için bile) kullanın ve css "will-change: transform;" değişecek öğeler üzerinde. Bu, dönüşümlerinizi yerel yenileme hızıyla mümkün olduğunca senkronize tutar, GPU'yu başlatır ve tarayıcıya en çok nereye konsantre olacağını söyler.

Yani bu sahte kod gibi bir şey olmalı ...

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

Bu, SI'daki istenen 'kare' hızıyla senkronize edilen veri nesneleri ve dönüştürme dizelerini ve RAF'taki GPU yenileme oranıyla senkronize edilen gerçek dönüşüm atamalarını günceller. Dolayısıyla, gerçek grafik güncellemeleri sadece RAF'tadır, ancak verilerdeki değişiklikler ve dönüştürme dizesinin oluşturulması SI'dadır, bu nedenle janki yok, ancak 'kare' istenen kare hızında akar.


Akış:

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

Yöntem 2. SI bir web çalışanı koyun. Bu FAAAST ve pürüzsüz!

Yöntem 1 ile aynı, ancak SI web çalışanı koyun. Tamamen ayrı bir iş parçacığında çalışacak ve sayfa yalnızca RAF ve kullanıcı arayüzüyle ilgilenecek. Hareketli grafik dizisini 'aktarılabilir nesne' olarak ileri ve geri aktarın. Bu buko hızlı. Klonlamak veya serileştirmek zaman almaz, ancak diğer taraftan referansın yok edilmesi nedeniyle referansla geçmek gibi değildir, bu nedenle her iki tarafın diğer tarafa geçmesine ve yalnızca mevcut olduğunda güncellemenize, sıralamanıza lisede kız arkadaşınızla bir not ileri geri geçmek gibi.

Aynı anda yalnızca biri okuyabilir ve yazabilir. Bir hatayı önlemek için tanımlanıp tanımlanmadığını kontrol ettikleri sürece bu iyidir. RAF HIZLI ve hemen geri dönecek, daha sonra geri gönderilip gönderilmediğini kontrol eden bir grup GPU çerçevesinden geçecek. Web çalışanındaki SI çoğu zaman sprite dizisine sahip olacak ve yeni dönüşüm dizesini oluşturmanın yanı sıra konum, hareket ve fizik verilerini güncelleyecek ve ardından sayfadaki RAF'a geri gönderecektir.

Bu, komut dosyası aracılığıyla öğeleri canlandırmayı bildiğim en hızlı yoldur. İki işlev tek bir js betiğinin çalışmadığı şekilde çok çekirdekli CPU'lardan yararlanarak iki ayrı iş parçacığında iki ayrı program olarak çalışacaktır. Çok iş parçacıklı javascript animasyonu.

Ve bunu sorunsuz bir şekilde yapacaktır, ancak gerçek belirtilen kare hızında, çok az sapma ile.


Sonuç:

Bu iki yöntemden herhangi biri, komut dosyanızın herhangi bir PC, telefon, tablet vb. Üzerinde aynı hızda çalışmasını sağlayacaktır (elbette cihazın ve tarayıcının yetenekleri dahilinde).


Bir yan not olarak - Yöntem 1'de, setInterval'ınızda çok fazla etkinlik varsa, tek iş parçacıklı zaman uyumsuzluk nedeniyle RAF'ınızı yavaşlatabilir. Bu aktiviteyi SI çerçevesinden daha fazla parçalayarak azaltabilirsiniz, böylece zaman uyumsuz kontrolü RAF'a daha hızlı geri gönderir. Unutmayın, RAF maksimum kare hızında gider, ancak grafik değişiklikleri ekranla senkronize eder, bu nedenle birkaç RAF çerçevesini atlamak uygundur - SI çerçevelerinden daha fazlasını atlamadığınız sürece, sarsmaz.
jdmayfield

Yöntem 2 daha sağlamdır, çünkü aslında iki döngüyü çoklu görev olarak yürütür, zaman uyumsuzluktan ileri geri geçiş yapmaz, ancak yine de SI çerçevenizin istediğiniz kare hızından daha uzun sürmesini önlemek istersiniz, bu nedenle bölme SI etkinliği hala olabilir tamamlanması için birden fazla SI çerçevesinin gerekeceği çok fazla veri manipülasyonu varsa istenir.
jdmayfield

İlgi çekici bir not olarak, bunun gibi eşleştirilmiş döngüler çalıştırmanın GPU'nun setInterval döngüsünde belirtilen kare hızında çalıştığını Chromes DevTools'a kaydettirdiğini belirtmeye değer! Yalnızca grafiksel değişikliklerin meydana geldiği RAF kareleri FPS ölçer tarafından kare olarak sayılır. Bu nedenle, yalnızca grafik olmayan çalışmanın, hatta yalnızca boş döngülerin bulunduğu RAF çerçeveleri, GPU ile ilgili olarak sayılmaz. Bunu daha ileri araştırmaların başlangıç ​​noktası olarak ilginç buluyorum.
jdmayfield

Bu çözüm, örneğin kullanıcı başka bir sekmeye geçtiği için rAF askıya alındığında çalışmaya devam etmesi sorununa sahip olduğuna inanıyorum.
N4ppeL

1
PS Bazı okumalar yaptım ve çoğu tarayıcı zamanlanmış olayları zaten arka plan sekmelerinde saniyede bir kez sınırlıyor gibi görünüyor (muhtemelen de bir şekilde ele alınması gerekir). Sorunu hala ele almak ve görünür olmadığında tamamen duraklatmak istiyorsanız, visibilitychangeolay var gibi görünüyor .
N4ppeL

3

Belirli bir FPS'ye kolayca nasıl gaz verilir:

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

Kaynak: JavaScript Oyun Döngülerinin ve Zamanlamasının Ayrıntılı Açıklaması / Isaac Sukin


1
Monitörüm 60 FPS'de çalışıyorsa ve oyunumun 58 FPS'de çalışmasını istiyorsanız maxFPS = 58 ayarladım, bu her 2 kareyi atlayacağı için 30 FPS'de çalışmasını sağlayacaktır.
Curtis

Evet, bunu da denedim. RAF'ın kendisini kısıtlamamayı seçiyorum - sadece değişiklikler setTimeout tarafından güncellenir. DevTools'taki okumalara göre, en azından Chrome'da bu, etkili fps'nin setTimeouts hızında çalışmasına neden olur. Tabii ki sadece video kartının hızında gerçek video karelerini güncelleyebilir ve yenileme hızını izleyebilir, ancak bu yöntem en az jankies, en pürüzsüz "görünür" fps kontrolü ile çalışıyor gibi görünüyor.
jdmayfield

JS nesnelerindeki tüm hareketleri RAF'tan ayrı olarak izlediğim için, bu animasyon mantığını, çarpışma algılamayı veya ihtiyacınız olanı RAF veya setTimeout'tan bağımsız olarak algısal olarak tutarlı bir hızda biraz fazladan matematikle tutar.
jdmayfield

2

Atlama isteği Animasyon Çerçevesi özel fps'de düzgün (istenen) animasyona neden olmaz .

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

@Tavnab tarafından orijinal kod.


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

Lütfen kodunuzun ne yaptığını açıklamak için birkaç cümle ekleyin, böylece cevabınız için daha fazla oy alabilirsiniz.
Bulanık Analiz

1

Ben her zaman zaman damgaları ile uğraşmadan bu çok basit bir şekilde yapmak:

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

1
Monitörünüz 120 fps ise bu çok hızlı çalışır.
Curtis

0

İşte buldum iyi bir açıklama: CreativeJS.com , bir setTimeou sarmak için) callAnimationFrame için iletilen fonksiyon içinde çağrı. "Sade" requestionAnimationFrame ile ilgili endişem, "ya sadece saniyede üç kez canlandırılmasını istersem ?" RequestAnimationFrame ile bile (setTimeout'un aksine) hala miktar "enerji" harcaması (yani Tarayıcı kodunun bir şey yapması ve muhtemelen sistemi yavaşlatması) 60 veya 120 veya saniyede birçok kez saniyede sadece iki ya da üç defa (istediğiniz gibi) karşı.

Çoğu zaman tarayıcılarımı bu nedenle JavaScript ile tamamen kapalı olarak çalıştırıyorum . Ancak, Yosemite 10.10.3 kullanıyorum ve bununla ilgili bir çeşit zamanlayıcı sorunu olduğunu düşünüyorum - en azından eski sistemimde (nispeten eski - 2011 anlamına geliyor).

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.