Animație de umplere SVG HTML5 cu CSS3 și JavaScript Vanilla

Publicat: 2022-03-10
Rezumat rapid ↬ În acest articol, puteți afla cum să creați afișajul animat al notelor de pe site-ul web Awwwards. Se discută despre elementul cerc SVG HTML5, proprietățile sale de contur și cum să le animați cu variabile CSS și JavaScript Vanilla.

SVG înseamnă S calable Vector G raphics și este un limbaj standard de marcare bazat pe XML pentru grafica vectorială. Vă permite să desenați trasee, curbe și forme prin determinarea unui set de puncte în planul 2D. În plus, puteți adăuga proprietăți de twitch pe acele căi (cum ar fi contur, culoare, grosime, umplere și multe altele) pentru a produce animații.

Din aprilie 2017, Modulul de umplere și contur CSS de nivel 3 permite setarea culorilor SVG și a modelelor de umplere dintr-o foaie de stil externă, în loc să se stabilească atribute pentru fiecare element. În acest tutorial, vom folosi o culoare hexagonală simplă, dar atât proprietățile de umplere, cât și de contur acceptă și modele, degrade și imagini ca valori.

Notă : Când vizitați site-ul web Awwwards, afișarea notă animată poate fi vizualizată numai cu lățimea browserului setată la 1024px sau mai mult.

Notă Afișează Demo Proiect
O demonstrație a rezultatului final (previzualizare mare)
  • Demo: Proiect de afișare a notă
  • Repo: Notă Afișează Repo
Mai multe după săritură! Continuați să citiți mai jos ↓

Structura fișierului

Să începem prin a crea fișierele în terminal:

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

HTML

Iată șablonul inițial care leagă fișierele css ș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>

Fiecare element de notă este format dintr-un element de listă: li care conține circle , valoarea note și label acesteia .

Listează elementul elementului și direcționează copiii
Elementul de listă și copiii săi direcți: .circle , .percent și .label . (Previzualizare mare)

.circle_svg este un element SVG, care include două elemente <circle>. Prima este calea care trebuie umplută, în timp ce a doua este umplerea care va fi animată.

elemente SVG
elemente SVG. Înveliș SVG și etichete cerc. (Previzualizare mare)

note este separată în numere întregi și zecimale, astfel încât li se pot aplica diferite dimensiuni de font. label este un simplu <span> . Deci, adunând toate acestea împreună arată astfel:

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

Atributele cx și cy definesc axa x și punctul central al axei y ale cercului. Atributul r își definește raza.

Probabil ați observat modelul de subliniere/liniuță în numele claselor. Acesta este BEM, care înseamnă block , element și modifier . Este o metodologie care face ca numirea elementelor dvs. să fie mai structurată, organizată și semantică.

Lectură recomandată : O explicație a BEM și de ce aveți nevoie de el

Pentru a finaliza structurile șablonului, să încapsulăm cele patru elemente de listă într-un element de listă neordonat:

Înveliș de listă neordonat
Învelișul de listă neordonat conține patru copii li (previzualizare mare)
 <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>

Trebuie să vă întrebați ce înseamnă etichetele Transparent , Reasonable , Usable și Exemplary . Cu cât vă familiarizați mai bine cu programarea, vă veți da seama că scrierea codului nu înseamnă doar să faceți aplicația funcțională, ci și să vă asigurați că va fi menținută și scalabilă pe termen lung. Acest lucru se realizează numai dacă codul dvs. este ușor de schimbat.

„Acronimul TRUE ar trebui să vă ajute să decideți dacă codul pe care îl scrieți va fi capabil să facă față schimbărilor în viitor sau nu.”

Deci, data viitoare, întreabă-te:

  • Transparent : Consecințele modificărilor codului sunt clare?
  • Reasonable : merită cost-beneficiu?
  • Usable : îl voi putea reutiliza în scenarii neașteptate?
  • Exemplary : prezintă calitate înaltă ca exemplu pentru codul viitor?

