Worklet animacji Houdiniego

Zwiększ atrakcyjność animacji w swojej aplikacji internetowej

TL;DR: moduł pracy animacji umożliwia tworzenie animacji imperatywnej, które działają z rodzinną częstotliwością odświeżania urządzenia, co zapewnia dodatkową płynność™, czyni animacje bardziej odpornymi na problemy z głównym wątkiem i umożliwia ich łączenie z przewijaniem zamiast czasu. Worklet animacji jest dostępny w Chrome Canary (za flagą „Experimental Web Platform features”), a w przyszłości planujemy testowanie origin w Chrome 71. Możesz zacząć z niego korzystać jako z funkcji ulepszania stopniowego już dziś.

Inny interfejs Animation API?

Nie, to rozszerzenie tego, co już mamy, i to nie bez powodu. Zacznijmy od początku. Jeśli chcesz animować dowolny element DOM w internecie, masz 2 i 1/2 opcji: przejścia CSS do prostych przejść A do B, animacja CSS do potencjalnie cyklicznych, bardziej złożonych animacji opartych na czasie oraz interfejs API animacji internetowych (WAAPI) do dowolnie złożonych animacji. Macierz obsługi WAAPI wygląda dość źle, ale rośnie. Do tego czasu możesz użyć polyfilla.

Wszystkie te metody mają to wspólne, że są bezosobowe i sterowane czasem. Jednak niektóre efekty, które próbują uzyskać deweloperzy, nie są ani zależne od czasu, ani stanowe. Na przykład słynne narzędzie do przewijania paralaksy polega na przewijaniu, jak wskazuje nazwa. Zaimplementowanie w internecie skutecznego mechanizmu paralaksy na potrzeby przewijania jest obecnie zaskakująco trudne.

A co z państwem? Pomyśl na przykład o pasku adresu Chrome na Androidzie. Jeśli przewiniesz w dół, zniknie z pola widzenia. Gdy przewiniesz w górę, wróci ono na swoje miejsce, nawet jeśli jesteś w połowie strony. Animacja zależy nie tylko od pozycji przewijania, ale też od poprzedniego kierunku przewijania. Jest stanowy.

Innym problemem jest stylizacja suwaków. Takie tytuły są wyjątkowo niestylowe, a przynajmniej za mało stylu. Co zrobić, jeśli chcę mieć kota Nyan jako suwaka? Niezależnie od wybranej metody tworzenie niestandardowego paska przewijania nie jest ani wydajne, ani łatwe.

Chodzi o to, że wszystkie te kwestie są niewygodne i trudne do skutecznego wdrożenia. Większość z nich korzysta z zdarzeń lub funkcji requestAnimationFrame, które mogą zapewnić płynność obrazu na poziomie 60 FPS, nawet jeśli Twój ekran obsługuje 90 FPS, 120 FPS lub wyższą wartość, i wykorzystywać ułamek cennego budżetu wątku głównego.

Worklet animacji rozszerza możliwości internetowego stosu animacji, aby uprościć tego rodzaju efekty. Zanim przejdziemy do szczegółów, przypomnijmy sobie podstawowe informacje o animacjach.

Wprowadzenie do animacji i osi czasu

WAAPI i Animation Worklet korzystają z osi czasu, aby umożliwić Ci sterowanie animacjami i efektami w wybrany sposób. W tej sekcji znajdziesz krótkie przypomnienie lub wprowadzenie do osi czasu i ich działania w przypadku animacji.

Każdy dokument zawiera document.timeline. Wartość ta wynosi 0 w momencie utworzenia dokumentu i jest zliczana w milisekundach od tego momentu. Wszystkie animacje dokumentu działają względem tej osi czasu.

Aby zyskać więcej konkretów, 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łujemy funkcję animation.play(), jako czas rozpoczęcia animacji pojawia się currentTime osi czasu. Nasza animacja ma opóźnienie 3000 ms, co oznacza, że animacja rozpocznie się (lub stanie się „aktywna”), gdy linia 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 linia czasu określa, na jakim etapie animacji jesteśmy.

Gdy animacja dojdzie do ostatniej klatki kluczowej, wróci do pierwszej klatki kluczowej i rozpocznie kolejną iterację animacji. Ten proces powtarza się łącznie 3 razy od ustawienia parametru iterations: 3. Jeśli chcemy, aby animacja nigdy się nie zatrzymywała, wpisujemy iterations: Number.POSITIVE_INFINITY. Oto wynik wykonania kodu powyżej.

WAAPI jest niesamowicie potężnym interfejsem API, który zawiera wiele innych funkcji, takich jak łagodnienie, przesunięcia początkowe, wagi klatek keyframe i zachowanie wypełniania, które wykraczają poza zakres tego artykułu. Jeśli chcesz dowiedzieć się więcej, przeczytaj ten artykuł na temat animacji CSS na stronie CSS Tricks.

