Pular para o conteúdo principal

Animações de Carregamento: Julho

· Leitura de 13 minutos

Sétima das doze animações.


Animações de Carregamento (série de 10 partes)
  1. Introdução
  2. Janeiro
  3. Fevereiro
  4. Março
  5. Abril
  6. Maio
  7. Junho
  8. Julho
  9. Agosto
  10. Setembro

Animação

Resultado final:

Introdução

Criei essa animação com SVG, mas desta vez desenvolvi também uma ferramenta interativa em JavaScript para gerar dinamicamente as animações. A estrutura segue o padrão das anteriores:

<!-- definições de tamanho --><svg>  <defs>    <!-- caminhos que os círculos percorrem -->    <path />    <path />    <path />    <path />    <path />    <path />    <path />  </defs>  <!-- plano de fundo -->  <rect />  <!-- desenhos dos caminhos que os círculos percorrem, para debug -->  <g />  <!-- círculos com diferentes animações -->  <g>    <ellipse />    <ellipse />    <ellipse />    <ellipse />    <ellipse />    <ellipse />    <ellipse />  </g></svg>

Ferramenta de geração

Para criar essa animação, desenvolvi uma ferramenta JavaScript que permite configurar dinamicamente:

  • Número de círculos: idealmente, pode variar de 1 a 12 (para representar os meses)
  • Duração da animação: controle sobre a velocidade
  • Visibilidade dos caminhos: toggle para mostrar/ocultar as trajetórias
Estrutura HTML
<form id="form">  <label>    circlesCount:    <input type="number" name="circlesCount" />  </label>  <br />  <label>    durationInMs:    <input type="number" name="durationInMs" />  </label>  <br />  <label>    showPath:    <input type="checkbox" name="showPath" />  </label>  <br />  <br />  <input type="submit" /></form><br /><svg  id="root"  xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 200 200"  width="200"  height="200">  <defs id="definitions"></defs>  <rect    id="background"    width="199"    height="199"    x="0.5"    y="0.5"    fill="white"    stroke="lightgray"    stroke-width="1"    rx="6"  ></rect>  <g id="paths" fill="none" stroke="lightgray" stroke-width="1"></g>  <g id="circles" fill="black"></g></svg>
Helpers
// função para criar elementos SVG com atributos
function createSvgElement(tag, attributes = {}) {
const element = document.createElementNS("http://www.w3.org/2000/svg", tag);

for (const [key, value] of Object.entries(attributes)) {
element.setAttribute(key, value);
}

return element;
}

// função para selecionar elementos do DOM
function $(selector) {
return document.querySelector(selector);
}
Elementos
// formulárioconst form = $("#form");// elementos SVG presentes na páginaconst root = $("#root");const definitions = $("#definitions");const background = $("#background");const paths = $("#paths");const circles = $("#circles");
Constantes
// tamanhoconst WIDTH = 200;const HEIGHT = 200;// curvas de animaçãoconst CURVE_MAP = {  linear: "0, 0, 1, 1",  ease: "0.25, 0.1, 0.25, 1",  easeIn: "0.42, 0, 1, 1",  easeOut: "0, 0, 0.58, 1",  easeInOut: "0.42, 0, 0.58, 1",};// atributos da animação de movimento que se repetem// usei a curva `ease`const ANIMATE_MOTION_ATTRIBUTES = {  repeatCount: "indefinite",  rotate: "auto",  calcMode: "spline",  keyTimes: "0; 0.5; 1",  keyPoints: "0; 1; 0",  keySplines: `    ${CURVE_MAP.ease};    ${CURVE_MAP.ease}  `,};// atributos da animação de raio que se repetem// usei a curva `linear`const ANIMATE_RADIUS = {  repeatCount: "indefinite",  calcMode: "spline",  keyTimes: "0; 0.25; 0.5; 0.75; 1",  keySplines: `    ${CURVE_MAP.linear};    ${CURVE_MAP.linear};    ${CURVE_MAP.linear};    ${CURVE_MAP.linear}  `,};
Event listeners
// define os valores padrão para julho
window.addEventListener("load", (event) => {
form.circlesCount.value = 7;
form.durationInMs.value = 1_600;
form.showPath.checked = true;

render(
form.circlesCount.value,
form.durationInMs.value,
form.showPath.checked
);
});

// atualiza a animação com os valores do formulário
form.addEventListener("submit", (event) => {
event.preventDefault();

const formData = new FormData(event.target);
const circlesCount = Number(formData.get("circlesCount"));
const durationInMs = Number(formData.get("durationInMs"));
const showPath = Boolean(formData.get("showPath"));

render(circlesCount, durationInMs, showPath);
});
Renderização
// a função principal que gera os elementos SVG dinamicamente
function render(circlesCount, durationInMs, showPath) {
// explicado a seguir
}

