Worklet animacji Houdiniego

Udoskonalaj animacje swojej aplikacji internetowej

TL;DR: aplikacja Worklet animacji umożliwia pisanie imperatywnych animacji, które będą wyświetlane z natywną szybkością klatek na urządzeniu, aby zapewnić dodatkową płynność działania, a także odporność na zawinięcie głównego wątku i możliwość przewijania. Worklet animacji jest dostępny w Chrome Canary (poniżej flagi „Eksperymentalne funkcje platformy internetowej”) i planujemy wersję próbną origin Chrome 71. Możesz zacząć używać go jako progresywnego ulepszenia już dziś.

Inny interfejs Animation API?

Właściwie to jest to rozszerzenie tego, co już mamy – i masz ku temu dobry powód. Zacznijmy od początku. Jeśli chcesz animować dowolny element DOM w internecie, masz obecnie do wyboru 2 1⁄2 możliwości: przejścia CSS do prostych przejść od A do B, animacje CSS – do potencjalnie cyklicznych, bardziej złożonych animacji opartych na czasie, oraz interfejs Web Animations API (WAAPI) do niemal dowolnie złożonych animacji. Tablica obsługi WAAPI wygląda dość kiepsko, ale jest w budowie. Do tego czasu służy polyfill.

Cechą wspólną wszystkich tych metod jest to, że są bezstanowe i zależą od czasu. Niektóre efekty, których próbują programiści, nie są zależne od czasu ani bezstanowe. Na przykład owiana złą sławą funkcja przewijania z paralaksą jest, jak sama nazwa wskazuje, przewijana. Wdrożenie w internecie wydajnego narzędzia do przewijania z paralaksą jest obecnie zaskakująco trudne.

A co z bezstanem? Popatrzmy na przykład na pasek adresu Chrome na Androidzie. Jeśli przewiniesz w dół, zniknie on z widoku. Gdy jednak przewiniesz stronę w górę, reklama pojawi się ponownie, nawet jeśli jesteś w połowie drogi w dół strony. Animacja zależy nie tylko od pozycji przewijania, ale także od poprzedniego kierunku przewijania. Ma wartość stanowa.

Inny problem to styl pasków przewijania. Są one często niezbyt stylowe, a przynajmniej za mało stylowe. Co zrobić, jeśli chcę, aby kot nyan pełnił funkcję paska przewijania? Niezależnie od wybranej techniki utworzenie niestandardowego paska przewijania nie jest ani skuteczne, ani łatwe.

Chodzi o to, że wszystkie te elementy są niezręczne i trudne do efektywnego wdrożenia. Większość z nich bazuje na zdarzeniach lub parametrach requestAnimationFrame, które mogą utrzymać szybkość 60 kl./s, nawet jeśli ekran działa z szybkością 90, 120 kl./s lub większą i wykorzystuje ułamek Twojego cennego budżetu na klatkę w wątku głównym.

Worklet animacji zwiększa możliwości internetowego stosu animacji, aby ułatwić korzystanie z tego rodzaju efektów. Zanim przejdziemy do szczegółów, sprawdźmy, czy opanujemy podstawy animacji.

Podstawowe informacje o animacjach i osi czasu

WAAPI i Animation Worklet wykorzystują oś czasu, aby umożliwić Ci organizowanie animacji i efektów w wybrany przez siebie sposób. Ta sekcja to krótkie przypomnienie i wprowadzenie do osi czasu i sposobu ich współpracy z animacjami.

Każdy dokument ma document.timeline. Zaczynamy od zera w momencie utworzenia dokumentu i zliczamy w milisekundach, które upłynęły od momentu utworzenia dokumentu. Wszystkie animacje w dokumencie działają względem tej osi czasu.

Spójrzmy na ten fragment kodu WAAPI,

const animation = new Animation(
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
      {
        transform: 'translateY(500px)',
      },
    ],
    {
      delay: 3000,
      duration: 2000,
      iterations: 3,
    }
  ),
  document.timeline
);

animation.play();

Gdy wywołujemy animation.play(), czas rozpoczęcia animacji określa currentTime osi czasu. Opóźnienie animacji wynosi 3000 ms, co oznacza, że animacja zostanie uruchomiona (lub stanie się aktywna), gdy oś czasu osiągnie „startTime”

  • 3000. After that time, the animation engine will animate the given element from the first keyframe (translateX(0)), through all intermediate keyframes (translateX(500px)) all the way to the last keyframe (translateY(500px)) in exactly 2000ms, as prescribed by thedurationoptions. Since we have a duration of 2000ms, we will reach the middle keyframe when the timeline'scurrentTimeisstartTime + 3000 + 1000and the last keyframe atstartTime + 3000 + 2000`. Chodzi o to, że oś czasu określa miejsce w animacji.

