Animacje

Chcieć opisać animacje stosowane na stronach, to jak próbować opisać gatunek ludzki i wszelkie jego zawiłości w jednym artykule. Praktycznie niewykonalna sprawa.

Zacznijmy od najprostszych tematów czyli animacji CSS i ich obsługi w Javascript.

Dzielimy je na dwie kategorie: płynne zmiany dzięki właściwości transition oraz klasyczne animacje tworzone za pomocą właściwości animation i składni @keyframes.

Transition

Właściwość transition po użyciu na elemencie sprawia, że gdy zmieni on swoje właściwości, takie zmiany będą dziać się płynnie.

Ogólna konstrukcja transition ma postać:


/* poszczególne właściwości */
transition-property: width, height; /* właściwości, których zmiany mają być płynne. Możemy wypisać kilka po przecinku, możemy też użyć domyślnej "all" co oznacza wszystkie właściwości */
transition-duration: 2s; /* jak długa ma być ta zmiana. Możemy podać w s lub ms */
transition-timing-function: linear; /* typ animacji - ease, ease-in, ease-out, ease-in-out, cubic-bezier */
transition-delay: 1s; /* opóźnienie rozpoczęcia */

/* wersja shorthand */
/* jeżeli szerokość się zmieni, to animacja będzie trwać 2s, z 1s opóźnieniem, a przebieg animacji będzie stały (linear) */
transition: width 2s 1s linear;

/* jeżeli tło się zmieni, to będzie to animowane 2s */
transition: 2s background;

/* jeżeli szerokość się zmieni, będzie to trwać 1s */
/* jeżeli dany element zostanie transformowany, będzie to trwać 2s z opóźnieniem 1s */
transition: 1s width, 2s 1s transform;

/* jeżeli cokolwiek się zmieni to zmiana będzie trwać 1s */
transition: 1s;

W większości przypadków (chociaż nie we wszystkich) wystarczy ostatni - najkrótszy zapis.


button {
    width: 150px;
    transition: 1s;
}
button:hover {
    background: rebeccapurple;
    border-radius: 50px;
}

Nie jest to rozwiązanie na każdą sytuację. Jeżeli dla przykładu w widoku mobilnym chciałbym, by powyższy przycisk zajmował całą szerokość, zmiana taka także była by płynna, czego raczej bym nie chciał. Dlatego w wielu przypadkach lepiej wypunktować właściwości, które mają się płynnie zmieniać np.: transition: 1s background, 1s border-radius.

Dość często transition jest stosowana dla elementów, dla których stosujemy :hover czy :focus. Nie musimy jednak ograniczać się tylko do tych stanów. Zmiany danej właściwości mogą być wprowadzone na różne sposoby - np. poprzez dodanie nowej klasy czy chociażby za pomocą setInterval()


.menu {
    width: 320px;
    transform: translate(-100%);
    height: 100%;
    position: fixed;
    top: 0;
    background: #000;
    transition: 1s ease-in-out transform;
}

.menu.is-active {
    transform: translateX(0); /* wysuwam menu z lewej strony ekranu */
}

const toggle = document.querySelector(".menu-toggle");
const menu = document.querySelector(".menu");

toggle.addEventListener("click", e => {
    e.preventDefault();
    menu.classList.toggle("is-active");
});
Lorem ipsum dolor sit Lorem ipsum dolor Lorem ipsum dolor sit Lorem ipsum

.element {
    transition: 2s background;
}

const el = document.querySelector(".element");

setInterval(() => {
    el.style.backgroundColor = `hsl(${Math.random() * 360}, 80%, 70%)`;
}, 2 * 1000);

Właściwość transition (ale też poniższe animacje) zadziała tylko z tymi właściwościami, które mogą się zmieniać stopniowo (np. zwiększając swój rozmiar). I tak dla przykładu właściwość width bez problemu zmienimy z 100px na 150px, color z #fff na #ddd, a opacity z 0 na 1. Ale już display z block na none, czy font-weight: bold na normal nie. Trzeba wtedy sięgnąć po inne techniki:


.element { /* ukryty */
    opacity: 0;
    pointer-events: none;
    transition: 1s opacity;
}