// a função que calcula as coordenadas dos caminhos
function getPathD(circlesCount, index) {
// explicado a seguir
}

Como funciona

info

Omiti alguns elementos e atributos nos blocos a seguir para manter a explicação concisa.

Tamanho

Mantive o tamanho igual às animações anteriores.

Resultado



Código

<svg  xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 200 200"  width="200"  height="200">  <rect    width="199"    height="199"    x="0.5"    y="0.5"    fill="white"    stroke="lightgray"    stroke-width="1"    rx="6"  /></svg>

Caminho

Os caminhos são gerados matematicamente usando trigonometria para criar uma distribuição radial perfeita. Cada caminho é uma linha reta que parte de uma borda até a borda oposta.

Resultado



Código

A função getPathD calcula as coordenadas de cada caminho:

Renderização
function getPathD(circlesCount, index) {  // define o raio, ângulo de deslocamento e ângulo baseado no índice  const radius = 70;  const angleOffset = Math.PI / 2;  const angle = (index / circlesCount) * Math.PI + angleOffset;  // define o deslocamento para centralizar o SVG  const xOffset = WIDTH * (1 / 2);  const yOffset = HEIGHT * (1 / 2);  // calcula as coordenadas do ponto inicial  const x1 = radius * Math.sin(angle) + xOffset;  const y1 = radius * Math.cos(angle) + yOffset;  // calcula o ponto oposto para criar uma linha que atravessa todo o SVG  const x2 = WIDTH - x1;  const y2 = HEIGHT - y1;  return `M${x1},${y1} L${x2},${y2}`;}

A matemática por trás:

  • angle: Divide o círculo em partes iguais baseado no número de círculos
  • angleOffset: Rotaciona o padrão para começar à esquerda
  • radius: Define a distância do centro até o ponto inicial do caminho
  • x2, y2: Calcula o ponto oposto para criar uma linha que atravessa todo o SVG

Geração Dinâmica

A função principal render gera todos os elementos SVG dinamicamente:

Renderização
function render(circlesCount, durationInMs, showPath) {  // limpa os elementos existentes  definitions.innerHTML = "";  paths.innerHTML = "";  circles.innerHTML = "";  // cria fragmentos para otimizar a inserção no DOM  const definitionsFragment = new DocumentFragment();  const pathsFragment = new DocumentFragment();  const circlesFragment = new DocumentFragment();  for (let i = 0; i < circlesCount; i++) {    // cálculo do atraso escalonado    const begin = `${-durationInMs + i * durationInMs * 0.036}ms`;    // cria o caminho e adiciona ao fragmento    const pathId = `path-${i}`;    const path = createSvgElement("path", {      id: pathId,      d: getPathD(circlesCount, i),    });    definitionsFragment.append(path);    // se ativado, adiciona o caminho ao SVG para visualização    if (showPath) {      const use = createSvgElement("use", {        href: `#${pathId}`,      });      pathsFragment.append(use);    }    // cria o elemento mpath para animação de movimento    const mpath = createSvgElement("mpath", {      href: `#${pathId}`,    });    // animação de movimento ao longo do caminho    const animateMotionId = `animateMotion-${i}`;    const animateMotion = createSvgElement("animateMotion", {      ...ANIMATE_MOTION_ATTRIBUTES,      id: animateMotionId,      begin: begin,      dur: `${durationInMs}ms`,    });    animateMotion.append(mpath);    // animações de deformação    const animateRx = createSvgElement("animate", {      ...ANIMATE_RADIUS,      attributeName: "rx",      values: "8; 8.4; 8; 8.4; 8",      begin: begin,      dur: `${durationInMs}ms`,    });    const animateRy = createSvgElement("animate", {      ...ANIMATE_RADIUS,      attributeName: "ry",      values: "8; 7.6; 8; 7.6; 8",      begin: begin,      dur: `${durationInMs}ms`,    });    // cria o círculo e anexa as animações    const ellipse = createSvgElement("ellipse", {      rx: 8,      ry: 8,    });    ellipse.append(animateMotion);    ellipse.append(animateRx);    ellipse.append(animateRy);    // adiciona o círculo ao fragmento    circlesFragment.append(ellipse);  }  // anexa os fragmentos ao SVG  definitions.append(definitionsFragment);  paths.append(pathsFragment);  circles.append(circlesFragment);}

Círculos e suas animações

Cada círculo é criado dinamicamente com suas animações de movimento e deformação. O timing escalonado cria o efeito de onda radiante.

Algoritmo de timing

O timing escalonado é a chave para o efeito visual. Cada círculo começa sua animação com um pequeno atraso calculado por:

// fórmula do atraso escalonadoconst begin = `${-durationInMs + i * durationInMs * 0.036}ms`;// onde:// - durationInMs: duração total da animação// - i: índice do círculo (0 a 6)// - 0.036: fator de escalonamento (3.6% da duração por círculo)

Isso significa que:

  • O primeiro círculo, i = 0, inicia com o maior atraso negativo
  • Cada círculo subsequente inicia em torno de 57ms depois (para duração de 1600ms)

O resultado é uma cascata suave onde os círculos se movem em sequência.

Animações que percorrem o caminho

A animação é simples, cada círculo se move ao longo de seu caminho definido. A animação é configurada com os seguintes parâmetros:

keyTimeskeyPointskeySplines
000.25, 0.1, 0.25, 1
0.510.25, 0.1, 0.25, 1
10-

Ou seja, a animação percorre o caminho do início ao fim e volta ao início, criando um efeito de onda contínuo.

Animações que esticam

Os círculos também têm animações de deformação que fazem com que eles estiquem e encolham suavemente durante o movimento. As animações de raio são configuradas com:

keyTimeskeySplinesrxryDescrição
00, 0, 1, 188Raio original
0.250, 0, 1, 18.47.6Estica horizontalmente e encolhe verticalmente
0.50, 0, 1, 188Retorna ao tamanho original
0.750, 0, 1, 18.47.6Estica horizontalmente e encolhe verticalmente
1-88Retorna ao tamanho original

Ou seja, os círculos esticam ao se mover de um ponto a outro, criando um efeito de velocidade e fluidez.

Resultado



Conclusão

Essa animação foi um bom exercício de código, ritmo visual e um pouco de matemática também. Criei uma ferramenta em JavaScript pra facilitar a geração das animações e testar diferentes configurações com rapidez. Os caminhos foram distribuídos de forma bem equilibrada com a ajuda de trigonometria, e pra manter a performance, montei tudo usando DocumentFragment, evitando manipulações desnecessárias no DOM.

O movimento dos círculos acontece em looping, com um escalonamento que cria ondas suaves. Eles deslizam pelos caminhos e ainda se deformam um pouquinho durante o percurso, o que deixa tudo mais fluido e natural. No fim das contas, foi divertido juntar código e animação de um jeito que convida à experimentação.

Resultado



Código

<svg  xmlns="http://www.w3.org/2000/svg"  viewBox="0 0 200 200"  width="200"  height="200">  <defs>    <path id="path-0" d="M170,100 L30,100"></path>    <path      id="path-1"      d="M163.06782075316934,69.62813826177094 L36.93217924683066,130.37186173822906"    ></path>    <path      id="path-2"      d="M143.64428613011137,45.271796227237914 L56.35571386988863,154.72820377276207"    ></path>    <path      id="path-3"      d="M115.57646537694201,31.755046147272353 L84.42353462305799,168.24495385272763"    ></path>    <path      id="path-4"      d="M84.423534623058,31.755046147272353 L115.576465376942,168.24495385272763"    ></path>    <path      id="path-5"      d="M56.35571386988866,45.27179622723791 L143.64428613011134,154.7282037727621"    ></path>    <path      id="path-6"      d="M36.93217924683067,69.62813826177091 L163.06782075316931,130.3718617382291"    ></path>  </defs>  <rect    width="199"    height="199"    x="0.5"    y="0.5"    fill="white"    stroke="lightgray"    stroke-width="1"    rx="6"  ></rect>  <g fill="none" stroke="lightgray" stroke-width="1">    <use href="#path-0"></use>    <use href="#path-1"></use>    <use href="#path-2"></use>    <use href="#path-3"></use>    <use href="#path-4"></use>    <use href="#path-5"></use>    <use href="#path-6"></use>  </g>  <g fill="black">    <!-- início: círculo 0 -->    <ellipse rx="8" ry="8">      <animateMotion        repeatCount="indefinite"        rotate="auto"        calcMode="spline"        keyTimes="0; 0.5; 1"        keyPoints="0; 1; 0"        keySplines="0.25, 0.1, 0.25, 1; 0.25, 0.1, 0.25, 1"        id="animateMotion-0"        begin="-1600ms"        dur="1600ms"      >        <mpath href="#path-0"></mpath>      </animateMotion>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="rx"        values="8; 8.4; 8; 8.4; 8"        begin="-1600ms"        dur="1600ms"      ></animate>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="ry"        values="8; 7.6; 8; 7.6; 8"        begin="-1600ms"        dur="1600ms"      ></animate>    </ellipse>    <!-- fim: círculo 0 -->    <!-- início: círculo 1 -->    <ellipse rx="8" ry="8">      <animateMotion        repeatCount="indefinite"        rotate="auto"        calcMode="spline"        keyTimes="0; 0.5; 1"        keyPoints="0; 1; 0"        keySplines="0.25, 0.1, 0.25, 1; 0.25, 0.1, 0.25, 1"        id="animateMotion-1"        begin="-1542.4ms"        dur="1600ms"      >        <mpath href="#path-1"></mpath>      </animateMotion>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="rx"        values="8; 8.4; 8; 8.4; 8"        begin="-1542.4ms"        dur="1600ms"      ></animate>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="ry"        values="8; 7.6; 8; 7.6; 8"        begin="-1542.4ms"        dur="1600ms"      ></animate>    </ellipse>    <!-- fim: círculo 1 -->    <!-- início: círculo 2 -->    <ellipse rx="8" ry="8">      <animateMotion        repeatCount="indefinite"        rotate="auto"        calcMode="spline"        keyTimes="0; 0.5; 1"        keyPoints="0; 1; 0"        keySplines="0.25, 0.1, 0.25, 1; 0.25, 0.1, 0.25, 1"        id="animateMotion-2"        begin="-1484.8ms"        dur="1600ms"      >        <mpath href="#path-2"></mpath>      </animateMotion>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="rx"        values="8; 8.4; 8; 8.4; 8"        begin="-1484.8ms"        dur="1600ms"      ></animate>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="ry"        values="8; 7.6; 8; 7.6; 8"        begin="-1484.8ms"        dur="1600ms"      ></animate>    </ellipse>    <!-- fim: círculo 2 -->    <!-- início: círculo 3 -->    <ellipse rx="8" ry="8">      <animateMotion        repeatCount="indefinite"        rotate="auto"        calcMode="spline"        keyTimes="0; 0.5; 1"        keyPoints="0; 1; 0"        keySplines="0.25, 0.1, 0.25, 1; 0.25, 0.1, 0.25, 1"        id="animateMotion-3"        begin="-1427.2ms"        dur="1600ms"      >        <mpath href="#path-3"></mpath>      </animateMotion>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="rx"        values="8; 8.4; 8; 8.4; 8"        begin="-1427.2ms"        dur="1600ms"      ></animate>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="ry"        values="8; 7.6; 8; 7.6; 8"        begin="-1427.2ms"        dur="1600ms"      ></animate>    </ellipse>    <!-- fim: círculo 3 -->    <!-- início: círculo 4 -->    <ellipse rx="8" ry="8">      <animateMotion        repeatCount="indefinite"        rotate="auto"        calcMode="spline"        keyTimes="0; 0.5; 1"        keyPoints="0; 1; 0"        keySplines="0.25, 0.1, 0.25, 1; 0.25, 0.1, 0.25, 1"        id="animateMotion-4"        begin="-1369.6ms"        dur="1600ms"      >        <mpath href="#path-4"></mpath>      </animateMotion>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="rx"        values="8; 8.4; 8; 8.4; 8"        begin="-1369.6ms"        dur="1600ms"      ></animate>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="ry"        values="8; 7.6; 8; 7.6; 8"        begin="-1369.6ms"        dur="1600ms"      ></animate>    </ellipse>    <!-- fim: círculo 4 -->    <!-- início: círculo 5 -->    <ellipse rx="8" ry="8">      <animateMotion        repeatCount="indefinite"        rotate="auto"        calcMode="spline"        keyTimes="0; 0.5; 1"        keyPoints="0; 1; 0"        keySplines="0.25, 0.1, 0.25, 1; 0.25, 0.1, 0.25, 1"        id="animateMotion-5"        begin="-1312ms"        dur="1600ms"      >        <mpath href="#path-5"></mpath>      </animateMotion>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="rx"        values="8; 8.4; 8; 8.4; 8"        begin="-1312ms"        dur="1600ms"      ></animate>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="ry"        values="8; 7.6; 8; 7.6; 8"        begin="-1312ms"        dur="1600ms"      ></animate>    </ellipse>    <!-- fim: círculo 5 -->    <!-- início: círculo 6 -->    <ellipse rx="8" ry="8">      <animateMotion        repeatCount="indefinite"        rotate="auto"        calcMode="spline"        keyTimes="0; 0.5; 1"        keyPoints="0; 1; 0"        keySplines="0.25, 0.1, 0.25, 1; 0.25, 0.1, 0.25, 1"        id="animateMotion-6"        begin="-1254.4ms"        dur="1600ms"      >        <mpath href="#path-6"></mpath>      </animateMotion>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="rx"        values="8; 8.4; 8; 8.4; 8"        begin="-1254.4ms"        dur="1600ms"      ></animate>      <animate        repeatCount="indefinite"        calcMode="spline"        keyTimes="0; 0.25; 0.5; 0.75; 1"        keySplines="0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1; 0, 0, 1, 1"        attributeName="ry"        values="8; 7.6; 8; 7.6; 8"        begin="-1254.4ms"        dur="1600ms"      ></animate>    </ellipse>    <!-- fim: círculo 6 -->  </g></svg>

Leitura recomendada