Notă : „Proiectare practică orientată pe obiecte în Ruby” de Sandi Metz explică TRUE împreună cu alte principii și cum să le realizezi prin modele de design. Dacă nu ți-ai făcut încă ceva timp pentru a studia modelele de design, ia în considerare adăugarea acestei cărți la lectura de la culcare.

CSS

Să importăm fonturile și să aplicăm o resetare tuturor elementelor:

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

Proprietatea box-sizing: border-box include valorile de umplutură și chenar în lățimea și înălțimea totală a unui element, astfel încât este mai ușor să calculați dimensiunile acestuia.

Notă : Pentru o explicație vizuală despre box-sizing , vă rugăm să citiți „Ușurează-ți viața cu dimensiunea casetei CSS”.

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

Prin combinarea regulilor de display: flex în body și margin-auto în .display-container , este posibil să centrați elementul copil atât pe verticală, cât și pe orizontală. Elementul .display-container va fi de asemenea un flex-container ; astfel, copiii săi vor fi plasați în același rând de-a lungul axei principale.

Elementul din lista .note-display va fi, de asemenea, un flex-container . Deoarece există mulți copii pentru centrare, să o facem prin proprietățile justify-content și align-items . Toate flex-items vor fi centrate de-a lungul cross și a axei main . Dacă nu sunteți sigur care sunt acestea, consultați secțiunea de aliniere din „CSS Flexbox Fundamentals Visual Guide”.

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

Să aplicăm o contur cercurilor setând regulile stroke-width , stroke-opacity -opacity și stroke-linecap care stilează capetele vii ale cursei. Apoi, să adăugăm o culoare fiecărui cerc:

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

Pentru a poziționa absolut elementul percent , este necesar să știți absolut la ce. Elementul .circle ar trebui să fie referința, așa că să adăugăm position: relative la acesta.

Notă : pentru o explicație vizuală mai profundă despre poziționarea absolută, vă rugăm să citiți „Cum să înțelegeți odată pentru totdeauna poziția CSS absolută”.

Un alt mod de centrare a elementelor este combinarea de top: 50% , left: 50% și transform: translate(-50%, -50%); care poziționează centrul elementului în centrul părintelui său.

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

Până acum, șablonul ar trebui să arate astfel:

Șablon inițial terminat
Elemente și stiluri de șablon finalizate (previzualizare mare)

Tranziție de umplere

Animația cercului poate fi creată cu ajutorul a două proprietăți SVG de cerc: stroke-dasharray și stroke-dashoffset .

stroke-dasharray definește modelul liniuță-decalaj într-un accident vascular cerebral.”

Poate lua până la patru valori:

  • Când este setat la un singur număr întreg ( stroke-dasharray: 10 ), liniuțele și spațiile au aceeași dimensiune;
  • Pentru două valori ( stroke-dasharray: 10 5 ), prima se aplică liniuțelor, a doua la goluri;
  • A treia și a patra formă ( stroke-dasharray: 10 5 2 și stroke-dasharray: 10 5 2 3 ) vor genera liniuțe și goluri de diferite dimensiuni.
Trageți valorile proprietății dasharray
valorile proprietății stroke-dasharray (previzualizare mare)

Imaginea din stânga arată proprietatea stroke-dasharray fiind setată de la 0 la 238px, care este lungimea circumferinței cercului.

A doua imagine reprezintă proprietatea stroke-dashoffset care compensează începutul matricei liniuțe. De asemenea, este setat de la 0 la lungimea circumferinței cercului.

Stroke dasharray și proprietățile dashoffset
stroke-dasharray şi stroke-dashoffset proprietăți (previzualizare mare)

Pentru a produce efectul de umplere, vom seta stroke-dasharray la lungimea circumferinței, astfel încât toată lungimea sa să fie umplută cu o liniuță mare și fără gol. De asemenea, îl vom compensa cu aceeași valoare, astfel încât să devină „ascuns”. Apoi stroke-dashoffset va fi actualizat la valoarea notei corespunzătoare, umplând stroke în mod corespunzător cu durata tranziției.

