Kiểm soát khung hình / giây với requestAnimationFrame?


140

Có vẻ như đây requestAnimationFramelà cách thực tế để làm sinh động mọi thứ bây giờ. Phần lớn nó hoạt động khá tốt đối với tôi, nhưng hiện tại tôi đang cố gắng thực hiện một số hoạt hình canvas và tôi đã tự hỏi: Có cách nào để đảm bảo nó chạy ở một khung hình / giây nhất định không? Tôi hiểu rằng mục đích của rAF là để tạo hiệu ứng hoạt hình mượt mà và tôi có thể gặp rủi ro khiến hoạt hình của mình bị biến dạng, nhưng ngay bây giờ nó dường như chạy ở tốc độ khác nhau khá tùy tiện, và tôi tự hỏi liệu có cách nào để chiến đấu không rằng bằng cách nào đó.

Tôi sẽ sử dụng setIntervalnhưng tôi muốn tối ưu hóa mà rAF cung cấp (đặc biệt là tự động dừng khi tab nằm trong tiêu điểm).

Trong trường hợp ai đó muốn xem mã của tôi, nó khá nhiều:

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

Trong đó Node.drawFlash () chỉ là một số mã xác định bán kính dựa trên một biến đếm và sau đó vẽ một vòng tròn.


1
Hoạt hình của bạn có bị lag không? Tôi nghĩ rằng lợi thế lớn nhất của requestAnimationFrame(là loại tên gợi ý) để yêu cầu một khung hình động chỉ khi cần thiết. Giả sử bạn hiển thị một khung màu đen tĩnh, bạn sẽ nhận được 0 khung hình / giây vì không cần khung mới. Nhưng nếu bạn đang hiển thị một hình ảnh động yêu cầu 60fps, bạn cũng sẽ nhận được điều đó. rAFchỉ cho phép "bỏ qua" các khung vô dụng và sau đó lưu CPU.
maxdec

setInterval không hoạt động trong tab không hoạt động.
ViliusL

Mã này chạy khác nhau trên màn hình 90hz so với màn hình 60hz so với màn hình 144hz.
manthrax

Câu trả lời:


190

Cách điều chỉnh requestAnimationFrame theo tốc độ khung hình cụ thể

Thử nghiệm điều chỉnh ở 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/

Phương pháp này hoạt động bằng cách kiểm tra thời gian đã trôi qua kể từ khi thực hiện vòng lặp khung cuối cùng.

Mã bản vẽ của bạn chỉ thực thi khi hết khoảng thời gian FPS đã chỉ định.

Phần đầu tiên của mã đặt một số biến được sử dụng để tính thời gian trôi qua.

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

Và mã này là vòng lặp requestAnimationFrame thực tế rút ra ở FPS được chỉ định của bạn.

// 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
Giải thích tuyệt vời và ví dụ. Điều này nên được đánh dấu là câu trả lời được chấp nhận
muxcmux

13
Bản demo đẹp - nó nên được chấp nhận. Ở đây, rẽ nhánh fiddle của bạn, để chứng minh bằng cách sử dụng window.performance.now () thay vì Date.now (). Điều này diễn ra tốt đẹp với dấu thời gian độ phân giải cao mà rAF đã nhận được, do đó không cần phải gọi Date.now () trong cuộc gọi lại: jsfiddle.net/chicagogroove/nRpVD/2
Dean Radcliffe

2
Cảm ơn liên kết được cập nhật bằng tính năng dấu thời gian rAF mới. Dấu thời gian của rAF mới bổ sung cơ sở hạ tầng hữu ích và nó cũng chính xác hơn Date.now.
đánh dấu

13
Đây là một bản demo thực sự hay, đã truyền cảm hứng cho tôi để tạo ra cái riêng của mình ( JSFiddle ). Sự khác biệt chính là sử dụng rAF (như bản demo của Dean) thay vì Date, thêm điều khiển để tự động điều chỉnh tốc độ khung hình đích, lấy mẫu tốc độ khung hình trên một khoảng riêng biệt từ hình ảnh động và thêm biểu đồ tốc độ khung hình lịch sử.
tavnab

