Controlando fps com requestAnimationFrame?

140

Parece que requestAnimationFrameé a maneira de fato de animar as coisas agora. Na maioria das vezes, funcionou muito bem para mim, mas agora estou tentando fazer algumas animações de tela e fiquei imaginando: existe alguma maneira de garantir que ele funcione a um certo fps? Entendo que o objetivo do rAF é animações consistentemente suaves, e posso correr o risco de tornar minha animação instável, mas agora parece correr a velocidades drasticamente diferentes de maneira bastante arbitrária, e estou me perguntando se há uma maneira de combater de alguma forma.

Eu usaria, setIntervalmas quero as otimizações que o rAF oferece (especialmente parando automaticamente quando a guia está em foco).

Caso alguém queira ver meu código, é praticamente:

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

Onde Node.drawFlash () é apenas um código que determina o raio com base em uma variável do contador e desenha um círculo.

robert.vinluan
fonte
1
A sua animação está atrasada? Eu acho que a maior vantagem requestAnimationFrameé (como o nome sugere) solicitar um quadro de animação somente quando necessário. Digamos que você mostre uma tela preta estática, você deve obter 0 qps porque nenhum novo quadro é necessário. Mas se você estiver exibindo uma animação que requer 60fps, você também deve obtê-la. rAFpermite apenas "pular" quadros inúteis e salvar a CPU.
maxdec
setInterval também não funciona na guia inativa.
precisa
Esse código é executado de maneira diferente na tela de 90 hz vs tela de 60 hz vs tela de 144 hz.
manthrax 12/01

Respostas:

190

Como acelerar requestAnimationFrame para uma taxa de quadros específica

Controle de demonstração a 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/

Esse método funciona testando o tempo decorrido desde a execução do último loop de quadro.

Seu código de desenho é executado somente quando o intervalo FPS especificado tiver decorrido.

A primeira parte do código define algumas variáveis ​​usadas para calcular o tempo decorrido.

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

E esse código é o loop requestAnimationFrame real que desenha no seu FPS especificado.

// 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
fonte
5
Excelente explicação e exemplo. Este deve ser marcado como a resposta aceita
muxcmux
13
Boa demonstração - deve ser aceita. Aqui, bifurcou seu violino, para demonstrar o uso de window.performance.now () em vez de Date.now (). Isto vai muito bem com o alto-res timestamp que FAr já recebe, por isso não há necessidade de chamar Date.now () dentro do callback: jsfiddle.net/chicagogrooves/nRpVD/2
Dean Radcliffe
2
Obrigado pelo link atualizado, usando o novo recurso de carimbo de data / hora rAF. O novo registro de data e hora rAF adiciona infraestrutura útil e também é mais preciso que o Date.now.
Marke
13
Esta é uma demonstração muito boa, que me inspirou a fazer a minha própria ( JSFiddle ). As principais diferenças são usar rAF (como a demo de Dean) em vez de Data, adicionando controles para ajustar dinamicamente a taxa de quadros de destino, amostrando a taxa de quadros em um intervalo separado da animação e adicionando um gráfico de taxas de quadros históricas.
tavnab
1
Tudo o que você pode controlar é quando você vai pular um quadro. Um monitor de 60 qps desenha sempre em intervalos de 16 ms. Por exemplo, se você deseja que seu jogo seja executado a 50fps, você quer pular cada sexto quadro. Você verifica se 20ms (1000/50) se esgotou e se não (apenas 16ms se passaram), para pular um quadro, o próximo quadro 32ms se passou desde que você desenhou, para desenhar e redefinir. Mas então você pulará metade dos quadros e executará a 30fps. Então, quando você redefine, lembre-se de ter esperado 12ms muito tempo da última vez. Portanto, no próximo quadro, outros 16ms passam, mas você o contabiliza como 16 + 12 = 28ms, para desenhar novamente e esperar 8ms por muito tempo
Curtis
47

Atualização 2016/6

O problema da limitação da taxa de quadros é que a tela tem uma taxa de atualização constante, geralmente 60 FPS.