Tworzenie animacji Worklet

Skoro znamy już koncepcję osi czasu, możemy więc przejść do Worklet animacji i dowiedzieć się, w jaki sposób pozwala on bawić się osiami czasu. Interfejs Animation Worklet API nie tylko opiera się na WAAPI, ale jest też – w rozumieniu rozszerzalnej sieci – prymitywem niższego poziomu, który wyjaśnia, jak działa WAAPI. Pod względem składni są 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, który jest nazwą elementu roboczego, który obsługuje tę animację.

Wykrywanie cech

Chrome jest pierwszą przeglądarką, która obsługuje tę funkcję, więc musisz się upewnić, że Twój kod nie oczekuje tylko AnimationWorklet. Dlatego przed załadowaniem workletu należy sprawdzić, czy przeglądarka użytkownika obsługuje AnimationWorklet. Aby to zrobić, wystarczy wykonać tę prostą czynność:

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

Wczytywanie workleta

Worklety to nowa koncepcja wprowadzona przez zespół zadaniowy Houdini, która ułatwia tworzenie i skalowanie wielu nowych interfejsów API. Szczegóły dotyczące workletów omówimy później, ale na razie możesz je traktować jako tanie i lekkie wątki (jak wątki robocze).

Zanim zadeklarujesz animację, musisz się upewnić, że został załadowany element roboczy 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? 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(). Po zakończeniu rejestracji obietnica zwrócona przez addModule() zostanie rozwiązana i będziemy mogli 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ć, przekazując currentTime osi czasu animacji oraz aktualnie przetwarzany efekt. Mamy tylko jeden efekt, KeyframeEffect, i używamy currentTime do ustawienia localTime efektu, dlatego ten animator nazywa się „passthrough”. Dzięki temu kodowi worklet, WAAPI i AnimationWorklet działają dokładnie tak samo, jak widać to w demo.

Godzina

Parametr currentTime metody animate() to currentTime osi czasu przekazanej do konstruktora WorkletAnimation(). W poprzednim przykładzie po prostu przekazaliśmy ten czas do efektu. Ponieważ jest to kod JavaScript, możemy zniekształcać 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 przypisujemy tę wartość do przedziału [0; 2000], czyli zakresu czasu, w którym zdefiniowany jest efekt. Teraz animacja wygląda zupełnie inaczej, mimo że nie zmieniono kluczowych klatek ani opcji animacji. Kod workletu może być dowolnie złożony i umożliwia definiowanie za pomocą kodu, które efekty mają być odtwarzane w jakiej kolejności i w jakim zakresie.

Opcje w opcjach

Możesz ponownie użyć workleta i zmienić jego numery. Dlatego konstruktor WorkletAnimation umożliwia przekazanie obiektu opcji do workletu:

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ą obsługiwane za pomocą tego samego kodu, ale z różnymi opcjami.

Podaj stan lokalny.

Jak już wspomniałem, jednym z kluczowych problemów, które ma rozwiązać zadanie animacji, są animacje stanowe. Elementy animacji mogą przechowywać stan. Jedną z głównych cech workletów jest jednak to, że można je przenieść do innego wątku, a nawet zniszczyć w celu zapisania zasobów, co także zniszczy ich stan. Aby zapobiec utracie stanu, moduł animacji udostępnia element wywoływania, który jest wywoływany przed usunięciem modułu, i którego można użyć do zwrócenia obiektu stanu. Ten obiekt zostanie przekazany do konstruktora podczas ponownego tworzenia modułu. Po utworzeniu parametr ten 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 to demo, kwadrat będzie się obracać w losowym kierunku z prawdopodobieństwem 50/50. Jeśli przeglądarka zniszczy worklet i przeniesie go do innego wątku, po utworzeniu wystąpi kolejne wywołanie Math.random(), które może spowodować nagłą zmianę kierunku. Aby temu zapobiec, zwracamy state jako kierunek losowo wybranej animacji i używamy go w konstruktorze, jeśli jest podany.

Łączenie z kontynentem czasoprzestrzennym: ScrollTimeline

Jak pokazano w poprzedniej sekcji, AnimationWorklet umożliwia nam definiowanie za pomocą kodu sposobu, w jaki przesuwanie osi czasu wpływa na efekty animacji. Do tej pory nasz harmonogram był zawsze document.timeline, co oznacza, że śledzi czas.

