[Javascript] التحكم في إطارا في الثانية مع ريكستانيماشيونفريم؟


Answers

تحديث 2016/6

المشكلة اختناق معدل الإطار هو أن الشاشة لديها معدل التحديث المستمر، عادة 60 فبس.

إذا كنا نريد 24 فبس ونحن لن تحصل على 24 إطارا في الثانية الحقيقية على الشاشة، ونحن يمكن أن الوقت على هذا النحو ولكن لا تظهر كما رصد يمكن أن تظهر فقط إطارات متزامنة في 15 إطارا في الثانية، 30 إطارا في الثانية أو 60 إطارا في الثانية (بعض الشاشات أيضا 120 إطارا في الثانية ).

ومع ذلك، لأغراض التوقيت يمكننا حساب وتحديث عندما يكون ذلك ممكنا.

يمكنك بناء كل منطق للسيطرة على معدل الإطار عن طريق تغليف الحسابات وردود الفعل في كائن:

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

ثم قم بإضافة بعض رموز وحدة التحكم والتكوين:

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

استعمال

يصبح بسيط جدا - الآن، كل ما علينا القيام به هو خلق مثيل عن طريق وضع وظيفة الاستدعاء ومعدل الإطار المطلوب تماما مثل هذا:

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

ثم ابدأ (الذي قد يكون السلوك الافتراضي إذا رغبت في ذلك):

fc.start();

هذا كل شيء، يتم التعامل مع المنطق داخليا.

عرض

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>

الجواب القديم

والغرض الرئيسي من requestAnimationFrame هو مزامنة التحديثات إلى معدل تحديث الشاشة. وهذا يتطلب منك تحريك في فبس من الشاشة أو عامل منه (أي 60، 30، 15 فبس لمعدل التحديث نموذجي @ 60 هرتز).

إذا كنت ترغب في فبس أكثر تعسفا ثم لا توجد نقطة باستخدام راف كما معدل الإطار لن تتطابق مع تحديث التحديث على أي حال (مجرد إطار هنا وهناك) والتي ببساطة لا يمكن أن تعطيك الرسوم المتحركة على نحو سلس (كما هو الحال مع كل إطار إعادة توقيت ) ويمكنك أيضا استخدام setTimeout أو setInterval بدلا من ذلك.

هذا هو أيضا مشكلة معروفة في صناعة الفيديو المهنية عندما تريد تشغيل الفيديو في فبس مختلفة ثم الجهاز يظهر تحديثه في. وقد استخدمت العديد من التقنيات مثل مزج الإطار ومعقدة إعادة توقيت إعادة بناء إطارات وسيطة على أساس ناقلات الحركة، ولكن مع قماش هذه التقنيات غير متوفرة والنتيجة ستكون دائما الفيديو متشنج.

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

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

    ... code for frame here
}

السبب في أننا وضع setTimeout أولا (ولماذا بعض مكان rAF أولا عندما يتم استخدام تعبئة بولي) هو أن هذا سيكون أكثر دقة كما setTimeout سوف قائمة الانتظار على الفور الحدث عند بدء حلقة بحيث بغض النظر عن مقدار الوقت المتبقي (شريطة أن لا تتجاوز الفاصل الزمني المهلة) المكالمة التالية ستكون في الفاصل الزمني يمثل (ل راف النقي ليس هذا ضروري كما راف سيحاول القفز على الإطار التالي في أي حال).

كما تجدر الإشارة إلى أن وضعه أولا سيخاطر أيضا المكالمات المكدسة كما هو الحال مع setInterval . setInterval قد تكون أكثر دقة قليلا لهذا الاستخدام.

ويمكنك استخدام setInterval بدلا من حلقة للقيام بنفس الشيء.

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

function loop() {

    ... code for frame here
}

ووقف حلقة:

clearInterval(rememberMe);

من أجل تقليل معدل الإطار عند عدم وضوح علامة التبويب يمكنك إضافة عامل مثل هذا:

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
}

بهذه الطريقة يمكنك تقليل فبس إلى 1/4 الخ.

Question

يبدو أن requestAnimationFrame هو الطريقة الفعلية لتحريك الأشياء الآن. عملت بشكل جيد بالنسبة لي بالنسبة للجزء الأكبر، ولكن الآن أنا أحاول أن أفعل بعض الرسوم المتحركة قماش وكنت أتساءل: هل هناك أي طريقة للتأكد من أنه يعمل في إطارا في الثانية معينة؟ وأنا أفهم أن الغرض من راف هو الرسوم المتحركة على نحو سلس على نحو سلس، وأنا قد تتعرض لخطر جعل بلدي الرسوم المتحركة متقطع، ولكن الآن يبدو أن تشغيل بسرعة مختلفة بشكل تعسفي بشكل تعسفي جدا، وأنا أتساءل عما إذا كان هناك وسيلة لمكافحة أن بطريقة أو بأخرى.

أود استخدام setInterval ولكن أريد التحسينات التي راف يقدم (خصوصا تلقائيا وقف عند علامة التبويب هو في التركيز).

في حالة شخص يريد أن ينظر في بلدي التعليمات البرمجية، انها الى حد كبير:

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

حيث Node.drawFlash () هو مجرد بعض التعليمات البرمجية التي تحدد دائرة نصف قطرها على أساس متغير عداد ثم رسم دائرة.

شكر!




تخطي ريكانتانيماشيونفريم السبب ليس سلاسة (المطلوب) الرسوم المتحركة في إطارا في الثانية مخصصة.

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