Actualizarea proprietăților se va face în scripturi prin Variabile CSS. Să declarăm variabilele și să setăm proprietățile:

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

Pentru a seta valoarea inițială și a actualiza variabilele, să începem prin a selecta toate elementele .note-display cu document.querySelectorAll . Durata transitionDuration va fi setată la 900 de milisecunde.

Apoi, repetăm ​​matricea displays, îi .circle__progress.circle__progress--fill și extragem atributul r setat în HTML pentru a calcula lungimea circumferinței. Cu asta, putem seta valorile inițiale --dasharray și --dashoffset .

Animația va apărea când variabila --dashoffset este actualizată cu un setTimeout de 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); });

Pentru ca tranziția să înceapă de sus, elementul .circle__svg trebuie rotit:

 .circle__svg { transform: rotate(-90deg); } 
Tranziția proprietăților cursei
Tranziția proprietăților cursei (previzualizare mare)

Acum, să calculăm valoarea dashoffset - relativ la notă. Valoarea notei va fi inserată în fiecare element li prin atributul data-*. * poate fi schimbat pentru orice nume care se potrivește nevoilor dvs. și apoi poate fi preluat în JavaScript prin setul de date al elementului: element.dataset.* .

Notă : puteți citi mai multe despre atributul data-* pe MDN Web Docs.

Atributul nostru se va numi „ 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 va converti șirul returnat de display.dataset.note într-un număr cu virgulă mobilă. offset -ul reprezintă procentul care lipsește pentru a atinge scorul maxim. Deci, pentru o notă de 7.50 , am avea (10 - 7.50) / 10 = 0.25 , ceea ce înseamnă că lungimea circumference ar trebui compensată cu 25% din valoarea sa:

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

Actualizarea 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); }); 
Proprietățile cursei trec la valoarea notei
Tranziția proprietăților cursei la valoarea notei (previzualizare mare)

Înainte de a trece mai departe, să extragem tranziția stoke la propria sa metodă:

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

Notă creșterea valorii

Mai există tranziția notei de la 0.00 la valoarea notei care urmează să fie construită. Primul lucru de făcut este să separați valorile întregi și zecimale. Vom folosi metoda string split() (este nevoie de un argument care determină unde va fi rupt șirul și returnează o matrice care conține ambele șiruri rupte). Acestea vor fi convertite în numere și transmise ca argumente la funcția de increaseNumber() , împreună cu elementul de display și un steag care indică dacă este un întreg sau o zecimală.

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

În funcția de increaseNumber() , selectăm fie elementul .percent__int sau .percent__dec , în funcție de className și, de asemenea, în cazul în care rezultatul ar trebui să conțină un punct zecimal sau nu. Am setat Durata transitionDuration la 900ms . Acum, pentru a anima un număr de la 0 la 7, de exemplu, durata trebuie împărțită la nota 900 / 7 = 128.57ms . Rezultatul reprezintă cât timp va dura fiecare iterație de creștere. Aceasta înseamnă că setInterval nostru se va declanșa la fiecare 128.57ms .

Cu acele variabile setate, să definim setInterval . Variabila counter va fi atașată elementului ca text și va fi mărită la fiecare iterație:

 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); } 
Creștere infinită a contorului
Creștere infinită a contorului (previzualizare mare)

Misto! Mărește valorile, dar o face pentru totdeauna. Trebuie să setInterval atunci când notele ating valoarea pe care o dorim. Acest lucru se face cu funcția 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); } 
Proiect de afișare a notelor finalizat
Proiect finalizat (previzualizare mare)

Acum numărul este actualizat până la valoarea notei și șters cu clearInterval() .

Cam asta este pentru acest tutorial. Sper ca ti-a placut!

Dacă doriți să construiți ceva mai interactiv, consultați Tutorialul meu Memory Game creat cu Vanilla JavaScript. Acesta acoperă concepte de bază HTML5, CSS3 și JavaScript, cum ar fi poziționarea, perspectiva, tranzițiile, Flexbox, gestionarea evenimentelor, timeout-uri și ternare.

Codare fericită!