Frontend

Fizyka na frontendzie. Jak sprężyny mogą pomóc w tworzeniu efektownych animacji

Animacje css vs. react spring - zobacz różnice, tutorial i stwórz animację

Zanim jeszcze całkiem pochłonął mnie front-end, miałem okazję studiować fizykę. Nie zawsze było mi z nią po drodze, ale muszę jej oddać to, że żadna inna nauka jaką znam, nie jest w stanie tak opisać otaczającego nas świata jak ona. W aplikacjach internetowych natomiast od zawsze pasjonowały mnie animacje. Internet jest przepełniony ich kreatywnymi przykładami, a ja za każdym razem, gdy na taki przykład trafiam, zastanawiam się: Jak coś takiego zrobić? Jak taka animacja wpływa na użytkownika aplikacji? Co mogę zrobić, żeby wyglądała bardziej naturalnie i przyjaźnie?

Mikołaj Żywczok. Z wykształcenia fizyk, z zamiłowania programista. Obecnie front-end developer w Ericsson. Poza kodowaniem fascynuje go design, UX i dobra kawa. Prywatnie mąż, pasjonat sztuk walki, akrobatyki i gry na gitarze.


Odpowiedzi na wiele z moich pytań znalazłem w dniu, w którym natrafiłem na bibliotekę react-spring. Pozwala ona tworzyć piękne animacje, wykorzystując do tego fizyczne właściwości sprężyny. Animacja stworzona w ten sposób jest płynna i naturalna – znacznie lepiej oddaje ruch, do którego jesteśmy przyzwyczajeni obserwując otaczający nas świat.

CSS vs. react-spring – porównajmy animacje

Żeby nie być gołosłownym, porównajmy dwie analogiczne animacje, z których pierwsza została stworzona tradycyjnie za pomocą CSS, a druga korzystając z react-spring:

Animacja stworzona w CSS w sumie jest w porządku, ale „szału nie robi” i wygląda raczej sztucznie. Brakuje jej płynności i naturalności, którą widać na drugiej animacji; tego magicznego składnika, który sprawia, że coś wygląda dla nas lepiej nawet jeśli nie zawsze potrafimy wytłumaczyć, dlaczego. Są to detale, o których rzadko myślimy, pracując nad dużymi projektami, ale najczęściej to właśnie one zadecydują o tym, czy nasz produkt zostanie przez użytkownika pokochany, czy będzie on w jego odczuciu jedynie poprawny i funkcjonalny.

W Ericsson zależy nam na tym, żeby produkty, które tworzymy były wzorcowe i dlatego każdy detal ma znaczenie. Skoro wciąż czytasz ten tekst (co mnie bardzo cieszy), to założę się, że myślisz podobnie.

Zmienne CSS i react-spring, jakie są różnice w modelowaniu?

Żeby lepiej zrozumieć skąd biorą się te różnice w odbiorze, przyjrzyjmy się sposobowi opisu animacji tworzonej za pomocą CSS oraz react-spring. Zmienne w CSS, które możemy wykorzystać do manipulowania animacją to:

  1. Duration określa czas trwania liczony od początku do końca animacji.
  2. Easing – parametr pozwalający przyspieszyć lub zwolnić animacje odpowiednio na jej początku bądź końcu.

To wszystko. Tworząc animację z wykorzystaniem wyłączenie CSS-a możemy jedynie określić,
w którymś momencie przyspieszyć bądź zwolnić. W przypadku biblioteki react-spring, która do opisu animacji korzysta z modelu matematycznego sprężyny, mamy trzy parametry służące do modelowania naszej animacji:

1. Masa – odnosi się do wagi poruszającego się elementu. Cięższe elementy będą poruszać się wolniej, ale z większą bezwładnością.

2. Napięcie – odnosi się do energii skumulowanej podczas naciągania sprężyny. Im większa jej wartość, tym animacja będzie bardziej dynamiczna i sprężysta.

3. Tarcie – dotyczy nie tyle samej sprężyny co otoczenia, w jakim się ona znajduje. Przykładowo, naciągnięta sprężyna zanurzona w smole będzie przez nią skutecznie hamowana (duże tarcie), przez co ruch szybko się zakończy. Gdybyśmy natomiast wysłali ją w kosmos, gdzie znalazłaby się w próżni (zerowe tarcie) to jej ruch trwałby w nieskończoność.

