Mengontrol fps dengan requestAnimationFrame?

146

Sepertinya requestAnimationFrameini cara de facto untuk menganimasikan sesuatu sekarang. Ini bekerja cukup baik bagi saya untuk sebagian besar, tetapi saat ini saya mencoba melakukan beberapa animasi kanvas dan saya bertanya-tanya: Apakah ada cara untuk memastikannya berjalan pada fps tertentu? Saya memahami bahwa tujuan rAF adalah untuk animasi yang mulus secara konsisten, dan saya mungkin berisiko membuat animasi saya berombak, tetapi saat ini tampaknya berjalan pada kecepatan yang sangat berbeda secara sewenang-wenang, dan saya bertanya-tanya apakah ada cara untuk melawannya. itu entah bagaimana.

Saya akan menggunakan setIntervaltetapi saya ingin pengoptimalan yang ditawarkan rAF (terutama secara otomatis berhenti ketika tab dalam fokus).

Jika seseorang ingin melihat kode saya, itu cukup banyak:

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

Dimana Node.drawFlash () hanyalah beberapa kode yang menentukan radius berdasarkan variabel counter dan kemudian menggambar sebuah lingkaran.

robert.vinluan
sumber
1
Apakah animasi Anda tertinggal? Saya pikir keuntungan terbesar dari requestAnimationFrameadalah (seperti namanya) untuk meminta bingkai animasi hanya jika diperlukan. Katakanlah Anda menunjukkan kanvas hitam statis, Anda harus mendapatkan 0 fps karena tidak diperlukan bingkai baru. Tetapi jika Anda menampilkan animasi yang membutuhkan 60fps, Anda juga harus mendapatkannya. rAFhanya memungkinkan untuk "melewati" frame yang tidak berguna dan kemudian menyimpan CPU.
maxdec
setInterval juga tidak berfungsi di tab tidak aktif.
ViliusL
Kode ini berjalan berbeda pada tampilan 90hz vs tampilan 60hz vs tampilan 144hz.
manthrax

Jawaban:

201

Cara membatasi requestAnimationFrame ke frekuensi gambar tertentu

Pelambatan demo pada 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/

Metode ini bekerja dengan menguji waktu yang telah berlalu sejak menjalankan perulangan bingkai terakhir.

Kode gambar Anda dijalankan hanya ketika interval FPS yang Anda tentukan telah berlalu.

Bagian pertama dari kode menetapkan beberapa variabel yang digunakan untuk menghitung waktu yang telah berlalu.

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

Dan kode ini adalah loop requestAnimationFrame aktual yang diambil pada FPS yang Anda tentukan.

// 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
sumber
17
Demo yang bagus - itu harus diterima. Di sini, percabangkan biola Anda, untuk mendemonstrasikan menggunakan window.performance.now () alih-alih Date.now (). Ini berjalan baik dengan stempel waktu resolusi tinggi yang sudah diterima rAF, jadi tidak perlu memanggil Date.now () di dalam panggilan balik: jsfiddle.net/chicagogrooves/nRpVD/2
Dean Radcliffe
2
Terima kasih atas tautan yang diperbarui menggunakan fitur cap waktu rAF baru. Stempel waktu rAF baru menambahkan infrastruktur yang berguna dan juga lebih tepat daripada Date.now.
tandaiE
13
Ini adalah demo yang sangat bagus, yang menginspirasi saya untuk membuat ( JSFiddle ) saya sendiri . Perbedaan utamanya adalah menggunakan rAF (seperti demo Dean), bukan Tanggal, menambahkan kontrol untuk menyesuaikan frekuensi gambar target secara dinamis, mengambil sampel frekuensi gambar pada interval terpisah dari animasi, dan menambahkan grafik frekuensi gambar historis.
tavnab
Ini adalah solusi yang cerdik - satu-satunya masalah adalah hal itu menciptakan overhead tambahan di RAF, dan dapat merusak frame-rate yang sebenarnya karena semua manipulasi data yang terjadi di dalam RAF. Untuk menghindari hal ini, pertahankan manipulasi data dalam setInterval terpisah, jika memungkinkan di web-worker sehingga ia memiliki utasnya sendiri. Idealnya RAF hanya memperbarui grafik, dan membaca objek js yang berisi data terkini. Manipulasi data harus dilakukan di luar RAF dan data baru ditempatkan di objek untuk dibaca oleh panggilan balik RAF Anda.
jdmayfield
1
Yang dapat Anda kendalikan adalah kapan Anda akan melewati satu frame. Monitor 60 fps selalu menggambar dengan interval 16ms. Misalnya jika Anda ingin game Anda berjalan pada 50fps, Anda ingin melewati setiap frame ke-6. Anda memeriksa apakah 20 md (1000/50) telah berlalu, dan belum (hanya 16 md yang telah berlalu) jadi Anda melewatkan satu bingkai, lalu 32 md bingkai berikutnya telah berlalu sejak Anda menggambar, jadi Anda menggambar dan menyetel ulang. Tapi kemudian Anda akan melewatkan setengah frame dan berjalan pada 30fps. Jadi ketika Anda mengatur ulang Anda ingat Anda menunggu terlalu lama 12ms terakhir kali. Jadi frame berikutnya 16ms berlalu tetapi Anda menghitungnya sebagai 16 + 12 = 28ms sehingga Anda menggambar lagi dan Anda menunggu terlalu lama 8ms
Curtis
50

