Sétima das doze animações.
Animações de Carregamento (série de 10 partes)
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
<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>// 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);
}
// formulárioconst form = $("#form");// elementos SVG presentes na páginaconst root = $("#root");const definitions = $("#definitions");const background = $("#background");const paths = $("#paths");const circles = $("#circles");// 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} `,};// 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);
});
// 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
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:
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írculosangleOffset: Rotaciona o padrão para começar à esquerdaradius: Define a distância do centro até o ponto inicial do caminhox2, 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:
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
57msdepois (para duração de1600ms)
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:
keyTimes | keyPoints | keySplines |
|---|---|---|
| 0 | 0 | 0.25, 0.1, 0.25, 1 |
| 0.5 | 1 | 0.25, 0.1, 0.25, 1 |
| 1 | 0 | - |
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:
keyTimes | keySplines | rx | ry | Descrição |
|---|---|---|---|---|
| 0 | 0, 0, 1, 1 | 8 | 8 | Raio original |
| 0.25 | 0, 0, 1, 1 | 8.4 | 7.6 | Estica horizontalmente e encolhe verticalmente |
| 0.5 | 0, 0, 1, 1 | 8 | 8 | Retorna ao tamanho original |
| 0.75 | 0, 0, 1, 1 | 8.4 | 7.6 | Estica horizontalmente e encolhe verticalmente |
| 1 | - | 8 | 8 | Retorna 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>