Myśląc o zastosowaniu fizycznych właściwości sprężyny w tworzeniu animacji wydawałoby się, że będziemy w stanie uzyskać w ten sposób jedynie skoczne, sprężynujące efekty, ale nic bardziej mylnego, możliwości tworzenia animacji z react-spring są znacznie większe! Zobaczcie sami:

React-spring tutorial – stwórzmy własną animację!

A teraz, uzbrojeni w całą potrzebną teorię, zobaczmy jak z tradycyjnej animacji CSS zrobić coś wyjątkowego!

via GIPHY

import React from "react";

const Raise = ({ height = 0, timing = 150, children }) => {
  const [isRaised, setIsRaised] = React.useState(false);
  const style = {
    display: "inline-block",
    backfaceVisibility: "hidden",
    transform: isRaised ? `translateY(-${height}px)` : "translateY(0)",
    transition: `transform ${timing}ms`
  };

  React.useEffect(() => {
    if (!isRaised) {
      return;
    }

    const timeoutId = window.setTimeout(() => {
      setIsRaised(false);
    }, timing);

    return () => window.clearTimeout(timeoutId);
  }, [isRaised, timing]);
  const trigger = () => setIsRaised(true);

  return (
    <span onMouseEnter={trigger} style={style}>
      {children}
    </span>
  );
};

export default Raise;

Codesandbox: Raise.js

Uff, trochę tego jest, ale spokojnie, zaraz przejdziemy przez niego i wyjaśnimy sobie, o co w nim chodzi. Animacja polega na tym, że po najechaniu kursorem myszy na dany element, ten się unosi. Dodatkowo, w tym samym momencie rusza timer, który po upływie określonego czasu daje sygnał elementowi, żeby ten wrócił do swojego stanu początkowego (nawet jeżeli nasz kursor wciąż znajduje się nad elementem). Informacja o stanie w jakim znajduje się nasz element przechowywana jest w zmiennej isRaised.

import React from "react";

const Raise = ({ height = 0, timing = 150, children }) => {
  const [isRaised, setIsRaised] = React.useState(false);
  const style = {
    display: "inline-block",
    backfaceVisibility: "hidden",
    transform: isRaised ? `translateY(-${height}px)` : "translateY(0)",
    transition: `transform ${timing}ms`
  };

  React.useEffect(() => {
    if (!isRaised) {
      return;
    }

    const timeoutId = window.setTimeout(() => {
      setIsRaised(false);
    }, timing);

    return () => window.clearTimeout(timeoutId);
  }, [isRaised, timing]);
  const trigger = () => setIsRaised(true);

  return (
    <span onMouseEnter={trigger} style={style}>
      {children}
    </span>
  );
};

export default Raise;

Codesandbox: Raise.js

Najechanie kursorem nad animowany element sprawia, że wartość isRaised zmienia się na true, co z kolei aktywuje naszą animację, a także hook useEffect, w którym zostaje nastawiony timer, odliczający czas, po którym wartość zmiennej isRaised zostaje zmieniona z powrotem na false.

Sam efekt polega na zastosowaniu transform: translateY. Wysokość, na którą ma się unieść element, a także czas trwania animacji możemy kontrolować za pomocą przesłanych propsów (height i timing). Dodatkowo ustawiamy display: inline-block (na elementach display: inline animacja by nie zadziałała), a także backface-visibility: hidden (dzięki temu pracę potrzebną do wyrenderowania naszej animacji zrzucamy na procesor graficzny, co z kolei przekłada się zwiększenie jej płynności).

import React from "react";

const Raise = ({ height = 0, timing = 150, children }) => {
  const [isRaised, setIsRaised] = React.useState(false);
  const style = {
    display: "inline-block",
    backfaceVisibility: "hidden",
    transform: isRaised ? `translateY(-${height}px)` : "translateY(0)",
    transition: `transform ${timing}ms`
  };

  React.useEffect(() => {
    if (!isRaised) {
      return;
    }

    const timeoutId = window.setTimeout(() => {
      setIsRaised(false);
    }, timing);

    return () => window.clearTimeout(timeoutId);
  }, [isRaised, timing]);
  const trigger = () => setIsRaised(true);

  return (
    <span onMouseEnter={trigger} style={style}>
      {children}
    </span>
  );
};