Perbarui 2016/6

Masalah membatasi kecepatan bingkai adalah layar memiliki kecepatan pembaruan konstan, biasanya 60 FPS.

Jika kita menginginkan 24 FPS, kita tidak akan pernah mendapatkan 24 fps yang sebenarnya di layar, kita dapat mengatur waktunya seperti itu tetapi tidak menampilkannya karena monitor hanya dapat menampilkan bingkai yang disinkronkan pada 15 fps, 30 fps atau 60 fps (beberapa monitor juga 120 fps ).

Namun, untuk tujuan pengaturan waktu, kami dapat menghitung dan memperbarui jika memungkinkan.

Anda bisa membangun semua logika untuk mengontrol frekuensi gambar dengan merangkum penghitungan dan callback ke dalam objek:

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

Kemudian tambahkan beberapa pengontrol dan kode konfigurasi:

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

Pemakaian

Ini menjadi sangat sederhana - sekarang, yang harus kita lakukan adalah membuat instance dengan mengatur fungsi panggilan balik dan frekuensi gambar yang diinginkan seperti ini:

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

Kemudian mulailah (yang bisa menjadi perilaku default jika diinginkan):

fc.start();

Itu saja, semua logika ditangani secara internal.

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>

Jawaban lama

Tujuan utamanya requestAnimationFrameadalah untuk menyinkronkan pembaruan ke kecepatan refresh monitor. Ini akan meminta Anda untuk menganimasikan pada FPS monitor atau faktornya (mis. 60, 30, 15 FPS untuk kecepatan refresh khas @ 60 Hz).

Jika Anda menginginkan FPS yang lebih sewenang-wenang maka tidak ada gunanya menggunakan rAF karena laju bingkai tidak akan pernah cocok dengan frekuensi pembaruan monitor (hanya bingkai di sana-sini) yang tidak dapat memberi Anda animasi yang mulus (seperti dengan semua pengaturan waktu bingkai ) dan Anda juga dapat menggunakan setTimeoutatau setIntervalsebagai gantinya.

Ini juga merupakan masalah umum dalam industri video profesional ketika Anda ingin memutar video pada FPS yang berbeda kemudian perangkat menampilkannya menyegarkan. Banyak teknik telah digunakan seperti pencampuran bingkai dan pengaturan waktu ulang yang kompleks membangun kembali bingkai perantara berdasarkan vektor gerak, tetapi dengan kanvas teknik ini tidak tersedia dan hasilnya akan selalu berupa video tersentak-sentak.

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

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

    ... code for frame here
}

Alasan mengapa kita menempatkan setTimeout pertama (dan mengapa beberapa tempat rAFpertama ketika poly-fill digunakan) adalah karena ini akan lebih akurat karena setTimeoutakan mengantri suatu acara segera ketika loop dimulai sehingga tidak peduli berapa banyak waktu yang tersisa kode akan digunakan (asalkan tidak melebihi interval batas waktu) panggilan berikutnya akan berada pada interval yang diwakilinya (untuk rAF murni ini tidak penting karena rAF akan mencoba untuk melompat ke frame berikutnya dalam hal apapun).

