Animacja wypełnienia SVG HTML5 z CSS3 i waniliowym JavaScript

Opublikowany: 2022-03-10
Szybkie podsumowanie ↬ W tym artykule dowiesz się, jak zbudować animowany wyświetlacz notatek ze strony internetowej Awwwards. Omawia element okręgu SVG HTML5, jego właściwości obrysu oraz sposób ich animacji za pomocą zmiennych CSS i Vanilla JavaScript.

SVG oznacza S calable V ector Graphics i jest standardowym językiem znaczników opartym na XML dla grafiki wektorowej. Pozwala na rysowanie ścieżek, krzywych i kształtów poprzez określenie zestawu punktów na płaszczyźnie 2D. Co więcej, możesz dodać do tych ścieżek właściwości drgania (takie jak obrys, kolor, grubość, wypełnienie i inne), aby tworzyć animacje.

Od kwietnia 2017 r. CSS Level 3 Fill and Stroke Module umożliwia ustawianie kolorów i wzorów wypełnienia SVG z zewnętrznego arkusza stylów, zamiast ustawiania atrybutów dla każdego elementu. W tym samouczku użyjemy prostego koloru szesnastkowego, ale zarówno właściwości wypełnienia, jak i obrysu akceptują również jako wartości wzory, gradienty i obrazy.

Uwaga : podczas odwiedzania witryny Awwwards animowane notatki można wyświetlać tylko przy szerokości przeglądarki ustawionej na 1024px lub więcej.

Uwaga Wyświetl demonstrację projektu
Demo efektu końcowego (duży podgląd)
  • Demo: Projekt wyświetlania notatki
  • Repo: Uwaga Wyświetl repozytorium
Więcej po skoku! Kontynuuj czytanie poniżej ↓

Struktura pliku

Zacznijmy od stworzenia plików w terminalu:

 mkdir note-display cd note-display touch index.html styles.css scripts.js

HTML

Oto początkowy szablon, który łączy zarówno pliki css , jak i js :

 <html lang="en"> <head> <meta charset="UTF-8"> <title>Note Display</title> <link rel="stylesheet" href="./styles.css"> </head> <body> <script src="./scripts.js"></script> </body> </html>

Każdy element notatki składa się z elementu listy: li zawierającego circle , wartość note i jej label .

Element listy i bezpośrednie dzieci
Element listy i jego bezpośrednie dzieci: .circle , .percent i .label . (duży podgląd)

.circle_svg to element SVG, który otacza dwa elementy <circle>. Pierwsza to ścieżka do wypełnienia, a druga to wypełnienie, które będzie animowane.

Elementy SVG
Elementy SVG. Opakowania SVG i znaczniki okręgów. (duży podgląd)

note jest podzielona na liczby całkowite i dziesiętne, dzięki czemu można do nich zastosować różne rozmiary czcionek. label to prosty <span> . Tak więc połączenie tego wszystkiego razem wygląda tak:

 <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li>

Atrybuty cx i cy definiują punkt środkowy osi x i y okręgu. Atrybut r określa jego promień.

Prawdopodobnie zauważyłeś wzór podkreślenia/myślnika w nazwach klas. To BEM, co oznacza block , element i modifier . Jest to metodologia, która sprawia, że ​​nazewnictwo elementów jest bardziej ustrukturyzowane, zorganizowane i semantyczne.

Zalecana lektura : Wyjaśnienie BEM i dlaczego go potrzebujesz

Aby zakończyć struktury szablonów, zapakujmy cztery elementy listy w nieuporządkowany element listy:

Nieuporządkowane opakowanie listy
Nieuporządkowane opakowanie listy zawiera czworo dzieci li (duży podgląd)
 <ul class="display-container"> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Reasonable</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Usable</span> </li> <li class="note-display"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Exemplary</span> </li> </ul>

Musisz zadać sobie pytanie, co oznaczają etykiety Transparent , Reasonable , Usable i Exemplary . Im bardziej zaznajomisz się z programowaniem, zdasz sobie sprawę, że pisanie kodu to nie tylko sprawienie, by aplikacja była funkcjonalna, ale także zapewniła, że ​​będzie ona długoterminowo utrzymywana i skalowalna. Można to osiągnąć tylko wtedy, gdy kod jest łatwy do zmiany.

„Skrót TRUE powinien pomóc zdecydować, czy kod, który piszesz, będzie w stanie uwzględnić zmiany w przyszłości, czy nie”.