Se quisermos 24 FPS, nunca obteremos os verdadeiros 24 fps na tela, podemos cronometrar como tal, mas não mostrá-lo, pois o monitor só pode mostrar quadros sincronizados a 15 fps, 30 fps ou 60 fps (alguns monitores também 120 fps )

No entanto, para fins de tempo, podemos calcular e atualizar sempre que possível.

Você pode criar toda a lógica para controlar a taxa de quadros encapsulando cálculos e retornos de chamada em um objeto:

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

Em seguida, adicione algum controlador e código de configuração:

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

Uso

Torna-se muito simples - agora, tudo o que precisamos fazer é criar uma instância, definindo a função de retorno de chamada e a taxa de quadros desejada da seguinte maneira:

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

Em seguida, inicie (que pode ser o comportamento padrão, se desejado):

fc.start();

É isso aí, toda a lógica é tratada internamente.

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>

Resposta antiga

O principal objetivo requestAnimationFrameé sincronizar atualizações com a taxa de atualização do monitor. Isso exigirá que você anime no FPS do monitor ou em um fator dele (ou seja, 60, 30, 15 FPS para uma taxa de atualização típica a 60 Hz).

Se você deseja um FPS mais arbitrário, não faz sentido usar rAF, pois a taxa de quadros nunca corresponderá à frequência de atualização do monitor (apenas um quadro aqui e ali) que simplesmente não pode fornecer uma animação suave (como em todos os tempos de novo quadro) ) e você pode usar setTimeoutou setIntervalnão.

Esse também é um problema conhecido no setor de vídeo profissional quando você deseja reproduzir um vídeo em um FPS diferente do dispositivo em que ele é atualizado. Muitas técnicas têm sido usadas, como mesclagem de quadros e re-construção complexa de quadros intermediários com base em vetores de movimento, mas com o canvas essas técnicas não estão disponíveis e o resultado será sempre um vídeo irregular.

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

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

    ... code for frame here
}

A razão pela qual colocamos em setTimeout primeiro lugar (e por que alguns colocam em rAFprimeiro lugar quando um preenchimento múltiplo é usado) é que isso será mais preciso, pois setTimeoutenfileirará um evento imediatamente quando o loop iniciar, para que, não importa quanto tempo o código restante use. (desde que não exceda o intervalo de tempo limite), a próxima chamada será no intervalo que representa (para rAF puro, isso não é essencial, pois o rAF tentará pular para o próximo quadro, em qualquer caso).

Também vale a pena notar que colocá-lo em primeiro lugar também arriscará o empilhamento de chamadas, como acontece com setInterval. setIntervalpode ser um pouco mais preciso para esse uso.

E você pode usar setIntervalem vez fora do circuito para fazer o mesmo.

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

function loop() {

    ... code for frame here
}

E para parar o loop:

clearInterval(rememberMe);

Para reduzir a taxa de quadros quando a guia fica embaçada, você pode adicionar um fator como este:

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
}

Dessa forma, você pode reduzir o FPS para 1/4, etc.


fonte
4
Em alguns casos, você não está tentando corresponder à taxa de quadros dos monitores, mas nas seqüências de imagens, por exemplo, soltar quadros. Excelente explicação btw
sidonaldson
3
Um dos maiores motivos para limitar o requestAnimationFrame seria alinhar a execução de algum código com o quadro de animação do navegador. As coisas acabam ficando muito mais suaves, especialmente se você estiver executando alguma lógica nos dados a cada quadro, como os visualizadores de música, por exemplo.
Chris Dolphin
4
Isso é ruim porque o principal uso de requestAnimationFrameé sincronizar as operações do DOM (leitura / gravação), portanto, não usá-lo prejudicará o desempenho ao acessar o DOM, pois as operações não serão enfileiradas para serem executadas juntas e forçarão a repintura de layout desnecessariamente.
vsync
1
Não há risco de "empilhamento de chamadas", pois o JavaScript é executado em thread único e nenhum evento de tempo limite é acionado enquanto o código está em execução. Portanto, se a função demorar mais que o tempo limite, ela será executada quase a qualquer momento o mais rápido possível, enquanto o navegador ainda realizará redesenhamentos e acionaria outros tempos limite entre as chamadas.
Dronus
Eu sei que você declara que a atualização da página não pode ser atualizada mais rapidamente do que o limite de fps na exibição. No entanto, é possível atualizar mais rapidamente, acionando o reflow da página? Por outro lado, é possível não observar vários refluxos de página se eles forem feitos mais rapidamente que a taxa de fps nativa?
Travis J
36