Juga perlu dicatat bahwa menempatkannya terlebih dahulu juga akan berisiko panggilan bertumpuk seperti halnya setInterval. setIntervalmungkin sedikit lebih akurat untuk penggunaan ini.

Dan Anda dapat menggunakan setIntervalbukan di luar loop untuk melakukan hal yang sama.

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

function loop() {

    ... code for frame here
}

Dan untuk menghentikan loop:

clearInterval(rememberMe);

Untuk mengurangi frekuensi gambar saat tab menjadi buram, Anda dapat menambahkan faktor seperti ini:

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
}

Dengan cara ini Anda dapat mengurangi FPS menjadi 1/4 dll.


sumber
4
Dalam beberapa kasus Anda tidak mencoba untuk mencocokkan monitor frame rate melainkan, dalam urutan gambar misalnya, bingkai jatuh. Penjelasan yang sangat baik btw
sidonaldson
3
Salah satu alasan terbesar untuk membatasi dengan requestAnimationFrame adalah untuk mengatur eksekusi beberapa kode dengan bingkai animasi browser. Semuanya akhirnya berjalan jauh lebih lancar, terutama jika Anda menjalankan logika pada data setiap frame, seperti dengan visualisator musik misalnya.
Chris Dolphin
4
Ini buruk karena penggunaan utama dari requestAnimationFrameadalah untuk menyinkronkan operasi DOM (baca / tulis) sehingga tidak menggunakannya akan mengganggu kinerja saat mengakses DOM, karena operasi tidak akan diantrekan untuk dilakukan bersama dan akan memaksa pengecatan ulang tata letak yang tidak perlu.
vsync
1
Tidak ada risiko "panggilan menumpuk", karena JavaScript menjalankan single threaded, dan tidak ada peristiwa waktu tunggu yang dipicu saat kode Anda berjalan. Jadi, jika fungsi membutuhkan waktu lebih lama daripada waktu tunggu, itu hanya berjalan hampir kapan saja secepat mungkin, sementara browser masih akan menggambar ulang dan memicu waktu tunggu lain di antara panggilan.
dronus
Saya tahu bahwa Anda menyatakan bahwa penyegaran halaman tidak dapat diperbarui lebih cepat dari batas fps pada tampilan. Namun, apakah mungkin untuk menyegarkan lebih cepat dengan memicu alur ulang halaman? Sebaliknya, apakah mungkin untuk tidak memperhatikan beberapa perubahan halaman jika dilakukan lebih cepat daripada tingkat fps asli?
Travis J
42

Saya sarankan untuk mengakhiri panggilan Anda ke requestAnimationFramedalam setTimeout:

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

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

Anda perlu memanggil requestAnimationFramedari dalam setTimeout, bukan sebaliknya, karena requestAnimationFramemenjadwalkan fungsi Anda untuk berjalan tepat sebelum pengecatan ulang berikutnya, dan jika Anda menunda pembaruan lebih lanjut menggunakan setTimeoutAnda akan melewatkan jendela waktu itu. Namun, melakukan sebaliknya adalah hal yang wajar, karena Anda hanya menunggu beberapa saat sebelum membuat permintaan.

Luke Taylor
sumber
1
Ini sebenarnya tampaknya berfungsi untuk menjaga framerate turun dan jadi tidak memasak CPU saya. Dan itu sangat sederhana. Bersulang!
Phocks
Ini adalah cara yang bagus dan sederhana untuk melakukannya untuk animasi yang ringan. Itu memang sedikit tidak sinkron, setidaknya pada beberapa perangkat. Saya menggunakan teknik ini di salah satu mesin lama saya. Ini bekerja dengan baik sampai semuanya menjadi rumit. Masalah terbesar adalah ketika dihubungkan ke sensor orientasi, itu akan tertinggal atau menjadi gelisah. Kemudian saya menemukan menggunakan setInterval terpisah dan pembaruan komunikasi antara sensor, frame setInterval, dan frame RAF melalui properti objek memungkinkan sensor dan RAF untuk berjalan secara real-time, sementara waktu animasi dapat dikontrol melalui pembaruan properti dari setInterval.
jdmayfield
Jawaban Terbaik ! Terima kasih;)
538ROMEO
1
Monitor saya adalah 60 FPS, jika saya menetapkan var fps = 60, saya hanya mendapatkan sekitar 50 FPS menggunakan kode ini. Saya ingin memperlambatnya menjadi 60 karena beberapa orang memiliki 120 monitor FPS, tetapi saya tidak ingin memengaruhi orang lain. Ini sangat sulit.
Curtis
Alasan mengapa Anda mendapatkan FPS yang lebih rendah dari yang diharapkan adalah karena setTimeout dapat mengeksekusi callback setelah lebih dari penundaan yang ditentukan. Ada beberapa kemungkinan alasan untuk ini. Dan setiap loop membutuhkan waktu untuk menyetel timer baru dan menjalankan beberapa kode sebelum menyetel batas waktu baru. Anda tidak memiliki cara untuk akurat dengan ini, Anda harus selalu mempertimbangkan hasil yang lebih lambat dari yang diharapkan, tetapi selama Anda tidak tahu seberapa lambat hasilnya, mencoba menurunkan penundaan juga tidak akan akurat. JS di browser tidak dimaksudkan untuk begitu akurat.
pdepmcp
19

