Décima das doze animações.
Animações de Carregamento (série de 13 partes)
Neste experimento, as animações podem não ser renderizadas de forma suave no Firefox.
Animação
Resultado final:
Introdução
Minha ideia inicial era criar uma animação em que o caminho em si fosse animado, e não os círculos.
A primeira implementação, onde o caminho é animado, conforme o planejado, funcionou no Chrome e no Firefox, mas não no Safari. A segunda implementação, onde o caminho não é animado, mas o mesmo efeito é alcançado por outro meio, funcionou nos três navegadores, embora a renderização no Firefox tenha sido a menos suave.
Os testes foram realizados no macOS 14.7.6, com Chrome 141.0.7390.123, Firefox 144.0.2 e Safari 18.5.
Versão 1
Em alguns blocos a seguir, omiti elementos e atributos para manter a explicação concisa.
Na versão 1, criei diversos senoides que iniciam em diferentes fases até completar uma volta, então usei o elemento animate para animar o atributo d do elemento path, criando assim a animação do caminho. Em seguida, usei o elemento animateMotion para posicionar os círculos de forma estática no caminho.
Caminho
Implementei um código para gerar os senoides. SEGMENT_AMOUNT = 10 (número de segmentos de linha) e PHASE_AMOUNT = 15 (número de fases ao longo da volta) são valores que encontrei que resultam em curvas e interpolações suaves o suficiente.
Resultado
Código
// voltaconst TAU = 2 * Math.PI; // https://en.wikipedia.org/wiki/Tau_(mathematics)// dimensõesconst WIDTH = 200;const HEIGHT = 200;// senoidesconst AMPLITUDE = HEIGHT / 10; // https://pt.wikipedia.org/wiki/Amplitudeconst SEGMENT_AMOUNT = 10;const PHASE_AMOUNT = 15;// resultadoconst values = { top: "", bottom: "" };for (let i = 0; i < PHASE_AMOUNT; i++) { const phase = i / (PHASE_AMOUNT - 1); const lines = { top: "", bottom: "" }; for (let j = 0; j <= SEGMENT_AMOUNT; j++) { const segment = j / SEGMENT_AMOUNT; const angle = segment * TAU + phase * TAU; const command = j === 0 ? "M" : "L"; const x = WIDTH * segment; const y = { top: HEIGHT * (2 / 5) + Math.sin(angle) * (AMPLITUDE / 2), bottom: HEIGHT * (3 / 5) - Math.sin(angle) * (AMPLITUDE / 2), }; lines.top += `${command} ${x} ${y.top} `; lines.bottom += `${command} ${x} ${y.bottom} `; } values.top += lines.top; values.bottom += lines.bottom; if (i < PHASE_AMOUNT - 1) { values.top += `; `; values.bottom += `; `; }}/*{ top: "M 0 80 L 20 85.87785252292473 L 40 89.51056516295154 L 60 89.51056516295154 L 80 85.87785252292473 L 100 80 L 120 74.12214747707527 L 140 70.48943483704846 L 160 70.48943483704846 L 180 74.12214747707526 L 200 80; ...; M 0 75.66116260882441 L 20 81.78556894798636 L 40 87.22794863827392 L 60 89.90949761767935 L 80 88.80595531856738 L 100 84.33883739117559 L 120 78.21443105201364 L 140 72.77205136172608 L 160 70.09050238232065 L 180 71.19404468143262 L 200 75.66116260882441", bottom: "M 0 120 L 20 114.12214747707527 L 40 110.48943483704846 L 60 110.48943483704846 L 80 114.12214747707527 L 100 120 L 120 125.87785252292473 L 140 129.51056516295154 L 160 129.51056516295154 L 180 125.87785252292474 L 200 120; ...; M 0 120 L 20 114.12214747707527 L 40 110.48943483704846 L 60 110.48943483704846 L 80 114.12214747707526 L 100 120 L 120 125.87785252292473 L 140 129.51056516295154 L 160 129.51056516295154 L 180 125.87785252292474 L 200 120"}*/<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200"> <defs> <path id="path_top"> <animate attributeName="d" values="values.top" dur="1500ms" repeatCount="indefinite" calcMode="linear" ></animate> </path> <path id="path_bottom"> <animate attributeName="d" values="values.bottom" dur="1500ms" repeatCount="indefinite" calcMode="linear" ></animate> </path> </defs> <rect width="199" height="199" rx="6" x="0.5" y="0.5" fill="white" stroke-width="1" stroke="lightgray" ></rect> <g fill="none" stroke="lightgray"> <use href="#path_top"></use> <use href="#path_bottom"></use> </g></svg>Exemplo sem interpolação, com SEGMENT_AMOUNT = 5 e PHASE_AMOUNT = 7, para evidenciar como a animação é construída. O número menor de segmentos faz a curva não ser suave, e o menor número de fases faz com que a diferença entre um senoide e outro seja maior.
Círculos
Para posicionar os círculos de forma estática no caminho, usei o elemento animateMotion com keyTimes="0; 1" e keyPoints com dois valores iguais. Ou seja, do início ao fim da animação, eles mantêm a mesma posição.
Posicionei os círculos espaçados igualmente no centro do caminho, com valores 0.3, 0.4, 0.5, 0.6 e 0.7.
Resultado
A animação pode não funcionar no Safari
Código
<svg> <defs></defs> <rect></rect> <g></g> <g fill="black" stroke="none"> <ellipse rx="8" ry="8"> <animateMotion keyPoints="0.3; 0.3" keyTimes="0; 1"> <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion keyPoints="0.4; 0.4" keyTimes="0; 1"> <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion keyPoints="0.5; 0.5" keyTimes="0; 1"> <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion keyPoints="0.6; 0.6" keyTimes="0; 1"> <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion keyPoints="0.7; 0.7" keyTimes="0; 1"> <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion keyPoints="0.3; 0.3" keyTimes="0; 1"> <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion keyPoints="0.4; 0.4" keyTimes="0; 1"> <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion keyPoints="0.5; 0.5" keyTimes="0; 1"> <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion keyPoints="0.6; 0.6" keyTimes="0; 1"> <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion keyPoints="0.7; 0.7" keyTimes="0; 1"> <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> </g></svg>Comparação
O Chrome renderizou a animação como eu esperava. O Firefox também renderizou, mas a animação ficou trêmula. O Safari renderizou a animação do caminho, mas não a dos círculos.
Chrome, Firefox e Safari, respectivamente
Versão 2
Em alguns blocos a seguir, omiti elementos e atributos para manter a explicação concisa.
Na versão 2, criei dois senoides que completam duas voltas e que se extendem o dobro da largura do retângulo. Então, usei o elemento g para agrupar os senoides, e o elemento animateTransform para mover o grupo da direita para a esquerda, criando assim a animação do caminho. Em seguida, usei o elemento animateMotion para mover os círculos da esquerda para a direita na mesma velocidade que o grupo é movido, o que faz com que os círculos fiquem estáticos.
Caminho
Fiz algumas mudanças no código anterior para gerar os senoides. SEGMENT_AMOUNT = 30 foi o valor que encontrei que resulta em uma curva suave o suficiente.
Resultado
Código
// voltaconst TAU = 2 * Math.PI;// dimensõesconst WIDTH = 200;const HEIGHT = 200;// senoidesconst AMPLITUDE = HEIGHT / 10;const SEGMENT_AMOUNT = 30;// resultadoconst lines = { top: "", bottom: "" };for (let i = 0; i <= SEGMENT_AMOUNT; i++) { const segment = i / SEGMENT_AMOUNT; const angle = 2 * TAU * segment; const command = i === 0 ? "M" : "L"; const x = 2 * WIDTH * segment; const y = { top: HEIGHT * (2 / 5) + Math.sin(angle) * (AMPLITUDE / 2), bottom: HEIGHT * (3 / 5) - Math.sin(angle) * (AMPLITUDE / 2), }; lines.top += `${command} ${x} ${y.top} `; lines.bottom += `${command} ${x} ${y.bottom} `;}/*{ top: "M 0 80 ... L 400 80", bottom: "M 0 120 ... L 400 120"}*/<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200"> <defs> <path id="path_top" d="line.top"></path> <path id="path_bottom" d="line.bottom"></path> </defs> <rect width="199" height="199" rx="6" x="0.5" y="0.5" fill="white" stroke-width="1" stroke="lightgray" ></rect> <g> <animateTransform attributeName="transform" attributeType="XML" type="translate" from="0" to="-200" dur="1500ms" repeatCount="indefinite" ></animateTransform> <g fill="none" stroke="lightgray"> <use href="#path_top"></use> <use href="#path_bottom"></use> </g> </g></svg>Ou seja, os senoides têm largura de 400 e se movem de x = 0 para x = -200, repetindo esse movimento indefinidamente.
Círculos
Para posicionar os círculos de forma que parecessem estáticos no caminho, usei o elemento animateMotion com keyTimes="0; 1" e keyPoints "0.15; 0.65", "0.2; 0.7", "0.25; 0.75", "0.3; 0.8" e "0.35; 0.85". Dessa forma, os círculos ficam espaçados igualmente no centro do caminho.
Resultado
Código
<svg> <defs></defs> <rect></rect> <g> <animateTransform></animateTransform> <g></g> <g fill="black" stroke="none"> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.15; 0.65" keyTimes="0; 1" > <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.2; 0.7" keyTimes="0; 1" > <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.25; 0.75" keyTimes="0; 1" > <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.3; 0.8" keyTimes="0; 1" > <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.35; 0.85" keyTimes="0; 1" > <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.15; 0.65" keyTimes="0; 1" > <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.2; 0.7" keyTimes="0; 1" > <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.25; 0.75" keyTimes="0; 1" > <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.3; 0.8" keyTimes="0; 1" > <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.35; 0.85" keyTimes="0; 1" > <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> </g> </g></svg>Ou seja, os círculos vão do ponto inicial ao ponto final no mesmo tempo em que o grupo. Comparação entre os círculos com e sem animação, respectivamente:
Comparação
O Chrome e o Safari renderizaram a animação corretamente. O Firefox novamente renderizou com um efeito trêmulo.
Chrome, Firefox e Safari, respectivamente
Conclusão
Este foi um experimento interessante, pois evidenciou limitações na forma como diferentes navegadores interpretam e renderizam animações SVG. Mesmo padrões abertos como o SVG não têm comportamento 100% uniforme entre navegadores, gerando resultados distintos mesmo em animações aparentemente simples.
Durante os testes, notei que:
- O Safari tende a ter limitações com o elemento
animateMotion, especialmente quando usado em conjunto com caminhos animados (pathcomanimateno atributod). - O Firefox sofre mais com problemas de suavidade (stuttering), mesmo quando a animação é suportada. Imagino que pode estar relacionado ao modo como ele interpola valores.
- O Chrome parece não ter grandes problemas com animações SVG.
O processo também foi útil para entender melhor o equilíbrio entre complexidade e suavidade em animações SVG. Por exemplo, aumentar o número de segmentos (SEGMENT_AMOUNT) e fases (PHASE_AMOUNT) melhora a qualidade visual, mas também eleva o custo de renderização e pode afetar o desempenho em alguns contextos. Outro ponto relevante é como pequenas mudanças, como animar o caminho versus animar o grupo que o contém, podem levar a resultados visualmente idênticos, mas com impacto na compatibilidade e desempenho.
Devido às limitações atuais do Safari, eu não usaria novamente a abordagem da versão 1, que depende diretamente da animação do atributo d do path. A abordagem da versão 2, por outro lado, mostrou-se mais estável e previsível entre navegadores. Dessa forma, escolhi a versão 2 como final.
Resultado
Código
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" width="200" height="200"> <defs> <path id="path_top" d="M 0 80 L 13.33 84.06 L 26.66 87.43 L 40 89.51 L 53.33 89.94 L 66.66 88.66 L 80 85.87 L 93.33 82.07 L 106.66 77.92 L 120 74.12 L 133.33 71.33 L 146.66 70.05 L 160 70.48 L 173.33 72.56 L 186.66 75.93 L 200 80 L 213.33 84.06 L 226.66 87.43 L 240 89.51 L 253.33 89.94 L 266.66 88.66 L 280 85.87 L 293.33 82.07 L 306.66 77.92 L 320 74.12 L 333.33 71.33 L 346.66 70.05 L 360 70.48 L 373.33 72.56 L 386.66 75.93 L 400 80" ></path> <path id="path_bottom" d="M 0 120 L 13.33 115.93 L 26.66 112.56 L 40 110.48 L 53.33 110.05 L 66.66 111.33 L 80 114.12 L 93.33 117.92 L 106.66 122.07 L 120 125.87 L 133.33 128.66 L 146.66 129.94 L 160 129.51 L 173.33 127.43 L 186.66 124.06 L 200 120 L 213.33 115.93 L 226.66 112.56 L 240 110.48 L 253.33 110.05 L 266.66 111.33 L 280 114.12 L 293.33 117.92 L 306.66 122.07 L 320 125.87 L 333.33 128.66 L 346.66 129.94 L 360 129.51 L 373.33 127.43 L 386.66 124.06 L 400 120" ></path> </defs> <rect width="199" height="199" rx="6" x="0.5" y="0.5" fill="white" stroke-width="1" stroke="lightgray" ></rect> <g> <animateTransform attributeName="transform" attributeType="XML" type="translate" from="0" to="-200" dur="1500ms" repeatCount="indefinite" ></animateTransform> <g fill="black" stroke="none"> <!-- início: círculo superior 1 --> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.15; 0.65" keyTimes="0; 1" > <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <!-- fim: círculo superior 1 --> <!-- início: círculo superior 2 --> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.2; 0.7" keyTimes="0; 1" > <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <!-- fim: círculo superior 2 --> <!-- início: círculo superior 3 --> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.25; 0.75" keyTimes="0; 1" > <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <!-- fim: círculo superior 3 --> <!-- início: círculo superior 4 --> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.3; 0.8" keyTimes="0; 1" > <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <!-- fim: círculo superior 4 --> <!-- início: círculo superior 5 --> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.35; 0.85" keyTimes="0; 1" > <mpath href="#path_top"></mpath> </animateMotion> </ellipse> <!-- fim: círculo superior 5 --> <!-- início: círculo inferior 1 --> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.15; 0.65" keyTimes="0; 1" > <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> <!-- fim: círculo inferior 1 --> <!-- início: círculo inferior 2 --> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.2; 0.7" keyTimes="0; 1" > <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> <!-- fim: círculo inferior 2 --> <!-- início: círculo inferior 3 --> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.25; 0.75" keyTimes="0; 1" > <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> <!-- fim: círculo inferior 3 --> <!-- início: círculo inferior 4 --> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.3; 0.8" keyTimes="0; 1" > <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> <!-- fim: círculo inferior 4 --> <!-- início: círculo inferior 5 --> <ellipse rx="8" ry="8"> <animateMotion dur="1500ms" repeatCount="indefinite" keyPoints="0.35; 0.85" keyTimes="0; 1" > <mpath href="#path_bottom"></mpath> </animateMotion> </ellipse> <!-- fim: círculo inferior 5 --> </g> </g></svg>