1
Tất cả những gì bạn có thể kiểm soát là khi bạn sẽ bỏ qua một khung. Một màn hình 60 khung hình / giây luôn vẽ ở các khoảng thời gian 16ms. Ví dụ: nếu bạn muốn trò chơi của mình chạy ở tốc độ 50fps, bạn muốn bỏ qua mọi khung hình thứ 6. Bạn kiểm tra xem 20ms (1000/50) đã trôi qua chưa và chưa (chỉ 16ms đã trôi qua) nên bạn bỏ qua một khung hình, sau đó khung hình 32ms tiếp theo đã trôi qua kể từ khi bạn vẽ, do đó bạn vẽ và đặt lại. Nhưng sau đó bạn sẽ bỏ qua một nửa khung hình và chạy ở tốc độ 30 khung hình / giây. Vì vậy, khi bạn thiết lập lại, bạn nhớ rằng bạn đã đợi 12ms quá lâu lần trước. Vì vậy, khung hình tiếp theo 16ms nữa trôi qua nhưng bạn tính nó là 16 + 12 = 28ms để bạn vẽ lại và bạn đã đợi 8ms quá lâu
Curtis

47

Cập nhật 2016/6

Vấn đề làm giảm tốc độ khung hình là màn hình có tốc độ cập nhật không đổi, thường là 60 FPS.

Nếu chúng ta muốn 24 FPS, chúng ta sẽ không bao giờ có được 24 khung hình / giây thực sự trên màn hình, chúng ta có thể hẹn giờ như vậy nhưng không hiển thị vì màn hình chỉ có thể hiển thị các khung được đồng bộ hóa ở 15 khung hình / giây, 30 khung hình / giây (một số màn hình cũng 120 khung hình / giây ).

Tuy nhiên, với mục đích thời gian, chúng tôi có thể tính toán và cập nhật khi có thể.

Bạn có thể xây dựng tất cả logic để kiểm soát tốc độ khung hình bằng cách gói gọn các tính toán và gọi lại vào một đối tượng:

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

Sau đó thêm một số bộ điều khiển và mã cấu hình:

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

Sử dụng

Nó trở nên rất đơn giản - bây giờ, tất cả những gì chúng ta phải làm là tạo một cá thể bằng cách đặt chức năng gọi lại và tốc độ khung hình mong muốn như thế này:

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

Sau đó bắt đầu (có thể là hành vi mặc định nếu muốn):

fc.start();

Đó là nó, tất cả logic được xử lý nội bộ.

Bản giới thiệu

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>

Câu trả lời cũ

Mục đích chính requestAnimationFramelà để đồng bộ hóa các bản cập nhật với tốc độ làm mới màn hình. Điều này sẽ yêu cầu bạn tạo hiệu ứng ở FPS của màn hình hoặc hệ số của nó (ví dụ: 60, 30, 15 FPS để có tốc độ làm mới thông thường @ 60 Hz).

Nếu bạn muốn có một FPS tùy ý hơn thì không có điểm nào sử dụng rAF vì tốc độ khung hình sẽ không bao giờ khớp với tần số cập nhật của màn hình (chỉ là một khung hình ở đây và ở đó) mà đơn giản là không thể cung cấp cho bạn một hình ảnh động mượt mà (như với tất cả các thời gian lại khung hình ) và bạn cũng có thể sử dụng setTimeouthoặc setIntervalthay vào đó.

Đây cũng là một vấn đề nổi tiếng trong ngành công nghiệp video chuyên nghiệp khi bạn muốn phát lại video ở một FPS khác sau đó thiết bị hiển thị nó làm mới tại. Nhiều kỹ thuật đã được sử dụng như trộn khung và sắp xếp lại các khung trung gian phức tạp dựa trên các vectơ chuyển động, nhưng với canvas, các kỹ thuật này không có sẵn và kết quả sẽ luôn là video bị giật.

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

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

    ... code for frame here
}

Lý do tại sao chúng tôi đặt setTimeout đầu tiên (và tại sao một số vị trí rAFđầu tiên khi sử dụng poly-fill) là vì điều này sẽ chính xác hơn vì setTimeoutsẽ xếp hàng một sự kiện ngay lập tức khi vòng lặp bắt đầu để cho dù mã còn lại sẽ sử dụng bao nhiêu thời gian (miễn là nó không vượt quá khoảng thời gian chờ) cuộc gọi tiếp theo sẽ ở khoảng thời gian mà nó thể hiện (đối với rAF thuần thì điều này không cần thiết vì rAF sẽ cố gắng nhảy vào khung tiếp theo trong mọi trường hợp).