Sugiro encerrar sua chamada requestAnimationFrameem um setTimeout. Se você chamar setTimeoutde dentro da função da qual solicitou o quadro de animação, estará perdendo o objetivo de requestAnimationFrame. Mas se você ligar requestAnimationFramede dentro setTimeout, funciona sem problemas:

var fps = 25
function animate() {
  setTimeout(function() {
    requestAnimationFrame(animate);
  }, 1000 / fps);
}
Luke Taylor
fonte
1
Na verdade, isso parece funcionar para manter a taxa de quadros baixa e não cozinhar minha CPU. E é tão simples. Felicidades!
phocks
Essa é uma maneira simples e agradável de fazer animações leves. No entanto, fica um pouco fora de sincronia, pelo menos em alguns dispositivos. Eu usei essa técnica em um dos meus motores anteriores. Funcionou bem até as coisas ficarem complexas. O maior problema era que, quando conectado a sensores de orientação, ficava para trás ou ficava nervoso. Mais tarde, descobri que o uso de um setInterval separado e as atualizações de comunicação entre sensores, quadros setInterval e quadros RAF por meio de propriedades de objetos permitiam que os sensores e o RAF passassem em tempo real, enquanto o tempo de animação podia ser controlado por meio de atualizações de propriedades de setInterval.
Jdmayfield 24/01/18
Melhor resposta ! Graças;)
538ROMEO
Meu monitor tem 60 FPS; se eu definir var fps = 60, recebo apenas 50 FPS usando esse código. Quero diminuir para 60 porque algumas pessoas têm monitores de 120 FPS, mas não quero afetar os outros. Isso é surpreendentemente difícil.
Curtis
A razão pela qual você obtém um FPS menor do que o esperado é porque o setTimeout pode executar o retorno de chamada após mais do que o atraso especificado. Há várias razões possíveis para isso. E todo loop leva tempo para definir um novo timer e executar algum código antes de definir o novo tempo limite. Você não tem como ser preciso com isso, sempre considere um resultado mais lento que o esperado, mas contanto que não saiba o quanto mais lento será, tentar diminuir o atraso também seria impreciso. O JS nos navegadores não deve ser tão preciso.
Pdepmcp 11/03/19
17

Todas essas são boas idéias em teoria, até você se aprofundar. O problema é que você não pode estrangular um RAF sem dessincronizá-lo, derrotando seu objetivo de existir. Assim, você o deixa rodar em velocidade máxima e atualiza seus dados em um loop separado , ou mesmo em um thread separado!

Sim, eu disse. Você pode fazer JavaScript multiencadeado no navegador!

Sei que existem dois métodos que funcionam extremamente bem sem brincadeiras, usando muito menos suco e criando menos calor. O tempo preciso em escala humana e a eficiência da máquina são o resultado líquido.

Desculpas se isso é um pouco prolixo, mas aqui vai ...


Método 1: Atualize dados via setInterval e gráficos via RAF.

Use um setInterval separado para atualizar os valores de conversão e rotação, física, colisões etc. Mantenha esses valores em um objeto para cada elemento animado. Atribua a sequência de transformação a uma variável no objeto cada 'frame' setInterval. Mantenha esses objetos em uma matriz. Defina seu intervalo para os fps desejados em ms: ms = (1000 / fps). Isso mantém um relógio constante que permite os mesmos fps em qualquer dispositivo, independentemente da velocidade RAF. Não atribua as transformações aos elementos aqui!

