Tworzenie wydajnych animacji rozwijania i zwijania

Stephen McGruer
Stephen McGruer

TL;DR

Podczas animowania klipów używaj transformacji skali. Możesz zapobiec rozciąganiu i zniekształcaniu elementów podczas animacji, stosując skalowanie w przeciwnym kierunku.

Wcześniej zamieszczaliśmy informacje o tym, jak tworzyć skuteczne efekty paralaksy i przewijane w nieskończoność. W tym poście przyjrzymy się, co jest potrzebne, jeśli chcemy tworzyć klipy z występami. Jeśli chcesz zobaczyć prezentację, otwórz repozytorium GitHub z przykładowymi elementami interfejsu użytkownika.

Weźmy na przykład menu rozwijane:

Niektóre opcje tworzenia takich komponentów dają lepsze efekty niż inne.

Złe rozwiązanie: animacja szerokości i wysokości elementu kontenera

Możesz użyć trochę kodu CSS, aby animować szerokość i wysokość elementu kontenera.

.menu {
  overflow: hidden;
  width: 350px;
  height: 600px;
  transition: width 600ms ease-out, height 600ms ease-out;
}

.menu--collapsed {
  width: 200px;
  height: 60px;
}

Bezpośrednim problemem związanym z tym podejściem jest to, że wymaga ono animowania elementów widthheight. Te właściwości wymagają obliczenia układu i wyświetlenia wyników w każdej klatce animacji. Może to być bardzo kosztowne i zwykle powoduje brak 60 FPS. Jeśli to dla Ciebie nowość, przeczytaj nasze porady dotyczące wydajności renderowania, aby dowiedzieć się więcej o tym, jak działa proces renderowania.

Złe rozwiązanie: użyj właściwości CSS clip lub clip-path

Alternatywnym sposobem animowania elementów width i height może być użycie (obecnie wycofanej) właściwości clip do animowania efektu rozwijania i zwijania. Możesz też użyć elementu clip-path. Użycie atrybutu clip-path jest jednak mniej obsługiwane niż użycie atrybutu clip. Interfejs clip został wycofany. W prawo. Nie rozpaczaj, to nie jest rozwiązanie, którego oczekujesz.

.menu {
  position: absolute;
  clip: rect(0px 112px 175px 0px);
  transition: clip 600ms ease-out;
}

.menu--collapsed {
  clip: rect(0px 70px 34px 0px);
}

Choć animowanie width i height elementu menu jest lepsze niż animowanie, wadą tego sposobu jest to, że nadal wyzwala wyrenderowanie. Ponadto właściwość clip, jeśli zdecydujesz się na ten sposób, wymaga, aby element, na którym działa, był umieszczony absolutnie lub sztywno, co może wymagać dodatkowych działań.

Dobre: skalowanie animacji

Ponieważ ten efekt polega na powiększaniu i zmniejszaniu czegoś, możesz użyć transformacji skali. To świetna wiadomość, ponieważ zmiana transformacji nie wymaga projektowania ani malowania, a przeglądarka może przekazać ją do procesora graficznego. Oznacza to, że efekt jest przyspieszony i znacznie bardziej prawdopodobne jest, że osiągnie 60 FPS.

Wadą tego podejścia, podobnie jak w przypadku większości elementów związanych z wydajnością renderowania, jest to, że wymaga ono nieco konfiguracji. Naprawdę warto!

Krok 1. Oblicz początkowy i końcowy stan

W przypadku podejścia, które wykorzystuje animacje skalowania, pierwszym krokiem jest odczytanie elementów, które informują o rozmiarze menu (zarówno po zwężeniu, jak i po rozwinięciu). W niektórych przypadkach nie można uzyskać obu tych informacji jednocześnie. Aby odczytać różne stany komponentu, trzeba na przykład przełączyć się między klasami. Jeśli jednak musisz to zrobić, zachowaj ostrożność: getBoundingClientRect() (lub offsetWidthoffsetHeight) powoduje, że przeglądarka przetwarza style i układy, jeśli zmieniły się one od ostatniego przetworzenia.

function calculateCollapsedScale () {
    // The menu title can act as the marker for the collapsed state.
    const collapsed = menuTitle.getBoundingClientRect();

    // Whereas the menu as a whole (title plus items) can act as
    // a proxy for the expanded state.
    const expanded = menu.getBoundingClientRect();
    return {
    x: collapsed.width / expanded.width,
    y: collapsed.height / expanded.height
    };
}

