Seventh of twelve animations.
Loading Animations (13 part series)
Animation
Final result:
Introduction
I created this animation with SVG, but this time I also developed an interactive JavaScript tool to dynamically generate the animations. The structure follows the same pattern as the previous ones:
<!-- size definitions --><svg> <defs> <!-- paths that circles follow --> <path /> <path /> <path /> <path /> <path /> <path /> <path /> </defs> <!-- background --> <rect /> <!-- drawings of paths that circles follow, for debugging --> <g /> <!-- circles with different animations --> <g> <ellipse /> <ellipse /> <ellipse /> <ellipse /> <ellipse /> <ellipse /> <ellipse /> </g></svg>Generation tool
To create this animation, I developed a JavaScript tool that allows dynamic configuration of:
- Number of circles: ideally, can vary from 1 to 12 (to represent the months)
- Animation duration: control over speed
- Path visibility: toggle to show/hide trajectories
<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>// function to create SVG elements with attributes
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;
}
// function to select DOM elements
function $(selector) {
return document.querySelector(selector);
}
// formconst form = $("#form");// SVG elements present on the pageconst root = $("#root");const definitions = $("#definitions");const background = $("#background");const paths = $("#paths");const circles = $("#circles");// sizeconst WIDTH = 200;const HEIGHT = 200;// animation curvesconst 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",};// repeating motion animation attributes// used the `ease` curveconst 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} `,};// repeating radius animation attributes// used the `linear` curveconst 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} `,};// sets default values for july
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
);
});
// updates animation with form values
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);
});
// the main function that dynamically generates SVG elements
function render(circlesCount, durationInMs, showPath) {
// explained below
}
// the function that calculates path coordinates
function getPathD(circlesCount, index) {
// explained below
}
How it works
I omitted some elements and attributes in the blocks below to keep the explanation concise.
Size
I kept the same size as the previous animations.
Result
Code
<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>Path
The paths are generated mathematically using trigonometry to create a perfect radial distribution. Each path is a straight line that goes from one edge to the opposite edge.
Result
Code
The getPathD function calculates the coordinates of each path:
function getPathD(circlesCount, index) { // defines radius, angle offset, and angle based on index const radius = 70; const angleOffset = Math.PI / 2; const angle = (index / circlesCount) * Math.PI + angleOffset; // defines offset to center the SVG const xOffset = WIDTH * (1 / 2); const yOffset = HEIGHT * (1 / 2); // calculates initial point coordinates const x1 = radius * Math.sin(angle) + xOffset; const y1 = radius * Math.cos(angle) + yOffset; // calculates opposite point to create a line that crosses the entire SVG const x2 = WIDTH - x1; const y2 = HEIGHT - y1; return `M${x1},${y1} L${x2},${y2}`;}The math behind it:
angle: Divides the circle into equal parts based on the number of circlesangleOffset: Rotates the pattern to start on the leftradius: Defines the distance from center to the initial point of the pathx2, y2: Calculates the opposite point to create a line that crosses the entire SVG
Dynamic generation
The main render function dynamically generates all SVG elements:
function render(circlesCount, durationInMs, showPath) { // clears existing elements definitions.innerHTML = ""; paths.innerHTML = ""; circles.innerHTML = ""; // creates fragments to optimize DOM insertion const definitionsFragment = new DocumentFragment(); const pathsFragment = new DocumentFragment(); const circlesFragment = new DocumentFragment(); for (let i = 0; i < circlesCount; i++) { // staggered delay calculation const begin = `${-durationInMs + i * durationInMs * 0.036}ms`; // creates path and adds to fragment const pathId = `path-${i}`; const path = createSvgElement("path", { id: pathId, d: getPathD(circlesCount, i), }); definitionsFragment.append(path); // if enabled, adds path to SVG for visualization if (showPath) { const use = createSvgElement("use", { href: `#${pathId}`, }); pathsFragment.append(use); } // creates mpath element for motion animation const mpath = createSvgElement("mpath", { href: `#${pathId}`, }); // motion animation along the path const animateMotionId = `animateMotion-${i}`; const animateMotion = createSvgElement("animateMotion", { ...ANIMATE_MOTION_ATTRIBUTES, id: animateMotionId, begin: begin, dur: `${durationInMs}ms`, }); animateMotion.append(mpath); // deformation animations 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`, }); // creates circle and attaches animations const ellipse = createSvgElement("ellipse", { rx: 8, ry: 8, }); ellipse.append(animateMotion); ellipse.append(animateRx); ellipse.append(animateRy); // adds circle to fragment circlesFragment.append(ellipse); } // attaches fragments to SVG definitions.append(definitionsFragment); paths.append(pathsFragment); circles.append(circlesFragment);}Circles and their animations
Each circle is dynamically created with its motion and deformation animations. The staggered timing creates the radiant wave effect.
Timing algorithm
Staggered timing is the key to the visual effect. Each circle starts its animation with a small delay calculated by:
// staggered delay formulaconst begin = `${-durationInMs + i * durationInMs * 0.036}ms`;// where:// - durationInMs: total animation duration// - i: circle index (0 to 6)// - 0.036: staggering factor (3.6% of duration per circle)This means that:
- The first circle,
i = 0, starts with the largest negative delay - Each subsequent circle starts around
57mslater (for 1600ms duration)
The result is a smooth cascade where circles move in sequence.
Animations that traverse the path
The animation is simple: each circle moves along its defined path. The animation is configured with the following parameters:
keyTimes | keyPoints | keySplines |
|---|---|---|
| 0 | 0 | 0.25, 0.1, 0.25, 1 |
| 0.5 | 1 | 0.25, 0.1, 0.25, 1 |
| 1 | 0 | - |
In other words, the animation travels the path from start to end and back to the beginning, creating a continuous wave effect.
Stretching animations
Circles also have deformation animations that make them stretch and shrink smoothly during movement. Radius animations are configured with:
keyTimes | keySplines | rx | ry | Description |
|---|---|---|---|---|
| 0 | 0, 0, 1, 1 | 8 | 8 | Original radius |
| 0.25 | 0, 0, 1, 1 | 8.4 | 7.6 | Stretches horizontally and shrinks vertically |
| 0.5 | 0, 0, 1, 1 | 8 | 8 | Returns to original size |
| 0.75 | 0, 0, 1, 1 | 8.4 | 7.6 | Stretches horizontally and shrinks vertically |
| 1 | - | 8 | 8 | Returns to original size |
In other words, circles stretch as they move from point to point, creating an effect of speed and fluidity.
Result
Conclusion
This animation was a good exercise in code, visual rhythm, and a bit of math too. I created a JavaScript tool to facilitate animation generation and quickly test different configurations. The paths were distributed in a well-balanced way with the help of trigonometry, and to maintain performance, I built everything using DocumentFragment, avoiding unnecessary DOM manipulations.
The circle movement happens in a loop, with staggering that creates smooth waves. They glide along the paths and even deform a little during the journey, which makes everything more fluid and natural. In the end, it was fun to combine code and animation in a way that invites experimentation.
Result
Code
<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"> <!-- start: circle 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> <!-- end: circle 0 --> <!-- start: circle 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> <!-- end: circle 1 --> <!-- start: circle 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> <!-- end: circle 2 --> <!-- start: circle 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> <!-- end: circle 3 --> <!-- start: circle 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> <!-- end: circle 4 --> <!-- start: circle 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> <!-- end: circle 5 --> <!-- start: circle 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> <!-- end: circle 6 --> </g></svg>