.element.is-show { /* widoczny */
    opacity: 1;
    pointer-events: all;
}

Warto też zwrócić uwagę na właściwość transition-delay, ponieważ pozwala ona tworzyć dość ciekawe efekty:


<div class="menu">
    <a href="" class="menu-icon"><svg ...></svg></a>
    <a href="" class="menu-icon"><svg ...></svg></a>
    <a href="" class="menu-icon"><svg ...></svg></a>
    <a href="" class="menu-icon"><svg ...></svg></a>
</div>

.menu-icon {
    transition: 0.5s;
    transform: translate(-150%);
}

/* zasada działania taka sama jak poprzednio */
.menu.is-show .menu-icon {
    transform: translate(0%);
}

.menu-icon:nth-child(1) { transition-delay: 0s; }
.menu-icon:nth-child(2) { transition-delay: 0.2s; }
.menu-icon:nth-child(3) { transition-delay: 0.4s; }
.menu-icon:nth-child(4) { transition-delay: 0.6s; }
.menu-icon:nth-child(5) { transition-delay: 0.8s; }
Zrób zakupy Taguj kogoś Odbierz paczkę Popraw błędy

Zdarzenia dla transition

Jeżeli chodzi o Javascript i właściwość transition, to mamy do użycia zdarzenia:

transitionstart odpalane gdy transition rozpocznie się animować
transitionrun odpalane gry transition zostało uruchomione, ale niekoniecznie jeszcze się animuje (bo użyta jest właściwość transition-delay)
transitionend odpalane gdy transition się zakończyło
transitioncancel odpalane gdy transition zostało przerwane (programowo)

const btn = document.querySelector("#transitionEventTest");
const text = "";

btn.addEventListener("transitionstart", () => {
    text.push("transitionstart");
    textElement.textContent = text.join(", ");
});

btn.addEventListener("transitionrun", () => {
    text.push("transitionrun");
    textElement.textContent = text.join(", ");
});

btn.addEventListener("transitionend", () => {
    text.push("transitionend");
    textElement.textContent = text.join(", ");
});
klik

Animation

Właściwość animation służy do podpinania do elementu animacji.

Wpierw opisujemy przebieg animacji za pomocą składni @keyframes. Przypomina to stawianie kluczowych klatek na osi czasu - znane praktycznie z każdego programu do obróbki video. W zakresie 0% - 100% opisujemy miejsca, w których możemy zmieniać wiele właściwości na raz:

animation timeline

Składnia @keyframes ma postać:


@keyframes testAnim {
    0% {
        width: 100px;
    }
    40% {
        border-radius: 10px;
    }
    100% {
        width: 100px;
        height: 200px;
    }
}

Następnie opisaną w ten sposób animację (lub kilka animacji podając je po przecinku) możemy podpiąć do elementu za pomocą właściwości:


.element {
    /* poszczególne właściwości */
    animation-name: testAnim; /* nazwa animacji, którą podpinamy */
    animation-duration: 1s; /* czas odtwarzania */
    animation-delay: 2s; /* początkowe opóźnienie animacji - domyślnie 0s */
    animation-iteration-count: 3; /* liczba powtórzeń - konkretna liczba lub infinite, domyślnie 1 */
    animation-direction: alternate; /* kierunek animacji - alternate, normal. Alternate grane jest od początku do końca, a potem od końca do przodu. */
    animation-timing-function: ease-out; /* sposób prędkości odtwarzania - ease, ease-in, ease-out, ease-in-out, linear, steps(4), frames(10) */
    animation-fill-mode: forwards; /* po podłączeniu animacji zaaplikuj style - forwards - z końcowej klatki, backwards - z początkowej, both - z obydwu, none - nie aplikuj */

    /* to co powyżej w składni shorthand - kolejność parametrów nie ma znaczenia poza kolejnością czasu odtwarzania i opóźnienia (1s 2s) */
    animation: testAnim 1s 2s 3 alternate forwards ease-in;
}

Przykład prostej animacji:


.element {
    width: 200px;
    height: 200px;
    background: crimson;
    animation: testAnimationA 1s 0s infinite alternate both;
}

