Animazione di riempimento SVG HTML5 con CSS3 e JavaScript vaniglia

Pubblicato: 2022-03-10
Riepilogo rapido ↬ In questo articolo, puoi imparare come costruire la visualizzazione animata delle note dal sito Web di Awwwards. Discute l'elemento del cerchio SVG HTML5, le sue proprietà del tratto e come animarle con le variabili CSS e Vanilla JavaScript.

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

Nota Visualizza la demo del progetto
Una demo del risultato finale (Anteprima grande)
  • Demo: progetto di visualizzazione delle note
  • Repo: Nota Visualizza Repo
Altro dopo il salto! Continua a leggere sotto ↓

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 .

Elenca elemento elemento e figli diretti
Elenca l'elemento dell'elemento e i suoi figli diretti: .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.

Elementi SVG
Elementi SVG. Involucro SVG e tag cerchio. (Grande anteprima)

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:

Wrapper elenco non ordinato
Il wrapper elenco non ordinato contiene quattro figli 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:

Modello iniziale finito
Elementi e stili del modello finiti (anteprima grande)

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 e stroke-dasharray: 10 5 2 3 ) genereranno trattini e spazi vuoti di varie dimensioni.
Traccia i valori delle proprietà di dasharray
valori delle proprietà 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.

Proprietà del tratto dasharray e dashoffset
stroke-dasharray e tratto trattino-offset proprietà (Anteprima grande)

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); } 
Transizione delle proprietà del tratto
Transizione proprietà tratto (Anteprima grande)

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); }); 
Le proprietà del tratto passano al valore della nota
Le proprietà del tratto passano al valore della nota (Anteprima grande)

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); } 
Infinito aumento del contatore
Incremento infinito del contatore (Anteprima grande)

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); } 
Progetto di visualizzazione delle note terminato
Progetto finito (Anteprima grande)

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!