API de requestAnimationFrame: ahora con precisión de menos de milisegundos

Ilmari Heikkinen

Si usaste requestAnimationFrame, te gustó ver que tus pinturas se sincronizan con la frecuencia de actualización de la pantalla, lo que genera animaciones de mayor fidelidad posibles. Además, les ahorras a los usuarios el ruido de los ventiladores de la CPU y la energía de la batería cuando cambian de pestaña.

Sin embargo, estará a punto de realizar un cambio en parte de la API. La marca de tiempo que se pasa a la función de devolución de llamada cambia de una marca de tiempo típica similar a Date.now() a una medición de alta resolución de milisegundos de punto flotante desde que se abrió la página. Si usas este valor, deberás actualizar tu código, según la explicación que aparece a continuación.

Para ser claros, esto es de lo que estoy hablando:

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

Si usas la corrección de compatibilidad requestAnimFrame común que se proporciona aquí, significa que no estás usando el valor de marca de tiempo. Ya no tienes ganas de usar el dispositivo. :)

Por qué

¿Por qué? La rAF te ayuda a obtener la máxima velocidad ideal de 60 fps, y 60 fps se traduce en 16.7 ms por fotograma. Pero medir con milisegundos de número entero significa que tenemos una precisión de 1/16 para todo lo que queremos observar y apuntar.

Comparación de gráficos de 16 ms frente a 16 ms de números enteros.

Como puedes ver arriba, la barra azul representa la cantidad máxima de tiempo que tienes para hacer todo el trabajo antes de pintar un nuevo fotograma (a 60 fps). Es probable que hagas más de 16 cosas, pero con los milisegundos enteros solo puedes programar y medir en esos incrementos muy fragmentados. Eso no es suficiente.

El temporizador de alta resolución proporciona una cifra mucho más precisa para solucionar este problema:

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

Actualmente, el temporizador de alta resolución está disponible en Chrome como window.performance.webkitNow() y, por lo general, este valor es igual al nuevo valor del argumento que se pasó a la devolución de llamada de rAF. Una vez que la especificación avance más en los estándares, el método quitará el prefijo y estará disponible a través de performance.now().

También notarás que los dos valores anteriores son diferentes órdenes de magnitud. performance.now() es una medición de milisegundos de punto flotante desde que esa página en particular comenzó a cargarse (para ser específico, la performance.navigationStart).

En uso

El problema clave de los recortes son las bibliotecas de animación que usan este patrón de diseño:

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

Editar para corregir esto es bastante fácil. Aumenta startTime y now para usar window.performance.now().

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

Esta es una implementación bastante simple: no usa un método now() con prefijo y también da por sentado que es compatible con Date.now(), que no está en IE8.

Detección de atributos

Si no usas el patrón anterior y solo quieres identificar qué tipo de valor de devolución de llamada obtienes, puedes usar esta técnica:

requestAnimationFrame(function(timestamp){

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

    // ...

Verificar if (timestamp < 1e12) es una prueba rápida para ver el tamaño del número que estamos tratando. Técnicamente, podría ser un falso positivo, pero solo si una página web está abierta de forma continua durante 30 años. Sin embargo, no podemos probar si es un número de punto flotante (en lugar de un número entero). Si pides suficientes temporizadores de alta resolución, es posible que obtengas valores enteros en algún momento.

Planeamos implementar este cambio en Chrome 21. Por lo tanto, si ya aprovechas este parámetro de devolución de llamada, asegúrate de actualizar el código.