Cũng đáng lưu ý rằng đặt nó đầu tiên cũng sẽ có nguy cơ các cuộc gọi xếp chồng lên như với setInterval. setIntervalcó thể chính xác hơn một chút cho việc sử dụng này.

Và bạn có thể sử dụng setIntervalthay vì bên ngoài vòng lặp để làm tương tự.

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

function loop() {

    ... code for frame here
}

Và để dừng vòng lặp:

clearInterval(rememberMe);

Để giảm tốc độ khung hình khi tab bị mờ, bạn có thể thêm một yếu tố như thế này:

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
}

Bằng cách này bạn có thể giảm FPS xuống còn 1/4, v.v.


4
Trong một số trường hợp, bạn không cố gắng khớp tốc độ khung hình của màn hình mà thay vào đó, trong chuỗi hình ảnh chẳng hạn, thả khung hình. Giải thích tuyệt vời btw
sidonaldson

3
Một trong những lý do lớn nhất để tăng tốc với requestAnimationFrame sẽ là sắp xếp thực thi một số mã với khung hoạt hình của trình duyệt. Mọi thứ cuối cùng sẽ chạy trơn tru hơn rất nhiều, đặc biệt là nếu bạn đang chạy một số logic trên dữ liệu mỗi khung hình, chẳng hạn như với trình hiển thị nhạc chẳng hạn.
Chris Dolphin

4
Điều này là xấu vì việc sử dụng chính requestAnimationFramelà để đồng bộ hóa các hoạt động DOM (đọc / ghi) vì vậy việc không sử dụng nó sẽ ảnh hưởng đến hiệu suất khi truy cập DOM, vì các hoạt động sẽ không được xếp hàng để được thực hiện cùng nhau và sẽ buộc bố trí lại không cần thiết.
vsync

1
Không có rủi ro "xếp chồng các cuộc gọi", vì JavaScript chạy một luồng và không có sự kiện hết thời gian nào được kích hoạt trong khi mã của bạn đang chạy. Vì vậy, nếu chức năng mất nhiều thời gian hơn thời gian chờ, nó chỉ chạy gần như mọi lúc có thể, trong khi trình duyệt vẫn thực hiện vẽ lại và kích hoạt thời gian chờ khác giữa các cuộc gọi.
dronus

Tôi biết rằng bạn nêu rõ việc làm mới trang không thể được cập nhật nhanh hơn giới hạn khung hình / giây trên màn hình. Tuy nhiên, có thể làm mới nhanh hơn bằng cách kích hoạt chỉnh lại dòng trang? Ngược lại, có thể không nhận thấy nhiều phản xạ trang nếu chúng được thực hiện nhanh hơn tốc độ khung hình / giây?
Travis J

36

Tôi đề nghị gói cuộc gọi của bạn requestAnimationFrametrong một setTimeout. Nếu bạn gọi setTimeouttừ bên trong chức năng mà bạn yêu cầu khung hình động, bạn sẽ đánh bại mục đích requestAnimationFrame. Nhưng nếu bạn gọi requestAnimationFrametừ bên trong setTimeoutnó hoạt động trơn tru:

var fps = 25
function animate() {
  setTimeout(function() {
    requestAnimationFrame(animate);
  }, 1000 / fps);
}

1
Điều này thực sự có vẻ hoạt động trong việc giữ tốc độ khung hình xuống và do đó không nấu CPU của tôi. Và nó thật đơn giản. Chúc mừng!
phocks

Đây là một cách tốt đẹp, đơn giản để làm điều đó cho hình ảnh động nhẹ. Mặc dù vậy, nó có một chút không đồng bộ, ít nhất là trên một số thiết bị. Tôi đã sử dụng kỹ thuật này trên một trong những động cơ cũ của tôi. Nó hoạt động tốt cho đến khi mọi thứ trở nên phức tạp. Vấn đề lớn nhất là khi được nối với các cảm biến định hướng, nó sẽ bị tụt lại phía sau hoặc bị giật. Sau đó, tôi đã tìm thấy bằng cách sử dụng setInterval riêng biệt và truyền thông cập nhật giữa các cảm biến, khung setInterval và khung RAF thông qua các thuộc tính đối tượng cho phép các cảm biến và RAF đi theo thời gian thực, trong khi thời gian hoạt hình có thể được kiểm soát thông qua cập nhật thuộc tính từ setInterval.
jdmayfield

