Animación de relleno SVG HTML5 con CSS3 y JavaScript Vanilla
Publicado: 2022-03-10SVG 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.
- Demostración: proyecto de visualización de notas
- Repositorio: Repositorio de visualización de notas
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
.
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á.
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:
<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í:
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
ystroke-dasharray: 10 5 2 3
) generarán guiones y espacios en varios tamaños.
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.
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); }
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); });
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); }
¡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); }
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!