Animazione di riempimento SVG HTML5 con CSS3 e JavaScript vaniglia
Pubblicato: 2022-03-10SVG sta per S calable V ector G raphics ed è un linguaggio di markup standard basato su XML per la grafica vettoriale. Ti consente di disegnare percorsi, curve e forme determinando un insieme di punti nel piano 2D. Inoltre, puoi aggiungere proprietà di contrazione su quei percorsi (come tratto, colore, spessore, riempimento e altro) per produrre animazioni.
Da aprile 2017, il modulo Fill and Stroke di livello 3 CSS consente di impostare i colori SVG e i pattern di riempimento da un foglio di stile esterno, invece di impostare attributi su ciascun elemento. In questo tutorial useremo un semplice colore esadecimale, ma sia le proprietà di riempimento che di tratto accettano anche modelli, sfumature e immagini come valori.
Nota : quando si visita il sito Web di Awwwards, la visualizzazione della nota animata può essere visualizzata solo con la larghezza del browser impostata su 1024 pixel o più.

- Demo: progetto di visualizzazione delle note
- Repo: Nota Visualizza Repo
Struttura del file
Iniziamo creando i file nel terminale:
mkdir note-display cd note-display touch index.html styles.css scripts.js
HTML
Ecco il modello iniziale che collega i file css
e 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>
Ogni elemento della nota è costituito da una voce di elenco: li
che contiene il circle
, il valore della note
e la sua label
.

.circle
, .percent
e .label
. (Grande anteprima) .circle_svg
è un elemento SVG, che racchiude due elementi <circle>. Il primo è il percorso da riempire mentre il secondo è il riempimento che verrà animato.

La note
è separata in numeri interi e decimali in modo che possano essere applicati caratteri di dimensioni diverse. L' label
è un semplice <span>
. Quindi, mettendo insieme tutto questo si presenta così:
<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>
Gli attributi cx
e cy
definiscono il punto centrale dell'asse x e dell'asse y del cerchio. L'attributo r
ne definisce il raggio.
Probabilmente hai notato lo schema di sottolineatura/trattino nei nomi delle classi. Questo è BEM, che sta per block
, element
e modifier
. È una metodologia che rende la denominazione dei tuoi elementi più strutturata, organizzata e semantica.
Letture consigliate : Una Spiegazione Di BEM E Perché Ne Hai Bisogno
Per completare le strutture del modello, avvolgiamo i quattro elementi dell'elenco in un elemento dell'elenco non ordinato:

li
(anteprima grande) <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>
Ti starai chiedendo cosa significano le etichette Transparent
, Reasonable
, Usable
ed Exemplary
. Più acquisirai familiarità con la programmazione, ti renderai conto che scrivere codice non significa solo rendere funzionale l'applicazione, ma anche assicurarne la manutenibilità e la scalabilità a lungo termine. Ciò si ottiene solo se il codice è facile da modificare.
"L'acronimo TRUE
dovrebbe aiutare a decidere se il codice che scrivi sarà in grado di accogliere i cambiamenti in futuro o meno."
Quindi, la prossima volta chiediti:
-
Transparent
: le conseguenze delle modifiche al codice sono chiare? -
Reasonable
: ne vale la pena? -
Usable
: sarò in grado di riutilizzarlo in scenari imprevisti? -
Exemplary
: presenta l'alta qualità come esempio per il codice futuro?
Nota : "Pratico design orientato agli oggetti in Ruby" di Sandi Metz spiega TRUE
insieme ad altri principi e come raggiungerli attraverso i modelli di progettazione. Se non ti sei ancora preso del tempo per studiare i modelli di progettazione, considera di aggiungere questo libro alla lettura della tua buonanotte.
CSS
Importiamo i font e applichiamo un reset a tutti gli elementi:
@import url('https://fonts.googleapis.com/css?family=Nixie+One|Raleway:200'); * { padding: 0; margin: 0; box-sizing: border-box; }
La proprietà box-sizing: border-box
include valori di riempimento e bordo nella larghezza e altezza totali di un elemento, quindi è più facile calcolarne le dimensioni.
Nota : per una spiegazione visiva sul box-sizing
, leggi "Rendi la tua vita più facile con il dimensionamento delle scatole CSS".
body { height: 100vh; color: #fff; display: flex; background: #3E423A; font-family: 'Nixie One', cursive; } .display-container { margin: auto; display: flex; }
Combinando le regole display: flex
nel body
e margin-auto
nel .display-container
, è possibile centrare l'elemento figlio sia verticalmente che orizzontalmente. L'elemento .display-container
sarà anche un flex-container
; in questo modo, i suoi figli verranno posizionati nella stessa riga lungo l'asse principale.
L'elemento dell'elenco .note-display
sarà anche un flex-container
. Dato che ci sono molti figli per la centratura, facciamolo attraverso le proprietà justify-content
e align-items
. Tutti flex-items
saranno centrati lungo la cross
e l'asse main
. Se non sei sicuro di cosa siano, controlla la sezione sull'allineamento in "Guida visiva ai fondamentali di CSS Flexbox".
.note-display { display: flex; flex-direction: column; align-items: center; margin: 0 25px; }
Applichiamo un tratto ai cerchi impostando le regole stroke-width
, stroke-opacity
e stroke-linecap
che insieme modellano le estremità live del tratto. Quindi, aggiungiamo un colore a ciascun cerchio:
.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; }
Per posizionare l'elemento percent
in modo assoluto, è necessario sapere assolutamente a cosa. L'elemento .circle
dovrebbe essere il riferimento, quindi aggiungiamo position: relative
ad esso.
Nota : per una spiegazione visiva più approfondita sul posizionamento assoluto, leggere "Come comprendere la posizione CSS assoluta una volta per tutte".
Un altro modo per centrare gli elementi è combinare top: 50%
, left: 50%
e transform: translate(-50%, -50%);
che posizionano il centro dell'elemento al centro del suo genitore.
.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; }
A questo punto, il modello dovrebbe essere simile a questo:

Riempi la transizione
L'animazione del cerchio può essere creata con l'aiuto di due proprietà SVG del cerchio: stroke-dasharray
e stroke-dashoffset
.

" stroke-dasharray
definisce il pattern di spaziatura del trattino in un tratto."
Può richiedere fino a quattro valori:
- Quando è impostato su un solo intero (
stroke-dasharray: 10
), i trattini e gli spazi hanno la stessa dimensione; - Per due valori (
stroke-dasharray: 10 5
), il primo viene applicato ai trattini, il secondo agli spazi vuoti; - La terza e la quarta forma (
stroke-dasharray: 10 5 2
estroke-dasharray: 10 5 2 3
) genereranno trattini e spazi vuoti di varie dimensioni.

stroke-dasharray
(anteprima grande) L'immagine a sinistra mostra la proprietà stroke-dasharray
impostata da 0 a 238px, che è la lunghezza della circonferenza del cerchio.
La seconda immagine rappresenta la proprietà stroke-dashoffset
che sposta l'inizio della matrice di trattini. Viene inoltre impostato da 0 alla lunghezza della circonferenza del cerchio.