@keyframes testAnimationA {
    0% {
        background: red;
        transform: translate(-100%);
    }
    50% {
        background: gold;
    }
    100% {
        background: dodgerblue;
        border-radius: 50%;
        transform: translate(100%) scale(0.8);
    }
}

Przy opisywaniu miejsc w @keyframes możemy też użyć słów kluczowych from i to, a także opisywać procentowe miejsca w dowolnej kolejności. Stworzona w ten sposób animacja będzie płynnie zmieniać dane właściwości w czasie. Jeżeli w konkretnym jej momencie chcemy skokową zmianę, powinniśmy utworzyć dwa punkty bardzo blisko siebie:


    @keyframes animationJump {
        0% {
            background: gold;
            border-radius: 50%;
        }
        20% {
            background: gold;
            border-radius: 50%;
        }
        20.0001% {
            background: red;
            border-radius: 0;
        }
        100% {
            transform: translate(300%);
            border-radius: 0;
            background: blue;
        }
    }

Zdarzenia dla animation

Od strony Javascript mamy dostęp do kilku zdarzeń związanych z animacjami:

animationstart odpalane gdy animacja zacznie się odtwarzać
animationiteration gdy pojedyncze powtórzenie animacji się zakończy i zacznie się odtwarzanie kolejnego
animationcancel odpalane gdy odgrywanie animacji zostanie niespodziewanie przerwane
animationend odpalane gdy animacja się zakończy

.test-anim div {
    ...
}

/* po kliknięciu dodam klasę .is-play, która nada animację */
.test-anim div.is-play {
    animation: testAnimationEvents 2s 0s 3 ease-in-out;
}

@keyframes testAnimationEvents {
    100% {
        background: dodgerblue;
        transform: rotate(1turn);
    }
}

const btn = document.querySelector("#animationEventTest");
const textElement = document.querySelector("#animationEventText");
let play = false;
let max = 3;
let count = max;
const text = [];

btn.addEventListener("click", () => {
    if (!play) {
        count = max;
        btn.classList.add("is-play");
        btn.innerHTML = count;
        play = true;
    }
});

btn.addEventListener("animationend", () => {
    btn.textContent = "klik";
    btn.classList.remove("is-play");
    text.push("animationend");
    textElement.textContent = text.join(", ");
    play = false;
});

btn.addEventListener("animationstart", () => {
    btn.textContent = `Jeszcze ${count}x`;
    text.push("animationstart");
    textElement.textContent = text.join(", ");
});

btn.addEventListener("animationiteration", () => {
    btn.textContent = `Jeszcze ${--count}x`;
    text.push("animationiteration");
    textElement.textContent = text.join(", ");
});
klik

Jeżeli są to animacje, które mają się odegrać tylko jeden raz, warto pamiętać, że dla zdarzeń mamy 3 parametr, w którym możemy określić, że dana funkcja zadziała tylko jeden raz:


function addAnimation(element, animationClass, callback) {
    element.classList.add(animationClass);
    element.addEventListener("animationend", () => {
        element.classLIst.remove(animationClass);
        callback();
    }, {once : true});
}

addAnimation(el, "is-play", () => {
    console.log("animacja się zakończyła");
});

Co warto animować

Zanim przejdziemy dalej, warto chwilę się zatrzymać przy tym co możemy i powinniśmy animować.

Żeby to zrozumieć, prześledźmy jak w ogóle powstaje obraz naszej strony. Składa się ono z kilku faz.

Fazy renderowania strony
  • Na samym początku jest tak zwane parsowanie kodu strony czyli czytanie kodu dokumentu i na jego podstawie układanie drzewa DOM. W tym momencie przeglądarka jeszcze nie wie jak te elementy będą wyglądać, a skupia się tylko na ich zależnościach.
  • W kolejnym kroku następuje aplikowanie styli do elementów.
  • Następnie przeglądarka sprawdza rozmiar elementów, tworzy dla nich "pudełka" (model pudełkowy) i odpowiednio je układa na stronie tworząc layout.
  • W kolejnym kroku pudełka są rysowane. Pudełka mogą być zgrupowane na oddzielnych warstwach (w zależności od tego czy użyliśmy odpowiednich właściwości w CSS).
  • Na samym końcu przeglądarka pobiera różne warstwy, aplikuje na nie kompozycyjne właściwości (takie jak np. transform) i rysuje na ekranie.

