Worklet animacji Houdiniego

Ulepsz animacje w aplikacji internetowej

TL;DR: Animation Worklet umożliwia pisanie imperatywnych animacji, które działają z natywną częstotliwością klatek urządzenia, co zapewnia dodatkową płynność™ bez zacinania się, sprawia, że animacje są bardziej odporne na zacinanie się wątku głównego, i można je powiązać z przewijaniem zamiast z czasem. Animation Worklet jest dostępny w Chrome Canary (za flagą „Eksperymentalne funkcje platformy internetowej”) i planujemy testowanie origin w Chrome 71. Możesz zacząć z niej korzystać jako z ulepszenia progresywnego już dziś.

Inny interfejs Animation API?

Nie, to rozszerzenie tego, co już mamy, i to z dobrego powodu. Zacznijmy od początku. Jeśli chcesz animować dowolny element DOM w internecie, masz 2,5 możliwości: przejścia CSS do prostych przejść z A do B, animacje CSS do potencjalnie cyklicznych, bardziej złożonych animacji opartych na czasie i interfejs Web Animations API (WAAPI) do niemal dowolnie złożonych animacji. Macierz obsługi WAAPI wygląda dość ponuro, ale sytuacja się poprawia. Do tego czasu dostępny jest polyfill.

Wszystkie te metody są bezstanowe i zależą od czasu. Niektóre efekty, które deweloperzy próbują uzyskać, nie są jednak ani zależne od czasu, ani bezstanowe. Na przykład słynny efekt paralaksy jest, jak sama nazwa wskazuje, oparty na przewijaniu. Wdrożenie wydajnego przewijania z efektem paralaksy w internecie jest obecnie zaskakująco trudne.

A co z bezpaństwowcami? Weźmy na przykład pasek adresu w Chrome na Androida. Jeśli przewiniesz w dół, zniknie z widoku. Ale gdy tylko przewiniesz w górę, pasek wróci, nawet jeśli jesteś w połowie strony. Animacja zależy nie tylko od pozycji przewijania, ale też od poprzedniego kierunku przewijania. Jest stanowa.

Kolejnym problemem jest stylizacja pasków przewijania. Są one znane z tego, że nie można ich stylizować lub przynajmniej nie w wystarczającym stopniu. Co zrobić, jeśli chcę, aby paskiem przewijania był nyan cat? Niezależnie od wybranej techniki utworzenie niestandardowego paska przewijania nie jest ani wydajne, ani łatwe.

Wszystkie te rzeczy są niewygodne i trudne lub wręcz niemożliwe do skutecznego wdrożenia. Większość z nich opiera się na zdarzeniach lub requestAnimationFrame, co może ograniczać liczbę klatek do 60 FPS, nawet jeśli ekran może wyświetlać 90, 120 FPS lub więcej, i wykorzystywać tylko niewielką część cennego budżetu klatek głównego wątku.

Animation Worklet rozszerza możliwości stosu animacji internetowych, aby ułatwić tworzenie tego rodzaju efektów. Zanim zaczniemy, upewnijmy się, że znamy podstawy animacji.

Podstawy animacji i osi czasu

WAAPI i Animation Worklet w dużym stopniu korzystają z osi czasu, aby umożliwić Ci koordynowanie animacji i efektów w wybrany przez Ciebie sposób. Ta sekcja zawiera krótkie przypomnienie lub wprowadzenie do osi czasu i wyjaśnienie, jak działają one w przypadku animacji.

Każdy dokument ma document.timeline. Zaczyna się od 0, gdy dokument zostanie utworzony, i odlicza milisekundy od momentu jego utworzenia. Wszystkie animacje w dokumencie działają w odniesieniu do tej osi czasu.