W przypadku menu możemy założyć, że będzie ono mieć początkowo swoją naturalną skalę (1, 1). Ta naturalna skala przedstawia rozszerzony stan, co oznacza, że będziesz musiał animować od wersji pomniejszonej (która została obliczona powyżej) do naturalnej skali.

Ale zaraz! Z pewnością można też dostosować zawartość menu, prawda? Tak, jak możesz zobaczyć poniżej.

Co możesz zrobić w tej sytuacji? Do zawartości można zastosować przekształcenie counter-. Na przykład: jeśli kontener został przeskalowany w dół do 1/5 normalnego rozmiaru, możesz skalować zawartość w górę o 5x, by zapobiec ściśnięciu zawartości. Należy pamiętać o 2 kwestiach:

  1. Odwrotna transformacja to też operacja skalowania. Jest to dobre, ponieważ można je przyspieszyć tak samo jak animację w kontenerze. Może być konieczne zapewnienie elementom animowanym własnego poziomu kompozytora (aby umożliwić GPU pomoc). W tym celu możesz dodać do elementu will-change: transform lub, jeśli musisz obsługiwać starsze przeglądarki, backface-visiblity: hidden.

  2. Odwrotna transformacja musi być obliczana dla każdej klatki. W tym miejscu sprawa może się skomplikować, ponieważ przy założeniu, że animacja jest w CSS i używa funkcji wygładzania, należy ją zrównoważyć podczas animacji przeciwnego przekształcenia. Obliczenie odwrotnej krzywej dla funkcji cubic-bezier(0, 0, 0.3, 1) nie jest jednak takie oczywiste.

Możesz więc zechcieć zastosować animację efektu za pomocą JavaScriptu. Możesz potem użyć równania wygładzania, aby obliczyć skalę i wartości licznika na klatkę. Wadą animacji opartych na JavaScript jest to, że gdy główny wątek (w którym działa JavaScript) jest zajęty innym zadaniem, Krótko mówiąc, animacja może się zacinać lub całkowicie zatrzymać, co nie wpływa korzystnie na UX.

Krok 2. Błyskawicznie twórz animacje CSS

Rozwiązaniem, które na początku może wydawać się dziwne, jest dynamiczne utworzenie animacji z klatkami kluczowymi z własną funkcją wygładzania i umieszczenie jej na stronie, by można było użyć jej w menu. (wielkie podziękowania dla inżyniera Chrome Roberta Flacka za zwrócenie na to uwagi) Główną zaletą jest to, że w kompozytorze można uruchamiać animacje z klatkami kluczowymi, które mutują przekształcenia, co oznacza, że nie mają na nią wpływu zadania z wątku głównego.

Aby utworzyć animację klatki kluczowej, zmieniamy zakres od 0 do 100 i obliczamy, jakie wartości skali byłyby potrzebne dla elementu i jego zawartości. Można je skompilować w formie ciągu znaków, który można wstawić na stronie jako element stylu. Wstrzyknięcie stylów spowoduje ponowne obliczenie stylów na stronie, co jest dodatkową pracą dla przeglądarki, ale będzie to zrobione tylko raz, gdy komponent się uruchamia.

function createKeyframeAnimation () {
    // Figure out the size of the element when collapsed.
    let {x, y} = calculateCollapsedScale();
    let animation = '';
    let inverseAnimation = '';

    for (let step = 0; step <= 100; step++) {
    // Remap the step value to an eased one.
    let easedStep = ease(step / 100);

    // Calculate the scale of the element.
    const xScale = x + (1 - x) * easedStep;
    const yScale = y + (1 - y) * easedStep;

    animation += `${step}% {
        transform: scale(${xScale}, ${yScale});
    }`;

    // And now the inverse for the contents.
    const invXScale = 1 / xScale;
    const invYScale = 1 / yScale;
    inverseAnimation += `${step}% {
        transform: scale(${invXScale}, ${invYScale});
    }`;

    }

    return `
    @keyframes menuAnimation {
    ${animation}
    }

    @keyframes menuContentsAnimation {
    ${inverseAnimation}
    }`;
}

Osoby ciekawskie mogą się zastanawiać, do czego służy funkcja ease() w pętli for. Aby mapować wartości od 0 do 1 na ich wygładzone odpowiedniki, możesz użyć czegoś takiego:

function ease (v, pow=4) {
  return 1 - Math.pow(1 - v, pow);
}

Możesz też wyszukiwać w Google, jak to wygląda. Handy! Jeśli szukasz innych równań dotyczących wygładzania, przejrzyj Tween.js firmy Soledad Penadés, który zawiera ich sporo.

Krok 3. Włącz animacje CSS

Po utworzeniu i umieszczeniu tych animacji na stronie w języku JavaScript ostatnim krokiem jest przełączenie klas uruchamiających animacje.

.menu--expanded {
  animation-name: menuAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

.menu__contents--expanded {
  animation-name: menuContentsAnimation;
  animation-duration: 0.2s;
  animation-timing-function: linear;
}

Spowoduje to uruchomienie animacji utworzonych w poprzednim kroku. Ponieważ wyrenderowane animacje są już wygładzone, funkcja timingu musi być ustawiona na linear, w przeciwnym razie wygładzanie między poszczególnymi klatkami kluczowymi będzie wyglądać bardzo dziwnie.

Jeśli chodzi o zwijanie elementu, masz 2 opcje: zaktualizuj animację CSS, aby działała w odwrotnej kolejności. To zadziała, ale „odczucie” animacji będzie odwrotne, więc jeśli użyjesz wygładzonej krzywej wygaszania, odwrócenie będzie wyglądać jak wygładzona krzywa wzrostu, co będzie powodować wrażenie spowolnienia. Bardziej odpowiednim rozwiązaniem jest utworzenie drugiej pary animacji do zwijania elementu. Można je tworzyć dokładnie tak samo jak animacje rozwijane klatki kluczowej, ale z zamiennymi wartościami początkowymi i końcowymi.

const xScale = 1 + (x - 1) * easedStep;
const yScale = 1 + (y - 1) * easedStep;

Bardziej zaawansowana wersja: informacje kołowe

Możesz też użyć tej techniki do tworzenia animacji rozszerzania i zwijania w krąg.

Zasady są w dużej mierze takie same jak w poprzedniej wersji, w której skalujesz element i odwrotnie skalujesz jego bezpośrednie elementy podrzędne. W tym przypadku element, który jest powiększany, ma border-radius 50%, co powoduje, że jest okrągły, i jest otoczony przez inny element, który ma overflow: hidden, co oznacza, że nie widzisz, jak koło wychodzi poza granice elementu.

Ostrzeżenie dotyczące tego konkretnego wariantu: podczas animacji w Chrome tekst jest rozmyty na ekranach o niskiej rozdzielczości DPI z powodu błędów zaokrągleń wynikających ze skalowania i odwrotnego skalowania tekstu. Jeśli chcesz dowiedzieć się więcej, przeczytaj raport o błędzie, który możesz oznaczyć gwiazdką i śledzić.

Kod efektu otwierania koła znajdziesz w repozytorium GitHub.

Podsumowanie

To taki sposób na tworzenie efektownych animacji klipów przy użyciu przekształceń skali. W idealnym świecie animacje klipów byłyby przyspieszone (jest na ten temat błąd w Chromium zgłoszony przez Jake'a Archibalda), ale dopóki tak się nie stanie, należy zachować ostrożność podczas animowania clip lub clip-path oraz unikać animowania width lub height.

Do takich efektów przydatne są też animacje internetowe, ponieważ mają interfejs API JavaScriptu, ale mogą działać na wątku kompozytora, jeśli animujesz tylko transformopacity. Niestety obsługa animacji w internecie nie jest dobra, ale przydaje się progresywne ulepszenie, jeśli są dostępne.

if ('animate' in HTMLElement.prototype) {
    // Animate with Web Animations.
} else {
    // Fall back to generated CSS Animations or JS.
}

Dopóki to się nie zmieni, możesz używać bibliotek opartych na JavaScript do tworzenia animacji, ale lepszym rozwiązaniem może być wyrenderowanie animacji CSS i używanie jej zamiast animacji JavaScript. Jeśli Twoja aplikacja już korzysta z JavaScriptu do animacji, lepiej będzie, jeśli będziesz używać kodu zgodnego z dotychczasowym.

Jeśli chcesz przyjrzeć się kodowi tego efektu, zajrzyj do repozytorium GitHub z przykładami elementów interfejsu. Daj nam znać w komentarzach poniżej, jak Ci idzie.