Ini semua adalah ide bagus secara teori, sampai Anda mendalami. Masalahnya adalah Anda tidak dapat membatasi RAF tanpa melakukan de-sinkronisasi, mengalahkan tujuannya untuk yang sudah ada. Jadi Anda membiarkannya berjalan dengan kecepatan penuh, dan memperbarui data Anda dalam loop terpisah , atau bahkan utas terpisah!

Ya, saya mengatakannya. Anda dapat melakukan JavaScript multi-utas di browser!

Ada dua metode yang saya tahu bekerja sangat baik tanpa jank, menggunakan lebih sedikit jus dan menghasilkan lebih sedikit panas. Penentuan waktu skala manusia yang akurat dan efisiensi mesin adalah hasil bersihnya.

Maaf jika ini sedikit bertele-tele, tapi ini dia ...


Metode 1: Perbarui data melalui setInterval, dan grafik melalui RAF.

Gunakan setInterval terpisah untuk memperbarui nilai terjemahan dan rotasi, fisika, tabrakan, dll. Simpan nilai tersebut dalam sebuah objek untuk setiap elemen animasi. Tetapkan string transformasi ke variabel di objek setiap 'frame' setInterval. Simpan objek-objek ini dalam satu larik. Atur interval Anda ke fps yang Anda inginkan dalam ms: ms = (1000 / fps). Ini menjaga jam tetap stabil yang memungkinkan fps yang sama pada perangkat apa pun, terlepas dari kecepatan RAF. Jangan tetapkan transformasi ke elemen di sini!

Dalam loop requestAnimationFrame, lakukan iterasi melalui array Anda dengan loop for-sekolah lama-- jangan gunakan formulir yang lebih baru di sini, mereka lambat!

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

Dalam fungsi rafUpdate Anda, dapatkan string transformasi dari objek js Anda dalam array, dan elemennya id. Anda harus sudah memiliki elemen 'sprite' yang dilampirkan ke variabel atau mudah diakses melalui cara lain sehingga Anda tidak kehilangan waktu untuk 'mendapatkannya' di RAF. Menyimpannya dalam objek yang dinamai menurut id html mereka bekerja dengan cukup baik. Atur bagian itu bahkan sebelum masuk ke SI atau RAF Anda.

Gunakan RAF untuk memperbarui transformasi Anda saja , gunakan hanya transformasi 3D (bahkan untuk 2d), dan setel css "will-change: transform;" pada elemen yang akan berubah. Ini membuat transformasi Anda tetap tersinkronisasi ke kecepatan refresh asli sebanyak mungkin, bekerja di GPU, dan memberi tahu browser tempat untuk paling berkonsentrasi.

Jadi Anda harus memiliki sesuatu seperti ini 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

Ini membuat pembaruan Anda ke objek data dan mengubah string disinkronkan ke tingkat 'bingkai' yang diinginkan di SI, dan tugas transformasi aktual di RAF disinkronkan ke kecepatan penyegaran GPU. Jadi pembaruan grafik sebenarnya hanya di RAF, tetapi perubahan pada data, dan membangun string transformasi ada di SI, sehingga tidak ada jankies tetapi 'waktu' mengalir pada kecepatan bingkai yang diinginkan.


Mengalir:

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

