Skip to main content

Loading Animations: October

· 12 min read

Tenth of twelve animations.


Loading Animations (13 part series)
  1. Introduction
  2. January
  3. February
  4. March
  5. April
  6. May
  7. June
  8. July
  9. August
  10. September
  11. October
  12. November
  13. December

info

In this experiment, animations may not render smoothly in Firefox.

Animation

Result:

Introduction

My initial idea was to create an animation where the path itself was animated, not the circles.

The first implementation, where the path is animated as planned, worked in Chrome and Firefox, but not in Safari. The second implementation, where the path is not animated but the same effect is achieved by other means, worked in all three browsers, although rendering in Firefox was the least smooth.

The tests were conducted on macOS 14.7.6, with Chrome 141.0.7390.123, Firefox 144.0.2, and Safari 18.5.

Version 1

info

In some blocks below, I omitted elements and attributes to keep the explanation concise.

In version 1, I created several sine waves that start at different phases until completing a turn, then I used the animate element to animate the d attribute of the path element, thus creating the animation of the path. Next, I used the animateMotion element to position the circles statically on the path.

Path

I implemented a script to generate the sine waves. SEGMENT_AMOUNT = 10 (number of line segments) and PHASE_AMOUNT = 15 (number of phases along the turn) are values I found that result in smooth enough curves and interpolations.

Result



Code

JavaScript
// turnconst TAU = 2 * Math.PI; // https://en.wikipedia.org/wiki/Tau_(mathematics)// dimensionsconst WIDTH = 200;const HEIGHT = 200;// sine wavesconst AMPLITUDE = HEIGHT / 10; // https://en.wikipedia.org/wiki/Amplitudeconst SEGMENT_AMOUNT = 10;const PHASE_AMOUNT = 15;// resultconst 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
<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>

Here's an example without interpolation, using SEGMENT_AMOUNT = 5 and PHASE_AMOUNT = 7, to better show how the animation is built. The smaller number of segments makes the curve less smooth, and the smaller number of phases makes the difference between one sine wave and another greater.

Circles

To position the circles statically along the path, I used the animateMotion element with keyTimes="0; 1" and keyPoints set to two identical values. In other words, from start to finish, they remain in the same position.

I placed the circles equally spaced around the center of the path, using values 0.3, 0.4, 0.5, 0.6, and 0.7.

Result

The animation may not work in Safari

Code

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

Comparison

Chrome rendered the animation as expected. Firefox also rendered it, but the animation was shaky. Safari rendered the path animation, but not the circles.

Chrome, Firefox, and Safari, respectively

Version 2

info

In some blocks below, I omitted elements and attributes to keep the explanation concise.

In version 2, I created two sine waves that complete two turns and extend twice the width of the rectangle. Then, I used the g element to group the sine waves, and the animateTransform element to move the group from right to left, thus creating the path animation. Next, I used the animateMotion element to move the circles from left to right at the same speed as the group is moved, which makes the circles appear static.

Path

I made some changes to the previous script to generate the sine waves. SEGMENT_AMOUNT = 30 was the value I found that results in a smooth enough curve.

Result



Code

JavaScript
// turnconst TAU = 2 * Math.PI; // https://en.wikipedia.org/wiki/Tau_(mathematics)// dimensionsconst WIDTH = 200;const HEIGHT = 200;// sine wavesconst AMPLITUDE = HEIGHT / 10; // https://en.wikipedia.org/wiki/Amplitudeconst SEGMENT_AMOUNT = 30;// resultconst 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
<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>

That means the sine waves have a width of 400 and move from x = 0 to x = -200, repeating indefinitely.

Circles

To position the circles so that they appear static on the path, I used the animateMotion element with keyTimes="0; 1" and keyPoints 0.15; 0.65, 0.2; 0.7, 0.25; 0.75, 0.3; 0.8, and 0.35; 0.85. This way, the circles are evenly spaced in the center of the path.

Result

Code

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

That is, the circles move from the start point to the endpoint in the same duration as the group. Below is a comparison between the circles with and without animation, respectively:


Comparison

Chrome and Safari rendered the animation correctly. Firefox, once again, rendered it with a shaky effect.

Chrome, Firefox, and Safari, respectively

Conclusion

This was an interesting experiment, as it highlighted limitations in how different browsers interpret and render SVG animations. Even open standards like SVG don't behave 100% uniformly across browsers, generating distinct results even in seemingly simple animations.

During the tests, I noticed that:

  • Safari tends to have limitations with the animateMotion element, especially when used in conjunction with animated paths (path with animate in the d attribute).
  • Firefox suffers more from stuttering issues, even when animation is supported. I imagine this may be related to how it interpolates values.
  • Chrome seems to have no major problems with SVG animations.

The process was also useful for better understanding the balance between complexity and smoothness in SVG animations. For example, increasing the number of segments (SEGMENT_AMOUNT) and phases (PHASE_AMOUNT) improves visual quality, but also increases rendering costs and can affect performance in some contexts. Another relevant point is how small changes, such as animating the path versus animating the group that contains it, can lead to visually identical results, but with impact on compatibility and performance.

Due to current Safari limitations, I would not use the version 1 approach again, which directly depends on animating the d attribute of the path. The version 2 approach, on the other hand, proved to be more stable and predictable across browsers. Therefore, I chose version 2 as the final version.

Result



Code

<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">      <!-- start: upper circle 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>      <!-- end: upper circle 1 -->      <!-- start: upper circle 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>      <!-- end: upper circle 2 -->      <!-- start: upper circle 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>      <!-- end: upper circle 3 -->      <!-- start: upper circle 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>      <!-- end: upper circle 4 -->      <!-- start: upper circle 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>      <!-- end: upper circle 5 -->      <!-- start: lower circle 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>      <!-- end: lower circle 1 -->      <!-- start: lower circle 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>      <!-- end: lower circle 2 -->      <!-- start: lower circle 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>      <!-- end: lower circle 3 -->      <!-- start: lower circle 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>      <!-- end: lower circle 4 -->      <!-- start: lower circle 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>      <!-- end: lower circle 5 -->    </g>  </g></svg>

Recommended reading