Câu trả lời tốt nhất ! Cảm ơn;)
538ROMEO

Màn hình của tôi là 60 FPS, nếu tôi đặt var fps = 60, tôi chỉ nhận được khoảng 50 FPS khi sử dụng mã này. Tôi muốn làm chậm nó xuống 60 vì một số người có màn hình 120 FPS, nhưng tôi không muốn ảnh hưởng đến những người khác. Đây là khó khăn đáng ngạc nhiên.
Curtis

Lý do khiến bạn nhận được FPS thấp hơn mong đợi là vì setTimeout có thể thực hiện cuộc gọi lại sau nhiều hơn độ trễ được chỉ định. Có một số lý do có thể cho việc này. Và mỗi vòng lặp cần có thời gian để đặt bộ hẹn giờ mới và thực thi một số mã trước khi đặt thời gian chờ mới. Bạn không có cách nào để chính xác với điều này, bạn nên luôn luôn xem xét kết quả chậm hơn dự kiến, nhưng miễn là bạn không biết nó sẽ chậm hơn bao nhiêu, cố gắng giảm độ trễ cũng không chính xác. JS trong trình duyệt không có nghĩa là quá chính xác.
pdepmcp

17

Đây là tất cả những ý tưởng tốt trong lý thuyết, cho đến khi bạn đi sâu. Vấn đề là bạn không thể điều tiết RAF mà không đồng bộ hóa nó, đánh bại mục đích hiện tại của nó. Vì vậy, bạn hãy để nó chạy ở tốc độ đầy đủ và cập nhật dữ liệu của bạn trong một vòng lặp riêng biệt , hoặc thậm chí một thread riêng biệt!

Vâng, tôi đã nói nó. Bạn có thể thực hiện JavaScript đa luồng trong trình duyệt!

Có hai phương pháp tôi biết rằng hoạt động cực kỳ tốt mà không cần jank, sử dụng ít nước trái cây và tạo ra ít nhiệt hơn. Thời gian chính xác quy mô con người và hiệu quả máy là kết quả ròng.

Xin lỗi nếu điều này hơi dài dòng, nhưng ở đây đi ...


Phương pháp 1: Cập nhật dữ liệu qua setInterval và đồ họa qua RAF.

Sử dụng một setInterval riêng để cập nhật các giá trị dịch và xoay, vật lý, va chạm, v.v. Giữ các giá trị đó trong một đối tượng cho mỗi thành phần hoạt hình. Gán chuỗi biến đổi cho một biến trong đối tượng mỗi setInterval 'frame'. Giữ các đối tượng trong một mảng. Đặt khoảng thời gian của bạn thành khung hình / giây mong muốn của bạn trong ms: ms = (1000 / khung hình / giây). Điều này giữ cho đồng hồ ổn định cho phép cùng một khung hình / giây trên bất kỳ thiết bị nào, bất kể tốc độ RAF. Đừng chỉ định các biến đổi cho các yếu tố ở đây!

Trong vòng lặp requestAnimationFrame, lặp qua mảng của bạn với vòng lặp trường cũ - không sử dụng các biểu mẫu mới hơn ở đây, chúng rất chậm!

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

Trong hàm rafUpdate của bạn, lấy chuỗi biến đổi từ đối tượng js của bạn trong mảng và id phần tử của nó. Bạn đã có các yếu tố 'sprite' của mình được gắn vào một biến hoặc có thể truy cập dễ dàng thông qua các phương tiện khác để bạn không mất thời gian 'lấy chúng trong RAF. Giữ chúng trong một đối tượng được đặt tên theo id html của chúng hoạt động khá tốt. Đặt phần đó lên trước khi nó đi vào SI hoặc RAF của bạn.

Sử dụng RAF để chỉ cập nhật các biến đổi của bạn , chỉ sử dụng các biến đổi 3D (thậm chí cho 2d) và đặt css "will-change: Transform;" trên các yếu tố sẽ thay đổi. Điều này giữ cho các biến đổi của bạn được đồng bộ hóa với tốc độ làm mới riêng càng nhiều càng tốt, khởi động GPU và cho trình duyệt biết nơi tập trung nhất.

Vì vậy, bạn nên có một cái gì đó giống như mã giả này ...

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