Metode 2. Letakkan SI di web-worker. Yang ini FAAAST dan mulus!

Sama seperti metode 1, tetapi letakkan SI di pekerja web. Ini akan berjalan pada utas yang benar-benar terpisah, meninggalkan halaman untuk hanya berurusan dengan RAF dan UI. Meneruskan array sprite bolak-balik sebagai 'objek yang dapat ditransfer'. Ini buko cepat. Tidak perlu waktu untuk mengkloning atau membuat serial, tetapi tidak seperti melewatkan referensi di mana referensi dari sisi lain dihancurkan, jadi Anda harus meminta kedua sisi lolos ke sisi lain, dan hanya memperbaruinya saat ada, sortir seperti menyampaikan pesan bolak-balik dengan pacar Anda di sekolah menengah.

Hanya satu yang dapat membaca dan menulis dalam satu waktu. Ini baik-baik saja selama mereka memeriksa apakah itu tidak ditentukan untuk menghindari kesalahan. RAF CEPAT dan akan segera menendangnya, lalu melewati sekumpulan bingkai GPU hanya untuk memeriksa apakah sudah dikirim kembali. SI di web-worker sebagian besar akan memiliki array sprite, dan akan memperbarui data posisi, pergerakan dan fisika, serta membuat string transformasi baru, lalu meneruskannya kembali ke RAF di halaman.

Ini adalah cara tercepat yang saya tahu untuk menganimasikan elemen melalui skrip. Kedua fungsi ini akan berjalan sebagai dua program terpisah, pada dua utas terpisah, memanfaatkan CPU multi-core dengan cara yang tidak dimiliki skrip js tunggal. Animasi javascript multi-utas.

Dan itu akan melakukannya dengan lancar tanpa jank, tetapi pada kecepatan bingkai yang ditentukan sebenarnya, dengan sedikit perbedaan.


Hasil:

Salah satu dari kedua metode ini akan memastikan skrip Anda akan berjalan dengan kecepatan yang sama di PC, ponsel, tablet, dll. (Dalam kemampuan perangkat dan browser, tentu saja).

jdmayfield.dll
sumber
Sebagai catatan tambahan-- dalam Metode 1, jika ada terlalu banyak aktivitas di setInterval Anda, ini dapat memperlambat RAF Anda karena asinkron berulir tunggal. Anda dapat mengurangi pemecahan aktivitas tersebut lebih dari pada bingkai SI, sehingga asinkron akan meneruskan kontrol kembali ke RAF lebih cepat. Ingat, RAF berjalan pada kecepatan bingkai maksimum, tetapi menyinkronkan perubahan grafis dengan tampilan, jadi boleh saja untuk melewatkan beberapa bingkai RAF - selama Anda tidak melewatkan lebih dari bingkai SI, itu tidak akan jank.
jdmayfield
Metode 2 lebih kuat, karena sebenarnya melakukan multi-tasking dua loop, tidak beralih bolak-balik melalui async, tetapi Anda masih ingin menghindari frame SI Anda memakan waktu lebih lama dari frame-rate yang Anda inginkan, jadi aktivitas SI pemisahan mungkin masih diinginkan jika ada banyak manipulasi data yang terjadi yang akan membutuhkan lebih dari satu frame SI untuk diselesaikan.
jdmayfield
Saya pikir perlu disebutkan, sebagai catatan yang menarik, bahwa menjalankan loop berpasangan seperti ini sebenarnya terdaftar di Chromes DevTools bahwa GPU berjalan pada kecepatan frame yang ditentukan dalam loop setInterval! Tampaknya hanya frame RAF di mana terjadi perubahan grafis yang dihitung sebagai frame oleh meter FPS. Jadi bingkai RAF yang hanya berfungsi non-grafis, atau bahkan hanya loop kosong, tidak dihitung sejauh menyangkut GPU. Saya menemukan ini menarik sebagai titik awal untuk penelitian lebih lanjut.
jdmayfield
Saya yakin solusi ini memiliki masalah yang terus berjalan saat rAF ditangguhkan, misalnya karena pengguna beralih ke tab lain.
N4ppeL
1
PS Saya membaca beberapa dan tampaknya sebagian besar browser membatasi kejadian waktunya menjadi sekali per detik di tab latar belakang (yang mungkin juga harus ditangani dengan cara tertentu). Jika Anda masih ingin mengatasi masalah dan benar-benar berhenti sejenak saat tidak terlihat, sepertinya ada visibilitychangeacaranya.
N4ppeL
5

