Управление fps с requestAnimationFrame?


кажется requestAnimationFrame это де-факто способ оживить вещи сейчас. Он работал довольно хорошо для меня по большей части, но сейчас я пытаюсь сделать некоторые анимации холста, и мне было интересно: есть ли способ убедиться, что он работает с определенным fps? Я понимаю, что цель rAF заключается в последовательной плавной анимации, и я могу рисковать сделать свою анимацию прерывистой, но сейчас она, похоже, работает на совершенно разных скоростях довольно произвольно, и мне интересно, если есть способ как-то с этим бороться.

Я хотел бы использовать setInterval но я хочу, чтобы оптимизация, которую предлагает rAF (особенно автоматически останавливается, когда вкладка находится в фокусе).

В случае если кто-то хочет посмотреть на мой код, это довольно много:

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

Где Узел.drawFlash () - это просто некоторый код, который определяет радиус на основе переменной счетчика, а затем рисует круг.

8 83

8 ответов:

как дросселировать requestAnimationFrame до определенной частоты кадров

демо дросселирование при 5 кадров в секунду:http://jsfiddle.net/m1erickson/CtsY3/

этот метод работает путем тестирования прошедшего времени с момента выполнения последнего цикла кадра.

ваш код чертежа выполняется только по истечении указанного интервала FPS.

первая часть кода устанавливает некоторые переменные, используемые для вычисления прошедших время.

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

и этот код является фактическим циклом requestAnimationFrame, который рисует в указанном FPS.

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

    }
}

обновление 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 для синхронизации обновлений с частотой обновления монитора. Это потребует от вас анимировать на FPS монитора или его фактор (т. е. 60, 30, 15 кадров в секунду для обычной частоте обновления 60 Гц).

если вы хотите более произвольный FPS тогда нет смысла использовать rAF, поскольку частота кадров никогда не будет соответствовать частоте обновления монитора в любом случае (просто кадр здесь и там), который просто не может дать вам плавную анимацию (как и при всех повторных таймингах кадров), и вы можете также использовать setTimeout или setInterval вместо.

это также хорошо известная проблема в профессиональной видеоиндустрии, когда вы хотите воспроизвести видео на другом FPS, а затем устройство, показывающее его обновление. Много методов имеет были использованы такие методы, как смешивание кадров и сложное повторное синхронизирующее перестроение промежуточных кадров на основе векторов движения, но с canvas эти методы недоступны, и результат всегда будет отрывистым видео.

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

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

    ... code for frame here
}

причина, по которой мы ставим setTimeoutпервый (а почему какое-то место rAF во-первых, когда используется Поли-заливка) является то, что это будет более точным, как setTimeout будет стоять в очереди событие сразу же при запуске цикла, так что независимо от того, сколько времени оставшийся код будет использовать (при условии, что он не превышает интервал тайм-аута) следующий вызов будет на интервале, который он представляет (для чистого rAF это не важно, поскольку rAF в любом случае попытается перейти на следующий кадр).

также стоит отметить, что размещение его первым также будет рисковать вызовами, складывающимися как с 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
}

таким образом, вы можете уменьшить FPS до 1/4 и т. д.

Я предлагаю обернуть ваш звонок в requestAnimationFrame на setTimeout. Если вы позвоните setTimeout из функции, из которой Вы запросили кадр анимации, вы побеждаете цель requestAnimationFrame. Но если вы позвоните requestAnimationFrame внутри setTimeout это работает:

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

Это все хорошие идеи в теории, пока не углублялся. проблема в том, что вы не можете дросселировать RAF без его де-синхронизации, победив его очень цель для существующих.таким образом, вы позволяете ему работать на полной скорости и обновлять свои данные в отдельном цикле,или даже отдельный поток!

Да, я это сказал. Ты можете сделать многопоточный JavaScript в браузере!

есть два метода, которые я знаю, что работают очень хорошо без Джанка, используя гораздо меньше сока и создавая меньше тепла. Точное время людск-маштаба и эффективность машины чистый результат.

извиняюсь, если это немного многословно, но здесь идет...


метод 1: Обновление данных через setInterval и графики через RAF.

используйте отдельный setInterval для обновления значений перевода и вращения, физики, столкновений и т. д. Сохраните эти значения в объекте для каждого анимированного элемент. Назначьте строку преобразования переменной в объекте каждый setInterval 'frame'. Храните эти объекты в массиве. Установите интервал до нужного fps в МС: ms=(1000/fps). Это держит устойчивые часы, что позволяет тот же fps на любом устройстве, независимо от скорости RAF. не назначайте преобразования элементам здесь!

в цикле requestAnimationFrame повторите свой массив с помощью цикла old-school for-Не используйте здесь новые формы, они медленно!

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