Całość można przyrównać do tworzenia grafiki 3d w np. Blenderze. Na początku czytasz z kartki swój pomysł. Potem modelujesz, układasz elementy, ustawiasz im wielkość itp. Jak już wszystko ułożysz, renderujesz obraz. Na samym końcu poddajesz taki obraz dodatkowej obróbce, czyli kompozycji. Układanie i modelowanie elementów to pracochłonna rzecz zajmująca często kilka godzin, dni a nawet tygodni. Renderowanie takiego obrazu też często zajmuje naście godzin (żeby...). Natomiast sama kompozycja to w zasadzie dodatek, który po odpowiedniej konfiguracji aplikowany jest na przygotowany obraz praktycznie natychmiastowo. Jak chcesz się o tym przekonać (a i zwiększyć swoje kompetencje) - zapraszam do jednego z ciekawszych tutoriali na te tematy. Szczerze polecam, bo zabawa przednia, a i w nadchodzących czasach praca ze światem 3d będzie w "meta cenie").

W przeglądarce działa to podobnie. Jeżeli np. wrzucisz na stronę za pomocą Javascript nowy kawałek html, przeglądarka będzie musiała wykonać wszystkie fazy - od parsowania do kompozycji.
Animowanie wielu właściwości (np. width, font-size) spowoduje, że przeglądarka będzie musiała dane elementy ponownie ułożyć, obliczyć jego pozycję i ponownie narysować. Niektóre właściwości nie zmienią ułożenia elementów (np. background-position) więc przy ich animowani przeglądarka nie będzie musiała wrócić do układania elementów, a tylko do ich przerysowania. Najmniej obciążające dla przeglądarki są właściwości, których rysowanie odbywa się w fazie kompozycji. Są to wszelkie transformacje transform oraz właściwość opacity (pełną listę znajdziesz tutaj). Dla nas oznacza to tyle, że animowanie tych właściwości jest zazwyczaj o wiele płynniejsze niż innych właściwości.

Nie zrozum mnie źle. Animować możesz praktycznie wszystko (i nie bój się tego!), po prostu testuj efekt końcowy. Gdybyśmy mogli polegać tylko na transform i opacity, w zasadzie nie było by mowy np. o zwijanych panelach, czy innych znanych efektach.

Dodatkowe informacje: https://www.youtube.com/watch?v=0Xg6r_MKGJ4 i w tym artykule.

Web Animation API

Powyższe właściwości mimo, że mają olbrzymią moc, nie zawsze będą wystarczające, a przy niektórych bardziej złożonych animacjach użycie samego CSS staje się wręcz zadaniem nierozwiązywalnym (szczególnie gdy chcemy mieć na stronie kilka zapętlonych zależnych od siebie animacji).

W dzisiejszych czasach do animacji możemy podejść na kilka sposobów:

  • możemy próbować animować z wykorzystaniem setInterval. Obecnie metoda ta wykorzystywana jest raczej przy zabawach z canvas i webgl, lub wszędzie tam gdzie potrzebujemy losowości.
  • możemy też to co się da animować w CSS, a w JS umiejętnie dodawać tylko odpowiednie klasy i ewentualnie reagować na odpowiednie zdarzenia. Metoda fajna przy prostych animacjach, ale raczej zawiedzie przy czymś bardziej skomplikowanym
  • w końcu możemy sięgnąć po odpowiednie biblioteki do animacji takie jak Green Sock (najlepsza w swoim fachu), http://animejs.com/ czy chociażby jQuery (plus naście innych).

W czystym Javascript przez wiele lat animacje były traktowane mocno po macoszemu. Sprawa zaczęła się zmieniać w ostatnich latach za sprawą tak zwanego Web Animate API, którego zadaniem jest utworzenie jednego standardy scalającego dwa światy - animacje tworzone w Javascript i CSS.

Cały mechanizm składa się z 3 kluczowych części: osi czasu, tak zwanych efektów, oraz obiektów animujących.