Więc następnym razem zadaj sobie pytanie:

  • Transparent : Czy konsekwencje zmian w kodzie są jasne?
  • Reasonable : czy opłacalność jest tego warta?
  • Usable : czy będę mógł go ponownie wykorzystać w nieoczekiwanych sytuacjach?
  • Exemplary : Czy prezentuje wysoką jakość jako przykład dla przyszłego kodu?

Uwaga : „Praktyczny projekt zorientowany obiektowo w języku Ruby” autorstwa Sandi Metz wyjaśnia TRUE wraz z innymi zasadami i sposobem ich osiągnięcia poprzez wzorce projektowe. Jeśli nie poświęciłeś jeszcze trochę czasu na studiowanie wzorców projektowych, rozważ dodanie tej książki do czytania przed snem.

CSS

Zaimportujmy czcionki i zastosujmy reset do wszystkich elementów:

 @import url('https://fonts.googleapis.com/css?family=Nixie+One|Raleway:200'); * { padding: 0; margin: 0; box-sizing: border-box; }

Właściwość box-sizing: border-box zawiera wartości dopełnienia i obramowania w całkowitej szerokości i wysokości elementu, dzięki czemu łatwiej jest obliczyć jego wymiary.

Uwaga : Aby uzyskać wizualne wyjaśnienie na temat box-sizing , przeczytaj „Ułatw sobie życie dzięki rozmiarowi pudełka CSS”.

 body { height: 100vh; color: #fff; display: flex; background: #3E423A; font-family: 'Nixie One', cursive; } .display-container { margin: auto; display: flex; }

Łącząc reguły display: flex w body i margin-auto w .display-container , możliwe jest wyśrodkowanie elementu potomnego zarówno w pionie, jak iw poziomie. Element .display-container będzie również flex-container ; w ten sposób jego dzieci zostaną umieszczone w tym samym rzędzie wzdłuż głównej osi.

Element listy .note-display będzie również flex-container . Ponieważ istnieje wiele dzieci do centrowania, zróbmy to za pomocą właściwości justify-content i align-items . Wszystkie flex-items zostaną wyśrodkowane wzdłuż cross i main osi. Jeśli nie masz pewności, co to jest, zapoznaj się z sekcją wyrównania w „Przewodniku wizualnym CSS Flexbox Fundamentals”.

 .note-display { display: flex; flex-direction: column; align-items: center; margin: 0 25px; }

Zastosujmy obrys do okręgów, ustawiając reguły stroke-width stroke-opacity stroke-linecap , które razem określają styl żywych końców obrysu. Następnie dodajmy kolor do każdego koła:

 .circle__progress { fill: none; stroke-width: 3; stroke-opacity: 0.3; stroke-linecap: round; } .note-display:nth-child(1) .circle__progress { stroke: #AAFF00; } .note-display:nth-child(2) .circle__progress { stroke: #FF00AA; } .note-display:nth-child(3) .circle__progress { stroke: #AA00FF; } .note-display:nth-child(4) .circle__progress { stroke: #00AAFF; }

Aby absolutnie pozycjonować element percent , trzeba wiedzieć absolutnie do czego. Element .circle powinien być referencją, więc dodajmy position: relative niego.

Uwaga : Aby uzyskać głębsze, wizualne wyjaśnienie dotyczące pozycjonowania bezwzględnego, przeczytaj „Jak zrozumieć raz na zawsze pozycję CSS bezwzględną”.

Innym sposobem wyśrodkowania elementów jest połączenie top: 50% , left: 50% i transform: translate(-50%, -50%); które ustawiają środek elementu na środku jego rodzica.

 .circle { position: relative; } .percent { width: 100%; top: 50%; left: 50%; position: absolute; font-weight: bold; text-align: center; line-height: 28px; transform: translate(-50%, -50%); } .percent__int { font-size: 28px; } .percent__dec { font-size: 12px; } .label { font-family: 'Raleway', serif; font-size: 14px; text-transform: uppercase; margin-top: 15px; }

Do tej pory szablon powinien wyglądać tak:

Gotowy szablon początkowy
Gotowe elementy i style szablonu (duży podgląd)

Wypełnij przejście

Animację okręgu można utworzyć za pomocą dwóch właściwości SVG okręgu: stroke-dasharray i stroke-dashoffset .

stroke-dasharray definiuje wzór przerwy między kreskami w pociągnięciu”.

Może przyjąć maksymalnie cztery wartości:

  • Gdy jest ustawiony na jedyną liczbę całkowitą ( stroke-dasharray: 10 ), myślniki i przerwy mają ten sam rozmiar;
  • W przypadku dwóch wartości ( stroke-dasharray: 10 5 ) pierwsza jest stosowana do kresek, druga do przerw;
  • Trzecia i czwarta forma ( stroke-dasharray: 10 5 2 i stroke-dasharray: 10 5 2 3 ) wygenerują myślniki i przerwy w różnych rozmiarach.
Wartości właściwości obrysu dasharray
wartości właściwości stroke-dasharray (duży podgląd)

Obraz po lewej stronie pokazuje właściwość stroke-dasharray ustawioną od 0 do 238 pikseli, czyli długość obwodu koła.

Drugi obraz reprezentuje właściwość stroke-dashoffset , która przesuwa początek tablicy kreski. Jest również ustawiany od 0 do długości obwodu koła.

Właściwości obrysu dasharray i dashoffset
stroke-dasharray i przesunięcie kreski obrysu właściwości (duży podgląd)

Aby uzyskać efekt wypełnienia, ustawimy stroke-dasharray na długość obwodu, tak aby cała jego długość została wypełniona dużą kreską i bez przerwy. Przesuniemy go również o tę samą wartość, więc zostanie „ukryty”. Następnie stroke-dashoffset zostanie zaktualizowane do odpowiedniej wartości nuty, wypełniając kreskę zgodnie z czasem trwania przejścia.

Aktualizacja właściwości zostanie wykonana w skryptach poprzez zmienne CSS. Zadeklarujmy zmienne i ustawmy właściwości:

 .circle__progress--fill { --initialStroke: 0; --transitionDuration: 0; stroke-opacity: 1; stroke-dasharray: var(--initialStroke); stroke-dashoffset: var(--initialStroke); transition: stroke-dashoffset var(--transitionDuration) ease; }

Aby ustawić wartość początkową i zaktualizować zmienne, zacznijmy od zaznaczenia wszystkich elementów .note-display za pomocą document.querySelectorAll . transitionDuration zostanie ustawiony na 900 milisekund.

Następnie iterujemy po tablicy display, wybieramy jej .circle__progress.circle__progress--fill i wyodrębniamy atrybut r ustawiony w HTML, aby obliczyć długość obwodu. Dzięki temu możemy ustawić początkowe wartości --dasharray i --dashoffset .

Animacja pojawi się, gdy zmienna --dashoffset zostanie zaktualizowana przez setTimeout 100ms:

 const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius; progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); progress.style.setProperty('--initialStroke', circumference); setTimeout(() => progress.style.strokeDashoffset = 50, 100); });

