Animație de umplere SVG HTML5 cu CSS3 și JavaScript Vanilla
Publicat: 2022-03-10SVG î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.
- Demo: Proiect de afișare a notă
- Repo: Notă Afișează Repo
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 .
.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ă.
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:
<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:
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
șistroke-dasharray: 10 5 2 3
) vor genera liniuțe și goluri de diferite dimensiuni.
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.
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); }
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); });
Î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); }
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); }
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ă!