Animacja wypełnienia SVG HTML5 z CSS3 i waniliowym JavaScript
Opublikowany: 2022-03-10SVG 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.

- Demo: Projekt wyświetlania notatki
- Repo: Uwaga Wyświetl repozytorium
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
.

.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.

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:

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:

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
istroke-dasharray: 10 5 2 3
) wygenerują myślniki i przerwy w różnych rozmiarach.

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.

stroke-dasharray
i 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); }

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); });

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); }

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); }

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!