API requestAnimationFrame - agora com precisão de menos de um milissegundo

Ilmari Heikkinen

Se você já usou requestAnimationFrame, deve ter notado que as pinturas são sincronizadas com a taxa de atualização da tela, resultando nas animações de maior fidelidade possíveis. Além disso, você economiza a energia da bateria e o ruído do ventilador da CPU quando os usuários mudam para outra guia.

No entanto, uma parte da API está prestes a mudar. O carimbo de data/hora transmitido para a função de callback muda de um carimbo de data/hora típico do tipo Date.now() para uma medição de alta resolução de milissegundos de ponto flutuante desde que a página foi aberta. Se você usar esse valor, vai precisar atualizar seu código com base na explicação abaixo.

Para deixar claro, estou falando sobre o seguinte:

// assuming requestAnimationFrame method has been normalized for all vendor prefixes..
requestAnimationFrame(function(timestamp){
    // the value of timestamp is changing
});

Se você estiver usando o shim requestAnimFrame comum fornecido aqui, não estará usando o valor do carimbo de data/hora. Você está livre. :)

Por quê?

Por quê? O rAF ajuda você a atingir a taxa ideal de 60 QPS, que equivale a 16,7 ms por frame. No entanto, medir com milissegundos inteiros significa que temos uma precisão de 1/16 para tudo o que queremos observar e segmentar.

Comparação de gráficos de 16 ms e 16 ms de números inteiros.

Como você pode ver acima, a barra azul representa o tempo máximo que você tem para fazer todo o trabalho antes de pintar um novo frame (a 60 fps). Você provavelmente está fazendo mais de 16 coisas, mas com milissegundos inteiros, só é possível programar e medir nesses incrementos muito grandes. Isso não é bom o suficiente.

O High Resolution Timer resolve isso fornecendo uma figura muito mais precisa:

Date.now()         //  1337376068250
performance.now()  //  20303.427000007

O timer de alta resolução está disponível no Chrome como window.performance.webkitNow(), e esse valor geralmente é igual ao novo valor do argumento transmitido para o callback rAF. Quando a especificação for implementada nos padrões, o método vai remover o prefixo e ficar disponível em performance.now().

Você também vai notar que os dois valores acima são muito diferentes. performance.now() é uma medição de milissegundos de ponto flutuante desde que a página começou a carregar (o performance.navigationStart, para ser específico).

Em uso

O principal problema são as bibliotecas de animação que usam este padrão de design:

function MyAnimation(duration) {
    this.startTime = Date.now();
    this.duration = duration;
    requestAnimFrame(this.tick.bind(this));
}
MyAnimation.prototype.tick = function(time) {
    var now = Date.now();
    if (time > now) {
        this.dispatchEvent("ended");
        return;
    }
    ...
    requestAnimFrame(this.tick.bind(this));
}

A edição para corrigir isso é bem fácil... aumente o startTime e o now para usar window.performance.now().

this.startTime = window.performance.now ?
                    (performance.now() + performance.timing.navigationStart) :
                    Date.now();

Essa é uma implementação bastante simples, que não usa um método now() com prefixo e também pressupõe suporte a Date.now(), que não está no IE8.

Detecção de recursos

Se você não estiver usando o padrão acima e quiser apenas identificar o tipo de valor de callback que está recebendo, use esta técnica:

requestAnimationFrame(function(timestamp){

    if (timestamp < 1e12){
        // .. high resolution timer
    } else {
        // integer milliseconds since unix epoch
    }

    // ...

A verificação de if (timestamp < 1e12) é um teste rápido para sabermos o tamanho do número com que estamos lidando. Tecnicamente, isso pode ser um falso positivo, mas apenas se uma página da Web permanecer aberta continuamente por 30 anos. Mas não é possível testar se é um número de ponto flutuante (em vez de ser arredondado para um número inteiro). Peça timers de alta resolução suficientes e você vai receber valores inteiros em algum momento.

Planejamos fazer essa mudança no Chrome 21. Se você já estiver usando esse parâmetro de callback, atualize seu código.