Gdy animacja osiągnie ostatnią klatkę kluczową, nastąpi powrót do pierwszej klatki kluczowej i rozpocznie się jej kolejne wystąpienie. Ten proces powtarza się łącznie 3 razy, od momentu ustawienia iterations: 3. Jeśli chcesz, żeby animacja nigdy się nie zatrzymywała, użyjbym parametru iterations: Number.POSITIVE_INFINITY. Oto wynik powyższego kodu.

Interfejs WAAPI jest niezwykle zaawansowany i zawiera wiele innych funkcji, takich jak wygładzanie, przesunięcia rozpoczęcia, ważenie klatek kluczowych i zachowanie wypełnienia, które nie mieszczą się w zakresie opisanym w tym artykule. Jeśli chcesz dowiedzieć się więcej, przeczytaj ten artykuł o animacjach CSS w tricks CSS.

Pisanie Worklet animacji

Skoro wspomnieliśmy już o osi czasu, możemy zająć się Workletem Animation i to, jak można go bawić z osią czasu. Interfejs Animation Worklet API nie tylko bazuje na WAAPI, ale też – w kontekście rozwijalnej sieci – jest podstawowym edytorem podstawowym objaśniającym sposób działania WAAPI. Pod względem składni są one bardzo podobne:

Worklet animacji protokół WAAPI
new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)'
      },
      {
        transform: 'translateX(500px)'
      }
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY
    }
  ),
  document.timeline
).play();
      
        new Animation(

        new KeyframeEffect(
        document.querySelector('#a'),
        [
        {
        transform: 'translateX(0)'
        },
        {
        transform: 'translateX(500px)'
        }
        ],
        {
        duration: 2000,
        iterations: Number.POSITIVE_INFINITY
        }
        ),
        document.timeline
        ).play();
        

Różnica dotyczy pierwszego parametru, który jest nazwą workletu, który generuje tę animację.

Wykrywanie funkcji

Chrome to pierwsza przeglądarka, która udostępnia tę funkcję, więc musisz się upewnić, że Twój kod nie zawiera jedynie AnimationWorklet. Przed wczytaniem workletu musimy więc sprawdzić, czy przeglądarka użytkownika obsługuje AnimationWorklet:

if ('animationWorklet' in CSS) {
  // AnimationWorklet is supported!
}

Wczytuję worklet

Worklety to nowa koncepcja wprowadzona przez zespół zadaniowy Houdini, aby ułatwić tworzenie i skalowanie wielu nowych interfejsów API. Szczegóły Workletów omówimy nieco później, ale dla uproszczenia można je na razie wyobrazić sobie jako tanie i lekkie wątki (takie jak instancje robocze).

Przed zadeklarowaniem animacji musimy się upewnić, że został wczytany worklet o nazwie „passthrough”:

// index.html
await CSS.animationWorklet.addModule('passthrough-aw.js');
// ... WorkletAnimation initialization from above ...

// passthrough-aw.js
registerAnimator(
  'passthrough',
  class {
    animate(currentTime, effect) {
      effect.localTime = currentTime;
    }
  }
);

Co się tutaj dzieje? Zapisujemy zajęcia jako animator, używając wywołania registerAnimator() funkcji AnimationWorklet, nadpisując je jako „passthrough”. Ta sama nazwa została użyta w konstruktorze WorkletAnimation() powyżej. Po zakończeniu rejestracji obietnica zwrócona przez addModule() przestanie działać i będziemy mogli zacząć tworzyć animacje przy użyciu tego workletu.

Metoda animate() w naszej instancji będzie wywoływana dla każdej klatki, którą przeglądarka chce wyrenderować. Przekazuje ona currentTime osi czasu animacji oraz aktualnie przetwarzany efekt. Mamy tylko jeden efekt – KeyframeEffect i używamy currentTime do ustawiania efektu localTime. Dlatego ten animator nazywa się „przekazywaniem”. Po zastosowaniu tego kodu dla workletu WAAPI i AnimationWorklet powyżej działają dokładnie tak samo, jak widać w prezentacji.

Godzina

Parametr currentTime metody animate() to wartość currentTime osi czasu przekazanej do konstruktora WorkletAnimation(). W poprzednim przykładzie omamy ten czas w efekcie. A ponieważ to kod JavaScript, możemy zniekształcić czas 💫

function remap(minIn, maxIn, minOut, maxOut, v) {
  return ((v - minIn) / (maxIn - minIn)) * (maxOut - minOut) + minOut;
}
registerAnimator(
  'sin',
  class {
    animate(currentTime, effect) {
      effect.localTime = remap(
        -1,
        1,
        0,
        2000,
        Math.sin((currentTime * 2 * Math.PI) / 2000)
      );
    }
  }
);

