Animation de remplissage HTML5 SVG avec CSS3 et JavaScript Vanilla
Publié: 2022-03-10SVG signifie S calable V ector G raphics et c'est un langage de balisage standard basé sur XML pour les graphiques vectoriels. Il vous permet de dessiner des chemins, des courbes et des formes en déterminant un ensemble de points dans le plan 2D. De plus, vous pouvez ajouter des propriétés de contraction sur ces chemins (telles que le trait, la couleur, l'épaisseur, le remplissage, etc.) afin de produire des animations.
Depuis avril 2017, le module CSS Level 3 Fill and Stroke permet de définir les couleurs et les motifs de remplissage SVG à partir d'une feuille de style externe, au lieu de définir des attributs sur chaque élément. Dans ce didacticiel, nous utiliserons une simple couleur hexadécimale unie, mais les propriétés de remplissage et de trait acceptent également les motifs, les dégradés et les images comme valeurs.
Remarque : Lors de la visite du site Web Awwwards, l'affichage de la note animée ne peut être visualisé qu'avec une largeur de navigateur définie sur 1024 pixels ou plus.
- Démo : projet d'affichage de notes
- Dépôt : dépôt d'affichage de notes
Structure du fichier
Commençons par créer les fichiers dans le terminal :
mkdir note-display cd note-display touch index.html styles.css scripts.js
HTML
Voici le modèle initial qui relie les fichiers css
et 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>
Chaque élément de note consiste en un élément de liste : li
qui contient le circle
, la valeur de la note
et son label
.
Le .circle_svg
est un élément SVG, qui enveloppe deux éléments <circle>. Le premier est le chemin à remplir tandis que le second est le remplissage qui sera animé.
La note
est séparée en nombres entiers et décimaux afin que différentes tailles de police puissent leur être appliquées. L' label
est un simple <span>
. Donc, mettre tout cela ensemble ressemble à ceci:
<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>
Les attributs cx
et cy
définissent le centre des axes x et y du cercle. L'attribut r
définit son rayon.
Vous avez probablement remarqué le motif trait de soulignement/tiret dans les noms de classes. C'est BEM, qui signifie block
, element
et modifier
. C'est une méthodologie qui rend la dénomination de vos éléments plus structurée, organisée et sémantique.
Lecture recommandée : Une explication de BEM et pourquoi vous en avez besoin
Pour terminer les structures de modèle, enveloppons les quatre éléments de liste dans un élément de liste non ordonné :
<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>
Vous devez vous demander ce que signifient les labels Transparent
, Reasonable
, Usable
et Exemplary
. Plus vous vous familiariserez avec la programmation, plus vous vous rendrez compte que l'écriture de code ne consiste pas seulement à rendre l'application fonctionnelle, mais aussi à s'assurer qu'elle sera maintenable et évolutive à long terme. Cela n'est possible que si votre code est facile à modifier.
"L'acronyme TRUE
devrait aider à décider si le code que vous écrivez sera capable de s'adapter aux changements à l'avenir ou non."
Alors, la prochaine fois, demandez-vous :
-
Transparent
: les conséquences des changements de code sont-elles claires ? -
Reasonable
: Le rapport coût-avantages en vaut-il la peine ? -
Usable
: pourrai-je le réutiliser dans des scénarios inattendus ? -
Exemplary
: présente-t-il une haute qualité comme exemple pour le futur code ?
Remarque : "Practical Object-Oriented Design in Ruby" de Sandi Metz explique TRUE
ainsi que d'autres principes et comment les atteindre grâce à des modèles de conception. Si vous n'avez pas encore pris le temps d'étudier les modèles de conception, envisagez d'ajouter ce livre à votre lecture au coucher.
CSS
Importons les polices et appliquons une réinitialisation à tous les éléments :
@import url('https://fonts.googleapis.com/css?family=Nixie+One|Raleway:200'); * { padding: 0; margin: 0; box-sizing: border-box; }
La propriété box-sizing: border-box
inclut les valeurs de remplissage et de bordure dans la largeur et la hauteur totales d'un élément, ce qui facilite le calcul de ses dimensions.
Remarque : Pour une explication visuelle sur le box-sizing
, veuillez lire "Simplifiez-vous la vie avec le dimensionnement des boîtes CSS".
body { height: 100vh; color: #fff; display: flex; background: #3E423A; font-family: 'Nixie One', cursive; } .display-container { margin: auto; display: flex; }
En combinant les règles display: flex
dans le body
et margin-auto
dans le .display-container
, il est possible de centrer l'élément enfant à la fois verticalement et horizontalement. L'élément .display-container
sera également un flex-container
; de cette façon, ses enfants seront placés dans la même ligne le long de l'axe principal.
L'élément de liste .note-display
sera également un flex-container
. Puisqu'il y a beaucoup d'enfants pour le centrage, faisons-le à travers les propriétés justify-content
et align-items
. Tous flex-items
seront centrés le long de l'axe cross
et main
. Si vous n'êtes pas sûr de ce que c'est, consultez la section d'alignement dans « CSS Flexbox Fundamentals Visual Guide ».
.note-display { display: flex; flex-direction: column; align-items: center; margin: 0 25px; }
Appliquons un trait aux cercles en définissant les règles stroke-width
, stroke-opacity
et stroke-linecap
qui stylisent ensemble les extrémités vivantes du trait. Ensuite, ajoutons une couleur à chaque cercle :
.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; }
Afin de positionner l'élément percent
de manière absolue, il est nécessaire de savoir absolument à quoi. L'élément .circle
devrait être la référence, alors ajoutons position: relative
à celui-ci.
Remarque : Pour une explication visuelle plus approfondie du positionnement absolu, veuillez lire "Comment comprendre la position CSS absolue une fois pour toutes".
Une autre façon de centrer les éléments est de combiner top: 50%
, left: 50%
et transform: translate(-50%, -50%);
qui positionnent le centre de l'élément au centre de son parent.
.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; }
À présent, le modèle devrait ressembler à ceci :
Remplir la transition
L'animation de cercle peut être créée à l'aide de deux propriétés SVG de cercle : stroke-dasharray
et stroke-dashoffset
.
" stroke-dasharray
définit le motif dash-gap dans un trait."
Il peut prendre jusqu'à quatre valeurs :
- Lorsqu'il est défini sur un seul entier (
stroke-dasharray: 10
), les tirets et les espaces ont la même taille ; - Pour deux valeurs (
stroke-dasharray: 10 5
), la première est appliquée aux tirets, la seconde aux espaces ; - Les troisième et quatrième formes (
stroke-dasharray: 10 5 2
etstroke-dasharray: 10 5 2 3
) généreront des tirets et des espaces de différentes tailles.
L'image de gauche montre la propriété stroke-dasharray
définie de 0 à 238px, qui correspond à la longueur de la circonférence du cercle.
La deuxième image représente la propriété stroke-dashoffset
qui décale le début du tableau de tirets. Il est également défini de 0 à la longueur de la circonférence du cercle.
Pour produire l'effet de remplissage, nous allons régler le stroke-dasharray
sur la longueur de la circonférence, de sorte que toute sa longueur soit remplie d'un gros tiret et sans espace. Nous allons également le compenser de la même valeur, de sorte qu'il soit "caché". Ensuite, le stroke-dashoffset
sera mis à jour avec la valeur de note correspondante, remplissant le trait en fonction de la durée de la transition.
La mise à jour des propriétés se fera dans les scripts via des variables CSS. Déclarons les variables et définissons les propriétés :
.circle__progress--fill { --initialStroke: 0; --transitionDuration: 0; stroke-opacity: 1; stroke-dasharray: var(--initialStroke); stroke-dashoffset: var(--initialStroke); transition: stroke-dashoffset var(--transitionDuration) ease; }
Afin de définir la valeur initiale et de mettre à jour les variables, commençons par sélectionner tous les éléments .note-display
avec document.querySelectorAll
. La transitionDuration
sera définie sur 900
millisecondes.
Ensuite, nous parcourons le tableau display, sélectionnons son .circle__progress.circle__progress--fill
et extrayons l'attribut r
défini dans le code HTML pour calculer la longueur de la circonférence. Avec cela, nous pouvons définir les valeurs initiales --dasharray
et --dashoffset
.
L'animation se produira lorsque la variable --dashoffset
mise à jour par 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); });
Pour obtenir la transition en partant du haut, l'élément .circle__svg
doit être pivoté :
.circle__svg { transform: rotate(-90deg); }
Maintenant, calculons la valeur dashoffset
- par rapport à la note. La valeur de la note sera insérée dans chaque élément li
via l'attribut data-*. Le *
peut être remplacé par n'importe quel nom qui correspond à vos besoins et il peut ensuite être récupéré en JavaScript via le jeu de données de l'élément : element.dataset.*
.
Remarque : Vous pouvez en savoir plus sur l'attribut data-* sur MDN Web Docs.
Notre attribut s'appellera « 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>
La méthode parseFloat
convertira la chaîne renvoyée par display.dataset.note
en un nombre à virgule flottante. Le offset
représente le pourcentage manquant pour atteindre le score maximum. Ainsi, pour un billet de 7.50
, nous aurions (10 - 7.50) / 10 = 0.25
, ce qui signifie que la longueur de la circumference
devrait être décalée de 25%
de sa valeur :
let note = parseFloat(display.dataset.note); let offset = circumference * (10 - note) / 10;
Mise à jour des 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); });
Avant de continuer, extrayons la transition stoke vers sa propre méthode :
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); + }
Augmentation de la valeur de la note
Il reste la transition de note de 0.00
à la valeur de note à construire. La première chose à faire est de séparer les valeurs entières et décimales. Nous utiliserons la méthode de chaîne split()
(elle prend un argument qui détermine où la chaîne sera brisée et renvoie un tableau contenant les deux chaînes brisées). Ceux-ci seront convertis en nombres et passés en arguments à la fonction increaseNumber()
, avec l'élément d' display
et un indicateur indiquant s'il s'agit d'un entier ou d'un décimal.
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'); });
Dans la fonction raiseNumber increaseNumber()
, nous sélectionnons l'élément .percent__int
ou .percent__dec
, selon le className
, et également au cas où la sortie devrait contenir un point décimal ou non. Nous avons défini notre transitionDuration
sur 900ms
. Maintenant, pour animer un nombre de 0 à 7, par exemple, la durée doit être divisée par la note 900 / 7 = 128.57ms
. Le résultat représente la durée de chaque itération d'augmentation. Cela signifie que notre setInterval
se déclenchera toutes les 128.57ms
.
Avec ces variables définies, définissons le setInterval
. La variable counter
sera ajoutée à l'élément sous forme de texte et augmentée à chaque itération :
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); }
Frais! Cela augmente les valeurs, mais cela le fait en quelque sorte pour toujours. Nous devons effacer le setInterval
lorsque les notes atteignent la valeur souhaitée. Cela se fait avec la fonction 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); }
Maintenant, le nombre est mis à jour jusqu'à la valeur de la note et effacé avec la fonction clearInterval()
.
C'est à peu près tout pour ce tutoriel. J'espère que tu as aimé!
Si vous avez envie de construire quelque chose d'un peu plus interactif, consultez mon tutoriel sur le jeu de mémoire créé avec Vanilla JavaScript. Il couvre les concepts de base HTML5, CSS3 et JavaScript tels que le positionnement, la perspective, les transitions, Flexbox, la gestion des événements, les délais d'attente et les ternaires.
Bon codage !