export default Raise;

Codesandbox: Raise.js

Animowany element, otaczamy tagiem span. Zapytacie pewnie:

– Dlaczego akurat span skoro, każdy szanujący się frontendowiec wie, że eventy powinny być obsługiwane w widocznych elementach, takich jak choćby button? Przecież span jest niemożliwy do zaznaczenia korzystając z klawiatury, a my chcemy pisać kod dostępny dla każdego!

Odpowiem Wam:

via GIPHY

Jednakże, w tym przypadku, animacja, którą rozważamy ma charakter czysto wizualny i podejrzewam, że mogłaby być irytująca dla osób poruszających się po stronie za pomocą klawiatury.

Stworzonego przez nas komponentu możemy użyć w następujący sposób:

import React from "react";
import { Icon } from "react-icons-kit";
import { ic_room } from "react-icons-kit/md/ic_room";
import { ic_thumb_up } from "react-icons-kit/md/ic_thumb_up";
import Raise from "./Raise.js";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <Raise height={20} timing={200}>
        <Icon icon={ic_room} size={90} className="Icon" />
      </Raise>
      <Raise height={20} timing={200}>
        <Icon icon={ic_thumb_up} size={90} className="Icon" />
      </Raise>
    </div>
  );
}

Codesandbox: App.js

Wszystko wyjaśnione, wiemy już jak działa nasz kod, a animacja wygląda w porządku. Jest nieźle, ale wiem, że stać nas na więcej! Doprawmy naszą animację odrobiną magii z pomocą react-spring.

via GIPHY

import React from "react";
import { animated, useSpring } from "react-spring";

const Raise = ({ height = 0, timing = 150, children }) => {
  const [isRaised, setIsRaised] = React.useState(false);
  const style = useSpring({
    display: "inline-block",
    backfaceVisibility: "hidden",
    transform: isRaised ? `translateY(-${height}px)` : "translateY(0px)",
    config: {
      mass: 1,
      tension: 400,
      friction: 15
    }
  });

  React.useEffect(() => {
    if (!isRaised) {
      return;
    }

    const timeoutId = window.setTimeout(() => {
      setIsRaised(false);
    }, timing);
    return () => {
      window.clearTimeout(timeoutId);
    };
  }, [isRaised, timing]);
  const trigger = () => {
    setIsRaised(true);
  };
  return (
    <animated.span onMouseEnter={trigger} style={style}>
      {children}
    </animated.span>
  );
};

export default Raise;

Codesandbox: Raise.js

Co się zmieniło? Po pierwsze, z biblioteki react-spring importujemy useSpring i animated. Hook useSpring pozwala nam opisać animacje korzystając z modelu matematycznego sprężyny, zamiast używanych w CSS krzywych Béziera. Jako parametr przesyłamy do niego obiekt z naszymi lekko zmodyfikowanymi stylami (linijkę z kodem transition: transform, zamieniamy na obiekt config, zawierający w sobie informacje na temat masy, napięcia i tarcia). Jako że opisywanie animacji w ten sposób, nie jest wspierane przez przeglądarki (jeszcze), nie możemy takiego obiektu przekazać bezpośrednio tagowi <span>. Tutaj do akcji wkracza animated, który możemy wykorzystać do stworzenia tagu <animated.span>. Działa on w sposób analogiczny do zwykłego <span> z tą jednak różnicą, że potrafi rozszyfrować nasz nowy sposób opisu animacji. Oto efekt końcowy, gratulacje!

Jakie jeszcze możliwości daje react-spring?

Przykładowo, nasz komponent można również prostym sposobem przerobić na hook, co przyda nam się szczególnie w przypadku, w którym mając dwa różne elementy. Chcielibyśmy, żeby jeden z nich aktywował animacje tego drugiego (co w przypadku naszego aktualnego komponentu jest niestety niemożliwe).

import React from "react";
import { useSpring } from "react-spring";