Osie czasu. Każdy dokument ma swoją oś czasu, która rozpoczyna się w momencie wczytania strony a kończy w nieskończoności. Na takiej osi układane są twoje animacje. W chwili pisania tego tekstu dla każdego dokumentu mamy dostęp tylko do jednej - głównej osi czasu document.timeline. Działa ona zupełnie tak samo jak w przypadku animacji CSS. Odpalamy animację i jest ona po prostu odgrywana w jakimś czasie. W przyszłości zapowiedziane są też inne rodzaje osi. Przykładowo postęp naszej animacji będzie zależny od przewinięcia strony czy innych gestów (a że świat mocno wchodzi w świat wirtualny...). Można to więc przyrównać do tego co już niedługo będziemy wyczyniać za pomocą CSS (1, 2, 3).

Efekty (AnimationEffect) to obiekty, które opisują przebieg animacji, czyli dla jakiego elementu w jaki sposób dane właściwości mają być animowane. Są więc zestawem informacji podobnych do CSSowego zapisu @keyframes i właściwości animation-. W chwili obecnej mamy tylko jeden typ efektów czyli KeyframeEffect (tak samo w CSS mamy tylko @keyframes). W przyszłości zapowiedziane są efekty pozwalające łączyć animacje, czy odpalać je sekwencyjnie (ale o tym później).

Same animacje (Animation) są z kolei obiektami, które scalają wszystko w całość pozwalając wreszcie odpalać naszą upragnioną animację. Przy ich tworzeniu musimy przekazać im zarówno efekty jak i oś, która zostanie użyta do odgrywania.

Tyle teorii - przejdźmy do praktyki.

Dodawanie animacji

Aby utworzyć animację, musimy zebrać wszystkie niezbędne informacje: jaki element ma być animowany, jak ma być odtwarzana animacja, oraz jaka oś ma być użyta do odtworzenia.

Możemy to osiągnąć korzystając z konstruktora Animation() oraz ze skróconego zapisu element.animation(). Zacznijmy od tego drugiego sposobu, bo prawdopodobnie to z niego będziesz najczęściej korzystał.

Aby dodać animację do elementu - podobnie jak w CSS musimy stworzyć opis animacji, a następnie podpiąć ją pod dany element.


const element = document.querySelector(".element");

//to co dawaliśmy w @keyframes
//tablica musi mieć minimum 2 wpisy, w przeciwnym razie dostaniemy błąd
const keyframes = [
    {
        transform: `rotate(0)`
    },
    {
        background: `gold`
    },
    {
        background: `dodgerblue`,
        borderRadius: `10px`,
        transform: `rotate(1turn)`
    }
];

//te same opcje co dla właściwości animation
const options = {
    iterations: 2, //liczba lub Infinity
    delay : 0, //w ms
    duration: 1000, //w ms
    fill : "forwards", //both, forward, backward
    direction : "alternate", //normal, backward,
    ease : "ease-in-out", //ease, linear, ease-in, ease-out, ease-in-out, cubic(), steps()
}

const animation = element.animate(keyframes, options);

Przy definiowaniu klatek animacji możemy skorzystać z właściwości offset, która przyjmuje wartość od 0 do 1. Właściwość ta określa moment na osi czasu i jest tym samym co zapis procentowy dla @keyframes w CSS.


const animation = element.animate([
    { //0%
        left : 0
    },
    { //45%
        left: 20%,
        offset: 0.45
    },
    { //75.5%
        left: 50%,
        offset: 0.755
    },
    { //100%
        left: 100%
    }
]);

W odróżnieniu od CSS możemy też określać konkretne przejścia kolejnymi punktami animacji:


const animation = element.animate([
    { //0%
        left : 0,
        ease: "ease-in"
    },
    { //70%
        left: 50%,
        offset: 0.7
        ease: "linear",
    },
    { //100%
        left: 100%
    }
]);

Najczęściej właściwość ease będziesz przekazywał w obiekcie opcji, co spowoduje, że będzie ona aplikowana na całą animację.

Jest jeszcze kilka wariantów opisu klatek, natomiast najczęściej spotkasz się z powyższym.

