Animación de relleno SVG HTML5 con CSS3 y JavaScript Vanilla

Publicado: 2022-03-10
Resumen rápido ↬ En este artículo, puede aprender cómo crear la visualización de notas animadas desde el sitio web de Awwwards. Analiza el elemento de círculo HTML5 SVG, sus propiedades de trazo y cómo animarlos con variables CSS y JavaScript Vanilla.

SVG significa Gráficos vectoriales escalables y es un lenguaje de marcado estándar basado en XML para gráficos vectoriales. Le permite dibujar caminos, curvas y formas determinando un conjunto de puntos en el plano 2D. Además, puede agregar propiedades de contracción en esas rutas (como trazo, color, grosor, relleno y más) para producir animaciones.

Desde abril de 2017, el Módulo de relleno y trazo de nivel 3 de CSS permite que los colores SVG y los patrones de relleno se establezcan desde una hoja de estilo externa, en lugar de establecer atributos en cada elemento. En este tutorial, usaremos un color hexadecimal simple, pero las propiedades de relleno y trazo también aceptan patrones, degradados e imágenes como valores.

Nota : al visitar el sitio web de Awwwards, la pantalla de notas animadas solo se puede ver con el ancho del navegador configurado en 1024 px o más.

Nota Mostrar proyecto de demostración
Una demostración del resultado final (vista previa grande)
  • Demostración: proyecto de visualización de notas
  • Repositorio: Repositorio de visualización de notas
¡Más después del salto! Continúe leyendo a continuación ↓

Estructura de archivos

Comencemos por crear los archivos en la terminal:

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

HTML

Aquí está la plantilla inicial que vincula los archivos css y 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>

Cada elemento de nota consta de un elemento de lista: li que contiene el circle , el valor de la note y su label .

Elemento de elemento de lista y elementos secundarios directos
Elemento de elemento de lista y sus hijos directos: .circle , .percent y .label . (Vista previa grande)

El .circle_svg es un elemento SVG, que envuelve dos elementos <circle>. El primero es el camino que se va a rellenar, mientras que el segundo es el relleno que se animará.

elementos SVG
elementos SVG. Envoltorio SVG y etiquetas circulares. (Vista previa grande)

La note se separa en enteros y decimales, por lo que se les pueden aplicar diferentes tamaños de letra. La label es un <span> simple. Entonces, poner todo esto junto se ve así:

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

Los atributos cx y cy definen el punto central del eje x y el eje y del círculo. El atributo r define su radio.

Probablemente haya notado el patrón de guión bajo/guión en los nombres de las clases. Eso es BEM, que significa block , element y modifier . Es una metodología que hace que la denominación de elementos sea más estructurada, organizada y semántica.

Lectura recomendada : una explicación de BEM y por qué lo necesita

Para terminar las estructuras de la plantilla, envolvamos los cuatro elementos de la lista en un elemento de lista desordenado:

Envoltura de lista desordenada
El envoltorio de lista desordenado contiene cuatro hijos li (vista previa 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>

Debes estar preguntándote qué significan las etiquetas Transparent , Reasonable , Usable y Exemplary . Cuanto más se familiarice con la programación, se dará cuenta de que escribir código no se trata solo de hacer que la aplicación sea funcional, sino también de garantizar que sea mantenible y escalable a largo plazo. Eso solo se logra si su código es fácil de cambiar.

“El acrónimo TRUE debería ayudar a decidir si el código que escriba podrá adaptarse a cambios en el futuro o no”.

Entonces, la próxima vez, pregúntate:

  • Transparent : ¿Son claras las consecuencias de los cambios de código?
  • Reasonable : ¿Vale la pena el costo-beneficio?
  • Usable : ¿Podré reutilizarlo en escenarios inesperados?
  • Exemplary : ¿Presenta alta calidad como ejemplo para código futuro?

Nota : "Diseño práctico orientado a objetos en Ruby" de Sandi Metz explica TRUE junto con otros principios y cómo lograrlos a través de patrones de diseño. Si aún no se ha tomado un tiempo para estudiar patrones de diseño, considere agregar este libro a su lectura antes de acostarse.

CSS

Importemos las fuentes y apliquemos un reinicio a todos los elementos:

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

La propiedad box-sizing: border-box incluye valores de borde y relleno en el ancho y alto total de un elemento, por lo que es más fácil calcular sus dimensiones.

Nota : para obtener una explicación visual sobre box-sizing , lea "Haga su vida más fácil con el tamaño de las cajas CSS".

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

Al combinar las reglas display: flex en el body y margin-auto en .display-container , es posible centrar el elemento secundario tanto vertical como horizontalmente. El elemento .display-container también será un flex-container ; de esa forma, sus hijos se colocarán en la misma fila a lo largo del eje principal.

El elemento de la lista .note-display también será un flex-container . Dado que hay muchos hijos para centrar, hagámoslo a través de las propiedades justify-content y align-items . Todos flex-items se centrarán a lo largo del eje cross y main . Si no está seguro de cuáles son, consulte la sección de alineación en la "Guía visual de conceptos básicos de CSS Flexbox".

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

Apliquemos un trazo a los círculos configurando las reglas stroke-width , stroke-opacity y stroke-linecap que en conjunto dan estilo a los extremos vivos del trazo. A continuación, agreguemos un color a cada círculo:

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

Para posicionar el elemento de percent de manera absoluta, es necesario saber absolutamente a qué. El elemento .circle debe ser la referencia, así que agreguemos una position: relative a él.

Nota : Para obtener una explicación visual más profunda sobre el posicionamiento absoluto, lea "Cómo entender el posicionamiento absoluto de CSS de una vez por todas".

Otra forma de centrar elementos es combinar top: 50% , left: 50% y transform: translate(-50%, -50%); que posicionan el centro del elemento en el centro de su padre.

 .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 estas alturas, la plantilla debería verse así:

Plantilla inicial terminada
Elementos y estilos de plantilla terminados (vista previa grande)

Transición de relleno

La animación circular se puede crear con la ayuda de dos propiedades SVG circulares: stroke-dasharray y stroke-dashoffset .

" stroke-dasharray define el patrón de espacio entre guiones en un trazo".

Puede tomar hasta cuatro valores:

  • Cuando se establece en un solo número entero ( stroke-dasharray: 10 ), los guiones y los espacios tienen el mismo tamaño;
  • Para dos valores ( stroke-dasharray: 10 5 ), el primero se aplica a los guiones, el segundo a los espacios;
  • Las formas tercera y cuarta ( stroke-dasharray: 10 5 2 y stroke-dasharray: 10 5 2 3 ) generarán guiones y espacios en varios tamaños.
Trazar valores de propiedad dasharray
valores de propiedad stroke-dasharray (vista previa grande)

La imagen de la izquierda muestra la propiedad stroke-dasharray configurada de 0 a 238px, que es la longitud de la circunferencia del círculo.

La segunda imagen representa la propiedad stroke-dashoffset que compensa el comienzo de la matriz de guiones. También se establece desde 0 hasta la longitud de la circunferencia del círculo.

Stroke dasharray y dashoffset propiedades
stroke-dasharray y desplazamiento de trazo propiedades (Vista previa grande)

Para producir el efecto de relleno, estableceremos el stroke-dasharray a la longitud de la circunferencia, de modo que toda su longitud se llene con un gran guión y sin espacios. También lo compensaremos por el mismo valor, por lo que se "oculta". Luego, el stroke-dashoffset se actualizará al valor de la nota correspondiente, llenando el trazo de acuerdo con la duración de la transición.

La actualización de propiedades se realizará en los scripts a través de Variables CSS. Declaremos las variables y establezcamos las propiedades:

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

Para establecer el valor inicial y actualizar las variables, comencemos seleccionando todos los elementos .note-display con document.querySelectorAll . La duración de la transitionDuration se establecerá en 900 milisegundos.

Luego, iteramos a través de la matriz de pantallas, seleccionamos su .circle__progress.circle__progress--fill y extraemos el atributo r establecido en el HTML para calcular la longitud de la circunferencia. Con eso, podemos establecer los valores iniciales --dasharray y --dashoffset .

La animación ocurrirá cuando la variable --dashoffset se actualice con un setTimeout de 100ms:

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

Para que la transición comience desde arriba, se debe rotar el elemento .circle__svg :

 .circle__svg { transform: rotate(-90deg); } 
Transición de propiedades de trazo
Transición de propiedades de trazo (vista previa grande)

Ahora, calculemos el valor de dashoffset , relativo a la nota. El valor de la nota se insertará en cada elemento li a través del atributo data-*. El * se puede cambiar por cualquier nombre que se adapte a sus necesidades y luego se puede recuperar en JavaScript a través del conjunto de datos del elemento: element.dataset.* .

Nota : Puede leer más sobre el atributo data-* en MDN Web Docs.

Nuestro atributo se llamará “ 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>

El método parseFloat convertirá la cadena devuelta por display.dataset.note en un número de punto flotante. El offset representa el porcentaje que falta para alcanzar la puntuación máxima. Entonces, para un billete de 7.50 , tendríamos (10 - 7.50) / 10 = 0.25 , lo que significa que la longitud de la circumference debe compensarse en un 25% de su valor:

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

Actualización de 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); }); 
Transición de las propiedades del trazo hasta el valor de la nota
Transición de las propiedades del trazo hasta el valor de la nota (vista previa grande)

Antes de continuar, extraigamos la transición de Stoke a su propio método:

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

Aumento del valor de la nota

Todavía queda la transición de la nota de 0.00 al valor de la nota por construir. Lo primero que debe hacer es separar los valores enteros y decimales. Usaremos el método de cadena split() (toma un argumento que determina dónde se romperá la cadena y devuelve una matriz que contiene ambas cadenas rotas). Esos se convertirán en números y se pasarán como argumentos a la función de increaseNumber() , junto con el elemento de display y una bandera que indica si es un número entero o un decimal.

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

En la función IncreaseNumber increaseNumber() , seleccionamos el elemento .percent__int o .percent__dec , según el className y también en caso de que la salida deba contener un punto decimal o no. Hemos establecido nuestra duración de transitionDuration en 900ms . Ahora, para animar un número del 0 al 7, por ejemplo, la duración se tiene que dividir por la nota 900 / 7 = 128.57ms . El resultado representa cuánto tiempo llevará cada iteración de aumento. Esto significa que nuestro setInterval se activará cada 128.57ms .

Con esas variables establecidas, definamos setInterval . La variable de counter se agregará al elemento como texto y se incrementará en cada iteración:

 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); } 
Incremento de contador infinito
Incremento de contador infinito (Vista previa grande)

¡Frio! Aumenta los valores, pero lo hace para siempre. Necesitamos borrar setInterval cuando las notas alcancen el valor que queremos. Eso se hace con la función 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); } 
Proyecto de exhibición de notas terminado
Proyecto terminado (Vista previa grande)

Ahora el número se actualiza hasta el valor de la nota y se borra con la función clearInterval() .

Eso es más o menos todo por este tutorial. ¡Espero que lo hayan disfrutado!

Si tiene ganas de construir algo un poco más interactivo, consulte mi tutorial de juego de memoria creado con Vanilla JavaScript. Cubre conceptos básicos de HTML5, CSS3 y JavaScript, como posicionamiento, perspectiva, transiciones, Flexbox, manejo de eventos, tiempos de espera y ternarios.

¡Feliz codificación!