Pobieramy Math.sin() z parametru currentTime i zmieniamy go na zakres [0; 2000], czyli zakres czasowy, dla którego został zdefiniowany nasz efekt. Teraz animacja wygląda zupełnie inaczej bez żadnych zmian w klatkach kluczowych i opcjach animacji. Kod workletu może być arbitralnie złożony i umożliwia automatyczne definiowanie, które efekty są odtwarzane w jakiej kolejności i w jakim stopniu.

Opcje dotyczące opcji

Warto ponownie użyć workletu i zmienić jego numery. Z tego powodu konstruktor WorkletAnimation pozwala na przekazanie do workletu obiektu opcji:

registerAnimator(
  'factor',
  class {
    constructor(options = {}) {
      this.factor = options.factor || 1;
    }
    animate(currentTime, effect) {
      effect.localTime = currentTime * this.factor;
    }
  }
);

new WorkletAnimation(
  'factor',
  new KeyframeEffect(
    document.querySelector('#b'),
    [
      /* ... same keyframes as before ... */
    ],
    {
      duration: 2000,
      iterations: Number.POSITIVE_INFINITY,
    }
  ),
  document.timeline,
  {factor: 0.5}
).play();

W tym przykładzie obie animacje są generowane za pomocą tego samego kodu, ale z różnymi opcjami.

Pokaż swój lokalny stan!

Jak już wspomniano, jednym z najważniejszych problemów, jakie należy rozwiązać w workletu animacji, są animacje stanowe. Worklety animacji mogą pozostawać w stanie. Jedną z głównych cech workletów jest to, że można je przenieść do innego wątku, a nawet zostać zniszczone, aby zaoszczędzić zasoby, co również spowodowałoby zniszczenie ich stanu. Aby zapobiec utracie stanu, skoroszyt animacji udostępnia zaczep, który jest wywoływany przed zniszczeniem workletu, który umożliwia zwrócenie obiektu stanu. Obiekt zostanie przekazany do konstruktora podczas ponownego tworzenia workletu. Po początkowym utworzeniu ten parametr będzie miał wartość undefined.

registerAnimator(
  'randomspin',
  class {
    constructor(options = {}, state = {}) {
      this.direction = state.direction || (Math.random() > 0.5 ? 1 : -1);
    }
    animate(currentTime, effect) {
      // Some math to make sure that `localTime` is always > 0.
      effect.localTime = 2000 + this.direction * (currentTime % 2000);
    }
    destroy() {
      return {
        direction: this.direction,
      };
    }
  }
);

Za każdym razem, gdy odświeżasz tę prezentację, masz prawdopodobieństwo 50/50, w którym kierunku kwadrat zacznie się obracać. Gdyby przeglądarka zniszczyła worklet i przeniosła go do innego wątku, w momencie tworzenia miałoby miejsce kolejne wywołanie Math.random(), które mogło spowodować nagłą zmianę kierunku. Aby tego uniknąć, zwracamy losowo wybrany kierunek animacji jako state i używamy go w konstruktorze, jeśli jest podany.

Wciągająca kontynuacja czasu w kosmosie: ScrollTimeline

Jak pokazaliśmy w poprzedniej sekcji, AnimationWorklet pozwala nam automatycznie definiować, jak przesuwanie osi czasu wpływa na efekty animacji. Na tę chwilę nasza oś czasu zawsze to document.timeline, która śledzi czas.

ScrollTimeline otwiera nowe możliwości i umożliwia generowanie animacji przez przewijanie, a nie na czas. Wykorzystamy nasz pierwszy worklet „przekazywalny” w tej prezentacji:

new WorkletAnimation(
  'passthrough',
  new KeyframeEffect(
    document.querySelector('#a'),
    [
      {
        transform: 'translateX(0)',
      },
      {
        transform: 'translateX(500px)',
      },
    ],
    {
      duration: 2000,
      fill: 'both',
    }
  ),
  new ScrollTimeline({
    scrollSource: document.querySelector('main'),
    orientation: 'vertical', // "horizontal" or "vertical".
    timeRange: 2000,
  })
).play();

Zamiast przekazywać document.timeline tworzymy nowy obiekt ScrollTimeline. Jak można się domyślić, ScrollTimeline nie używa czasu, ale pozycja przewijania elementu scrollSource ustawia currentTime w workletu. Przewinięcie do samej góry (lub w lewo) powoduje ustawienie wartości currentTime = 0, a przewijanie do samego dołu (lub w prawo) powoduje ustawienie wartości currentTime na timeRange. Możesz kontrolować pozycję czerwonego pola, jeśli przewiniesz pole w tej prezentacji.