Em um loop requestAnimationFrame, itere através de sua matriz com um loop for old-school - não use os formulários mais recentes aqui, eles são lentos!

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

Na sua função rafUpdate, obtenha a string de transformação do seu objeto js na matriz e o ID dos seus elementos. Você já deve ter seus elementos de 'sprite' anexados a uma variável ou facilmente acessíveis por outros meios, para não perder tempo 'obtendo-os' na RAF. Mantê-los em um objeto com o nome de seus IDs html funciona muito bem. Configure essa peça antes mesmo de entrar no seu SI ou RAF.

Use o RAF para atualizar apenas suas transformações , use apenas transformações 3D (mesmo para 2d) e defina o css "will-change: transform;" em elementos que mudarão. Isso mantém suas transformações sincronizadas com a taxa de atualização nativa o máximo possível, ativa a GPU e informa ao navegador onde se concentrar mais.

Então você deve ter algo como este pseudocódigo ...

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

Isso mantém suas atualizações nos objetos de dados e as seqüências de transformação sincronizadas com a taxa de 'quadro' desejada no SI, e as atribuições de transformação reais no RAF sincronizadas com a taxa de atualização da GPU. Portanto, as atualizações gráficas reais estão apenas no RAF, mas as alterações nos dados e a construção da cadeia de transformação estão no SI, portanto, não há bobagens, mas o 'tempo' flui na taxa de quadros desejada.


Fluxo:

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

Método 2. Coloque o SI em um trabalhador da Web. Este é FAAAST e suave!

Igual ao método 1, mas coloque o SI no web-worker. Ele será executado em um segmento totalmente separado, deixando a página para lidar apenas com o RAF e a interface do usuário. Passe a matriz de sprites para frente e para trás como um 'objeto transferível'. Isso é buko rápido. Não leva tempo para clonar ou serializar, mas não é como passar por referência, pois a referência do outro lado é destruída; portanto, você precisará que os dois lados passem para o outro lado e os atualize apenas quando presentes, classifique de passar uma nota para frente e para trás com sua namorada no ensino médio.

Somente um pode ler e escrever de cada vez. Isso é bom desde que verifique se não está indefinido para evitar um erro. O RAF é RÁPIDO e o retrocede imediatamente, passando por vários quadros de GPU, verificando se já foi enviado de volta. O SI no trabalhador da Web terá o conjunto de sprites na maior parte do tempo e atualizará os dados posicionais, de movimento e físicos, além de criar a nova sequência de transformação, depois passará para a RAF na página.

Essa é a maneira mais rápida que conheço de animar elementos via script. As duas funções serão executadas como dois programas separados, em dois threads separados, aproveitando as CPUs de vários núcleos de uma maneira que um único script js não. Animação em javascript multiencadeada.

E será feito sem problemas, mas na taxa de quadros especificada real, com muito pouca divergência.


Resultado:

Qualquer um desses dois métodos garantirá que o script seja executado na mesma velocidade em qualquer PC, telefone, tablet etc. (dentro dos recursos do dispositivo e do navegador, é claro).