Điều này giữ cho các cập nhật của bạn đến các đối tượng dữ liệu và các chuỗi biến đổi được đồng bộ hóa với tốc độ 'khung hình' mong muốn trong SI và các phép gán biến đổi thực tế trong RAF được đồng bộ hóa với tốc độ làm mới GPU. Vì vậy, các bản cập nhật đồ họa thực tế chỉ có trong RAF, nhưng những thay đổi về dữ liệu và xây dựng chuỗi biến đổi nằm trong SI, do đó không có jankies mà 'thời gian' chảy ở tốc độ khung hình mong muốn.


Lưu lượng:

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

Phương pháp 2. Đặt SI trong một nhân viên web. Đây là một FAAAST và mịn!

Tương tự như phương pháp 1, nhưng đặt SI trong web-worker. Sau đó, nó sẽ chạy trên một luồng hoàn toàn riêng biệt, để trang chỉ xử lý RAF và UI. Truyền mảng sprite qua lại dưới dạng 'đối tượng có thể chuyển'. Đây là buko nhanh. Không mất thời gian để sao chép hoặc tuần tự hóa, nhưng nó không giống như chuyển qua tham chiếu ở chỗ tham chiếu từ phía bên kia bị hủy, vì vậy bạn sẽ cần phải chuyển cả hai bên sang phía bên kia và chỉ cập nhật chúng khi có mặt, sắp xếp giống như chuyển một ghi chú qua lại với bạn gái của bạn ở trường trung học.

Chỉ một người có thể đọc và viết tại một thời điểm. Điều này là tốt miễn là họ kiểm tra nếu nó không được xác định để tránh lỗi. RAF là NHANH CHÓNG và sẽ khởi động lại ngay lập tức, sau đó đi qua một loạt các khung GPU chỉ để kiểm tra xem nó đã được gửi lại chưa. SI trong trình xử lý web sẽ có mảng sprite hầu hết thời gian và sẽ cập nhật dữ liệu vị trí, chuyển động và vật lý, cũng như tạo chuỗi biến đổi mới, sau đó chuyển nó trở lại RAF trong trang.

Đây là cách nhanh nhất mà tôi biết để làm động các yếu tố thông qua kịch bản. Hai chức năng sẽ chạy như hai chương trình riêng biệt, trên hai luồng riêng biệt, tận dụng CPU đa lõi theo cách mà một tập lệnh js duy nhất không có. Hoạt hình javascript đa luồng.

Và nó sẽ làm rất trơn tru mà không cần jank, nhưng ở tốc độ khung hình được chỉ định thực tế, với rất ít sự phân kỳ.


Kết quả:

Một trong hai phương pháp này sẽ đảm bảo tập lệnh của bạn sẽ chạy ở cùng tốc độ trên mọi PC, điện thoại, máy tính bảng, v.v. (tất nhiên là trong khả năng của thiết bị và trình duyệt).


Như một lưu ý phụ-- trong Phương pháp 1, nếu có quá nhiều hoạt động trong setInterval của bạn, nó có thể làm chậm RAF của bạn do không đồng bộ một luồng. Bạn có thể giảm thiểu việc chia nhỏ hoạt động này hơn trên khung SI, vì vậy async sẽ chuyển điều khiển trở lại RAF nhanh hơn. Hãy nhớ rằng, RAF đạt tốc độ khung hình tối đa, nhưng đồng bộ hóa các thay đổi đồ họa với màn hình, vì vậy bạn có thể bỏ qua một vài khung RAF - miễn là bạn không bỏ qua nhiều khung SI hơn.
jdmayfield

Phương pháp 2 mạnh mẽ hơn, vì nó thực sự đa tác vụ cho hai vòng lặp, không chuyển đổi qua lại không đồng bộ, nhưng bạn vẫn muốn tránh khung SI của mình mất nhiều thời gian hơn tốc độ khung hình mong muốn của bạn, vì vậy việc chia tách hoạt động SI vẫn có thể mong muốn nếu nó có nhiều thao tác dữ liệu đang diễn ra sẽ mất nhiều hơn một khung SI để hoàn thành.
jdmayfield

