Animação de preenchimento HTML5 SVG com CSS3 e Vanilla JavaScript

Publicados: 2022-03-10
Resumo rápido ↬ Neste artigo, você pode aprender como construir a exibição de notas animadas no site da Awwwards. Ele discute o elemento circle SVG HTML5, suas propriedades de traço e como animá-los com variáveis ​​CSS e Vanilla JavaScript.

SVG significa Scalable V ector G raphics e é uma linguagem de marcação padrão baseada em XML para gráficos vetoriais. Ele permite desenhar caminhos, curvas e formas determinando um conjunto de pontos no plano 2D. Além disso, você pode adicionar propriedades de contração nesses caminhos (como traçado, cor, espessura, preenchimento e muito mais) para produzir animações.

Desde abril de 2017, o Módulo de preenchimento e traçado CSS nível 3 permite que cores SVG e padrões de preenchimento sejam definidos a partir de uma folha de estilo externa, em vez de definir atributos em cada elemento. Neste tutorial, usaremos uma cor hexadecimal simples, mas as propriedades de preenchimento e traçado também aceitam padrões, gradientes e imagens como valores.

Nota : Ao visitar o site da Awwwards, a exibição da nota animada só pode ser visualizada com a largura do navegador definida para 1024px ou mais.

Nota Exibir demonstração do projeto
Uma demonstração do resultado final (visualização grande)
  • Demonstração: projeto de exibição de notas
  • Repo: Repositório de Exibição de Notas
Mais depois do salto! Continue lendo abaixo ↓

Estrutura do arquivo

Vamos começar criando os arquivos no terminal:

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

HTML

Aqui está o modelo inicial que vincula os arquivos css e 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 consiste em um item de lista: li que contém o circle , o valor da note e seu label .

Elemento de item de lista e filhos diretos
Elemento de item de lista e seus filhos diretos: .circle , .percent e .label . (Visualização grande)

O .circle_svg é um elemento SVG, que envolve dois elementos <circle>. O primeiro é o caminho a ser preenchido enquanto o segundo é o preenchimento que será animado.

Elementos SVG
Elementos SVG. Wrapper SVG e tags de círculo. (Visualização grande)

A note é separada em números inteiros e decimais para que diferentes tamanhos de fonte possam ser aplicados a elas. O label é um <span> simples. Então, juntando tudo isso fica assim:

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

Os atributos cx e cy definem o ponto central do eixo x e do eixo y do círculo. O atributo r define seu raio.

Você provavelmente notou o padrão de sublinhado/traço nos nomes das classes. Isso é BEM, que significa block , element e modifier . É uma metodologia que torna a nomeação de seus elementos mais estruturada, organizada e semântica.

Leitura recomendada : Uma explicação do BEM e por que você precisa dele

Para finalizar as estruturas do template, vamos agrupar os quatro itens de lista em um elemento de lista não ordenado:

Wrapper de lista não ordenada
O wrapper de lista não ordenado contém quatro filhos li (visualização 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>

Você deve estar se perguntando o que significam os rótulos Transparent , Reasonable , Usable e Exemplary . Quanto mais você se familiarizar com a programação, perceberá que escrever código não é apenas tornar o aplicativo funcional, mas também garantir que ele seja sustentável e escalável a longo prazo. Isso só é alcançado se o seu código for fácil de alterar.

“A sigla TRUE deve ajudar a decidir se o código que você escreve será capaz de acomodar mudanças no futuro ou não.”

Então, da próxima vez, pergunte-se:

  • Transparent : As consequências das alterações de código são claras?
  • Reasonable : O custo benefício vale a pena?
  • Usable : Poderei reutilizá-lo em cenários inesperados?
  • Exemplary : Apresenta alta qualidade como exemplo para código futuro?

Nota : “Practical Object-Oriented Design in Ruby” de Sandi Metz explica TRUE juntamente com outros princípios e como alcançá-los através de padrões de projeto. Se você ainda não dedicou algum tempo para estudar padrões de design, considere adicionar este livro à sua leitura antes de dormir.

CSS

Vamos importar as fontes e aplicar um reset em todos os itens:

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

A propriedade box-sizing: border-box inclui valores de preenchimento e de borda na largura e altura totais de um elemento, portanto, é mais fácil calcular suas dimensões.

Nota : Para obter uma explicação visual sobre box-sizing , leia “Facilite sua vida com dimensionamento de caixas CSS”.

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

Combinando as regras display: flex no body e margin-auto no .display-container , é possível centralizar o elemento filho verticalmente e horizontalmente. O elemento .display-container também será um flex-container ; dessa forma, seus filhos serão colocados na mesma linha ao longo do eixo principal.

O item de lista .note-display também será um flex-container . Como existem muitos filhos para centralização, vamos fazer isso através das propriedades justify-content e align-items . Todos flex-items serão centralizados ao longo do eixo cross e main . Se você não tiver certeza do que são, confira a seção de alinhamento em “Guia Visual de Fundamentos do CSS Flexbox”.

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

Vamos aplicar um traço aos círculos definindo as regras stroke-width , stroke-opacity e stroke-linecap que estilizam as extremidades ao vivo do traço. Em seguida, vamos adicionar uma cor 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 o elemento percent de forma absoluta, é necessário saber absolutamente para quê. O elemento .circle deve ser a referência, então vamos adicionar position: relative a ele.

Nota : Para uma explicação visual mais profunda sobre o posicionamento absoluto, leia “Como entender a posição absoluta do CSS de uma vez por todas”.

Outra maneira de centralizar elementos é combinar top: 50% , left: 50% e transform: translate(-50%, -50%); que posicionam o centro do elemento no centro de seu pai.

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

Até agora, o modelo deve estar assim:

Modelo inicial concluído
Elementos e estilos de modelo finalizados (visualização grande)

Transição de preenchimento

A animação do círculo pode ser criada com a ajuda de duas propriedades SVG do círculo: stroke-dasharray e stroke-dashoffset .

stroke-dasharray define o padrão de intervalo de traço em um traço.”

Pode assumir até quatro valores:

  • Quando é definido como um único inteiro ( stroke-dasharray: 10 ), traços e lacunas têm o mesmo tamanho;
  • Para dois valores ( stroke-dasharray: 10 5 ), o primeiro é aplicado aos traços, o segundo às lacunas;
  • A terceira e quarta formas ( stroke-dasharray: 10 5 2 e stroke-dasharray: 10 5 2 3 ) gerarão traços e lacunas em vários tamanhos.
Traçar valores de propriedade dasharray
valores da propriedade stroke-dasharray (visualização grande)

A imagem à esquerda mostra a propriedade stroke-dasharray sendo definida de 0 a 238px, que é o comprimento da circunferência do círculo.

A segunda imagem representa a propriedade stroke-dashoffset que desloca o início da matriz de traço. Também é definido de 0 ao comprimento da circunferência do círculo.

Propriedades de traço dasharray e dashoffset
stroke-dasharray e deslocamento-traço propriedades (visualização grande)

Para produzir o efeito de preenchimento, definiremos o stroke-dasharray para o comprimento da circunferência, de modo que todo o seu comprimento seja preenchido com um traço grande e sem lacunas. Também o compensaremos pelo mesmo valor, para que fique “oculto”. Então o stroke-dashoffset será atualizado para o valor da nota correspondente, preenchendo o traço de acordo com a duração da transição.

A atualização das propriedades será feita nos scripts através de Variáveis ​​CSS. Vamos declarar as variáveis ​​e definir as propriedades:

 .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 definir o valor inicial e atualizar as variáveis, vamos começar selecionando todos os elementos .note-display com document.querySelectorAll . A transitionDuration será definida como 900 milissegundos.

Em seguida, iteramos pelo array displays, selecionamos seu .circle__progress.circle__progress--fill e extraímos o atributo r definido no HTML para calcular o comprimento da circunferência. Com isso, podemos definir os valores iniciais --dasharray e --dashoffset .

A animação ocorrerá quando a variável --dashoffset for atualizada por um 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 obter a transição a partir do topo, o elemento .circle__svg deve ser girado:

 .circle__svg { transform: rotate(-90deg); } 
Transição das propriedades do traço
Transição das propriedades do traço (visualização grande)

Agora, vamos calcular o valor do dashoffset — relativo à nota. O valor da nota será inserido em cada item li através do atributo data-*. O * pode ser trocado por qualquer nome que atenda às suas necessidades e pode então ser recuperado em JavaScript através do conjunto de dados do elemento: element.dataset.* .

Observação : você pode ler mais sobre o atributo data-* no MDN Web Docs.

Nosso atributo será chamado de “ 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>

O método parseFloat converterá a string retornada por display.dataset.note em um número de ponto flutuante. O offset representa a porcentagem que falta para atingir a pontuação máxima. Assim, para uma nota de 7.50 , teríamos (10 - 7.50) / 10 = 0.25 , o que significa que o comprimento da circumference deve ser compensado em 25% do seu valor:

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

Atualizando o 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); }); 
As propriedades do traço transitam até o valor da nota
As propriedades do traço mudam para o valor da nota (visualização grande)

Antes de prosseguirmos, vamos extrair a transição do stoke para seu próprio 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 do valor da nota

Ainda há a transição da nota de 0.00 para o valor da nota a ser construída. A primeira coisa a fazer é separar os valores inteiros e decimais. Usaremos o método de string split() (pega um argumento que determina onde a string será quebrada e retorna um array contendo ambas as strings quebradas). Eles serão convertidos em números e passados ​​como argumentos para a função increaseNumber() , junto com o elemento display e um sinalizador indicando se é um inteiro ou um 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'); });

Na função raiseNumber increaseNumber() , selecionamos o elemento .percent__int ou .percent__dec , dependendo do className , e também caso a saída deva conter um ponto decimal ou não. Definimos nossa transitionDuration como 900ms . Agora, para animar um número de 0 a 7, por exemplo, a duração tem que ser dividida pela nota 900 / 7 = 128.57ms . O resultado representa quanto tempo cada iteração de aumento levará. Isso significa que nosso setInterval será acionado a cada 128.57ms .

Com essas variáveis ​​definidas, vamos definir o setInterval . A variável counter será anexada ao elemento como texto e aumentada em cada iteração:

 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); } 
Aumento de contador infinito
Aumento de contador infinito (visualização grande)

Frio! Aumenta os valores, mas meio que faz isso para sempre. Precisamos limpar o setInterval quando as notas atingirem o valor que desejamos. Isso é feito com a função 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); } 
Projeto de exibição de notas finalizado
Projeto finalizado (visualização grande)

Agora o número é atualizado até o valor da nota e limpo com a função clearInterval() .

Isso é muito bonito para este tutorial. Espero que tenha gostado!

Se você quiser construir algo um pouco mais interativo, confira meu Tutorial do Jogo da Memória criado com Vanilla JavaScript. Abrange conceitos básicos de HTML5, CSS3 e JavaScript, como posicionamento, perspectiva, transições, Flexbox, manipulação de eventos, tempos limite e ternários.

Boa codificação!