Jeżeli chcemy odtworzyć animację tylko jeden raz z domyślnymi parametrami, możemy też nie podawać żadnych opcji, a zamiast nich przekazać czas w milisekundach:


document.querySelector(".element").animate(
    [
        { opacity: 0, pointerEvents: "none" },
        { opacity: 1, pointerEvents: "all" }
    ],
    2000
)

Do pojedynczego elementu możemy podłączyć w ten sposób dowolną liczbę animacji.

Funkcje zwrotne, właściwości i metody

W przypadku tak utworzonych animacji mamy dostęp do kilku funkcji zwrotnych:


//gdy animacja zostanie usunięta z danego elementu
animation.onremove = () => {
    ...
}

//gdy animacja zostanie przerwana
animation.oncancel = () => {
    ...
}

//gdy animacja się zakończy
animation.onfinish = () => {
    ...
}

Kilku dodatkowych metod:

animation.cancel()przerwij działanie, wróć do stanu sprzed animacji
animation.commitStyles()zaaplikuj elementowi końcowe style animacji. Style zostaną zapisane w atrybucie style
animation.finish()przerwij odtwarzanie animacji, przejdź do końca animacji (nie można przerwać nieskończonych animacji)
animation.pause()pauzuje odtwarzanie
animation.play()wznów odtwarzanie animacji
animation.reverse()odgrywając do tyłu cofa animację do momentu kiedy zaczęła się odgrywać (animacja zatrzyma się w momencie kiedy wystartowała). Podobne efekty daje animation.playbackRate = -animation.
animation.updatePlaybackRate()ustawia szybkość odtwarzania animacji tuż po pierwszej synchronizacji pozycji odtwarzania

Oraz kilku właściwości:

animation.currentTimezwraca lyb ustawia aktualny czas na osi danej animacji w ms, lub null jeżeli animacja jeszcze nie została odtworzona
animation.effectzwraca lub ustawia zestaw AnimationEffect powiązany z tą animacją
animation.finishedzwraca true/false czy dana animacja zakończyła odtwarzanie
animation.idzwraca id animacji w postaci tekstu
animation.pendingtylko do odczytu - czy dana animacja oczekuje na asynchroniczną operację (pauzowanie lub włączenie odtwarzania)
animation.playStatetylko do odczytu - zwraca lub ustawia stan animacji (idle, running, paused, finished)
animation.playbackRatezwraca lub ustawia szybkość odtwarzania animacji
animation.readytylko do odczytu - zwraca obietnicę czy dana animacja jest gotowa do odtworzenia
animation.replaceStatetylko do odczytu - zwraca czy dana animacja została podmieniona
animation.startTimepobiera lub ustawia czas kiedy ma się rozpocząć odtwarzanie animacji
animation.timelinepobiera lub ustawia timeline dołączone do tej animacji

Przykłady

klik

const el = document.querySelector("#testAnimationEasy");

const animation = el.animate([
    { transform: `rotate(0)`, },
    { transform: `rotate(0.5turn)` }
], {
    duration: 2000,
    iterations: Infinity,
    ease: 'linear',
});

el.addEventListener("mouseenter", () => {
    animation.reverse();
    el.style.background = "crimson";
});

el.addEventListener("mouseleave", () => {
    animation.reverse();
    el.style.background = "";
});

el.addEventListener("click", () => {
    if (animation.playState === "paused") {
        animation.play();
    } else {
        animation.pause();
    }
});

test

const element = document.querySelector(".element");
const btnStart = document.querySelector("#start");
const btnRestart = document.querySelector("#restart");
const btnPlay = document.querySelector("#play");
const btnStop = document.querySelector("#stop");
const btnReverse = document.querySelector("#reverse");
const btnSpeedUp = document.querySelector("#speedUp");
const btnSpeedDown = document.querySelector("#speedDown");

const options = {
    iterations: 1,
    delay : 0,
    duration: 10 * 1000,
    fill : "forwards"
}

const keyframes = {
    ...
}

//załączam animację i od razu ją pauzuję
const animation = element.animate(keyframes, options);
animation.pause();
animation.onfinish = () => {
    console.log("koniec");
    btnPlay.textContent = "play";
}