jdmayfield
fonte
Como uma observação lateral - no Método 1, se houver muita atividade no seu setInterval, ele poderá abrandar o seu RAF devido ao assíncrono de thread único. Você pode atenuar esse desmembramento dessa atividade em mais do que no quadro SI, para que o assíncrono passe o controle de volta ao RAF mais rapidamente. Lembre-se de que o RAF atinge a taxa de quadros máxima, mas sincroniza as alterações gráficas com a tela, então não há problema em ignorar alguns quadros RAF - contanto que você não pule mais do que os quadros SI, ele não será afetado.
Jdmayfield
O método 2 é mais robusto, pois na verdade é multitarefa nos dois loops, não alternando de maneira assíncrona, mas você ainda deseja evitar que o quadro do SI demore mais do que a taxa de quadros desejada, portanto a divisão da atividade do SI ainda pode ser desejável se houver muita manipulação de dados que levaria mais de um quadro de SI para ser concluído.
Jdmayfield
Eu pensei que vale a pena mencionar, como uma nota de interesse, que a execução de loops emparelhados como esse realmente registra no Chromes DevTools que a GPU está sendo executada na taxa de quadros especificada no loop setInterval! Aparece apenas os quadros RAF nos quais ocorrem alterações gráficas são contados como quadros pelo medidor de FPS. Portanto, os quadros RAF nos quais apenas o trabalho não gráfico, ou mesmo apenas os loops em branco, não contam no que diz respeito à GPU. Acho isso interessante como ponto de partida para futuras pesquisas.
Jdmayfield # 9/18
Acredito que esta solução tenha o problema de continuar em execução quando o rAF for suspenso, por exemplo, porque o usuário mudou para outra guia.
N4ppeL 11/07/19
1
PS: Eu fiz algumas leituras e parece que a maioria dos navegadores limita os eventos cronometrados a uma vez por segundo nas guias em segundo plano (o que provavelmente também deve ser tratado de alguma forma). Se você ainda deseja resolver o problema e pausar completamente quando não estiver visível, parece haver o visibilitychangeevento.
N4ppeL 11/07/19
3

Como acelerar facilmente para um FPS específico:

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

Fonte: Uma explicação detalhada dos intervalos e tempo dos jogos em JavaScript de Isaac Sukin

Rustem Kakimov
fonte
1
Se o meu monitor rodar a 60 FPS e eu quero que o meu jogo corra a 58 FPS, defino maxFPS = 58, isso fará com que funcione a 30 FPS, porque passará a cada segundo quadro.
Curtis
Sim, eu tentei este também. Eu escolho não realmente estrangular o próprio RAF - apenas as alterações são atualizadas pelo setTimeout. No Chrome, pelo menos, isso faz com que os fps efetivos sejam executados no ritmo setTimeouts, de acordo com as leituras do DevTools. É claro que ele só pode atualizar quadros de vídeo reais na velocidade da placa de vídeo e monitorar a taxa de atualização, mas esse método parece operar com o mínimo de excêntricos, o controle fps "aparente" mais suave, e é isso que eu estou procurando.
Jdmayfield 07/02/19
Como eu acompanho todo o movimento dos objetos JS separadamente do RAF, isso mantém a lógica da animação, a detecção de colisão ou o que você precisar, executando a uma taxa perceptivamente consistente, independentemente do RAF ou do setTimeout, com um pouco de matemática extra.
Jdmayfield 07/02/19
2

Ignorar requestAnimationFrame não causa animação suave (desejada) em fps personalizados.

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

Código original de @tavnab.

befzz
fonte
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
fonte
Adicione algumas frases para explicar o que seu código está fazendo, para que você possa obter mais votos positivos para sua resposta.
Análise difusa
1

Eu sempre faço isso de maneira muito simples, sem mexer nos carimbos de data e hora:

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
fonte
1
Isso será executado muito rápido se o seu monitor estiver em 120 fps.
Curtis
0

Aqui está uma boa explicação que encontrei: CreativeJS.com , para agrupar uma chamada setTimeou) dentro da função passada para requestAnimationFrame. Minha preocupação com uma requestAnimationFrame "simples" seria "e se eu apenas desejar animar três vezes por segundo?" Mesmo com requestAnimationFrame (ao contrário de setTimeout) é que ele ainda desperdiça (parte) uma quantidade de "energia" (o que significa que o código do navegador está fazendo algo e possivelmente diminuindo a velocidade do sistema) 60 ou 120 ou, no entanto, muitas vezes por segundo, como em vez de apenas duas ou três vezes por segundo (como desejar).

Na maioria das vezes, eu executo meus navegadores com JavaScript desativado por esse motivo. Mas estou usando o Yosemite 10.10.3 e acho que há algum tipo de problema com o temporizador - pelo menos no meu sistema antigo (relativamente antigo - ou seja, 2011).

Jim Witte
fonte