API requestAnimationFrame, ora con una precisione inferiore al millisecondo

Ilmari Heikkinen

Se hai usato requestAnimationFrame, ti è piaciuto vedere i colori sincronizzati con la frequenza di aggiornamento dello schermo, ottenendo le animazioni ad alta precisione possibili. Inoltre, risparmi il rumore della ventola della CPU e l'energia della batteria quando passano a un'altra scheda.

Tuttavia, sta per essere apportata una modifica a una parte dell'API. Il timestamp che viene passato alla tua funzione di callback sta cambiando da un tipico timestamp Date.now() a una misurazione ad alta risoluzione dei millisecondi in virgola mobile dall'apertura della pagina. Se utilizzi questo valore, dovrai aggiornare il codice in base alla spiegazione riportata di seguito.

Giusto per essere chiari, ecco di cosa sto parlando:

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

Se utilizzi lo shim requestAnimFrame comune fornito qui, non devi utilizzare il valore del timestamp. Sei fuori gioco. :)

Perché

Perché? Bene rAF ti aiuta a ottenere gli ultimi 60 fps che è l'ideale, e 60 fps si traduce in 16,7 ms per frame. Tuttavia, la misurazione con millisecondi interi significa che abbiamo una precisione di 1/16 per tutto ciò che vogliamo osservare e scegliere come target.

Confronto del grafico in 16 ms contro 16 ms interi.

Come puoi vedere sopra, la barra blu rappresenta la quantità massima di tempo che hai a disposizione per completare il lavoro prima di dipingere un nuovo fotogramma (a 60 fps). Probabilmente stai effettuando più di 16 passaggi, ma con i millisecondi interi hai la possibilità di pianificare e misurare solo in questi incrementi molto consistenti. Non è abbastanza.

Il Timer ad alta risoluzione risolve questo problema fornendo un valore molto più preciso:

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

Il timer ad alta risoluzione è attualmente disponibile in Chrome come window.performance.webkitNow() e questo valore in genere corrisponde al valore del nuovo argomento passato al callback rAF. Una volta che le specifiche avranno superato gli standard, il metodo eliminerà il prefisso e sarà disponibile fino al giorno performance.now().

Noterai inoltre che i due valori sopra riportati presentano molti ordini di grandezza diversi. performance.now() indica i millisecondi in virgola mobile da quando è iniziato il caricamento di quella pagina specifica (performance.navigationStart per essere precisi).

In uso

Il problema principale che viene ritagliato riguarda le librerie di animazioni che utilizzano questo pattern di progettazione:

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

Una modifica per risolvere il problema è piuttosto semplice... aumenta startTime e now per usare window.performance.now().

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

Questa è un'implementazione abbastanza ingenua, non utilizza un metodo now() con prefisso e presuppone anche il supporto di Date.now(), che non è in IE8.

Rilevamento delle funzionalità

Se non utilizzi lo schema riportato sopra e desideri solo identificare il tipo di valore di callback che ricevi, puoi utilizzare questa tecnica:

requestAnimationFrame(function(timestamp){

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

    // ...

Controllare if (timestamp < 1e12) è un rapido test per capire con che numero abbiamo a che fare. Tecnicamente potrebbe essere un falso positivo, ma solo se una pagina web rimane aperta ininterrottamente per 30 anni. Tuttavia, non siamo in grado di verificare se si tratta di un numero in virgola mobile (invece che inferiore a un numero intero). Richiedi un numero sufficiente di timer per l'alta risoluzione e prima o poi riceverai valori interi.

Abbiamo intenzione di implementare questa modifica in Chrome 21, quindi se utilizzi già questo parametro di callback, assicurati di aggiornare il codice.