const useRaise = ({
  height = 10,
  timing = 200,
  springConfig = {
    mass: 1,
    tension: 400,
    friction: 15
  }
}) => {
  const [isRaised, setIsRaised] = React.useState(false);
  const style = useSpring({
    display: "inline-block",
    backfaceVisibility: "hidden",
    transform: isRaised ? `translateY(-${height}px)` : "translateY(0px)",
    config: springConfig
  });

  React.useEffect(() => {
    if (!isRaised) {
      return;
    }

    const timeoutId = window.setTimeout(() => {
      setIsRaised(false);
    }, timing);

    return () => window.clearTimeout(timeoutId);
  }, [isRaised, timing]);

  const trigger = () => setIsRaised(true);

  return [style, trigger];
};

export default useRaise;

Codesandbox: useRaise.js

Do naszego hooka całą konfigurację animacji przesyłamy w parametrze, on przeprowadza dla nas odpowiednie obliczenia, a następnie zwraca tablicę zawierającą obiekt ze stylem animacji oraz trigger, za pomocą którego możemy ją aktywować. Zostało nam już tylko go użyć:

import React from "react";
import { animated } from "react-spring";
import { Icon } from "react-icons-kit";
import Button from "@material-ui/core/Button";
import { ic_thumb_up } from "react-icons-kit/md/ic_thumb_up";
import useRaise from "./useRaise.js";
import "./styles.css";

export default function App() {
  const [style, trigger] = useRaise({ height: 10, timing: 200 });

  return (
    <div className="App">
      <animated.span style={style}>
        <Icon icon={ic_thumb_up} size={64} className="Icon" />
      </animated.span>
      <Button onClick={trigger} className="Button">
        Click me!
      </Button>
    </div>
  );
}

Codesandbox: App.js

Obiekt style przypisujemy do elementu, który chcemy animować, a trigger do elementu, który ma tę animację wywołać i gotowe. A teraz najlepsze – nasz hook jest ekstra, ale jeśli woleliśmy używać wcześniejszego komponentu, a sytuacja nam na to pozwala, to przecież wcale nie musimy z niego rezygnować. Dodatkowo, wykorzystując nasz hook możemy go odrobinę odchudzić.

import React from "react";
import { animated } from "react-spring";
import useRaise from "./useRaise";

const Raise = ({ children, ...animationConfig }) => {
  const [style, trigger] = useRaise(animationConfig);

  return (
    <animated.span style={style} onMouseEnter={trigger}>
      {children}
    </animated.span>
  );
};

export default Raise;

Codesandbox: Raise.js

W efekcie możemy korzystać z naszej animacji na dwa sposoby, a cały kod pozostaje zwięzły i wolny od niepotrzebnych duplikatów. W moim odczuciu react-spring sprawia wrażenie biblioteki bardzo dobrze przemyślanej – jest wydajna, prosta w obsłudze i wszechstronna. Można z niej korzystać zarówno poprzez nowoczesne hook API (tak jak zrobiliśmy to my), jak i class API, w zależności od preferencji i potrzeb developera. Całym sobą zachęcam do eksperymentowania z tą biblioteką, ponieważ jej potencjał jest ogromny (po odrobinę inspiracji odsyłam do sekcji z przykładami na oficjalnej stronie).

Na zakończenie…

Dziękuję za czas, który mi poświęciłeś (mam nadzieje, że w twoim odczuciu nie był to czas stracony). Daj proszę znać, co myślisz na temat tego tekstu w komentarzu lub pisząc do mnie na LinkedInie, a jeśli w trakcie czytania nasunęły Ci się jakieś pytania, też chętnie na nie odpowiem.

Pomyślnego kodowania!


Zdjęcie główne artykułu pochodzi z unsplash.com.

Frontend Developer w Ericsson

Z wykształcenia fizyk, z zamiłowania programista. Obecnie front-end developer w Ericsson. Poza kodowaniem fascynuje go design, UX i dobra kawa. Prywatnie mąż, pasjonat sztuk walki, akrobatyki i gry na gitarze.

Podobne artykuły

[wpdevart_facebook_comment curent_url="https://geek.justjoin.it/fizyka-na-frontendzie-jak-sprezyny-moga-pomoc-w-tworzeniu-efektownych-animacji/" order_type="social" width="100%" count_of_comments="8" ]