btnRestart.addEventListener("click", () => {
    animation.currentTime = 0; //restartowanie - czyli przewinięcie na początek
})

btnPlay.addEventListener("click", () => {
    if (anim.playState !== "running") {
        animation.play(); //rozpoczęcie odgrywania
        btnPlay.textContent = "pause";
    } else {
        animation.pause();
        btnPlay.textContent = "play";
    }
})

btnStop.addEventListener("click", () => {
    animation.finish(); //przeniesienie na koniec animacji
    btnPlay.textContent = "play";
})

btnReverse.addEventListener("click", e => {
    animation.playbackRate = -animation.playbackRate; //odwrócenie kierunku animacji
    e.target.textContent = (animation.playbackRate < 0) ? "forward" : "reverse";
})

btnSpeedUp.addEventListener("click", () => {
    //jeżeli animacja jest w przeciwnym kierunku
    //by ją zwiększyć musimy odejmować od playbackRate
    if (animation.playbackRate < 0) {
        animation.playbackRate = animation.playbackRate - 1;
    } else {
        animation.playbackRate = animation.playbackRate + 1;
    }
})

btnSpeedDown.addEventListener("click", () => {
    if (animation.playbackRate < 0) {
        animation.playbackRate = animation.playbackRate + 1;
    } else {
        animation.playbackRate = animation.playbackRate - 1;
    }
})

Pobieranie animacji ze strony

Funkcja document.getAnimations() służy do pobrania animacji, które ustawiliśmy za pomocą animation API ale też i CSS.


const allAnimations = document.getAnimations();
for (let animation of allAnimations) {
    console.log(animation);
}

Gdy odpalisz powyższy kod, zobaczysz w konsoli, że na tej stronie jest użytych kilka typów animacji. Część z nich jest typu CSSAnimation, niektóre to Animation, a z kolei inne to CSSTransition. Łatwo się domyślić, że część z wymienionych w konsoli obiektów to animacje użyte w powyższych przykładach, transition z kolei to efekt przejścia tła dla przycisku.

Dla każdej takiej animacji możesz korzystać z wymienionych powyżej właściwości i metod, np. zatrzymując wszystkie animacje na stronie.


//jeżeli użytkownik nie lubi animacji
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
if (media.matches) {
    const allAnimations = document.getAnimations();
    for (let animation of allAnimations) {
        animation.pause(); //stopuje wszystkie animacje na stronie
        animation.playbackRate *= 0.5; //lub je spowalniam
    }
}

Podobną funkcję możemy też odpalić dla każdego elementu z osobna:


const el = document.querySelector(".element");
el.addEventListener("click", () => {
    const animations = el.getAnimations();
    console.log(animations);
})

Po kliknięciu na poniższy element dodaję mu animację CSS, animację Javascript oraz transition, a następnie wypisuję je w konsoli.

klik

Konstruktor Animation()

Animację możemy też utworzyć za pomocą konstruktora Animation(), do którego musimy przekazać obiekt z efektami, a także oś jaka zostanie użyta do odtwarzania (domyślnie będzie użyta oś document.timeline).

Gdy w powyższym przykładzie zbadasz w konsoli wypisane elementy, zauważysz, że wśród ich właściwości znajduje się właściwość effect. Wskazuje ona na wspomniane wcześniej efekty zawierające zbiór informacji o tym jaki element jest animowany (effect.target), oraz przebiegu i parametrach odtwarzania animacji. W przypadku powyższych animacji jest to obiekt typu KeyframeEffect.

W skróconej składni opisywaliśmy po prostu wygląd klatek i użytych parametrów. W przypadku konstruktora Animation() musimy wcześniej stworzyć podobny obiekt za pomocą konstruktora KeyFrameEffect():


const el = document.querySelector(".element");

const keyframes = [
    {
        transform: `rotate(0)`
    },
    {
        background: `gold`
    },
    {
        background: `dodgerblue`,
        borderRadius: `10px`,
        transform: `rotate(1turn)`
    }
];

const options = {
    iterations: 1,
    duration: 4000
};