в функции rafUpdate получите строку преобразования из объекта js в массиве и идентификатор его элементов. Вы уже должны иметь свои "спрайтовые" элементы, прикрепленные к переменной или легко доступные с помощью других средств, чтобы вы не теряли времени на "получение" их в RAF. Сохранение их в объекте, названном в честь их html-идентификатора, работает довольно хорошо. Установите эту часть до того, как она даже войдет в ваш SI или RAF.

используйте RAF для обновления преобразований только, используйте только 3D преобразования (даже для 2d) и установите css "will-change: transform;" на элементах, которые будут меняться. Это позволяет максимально синхронизировать ваши преобразования с собственной частотой обновления, запускает графический процессор и сообщает браузеру, где лучше всего сосредоточиться.

Так что вы должны иметь что-то вроде этого псевдокода...

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

это сохраняет ваши обновления объектов данных и строк преобразования синхронизированы с требуемой частотой кадров в SI, и фактические назначения преобразования в RAF синхронизируются с частотой обновления GPU. Таким образом, фактические обновления графики находятся только в RAF, но изменения в данных и построение строки преобразования находятся в SI, таким образом, нет jankies, но "время" течет с требуемой частотой кадров.


расход:

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

Способ 2. Поместите SI в веб-рабочий. Это один FAAAST и гладкий!

то же самое, что и метод 1, но поместите SI в web-worker. Он будет работать на затем полностью отдельный поток, оставляя страницу для работы только с RAF и пользовательским интерфейсом. Передайте массив спрайтов назад и вперед как "переносимый объект". Это Буко фаст. Это не займет много времени, чтобы клонировать или сериализовать, но это не похоже на передачу по ссылке в том, что ссылка с другой стороны уничтожена, поэтому вам нужно будет иметь обе стороны, чтобы перейти на другую сторону, и только обновлять их, когда они присутствуют, вроде как передавая записку взад и вперед с вашей подругой в другом месте. средняя школа.

только один может читать и писать одновременно. Это нормально, пока они проверяют, не определено ли это, чтобы избежать ошибки. RAF быстро и сразу же отбросит его, а затем пройдет через кучу кадров GPU, просто проверяя, был ли он отправлен обратно. SI в веб-работнике будет иметь массив спрайтов большую часть времени и будет обновлять данные о положении, движении и физике, а также создавать новую строку преобразования, а затем передавать ее обратно в RAF на странице.

Это самый быстрый способ, который я знаю, чтобы анимировать элементы с помощью скрипта. Эти две функции будут работать как две отдельные программы, на двух отдельных потоках, используя преимущества многоядерного процессора таким образом, что один сценарий js этого не делает. Многопоточная анимация javascript.

и он будет делать это плавно без jank,но с фактической заданной частотой кадров, с очень небольшим расхождением.


результат:

любой из этих двух методов убедитесь, что ваш скрипт будет работать с одинаковой скоростью на любом ПК, телефоне, планшете и т. д. (В пределах возможностей устройства и браузера, конечно).

пропуск requestAnimationFrame причина не гладко(желаемая) анимация на пользовательских кадров в секунду.

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

Как легко дросселировать до определенного FPS:

// timestamps are ms passed since document creation.
// lastTimestamp can be initialized to 0, if main loop is executed immediately
var lastTimestamp = 0,
    maxFPS = 30,
    timestep = 1000 / maxFPS; // ms for each frame

function main(timestamp) {
    window.requestAnimationFrame(main);

    // skip if timestep ms hasn't passed since last frame
    if (timestamp - lastTimestamp < timestep) return;

    lastTimestamp = timestamp;

    // draw frame here
}

window.requestAnimationFrame(main);

источник: подробное объяснение игровых циклов JavaScript и времени Исаак сукин

Я всегда делаю это очень простым способом, не трогая метки:

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

вот хорошее объяснение, которое я нашел:CreativeJS.com, чтобы обернуть вызов setTimeou) внутри функции, переданной в requestAnimationFrame. Моя забота с "простой" requestionAnimationFrame будет: "что, если я только хочу это анимировать три раза в секунду?"Даже с requestAnimationFrame (в отличие от setTimeout) является то, что это еще тратит (некоторое) количество "энергии" (это означает, что код браузера что-то делает и, возможно, замедляет система вниз) 60 или 120 или сколько раз в секунду, а не только два или три раза в секунду (как вы хотите).

большую часть времени я запускаю свои браузеры с JavaScript intentially выкл именно по этой причине. Но я использую Yosemite 10.10.3, и я думаю, что с ним есть какая - то проблема с таймером - по крайней мере, в моей старой системе (относительно старый-2011).