API requestAnimationFrame – teraz z dokładnością do mniej niż milisekundy

Ilmari Heikkinen

Jeśli korzystasz z requestAnimationFrame, podoba Ci się to, że malowanie jest zsynchronizowane z częstotliwością odświeżania ekranu, co zapewnia najwyższą możliwą jakość animacji. Poza tym oszczędzasz użytkownikom hałas generowany przez wentylator procesora i zużycie baterii, gdy przełączają się na inną kartę.

Wkrótce nastąpi jednak zmiana części interfejsu API. Sygnatura czasowa przekazywana do funkcji wywołania zwrotnego zmienia się z typowej sygnatury czasowej w stylu Date.now() na pomiar o wysokiej rozdzielczości w milisekundach od otwarcia strony. Jeśli używasz tej wartości, musisz zaktualizować kod zgodnie z opisem poniżej.

Dla jasności:

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

Jeśli używasz wspólnego requestAnimFrameshim podanego tutaj, nie używasz wartości sygnatury czasowej. Nie musisz się już martwić. :)

Dlaczego

Dlaczego? Dzięki temu możesz uzyskać maksymalną liczbę klatek na sekundę, czyli 60 FPS, co odpowiada 16,7 ms na klatkę. Jednak pomiar z dokładnością do liczby całkowitej oznacza, że w przypadku wszystkich obserwowanych i docelowych wartości mamy dokładność 1/16.

Porównanie wykresu 16 ms i 16 całkowitych w ms.

Jak widać powyżej, niebieski pasek oznacza maksymalny czas, jaki masz na wykonanie wszystkich działań przed narysowaniem nowego klatki (przy 60 fps). Prawdopodobnie wykonujesz więcej niż 16 działań, ale przy ułamkach milisekund możesz zaplanować i zmierzyć tylko te bardzo duże przyrosty. To za mało.

Minutnik w wysokiej rozdzielczości rozwiązuje ten problem, podając znacznie dokładniejszą wartość:

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

Zegar o wysokiej rozdzielczości jest obecnie dostępny w Chrome jako window.performance.webkitNow(). Ta wartość jest zwykle równa nowej wartości argumentu przekazanej do wywołania zwrotnego rAF. Gdy specyfikacja będzie przebiegać dalej, metoda będzie pomijać prefiks i będzie dostępna przez performance.now().

Zauważ też, że te 2 wartości różnią się o wiele rzędy wielkości. performance.now() to pomiar w milisekundach z przecinkiem dziesiętnym od momentu rozpoczęcia wczytywania danej strony (dokładnie performance.navigationStart).

W użyciu

Największym problemem są biblioteki animacji, które korzystają z tego schematu projektowania:

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

Naprawienie tego błędu jest bardzo proste. Wystarczy zastąpić startTimenow wartością window.performance.now().

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

Jest to dość naiwne rozwiązanie, które nie używa metody z prefiksem now() i zakłada, że Date.now() jest obsługiwana, co nie jest prawdą w przypadku IE8.

Wykrywanie cech

Jeśli nie używasz opisanego powyżej wzorca i chcesz tylko sprawdzić, jaką wartość zwraca funkcja wywołania zwrotnego, możesz użyć tej metody:

requestAnimationFrame(function(timestamp){

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

    // ...

Sprawdzanie if (timestamp < 1e12) to szybki test, który pozwala określić, jak duża jest liczba. Teoretycznie może to być fałszywy alarm, ale tylko wtedy, gdy strona jest otwarta przez 30 lat. Nie możemy jednak sprawdzić, czy jest to liczba zmiennoprzecinkowa (a nie zaokrąglona do liczby całkowitej). Jeśli użyjesz wystarczającej liczby zegarów o wysokiej rozdzielczości, w pewnym momencie otrzymasz wartości całkowite.

Planujemy wdrożenie tej zmiany w Chrome 21, więc jeśli korzystasz już z tego parametru wywołania zwrotnego, zaktualizuj swój kod.