stroke-dasharray
e Per produrre l'effetto di riempimento, imposteremo il stroke-dasharray
sulla lunghezza della circonferenza, in modo che tutta la sua lunghezza venga riempita con un grande trattino e senza spazi vuoti. Lo compenseremo anche dello stesso valore, in modo che venga "nascosto". Quindi il stroke-dashoffset
verrà aggiornato al valore della nota corrispondente, riempiendo il tratto in base alla durata della transizione.
L'aggiornamento delle proprietà avverrà negli script tramite le variabili CSS. Dichiariamo le variabili e impostiamo le proprietà:
.circle__progress--fill { --initialStroke: 0; --transitionDuration: 0; stroke-opacity: 1; stroke-dasharray: var(--initialStroke); stroke-dashoffset: var(--initialStroke); transition: stroke-dashoffset var(--transitionDuration) ease; }
Per impostare il valore iniziale e aggiornare le variabili, iniziamo selezionando tutti gli elementi .note-display
con document.querySelectorAll
. La transitionDuration
sarà impostata su 900
millisecondi.
Quindi, ripetiamo l'array display, selezioniamo il suo .circle__progress.circle__progress--fill
ed estraiamo l'attributo r
impostato nell'HTML per calcolare la lunghezza della circonferenza. Con ciò, possiamo impostare i valori --dasharray
e --dashoffset
iniziali.
L'animazione si verificherà quando la variabile --dashoffset
viene aggiornata di un setTimeout di 100 ms:
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); });
Per ottenere la transizione partendo dall'alto, l'elemento .circle__svg
deve essere ruotato:
.circle__svg { transform: rotate(-90deg); }

Ora calcoliamo il valore di dashoffset
, relativo alla nota. Il valore della nota verrà inserito in ogni elemento li
tramite l'attributo data-*. *
può essere cambiato con qualsiasi nome adatto alle tue esigenze e può quindi essere recuperato in JavaScript tramite il set di dati dell'elemento: element.dataset.*
.
Nota : puoi leggere ulteriori informazioni sull'attributo data-* su MDN Web Docs.
Il nostro attributo sarà chiamato " 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>
Il metodo parseFloat
converte la stringa restituita da display.dataset.note
in un numero a virgola mobile. L' offset
rappresenta la percentuale mancante per raggiungere il punteggio massimo. Quindi, per una nota da 7.50
, avremmo (10 - 7.50) / 10 = 0.25
, il che significa che la lunghezza della circumference
dovrebbe essere compensata del 25%
del suo valore:
let note = parseFloat(display.dataset.note); let offset = circumference * (10 - note) / 10;
Aggiornamento di 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); });

Prima di andare avanti, estraiamo la transizione dello stoke al proprio metodo:
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); + }
Nota Aumento del valore
C'è ancora la transizione della nota da 0.00
al valore della nota da costruire. La prima cosa da fare è separare i valori interi e decimali. Useremo il metodo delle stringhe split()
(prende un argomento che determina dove la stringa verrà interrotta e restituisce un array contenente entrambe le stringhe spezzate). Questi verranno convertiti in numeri e passati come argomenti alla funzione increaseNumber()
, insieme all'elemento display
e un flag che indica se è un numero intero o un decimale.
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'); });
Nella funzione IncreaseNumber increaseNumber()
, selezioniamo l'elemento .percent__int
o .percent__dec
, a seconda di className
, e anche nel caso in cui l'output contenga o meno un punto decimale. Abbiamo impostato la nostra transitionDuration
su 900ms
. Ora, per animare un numero da 0 a 7, ad esempio, la durata deve essere divisa per la nota 900 / 7 = 128.57ms
. Il risultato rappresenta quanto tempo impiegherà ogni iterazione di aumento. Ciò significa che il nostro setInterval
si attiverà ogni 128.57ms
.
Con queste variabili impostate, definiamo il setInterval
. La variabile counter
verrà aggiunta all'elemento come testo e aumentata ad ogni iterazione:
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); }

Freddo! Aumenta i valori, ma lo fa per sempre. Abbiamo bisogno di cancellare il setInterval
quando le note raggiungono il valore che vogliamo. Questo viene fatto con la funzione 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); }

Ora il numero viene aggiornato fino al valore della nota e cancellato con la funzione clearInterval()
.
Questo è praticamente tutto per questo tutorial. Spero ti sia piaciuto!
Se hai voglia di costruire qualcosa di un po' più interattivo, dai un'occhiata al mio tutorial sui giochi di memoria creato con Vanilla JavaScript. Copre concetti di base di HTML5, CSS3 e JavaScript come posizionamento, prospettiva, transizioni, Flexbox, gestione degli eventi, timeout e ternari.
Buona codifica!