Cara dengan mudah mencekik ke FPS tertentu:

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

Sumber: Penjelasan Mendetail tentang JavaScript Game Loops and Timing oleh Isaac Sukin

Rustem Kakimov
sumber
2
Jika monitor saya berjalan pada 60 FPS dan saya ingin game saya berjalan pada 58 FPS, saya menetapkan maxFPS = 58, ini akan membuatnya berjalan pada 30 FPS karena akan melewati setiap frame ke-2.
Curtis
1
Ya, saya mencoba yang ini juga. Saya memilih untuk tidak mencekik RAF itu sendiri - hanya perubahan yang diperbarui oleh setTimeout. Setidaknya di Chrome, ini menyebabkan fps efektif berjalan pada kecepatan setTimeout, menurut bacaan di DevTools. Tentu saja itu hanya dapat memperbarui bingkai video nyata dengan kecepatan kartu video dan kecepatan penyegaran monitor, tetapi metode ini tampaknya beroperasi dengan paling sedikit jankies, sehingga kontrol fps "nyata" yang paling halus, itulah yang akan saya lakukan.
jdmayfield
1
Karena saya melacak semua gerakan dalam objek JS secara terpisah dari RAF, ini membuat logika animasi, deteksi tabrakan, atau apa pun yang Anda butuhkan, berjalan pada tingkat yang konsisten secara perseptual, terlepas dari RAF atau setTimeout, dengan sedikit matematika tambahan.
jdmayfield
1
Jawaban ini adalah permata di seluruh samudra dan harus menjadi jawaban yang diterima.
Ivanzinho
2

Melewati requestAnimationFrame menyebabkan animasi tidak mulus (diinginkan) pada fps khusus.

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

Kode asli oleh @tavnab.

befzz
sumber
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
sumber
Tambahkan beberapa kalimat untuk menjelaskan apa yang dilakukan kode Anda, sehingga Anda bisa mendapatkan lebih banyak suara positif untuk jawaban Anda.
Analisis Fuzzy
1

Saya selalu melakukannya dengan cara yang sangat sederhana ini tanpa mengotak-atik cap waktu:

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
sumber
1
Ini akan berjalan terlalu cepat jika monitor Anda 120 fps.
Curtis
1

Solusi sederhana untuk masalah ini adalah kembali dari render loop jika frame tidak diperlukan untuk merender:

const FPS = 60;
let prevTick = 0;    

function render() 
{
    requestAnimationFrame(render);

    // clamp to fixed framerate
    let now = Math.round(FPS * Date.now() / 1000);
    if (now == prevTick) return;
    prevTick = now;

    // otherwise, do your stuff ...
}

Penting untuk diketahui bahwa requestAnimationFrame bergantung pada kecepatan refresh monitor pengguna (vsync). Jadi, mengandalkan requestAnimationFrame untuk kecepatan game misalnya akan membuatnya tidak dapat dimainkan di monitor 200Hz jika Anda tidak menggunakan mekanisme pengatur waktu terpisah dalam simulasi Anda.

movAX13h
sumber
0

Berikut penjelasan bagus yang saya temukan: CreativeJS.com , untuk membungkus panggilan setTimeou) di dalam fungsi yang diteruskan ke requestAnimationFrame. Perhatian saya dengan requestionAnimationFrame "biasa" adalah, "bagaimana jika saya hanya ingin animasi tiga kali per detik?" Bahkan dengan requestAnimationFrame (kebalikan dari setTimeout) adalah bahwa ia masih membuang (beberapa) jumlah "energi" (artinya bahwa kode Browser melakukan sesuatu, dan mungkin memperlambat sistem) 60 atau 120 atau berapa kali dalam satu detik, karena menentang hanya dua atau tiga kali per detik (seperti yang Anda inginkan).

Sebagian besar waktu saya menjalankan browser saya dengan JavaScript mati secara sengaja hanya karena alasan ini. Tapi, saya menggunakan Yosemite 10.10.3, dan saya pikir ada masalah pengatur waktu - setidaknya di sistem lama saya (relatif lama - artinya 2011).

Jim Witte
sumber