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

Ilmari Heikkinen

Jeśli korzystasz z requestAnimationFrame, pewnie podoba Ci się, że animacje są synchronizowane z częstotliwością odświeżania ekranu, co daje jak najbardziej realistyczne animacje. 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, oto o co mi chodzi:

// 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ę wynoszącą 60 FPS, która jest idealna, ponieważ 60 FPS to 16,7 ms na klatkę. Jednak pomiar z dokładnością do liczby całkowitej oznacza, że w przypadku wszystkich obserwowanych i docelowanych wartości mamy dokładność 1/16.

Porównanie wykresów z wartością 16 ms i 16 ms w liczbach całkowitych.

Jak widać powyżej, niebieski pasek oznacza maksymalny czas, jaki masz na wykonanie wszystkich działań przed narysowaniem nowego kadru (przy 60 fps). Prawdopodobnie wykonujesz więcej niż 16 działań, ale przy całkowitych milisekundach możesz zaplanować i zmierzyć tylko te bardzo duże przyrosty. To nie wystarczy.

Rozwiązaniem jest timer wysokiej rozdzielczości, który zapewnia znacznie dokładniejsze dane:

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 zostanie wdrożona w ramach standardów, metoda utraci prefiks i będzie dostępna w 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 wzorca 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 musisz otrzymać 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.