Aby uzyskać przejście od góry, element .circle__svg musi zostać obrócony:

 .circle__svg { transform: rotate(-90deg); } 
Przejście właściwości obrysu
Przejście właściwości obrysu (duży podgląd)

Teraz obliczmy wartość dashoffset — względem nuty. Wartość notatki zostanie wstawiona do każdego elementu li za pomocą atrybutu data-*. Znak * można zamienić na dowolną nazwę, która odpowiada Twoim potrzebom, a następnie można go pobrać w JavaScript za pomocą zestawu danych elementu: element.dataset.* .

Uwaga : Możesz przeczytać więcej o atrybucie data-* w MDN Web Docs.

Nasz atrybut będzie nazywał się „ data-note ”:

 <ul class="display-container"> + <li class="note-display" data-note="7.50"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Transparent</span> </li> + <li class="note-display" data-note="9.27"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Reasonable</span> </li> + <li class="note-display" data-note="6.93"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Usable</span> </li> + <li class="note-display" data-note="8.72"> <div class="circle"> <svg width="84" height="84" class="circle__svg"> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--path"></circle> <circle cx="41" cy="41" r="38" class="circle__progress circle__progress--fill"></circle> </svg> <div class="percent"> <span class="percent__int">0.</span> <span class="percent__dec">00</span> </div> </div> <span class="label">Exemplary</span> </li> </ul>

Metoda parseFloat przekonwertuje ciąg zwrócony przez display.dataset.note na liczbę zmiennoprzecinkową. offset reprezentuje procent brakujący do osiągnięcia maksymalnego wyniku. Tak więc dla nuty 7.50 mielibyśmy (10 - 7.50) / 10 = 0.25 , co oznacza, że ​​długość circumference powinna być przesunięta o 25% jego wartości:

 let note = parseFloat(display.dataset.note); let offset = circumference * (10 - note) / 10;

Aktualizacja scripts.js :

 const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let progress = display.querySelector('.circle__progress--fill'); let radius = progress.r.baseVal.value; let circumference = 2 * Math.PI * radius; + let note = parseFloat(display.dataset.note); + let offset = circumference * (10 - note) / 10; progress.style.setProperty('--initialStroke', circumference); progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); + setTimeout(() => progress.style.strokeDashoffset = offset, 100); }); 
