API requestAnimationFrame, ora con una precisione inferiore al millisecondo

Ilmari Heikkinen

Se hai utilizzato requestAnimationFrame, avrai apprezzato la possibilità di vedere i tuoi dipinti sincronizzati con la frequenza di aggiornamento dello schermo, ottenendo le animazioni con la massima fedeltà possibile. Inoltre, riduci il rumore della ventola della CPU e il consumo della batteria degli utenti quando passano a un'altra scheda.

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

Per chiarezza, 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 utilizzi il valore del timestamp. Hai finito. :)

Perché

Perché? La modalità rAF ti consente di ottenere i 60 fps ideali e 60 fps si traducono in 16, 7 ms per frame. Tuttavia, la misurazione in millisecondi interi significa che abbiamo una precisione di 1/16 per tutto ciò che vogliamo osservare e scegliere come target.

Confronto tra grafici di 16 ms e 16 ms interi.

Come puoi vedere sopra, la barra blu rappresenta il tempo massimo a tua disposizione per completare tutte le operazioni prima di dipingere un nuovo frame (a 60 fps). Probabilmente stai facendo più di 16 cose, ma con i millisecondi interi hai la possibilità di pianificare e misurare solo in incrementi molto grandi. Non è abbastanza.

Il timer ad alta risoluzione risolve il problema fornendo un dato 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 è generalmente uguale al nuovo valore dell'argomento passato al callback rAF. Una volta che la specifica avanza ulteriormente negli standard, il metodo trascinerà il prefisso e sarà disponibile fino a performance.now().

Inoltre, noterai che i due valori precedenti sono diversi di molti ordini di grandezza. performance.now() è una misurazione dei millisecondi con rappresentazione in virgola mobile dall'inizio del caricamento della pagina in questione (performance.navigationStart per essere precisi).

In uso

Il problema principale che si verifica è che le librerie di animazione 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 è abbastanza semplice: aumenta startTime e now per utilizzare window.performance.now().

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

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

Rilevamento di funzionalità

Se non utilizzi il pattern riportato sopra e vuoi 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
    }

    // ...

Il controllo di if (timestamp < 1e12) è un rapido test per verificare l'entità. Tecnicamente potrebbe trattarsi di un falso positivo, ma solo se una pagina web è aperta ininterrottamente per 30 anni. Tuttavia, non siamo in grado di verificare se si tratta di un numero in virgola mobile (anziché arrotondato per difetto a un numero intero). Se richiedi un numero sufficiente di timer ad alta risoluzione, a un certo punto dovresti ottenere valori interi.

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