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.jsHTML
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 2istroke-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!