Tôi nghĩ rằng đáng để đề cập, như một lưu ý quan tâm, rằng việc chạy các vòng lặp được ghép nối như thế này thực sự đăng ký trong Chromes DevTools rằng GPU đang chạy ở tốc độ khung hình được chỉ định trong vòng lặp setInterval! Nó chỉ xuất hiện các khung RAF trong đó các thay đổi đồ họa xảy ra được tính là các khung theo máy đo FPS. Vì vậy, các khung RAF trong đó chỉ hoạt động phi đồ họa, hoặc thậm chí chỉ là các vòng trống, không được tính đến mức có liên quan đến GPU. Tôi thấy điều này thú vị như một điểm khởi đầu để nghiên cứu thêm.
jdmayfield

Tôi tin rằng giải pháp này có vấn đề là nó tiếp tục chạy khi rAF bị treo, ví dụ như vì người dùng đã chuyển sang một tab khác.
N4ppeL

1
PS Tôi đã đọc một số thứ và dường như hầu hết các trình duyệt đều giới hạn các sự kiện được tính thời gian một lần mỗi giây trong các tab nền (điều này có lẽ cũng nên được xử lý theo một cách nào đó). Nếu bạn vẫn muốn giải quyết vấn đề và hoàn toàn tạm dừng khi không nhìn thấy, dường như có visibilitychangesự kiện.
N4ppeL

3

Cách dễ dàng điều tiết đến một FPS cụ thể:

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

Nguồn: Giải thích chi tiết về các vòng lặp và thời gian trò chơi JavaScript của Isaac Sukin


1
Nếu màn hình của tôi chạy ở tốc độ 60 FPS và tôi muốn trò chơi của mình chạy ở mức 58 FPS, tôi đặt maxFPS = 58, điều này sẽ khiến nó chạy ở tốc độ 30 FPS vì nó sẽ bỏ qua mọi khung hình thứ 2.
Curtis

Vâng, tôi đã thử cái này là tốt. Tôi chọn không thực sự điều tiết RAF - chỉ những thay đổi được cập nhật bởi setTimeout. Trong Chrome ít nhất, điều này khiến các khung hình / giây hiệu quả chạy ở tốc độ setTimeouts, theo các bài đọc trong DevTools. Tất nhiên, nó chỉ có thể cập nhật các khung hình video thực ở tốc độ của card màn hình và tốc độ làm mới màn hình, nhưng phương pháp này dường như hoạt động với ít jankies nhất, điều khiển fps "rõ ràng" nhất, đó là điều tôi sẽ làm.
jdmayfield

Vì tôi theo dõi tất cả các chuyển động trong các đối tượng JS tách biệt với RAF, điều này giữ cho logic hoạt hình, phát hiện va chạm hoặc bất cứ điều gì bạn cần, chạy ở tốc độ phù hợp về mặt nhận thức, bất kể RAF hay setTimeout, có thêm một chút toán học.
jdmayfield

2

Bỏ qua requestAnimationFrame gây ra hiệu ứng hoạt hình không mong muốn (mong muốn) tại khung hình / giây tùy chỉnh.

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

Mã gốc của @tavnab.


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

Vui lòng thêm một vài câu để giải thích mã của bạn đang làm gì, để bạn có thể nhận được nhiều thông báo hơn cho câu trả lời của mình.
Phân tích mờ

1

Tôi luôn luôn làm theo cách rất đơn giản này mà không bị rối với dấu thời gian:

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
Điều này sẽ chạy quá nhanh nếu màn hình của bạn là 120 khung hình / giây.
Curtis

0

Đây là một lời giải thích tốt mà tôi đã tìm thấy: CreativeJS.com , để bọc một cuộc gọi setTimeou) bên trong hàm được truyền cho requestAnimationFrame. Mối quan tâm của tôi với requestionAnimationFrame "đơn giản" sẽ là "nếu tôi chỉ muốn nó hoạt hình ba lần một giây thì sao?" Ngay cả với requestAnimationFrame (trái ngược với setTimeout), nó vẫn lãng phí (một phần) lượng "năng lượng" (có nghĩa là mã Trình duyệt đang làm gì đó và có thể làm chậm hệ thống) 60 hoặc 120 hoặc nhiều lần trong một giây, như trái ngược với chỉ hai hoặc ba lần một giây (như bạn có thể muốn).

Hầu hết thời gian tôi chạy trình duyệt của mình với JavaScript chủ yếu tắt vì lý do này. Nhưng, tôi đang sử dụng Yosemite 10.10.3 và tôi nghĩ rằng có một số loại vấn đề hẹn giờ với nó - ít nhất là trên hệ thống cũ của tôi (tương đối cũ - có nghĩa là năm 2011).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.