Przejście właściwości obrysu do wartości nuty
Przejście właściwości obrysu do wartości nuty (duży podgląd)

Zanim przejdziemy dalej, wyodrębnijmy przejście stoke do jego własnej metody:

 const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { - let progress = display.querySelector('.circle__progress--fill'); - let radius = progress.r.baseVal.value; - let circumference = 2 * Math.PI * radius; let note = parseFloat(display.dataset.note); - let offset = circumference * (10 - note) / 10; - progress.style.setProperty('--initialStroke', circumference); - progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); - setTimeout(() => progress.style.strokeDashoffset = offset, 100); + strokeTransition(display, note); }); + function strokeTransition(display, note) { + let progress = display.querySelector('.circle__progress--fill'); + let radius = progress.r.baseVal.value; + let circumference = 2 * Math.PI * radius; + let offset = circumference * (10 - note) / 10; + progress.style.setProperty('--initialStroke', circumference); + progress.style.setProperty('--transitionDuration', `${transitionDuration}ms`); + setTimeout(() => progress.style.strokeDashoffset = offset, 100); + }

Uwaga Zwiększenie wartości

Nadal istnieje przejście nuty od 0.00 do wartości nuty do zbudowania. Pierwszą rzeczą do zrobienia jest oddzielenie wartości całkowitych i dziesiętnych. Użyjemy metody łańcuchowej split() (pobiera argument, który określa, gdzie zostanie przerwany łańcuch i zwraca tablicę zawierającą oba złamane łańcuchy). Zostaną one przekonwertowane na liczby i przekazane jako argumenty do funkcji increaseNumber() , wraz z elementem display i flagą wskazującą, czy jest to liczba całkowita, czy dziesiętna.

 const displays = document.querySelectorAll('.note-display'); const transitionDuration = 900; displays.forEach(display => { let note = parseFloat(display.dataset.note); + let [int, dec] = display.dataset.note.split('.'); + [int, dec] = [Number(int), Number(dec)]; strokeTransition(display, note); + increaseNumber(display, int, 'int'); + increaseNumber(display, dec, 'dec'); });

W funkcji IncreaseNumber increaseNumber() wybieramy element .percent__int lub .percent__dec , w zależności od className , a także w przypadku, gdy wyjście powinno zawierać kropkę dziesiętną, czy nie. Ustawiliśmy czas trwania transitionDuration na 900ms . Aby na przykład animować liczbę od 0 do 7, czas trwania należy podzielić przez nutę 900 / 7 = 128.57ms . Wynik reprezentuje czas trwania każdej iteracji zwiększania. Oznacza to, że nasz setInterval będzie uruchamiany co 128.57ms .

Mając ustawione te zmienne, zdefiniujmy setInterval . Zmienna counter zostanie dołączona do elementu jako tekst i zwiększona po każdej iteracji:

 function increaseNumber(display, number, className) { let element = display.querySelector(`.percent__${className}`), decPoint = className === 'int' ? '.' : '', interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() => { element.textContent = counter + decPoint; counter++; }, interval); } 
Nieskończony wzrost licznika
Nieskończony wzrost licznika (duży podgląd)

Fajny! Zwiększa wartości, ale w pewnym sensie robi to na zawsze. Musimy wyczyścić setInterval , gdy nuty osiągną żądaną wartość. Odbywa się to za pomocą funkcji clearInterval :

 function increaseNumber(display, number, className) { let element = display.querySelector(`.percent__${className}`), decPoint = className === 'int' ? '.' : '', interval = transitionDuration / number, counter = 0; let increaseInterval = setInterval(() => { + if (counter === number) { window.clearInterval(increaseInterval); } element.textContent = counter + decPoint; counter++; }, interval); } 
Zakończony projekt wyświetlania notatek
Gotowy projekt (duży podgląd)

Teraz numer jest aktualizowany do wartości nuty i usuwany za pomocą funkcji clearInterval() .

To tyle w tym samouczku. Mam nadzieję, że ci się podobało!

Jeśli masz ochotę zbudować coś bardziej interaktywnego, sprawdź mój samouczek gry pamięciowej stworzony za pomocą Vanilla JavaScript. Obejmuje podstawowe koncepcje HTML5, CSS3 i JavaScript, takie jak pozycjonowanie, perspektywa, przejścia, Flexbox, obsługa zdarzeń, limity czasu i trójniki.

Udanego kodowania!