Aby to lepiej zobrazować, przyjrzyjmy się temu fragmentowi 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łamy animation.play(), animacja użyje currentTime osi czasu jako czasu rozpoczęcia. Nasza animacja ma opóźnienie 3000 ms, co oznacza, że rozpocznie się (lub stanie się „aktywna”), gdy oś czasu osiągnie wartość `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, w którym momencie animacji się znajdujemy.

Gdy animacja osiągnie ostatnią klatkę kluczową, wróci do pierwszej i rozpocznie kolejną iterację. Ten proces powtarza się łącznie 3 razy, ponieważ ustawiliśmy wartość iterations: 3. Jeśli chcemy, aby animacja nigdy się nie zatrzymywała, wpisujemy iterations: Number.POSITIVE_INFINITY. Oto wynik powyższego kodu.

WAAPI to bardzo zaawansowany interfejs API, który ma wiele innych funkcji, takich jak wygładzanie, przesunięcia początkowe, wagi klatek kluczowych i zachowanie wypełnienia, które wykraczają poza zakres tego artykułu. Jeśli chcesz dowiedzieć się więcej, przeczytaj ten artykuł o animacjach CSS w CSS Tricks.

Pisanie Animation Worklet

Teraz, gdy znamy już koncepcję osi czasu, możemy zacząć przyglądać się Animation Worklet i sposobom, w jakie pozwala ona manipulować osiami czasu. Interfejs Animation Worklet API nie tylko bazuje na WAAPI, ale – w sensie rozszerzalnej sieci – jest też prymitywem niższego poziomu, który wyjaśnia, jak działa WAAPI. Pod względem składni są one bardzo podobne:

Worklet animacji 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 polega na pierwszym parametrze, czyli nazwie workletu, który steruje tą animacją.

Wykrywanie cech

Chrome jest pierwszą przeglądarką, która udostępnia tę funkcję, więc musisz zadbać o to, aby Twój kod nie zakładał, że AnimationWorklet zawsze będzie dostępny. Zanim więc wczytamy worklet, powinniśmy sprawdzić, czy przeglądarka użytkownika obsługuje AnimationWorklet. Możemy to zrobić w prosty sposób:

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

Wczytywanie workletu

Worklety to nowe pojęcie wprowadzone przez zespół Houdini, aby ułatwić tworzenie i skalowanie wielu nowych interfejsów API. Szczegóły dotyczące workletów omówimy później, ale na razie możesz myśleć o nich jako o tanich i lekkich wątkach (takich jak instancje robocze).

Musimy się upewnić, że wczytaliśmy worklet o nazwie „passthrough”, zanim zadeklarujemy animację:

// 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? Rejestrujemy klasę jako animatora za pomocą wywołania registerAnimator() AnimationWorklet, nadając jej nazwę „passthrough”. Jest to ta sama nazwa, której użyliśmy w konstruktorze WorkletAnimation() powyżej. Po zakończeniu rejestracji obietnica zwrócona przez addModule() zostanie spełniona i będzie można zacząć tworzyć animacje za pomocą tego workletu.

Metoda animate() naszej instancji będzie wywoływana dla każdej klatki, którą przeglądarka chce wyrenderować. Przekazywane będą currentTime osi czasu animacjicurrentTime oraz efekt, który jest aktualnie przetwarzany. Mamy tylko jeden efekt, KeyframeEffect, i używamy currentTime, aby ustawić localTime efektu, dlatego ten animator nazywa się „passthrough”. Dzięki temu kodowi w przypadku workletu WAAPI i AnimationWorklet powyżej zachowują się dokładnie tak samo, co widać w demonstracji.

Godzina

Parametr currentTime naszej metody animate() to currentTime osi czasu, którą przekazaliśmy do konstruktora WorkletAnimation(). W poprzednim przykładzie po prostu przekazaliśmy ten czas do efektu. Ale ponieważ jest 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)
      );
    }
  }
);

Bierzemy Math.sin()currentTime i mapujemy tę wartość na zakres [0; 2000], czyli zakres czasu, dla którego zdefiniowany jest nasz efekt. Teraz animacja wygląda zupełnie inaczej, mimo że nie zmieniliśmy klatek kluczowych ani opcji animacji. Kod workletu może być dowolnie złożony i umożliwia programowe określanie, które efekty mają być odtwarzane, w jakiej kolejności i w jakim stopniu.

Opcje dotyczące opcji

Możesz ponownie użyć komponentu i zmienić w nim liczby. Dlatego konstruktor WorkletAnimation umożliwia 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ą sterowane tym samym kodem, ale z różnymi opcjami.

Podaj nazwę stanu, w którym mieszkasz.

Jak już wspomniałem, jednym z głównych problemów, które ma rozwiązać animation worklet, są animacje stanowe. Worklety animacji mogą przechowywać stan. Jedną z głównych cech workletów jest jednak to, że można je przenosić do innego wątku, a nawet usuwać, aby oszczędzać zasoby, co spowoduje też usunięcie ich stanu. Aby zapobiec utracie stanu, element roboczy animacji udostępnia wywołanie zwrotne, które jest wywoływane przed zniszczeniem elementu roboczego. Możesz go użyć do zwrócenia obiektu stanu. Ten obiekt zostanie przekazany do konstruktora podczas ponownego tworzenia workletu. Po 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żysz ten tryb demo, kwadrat ma 50% szans na obrócenie się w każdym z kierunków. Jeśli przeglądarka zamknie worklet i przeniesie go do innego wątku, przy jego tworzeniu nastąpi kolejne wywołanie Math.random(), co może spowodować nagłą zmianę kierunku. Aby tego uniknąć, zwracamy animacje w losowo wybranym kierunku jako stan i używamy go w konstruktorze, jeśli jest dostępny.

Wpływanie na kontinuum czasoprzestrzenne: ScrollTimeline

Jak pokazaliśmy w poprzedniej sekcji, AnimationWorklet umożliwia programowe określanie, jak przesuwanie osi czasu wpływa na efekty animacji. Do tej pory nasza oś czasu zawsze była document.timeline, która śledzi czas.

ScrollTimeline otwiera nowe możliwości i pozwala sterować animacjami za pomocą przewijania zamiast czasu. Na potrzeby tego pokazu ponownie wykorzystamy nasz pierwszy worklet „passthrough”:

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ć wartość document.timeline, tworzymy nową wartość ScrollTimeline. Jak zapewne się domyślasz, ScrollTimeline nie używa czasu, ale pozycji przewijania scrollSource, aby ustawić currentTime w worklecie. Przewinięcie do góry (lub w lewo) oznacza currentTime = 0, a przewinięcie do dołu (lub w prawo) ustawia currentTime na timeRange. Jeśli przewiniesz pole w tej demonstracji, możesz kontrolować położenie czerwonego pola.

Jeśli utworzysz ScrollTimeline z elementem, który się nie przewija, currentTime na osi czasu będzie NaN. Dlatego, zwłaszcza w przypadku projektowania responsywnego, zawsze musisz być przygotowany na NaN jako currentTime. Często warto ustawić domyślną wartość 0.

Łączenie animacji z pozycją przewijania to coś, czego od dawna poszukiwano, ale nigdy nie udało się osiągnąć tego poziomu wierności (poza nieeleganckimi obejściami z CSS3D). Animation Worklet umożliwia proste wdrożenie tych efektów przy zachowaniu wysokiej wydajności. Na przykład efekt przewijania z paralaksą, taki jak na tym demo, pokazuje, że zdefiniowanie animacji opartej na przewijaniu zajmuje teraz tylko kilka wierszy.

Dla zaawansowanych

Worklety

Worklety to konteksty JavaScript o izolowanym zakresie i bardzo małej powierzchni interfejsu API. Niewielka powierzchnia interfejsu API umożliwia bardziej agresywną optymalizację w przeglądarce, zwłaszcza na urządzeniach z niższej półki. Dodatkowo worklety nie są powiązane z określoną pętlą zdarzeń, ale w razie potrzeby można je przenosić między wątkami. Jest to szczególnie ważne w przypadku AnimationWorklet.

Synchronizacja kompozytora

Możesz wiedzieć, że niektóre właściwości CSS można szybko animować, a inne nie. Niektóre właściwości wymagają tylko pracy procesora graficznego, aby można było je animować, a inne zmuszają przeglądarkę do ponownego układania całego dokumentu.

W Chrome (podobnie jak w wielu innych przeglądarkach) mamy proces o nazwie kompozytor, którego zadaniem jest – i bardzo to upraszczam – rozmieszczanie warstw i tekstur, a następnie wykorzystywanie procesora graficznego do jak najczęstszego odświeżania ekranu, najlepiej tak szybko, jak to możliwe (zwykle 60 Hz). W zależności od tego, które właściwości CSS są animowane, przeglądarka może potrzebować tylko pracy kompozytora, podczas gdy inne właściwości wymagają uruchomienia układu, czyli operacji, którą może wykonać tylko wątek główny. W zależności od tego, które właściwości planujesz animować, Twój moduł roboczy animacji będzie powiązany z wątkiem głównym lub będzie działać w osobnym wątku zsynchronizowanym z kompozytorem.

pobłażliwe potraktowanie,

Zwykle jest tylko jeden proces kompozytora, który może być współdzielony przez wiele kart, ponieważ GPU jest zasobem o dużej konkurencji. Jeśli kompozytor zostanie w jakiś sposób zablokowany, cała przeglądarka przestanie działać i nie będzie reagować na działania użytkownika. Należy tego za wszelką cenę unikać. Co się stanie, jeśli worklet nie dostarczy kompozytorowi danych potrzebnych do wyrenderowania klatki na czas?

W takim przypadku element roboczy może się „poślizgnąć” zgodnie ze specyfikacją. Jest on opóźniony w stosunku do kompozytora, który może ponownie wykorzystać dane z ostatniej klatki, aby utrzymać wysoką liczbę klatek na sekundę. Wizualnie będzie to wyglądać jak zacinanie się, ale duża różnica polega na tym, że przeglądarka nadal reaguje na działania użytkownika.

Podsumowanie

AnimationWorklet ma wiele aspektów i przynosi wiele korzyści w internecie. Oczywiste korzyści to większa kontrola nad animacjami i nowe sposoby ich tworzenia, które pozwalają osiągnąć nowy poziom wierności wizualnej w internecie. Jednak konstrukcja interfejsów API pozwala też zwiększyć odporność aplikacji na zacinanie się, a jednocześnie uzyskać dostęp do wszystkich nowych funkcji.

Animation Worklet jest dostępny w wersji Canary, a w Chrome 71 planujemy wprowadzić testowanie źródła. Z niecierpliwością czekamy na Twoje nowe, wspaniałe rozwiązania internetowe i opinie na temat tego, co możemy ulepszyć. Dostępny jest też polyfill, który udostępnia ten sam interfejs API, ale nie zapewnia izolacji wydajności.

Pamiętaj, że przejścia CSS i animacje CSS są nadal prawidłowe i mogą być znacznie prostsze w przypadku podstawowych animacji. Jeśli jednak potrzebujesz bardziej zaawansowanych funkcji, możesz skorzystać z interfejsu AnimationWorklet.