const effect = new KeyFrameEffect(el, keyframes, options);

Utworzony w ten sposób obiekt możemy wykorzystać do utworzenia obiektu z animacją.


const el = document.querySelector(".element");

const keyframes = [
    ...
];

const options = {
    ...
};

const effect = new KeyFrameEffect(el, keyframes, options);

const animation = new Animation(el, effect, document.timeline);
animation.play(); //plus wszystkie właściwości

Główna różnica w porównaniu do skróconej składni jest taka, że do konstruktora Animation możemy przekazać obiekt z efektami. W przyszłości będziemy mogli tworzyć dodatkowe typy efektów takie jak "grupujące kilka animacji" (GroupEffect) oraz "odpalające animację sekwencyjnie" (SequenceEffects) plus pewnie kilka innych. Na chwilę obecną żadne z tych rozwiązań niestety domyślnie nie zadziała, dlatego aby ich użyć, skorzystam ze skryptu polyfill.


<script src="https://s3-us-west-2.amazonaws.com/s.cdpn.io/61811/web-animations-next-2.2.1.min.js"></script>

const btn = document.querySelector(".button");
const el1 = document.querySelector('.element1');
const el2 = document.querySelector('.element2');
const el3 = document.querySelector('.element3');

const options = {
    duration: 1000,
    iterations: 5,
    ease : "ease-in-out"
}

const keyframes = [
    { transform: `rotate(0)`, },
    { transform: `rotate(1turn)`, },
];

const group = new GroupEffect([
    new KeyframeEffect(el1, keyframes, {...options, duration: 1000}),
    new KeyframeEffect(el2, keyframes, {...options, duration: 2000}),
    new KeyframeEffect(el3, keyframes, {...options, duration: 3000})
]);

btn.addEventListener("click", e => {
    document.timeline.play(group);
})

const btn = document.querySelector("#testSequencePlay");
const elements = [...document.querySelectorAll('#testSequence > div')];

const options = {
    duration: 600,
    iterations: 1,
    ease : "ease-in-out",
    fill : "forwards"
}

const group = new SequenceEffect([
    new KeyframeEffect(elements[0], keyframes, options),
    new KeyframeEffect(elements[1], keyframes, options),
    new KeyframeEffect(elements[2], keyframes, options),
    new KeyframeEffect(elements[3], keyframes, options)
]);

//lub
// const group = new SequenceEffect([
//     ...elements.map(el => new KeyframeEffect(el, keyframes, options),)
// ]);

btn.addEventListener("click", e => {
    document.timeline.play(group);
})

Niestety na chwilę obecną są to nie tylko oficjalnie nie działające, ale też dość ograniczone rzeczy (np. brakuje tutaj powtarzania takich animacji).

Podsumowanie

Jak widzisz Javascript i CSS dają całkiem spore możliwości jeżeli chodzi o animacje. Mimo to wydaje się, że przy bardziej skomplikowanych projektach raczej nie zastąpią bibliotek takich jak Green Sock czy animejs, która to jest jeszcze wygodniejsza w użyciu i ma większe możliwości (a i lepiej sobie radzą przy animacji svg).

Tak naprawdę w powyższym tekście tylko lekko drasnęliśmy temat animacji. Im dalej w las tym bardziej będziesz się przekonywał, że w dzisiejszych czasach mamy wręcz nieograniczone możliwości w tym temacie.

Z mojej strony mogę ci polecić kilka materiałów, z których sam często korzystam:

A co dalej? Tyle się teraz mówi o Metaverse czy Neurolinku. Wszystko więc wskazuje na to, że dużymi krokami zbliża się kolejna "rewolucja" w internecie. Przykład? https://www.wirtualnemedia.pl/artykul/h-m-sklep-metaverse.
Może właśnie umiejętność tworzenia światów 3d i ich animacji będzie jedną z kluczowych? A jeżeli tak - three.js i zabawa z trójwymiarowymi światami to bardzo dobry wybór na kolejne eksperymenty.

Wszelkie prawa zastrzeżone. Jeżeli chcesz używać jakiejś części tego kursu, skontaktuj się z autorem.
Aha - i ta strona korzysta z ciasteczek.