Tenth of twelve animations.
Loading Animations (13 part series)
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
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
// 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 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
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
// 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 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
animateMotionelement, especially when used in conjunction with animated paths (pathwithanimatein thedattribute). - 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>