ScrollTimeline otwiera nowe możliwości i pozwala tworzyć animacje przez przewijanie zamiast czasu. W tej wersji demonstracyjnej użyjemy pierwszego Workletu „przejściowego”:

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 wartości document.timeline tworzymy nowy ScrollTimeline. Jak pewnie się spodziewasz, ScrollTimeline nie używa czasu, a pozycji przewijania sekcji scrollSource do ustawienia currentTime w workletze. Przewinięcie do końca na górę (lub w lewo) oznacza currentTime = 0, a przewinięcie do końca na dół (lub w prawo) ustawia currentTime na timeRange. Po przewinięciu pola w tej wersji demonstracyjnej możesz kontrolować jego położenie.

Jeśli utworzysz ScrollTimeline z elementem, który nie przewija się, currentTime osi czasu będzie NaN. Dlatego, zwłaszcza w przypadku stron responsywnych, zawsze musisz mieć na uwadze, że NaN może być Twoim currentTime. Często warto ustawić domyślną wartość 0.

Połączenie animacji z pozycją przewijania to coś, czego od dawna poszukiwano, ale nigdy nie osiągnięto takiego poziomu wierności (poza niechlujnymi obejściami za pomocą CSS3D). Worklet animacji pozwala na implementację tych efektów w prosty sposób, a jednocześnie jest bardzo wydajny. Na przykład: efekt paralaksy w przypadku przewijania, jak w tym demo, pokazuje, że definiowanie animacji opartej na przewijaniu zajmuje teraz tylko kilka linii kodu.

Na zapleczu

Worklety

Worklety to konteksty JavaScriptu z ograniczonym zakresem i bardzo małą powierzchnią interfejsu API. Mały interfejs API umożliwia bardziej agresywną optymalizację przez przeglądarkę, zwłaszcza na urządzeniach niższych klas. Co więcej, nie są one powiązane z konkretną 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

Pewnie wiesz, że niektóre właściwości CSS są łatwe do animowania, a inne nie. Niektóre właściwości wymagają tylko pracy na karcie graficznej, aby można było je animować, a inne zmuszają przeglądarkę do ponownego ułożenia całego dokumentu.

W Chrome (i w wielu innych przeglądarkach) mamy proces o nazwie kompozytor, którego zadaniem jest – i bardzo to upraszczam – układanie warstw i tekstur, a potem wykorzystanie karty graficznej do aktualizowania ekranu tak często, jak to możliwe, najlepiej z największą możliwą częstotliwością (zazwyczaj 60 Hz). W zależności od tego, które właściwości CSS są animowane, przeglądarka może potrzebować po prostu kompozytora, który pracuje, a inne – „układ”, czyli operacji, którą może wykonać tylko wątek główny. 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 osobnym wątku zsynchronizowany z kompozytorem.

Klepnięcie w nadgarstku

Zwykle jest tylko jeden proces kompozytora, który może być współdzielony na wielu kartach, ponieważ GPU jest zasobem o wysokiej konkurencji. Jeśli kompozytor zostanie w jakiś sposób zablokowany, przeglądarka się zatrzyma i nie będzie reagować na dane wejściowe użytkownika. Należy tego unikać za wszelką cenę. Co się więc dzieje, jeśli Twój zbiór nie może dostarczyć danych, których kompozytor potrzebuje w czasie do renderowania klatki?

W takim przypadku zgodnie ze specyfikacją można „przesunąć” worklet. Zaczyna ona się opóźniać w stosunku do kompozytora, który może ponownie użyć danych z ostatniej klatki, aby utrzymać wysoką częstotliwość wyświetlania klatek. Wizualnie może to wyglądać na zacięte, ale największą różnicą jest to, że przeglądarka reaguje na dane wejściowe użytkownika.

Podsumowanie

AnimationWorklet ma wiele zalet i może być wykorzystywany na wiele sposobów. Oczywistymi zaletami są większa kontrola nad animacjami i nowe sposoby tworzenia animacji, które zapewniają wyższą jakość wizualną w internecie. Interfejsy API zapewniają też większą odporność na zacinanie się aplikacji, a jednocześnie zapewniają dostęp do wszystkich nowych funkcji.

Worklet animacji jest dostępny w Canary, a naszym celem jest przeprowadzenie testów Origin w Chrome 71. Nie możemy się doczekać, aż zaczniesz korzystać z nowych funkcji i dasz nam znać, co możemy jeszcze ulepszyć. Dostępny jest też polyfill, który zapewnia ten sam interfejs API, ale nie zapewnia izolacji pod względem wydajności.

Pamiętaj, że przejścia i animacje CSS są nadal prawidłowymi opcjami i w przypadku podstawowych animacji mogą być znacznie prostsze. Jeśli jednak chcesz stworzyć coś bardziej skomplikowanego, możesz skorzystać z AnimationWorklet.