Jeśli utworzysz ScrollTimeline z elementem, który się nie przewija, currentTime osi czasu będzie mieć wartość NaN. Jeśli korzystasz z elastycznego projektowania stron, przygotuj się na NaN jako currentTime. Często dobrze jest ustawić wartość domyślną 0.

Od dawna szukaliśmy sposobu łączenia animacji z pozycją przewijania, ale tak naprawdę nigdy nie udało się osiągnąć takiego poziomu wierności (poza chaotycznymi sposobami obejścia problemu z CSS3D). Worklet animacji umożliwia łatwe implementowanie tych efektów przy zachowaniu dużej skuteczności. Na przykład: efekt przewijania z efektem paralaksy taki jak ta prezentacja pokazuje, że do zdefiniowania animacji przewijanej potrzeba teraz zaledwie kilku wierszy.

Dla zaawansowanych

Worklety

Worklety to konteksty JavaScript o izolowanym zakresie i bardzo małą powierzchnią interfejsu API. Niewielka powierzchnia interfejsu API umożliwia bardziej agresywną optymalizację z poziomu przeglądarki, zwłaszcza na słabszych urządzeniach. Poza tym Worklety nie są powiązane z konkretną pętlą zdarzeń, ale w razie potrzeby mogą być przenoszone między wątkami. Jest to szczególnie ważne w przypadku AnimationWorklet.

Kompozytor NSync

Pewne właściwości CSS szybko się animują, a inne nie. Niektóre właściwości wymagają tylko trochę pracy na GPU, a inne wymuszają zmianę układu całego dokumentu przez przeglądarkę.

W Chrome (podobnie jak w wielu innych przeglądarkach) mamy do czynienia z procesem nazywanym kompozytorem, który to zadanie – i tu bardzo upraszczam – układanie warstw i tekstur oraz układanie warstw i tekstur przez GPU, a następnie jak najszybsze aktualizowanie ekranu (najlepiej jak najszybciej (zwykle 60 Hz). W zależności od tego, jakie właściwości CSS mają być animowane, przeglądarka może po prostu potrzebować działania kompozytora, podczas gdy inne właściwości muszą uruchamiać układ, co jest możliwe tylko dla wątku głównego. W zależności od właściwości, które chcesz animować, Worklet animacji będzie powiązany z wątkiem głównym lub uruchomiony w oddzielnym wątku zsynchronizowanym z kompozytorem.

Klaszcz na nadgarstku

Zwykle dostępny jest tylko 1 proces kompozytora, który może być współużytkowany na wielu kartach, ponieważ GPU jest intensywnym zasobem. Jeśli kompozytor zostanie w jakiś sposób zablokowany, cała przeglądarka zostanie zatrzymana i przestanie reagować na dane wejściowe użytkownika. Należy tego unikać za wszelką cenę. Co się więc stanie, jeśli Twój worklet nie będzie mógł dostarczyć danych, których kompozytor potrzebuje na czas, aby wyrenderować ramkę?

W takim przypadku worklet jest dozwolony – zgodnie ze specyfikacją – do „slip”. Trafia on za kompozytor, który może ponownie wykorzystać dane ostatniej klatki, aby utrzymać liczbę klatek na sekundę. Pod względem wizualnym będzie to wyglądać jak dziwna sprawa, ale główna różnica polega na tym, że przeglądarka wciąż reaguje na dane wejściowe użytkownika.

Podsumowanie

AnimationWorklet ma wiele aspektów i korzyści, jakie przynosi ono w internecie. Oczywistymi zaletami są większą kontrolę nad animacjami i nowe sposoby na wprowadzanie animacji w internecie na zupełnie nowy poziom jakości wizualnej. Projektowanie interfejsów API sprawia też, że aplikacje są odporne na uszkodzenia, a jednocześnie zapewniają dostęp do wielu nowych funkcji.

Worklet animacji jest dostępny w wersji Canary. Dążymy do tego, aby była dostępna w wersji próbnej origin Chrome 71. Czekamy z niecierpliwością na Twoje nowe wrażenia z korzystania z internetu i o tym, co możemy ulepszyć. Dostępny jest też polyfill, który zapewnia ten sam interfejs API, ale nie zapewnia izolacji wydajności.

Pamiętaj, że przejścia CSS i animacje CSS nadal są prawidłowymi opcjami, ale w przypadku podstawowych animacji mogą być znacznie prostsze. Jeśli jednak chcesz zrobić coś fantazyjnego, skorzystaj z AnimationWorklet.