Animation de remplissage HTML5 SVG avec CSS3 et JavaScript Vanilla

Publié: 2022-03-10
Résumé rapide ↬ Dans cet article, vous pouvez apprendre à créer l'affichage des notes animées à partir du site Web Awwwards. Il traite de l'élément de cercle HTML5 SVG, de ses propriétés de trait et de la manière de les animer avec des variables CSS et du JavaScript Vanilla.

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

Remarque Afficher la démo du projet
Une démo du résultat final ( Grand aperçu )
  • Démo : projet d'affichage de notes
  • Dépôt : dépôt d'affichage de notes
Plus après saut! Continuez à lire ci-dessous ↓

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 .

Élément d'élément de liste et enfants directs
Élément d'élément de liste et ses enfants directs : .circle , .percent et .label . ( Grand aperçu )

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

Éléments SVG
Éléments SVG. Enveloppe SVG et balises circulaires. ( Grand aperçu )

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

Enveloppe de liste non ordonnée
Un wrapper de liste non ordonné contient quatre enfants li ( Grand aperçu )
 <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 :

Modèle initial terminé
Éléments et styles de modèle finis ( Grand aperçu )

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 et stroke-dasharray: 10 5 2 3 ) généreront des tirets et des espaces de différentes tailles.
Valeurs de la propriété Stroke dasharray
valeurs de la propriété stroke-dasharray ( Grand aperçu )

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.

Stroke dasharray et dashoffset propriétés
stroke-dasharray et trait-dashoffset propriétés ( Grand aperçu )

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); } 
Transition des propriétés de trait
Transition des propriétés de trait ( Grand aperçu )

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); }); 
Transition des propriétés de trait jusqu'à la valeur de la note
Transition des propriétés de trait jusqu'à la valeur de la note ( Grand aperçu )

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); } 
Augmentation infinie du compteur
Augmentation infinie du compteur ( Grand aperçu )

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); } 
Projet d'affichage de notes terminé
Projet terminé ( Grand aperçu )

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 !