Animating Complex Shapes along SVG Path
Take Static SVG and Animate like below.
Logo SVG
<?xml version="1.0" encoding="UTF-8"?>
<svg id="animatable-logo" class="logo-svg" data-name="animatable-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 84.33 110.18">
<defs>
<style>
.cls-1 {
stroke-linecap: round;
}
.cls-1, .cls-2 {
fill: none;
stroke-width: 2px;
}
.cls-1, .cls-2, .cls-3 {
stroke: #000;
stroke-miterlimit: 10;
}
</style>
</defs>
<g class="logo-container">
<g class="crystal">
<polyline class="top-half cls-2" points="56.84 45.8 58.48 16.5 44.06 1.54 30.53 19.39 29.82 51.98"></polyline>
<polyline class="bottom-half cls-2" points="29.22 79.54 29.02 88.53 42.37 108.45 54.24 92.44 55.19 75.43"></polyline>
<polygon class="trap-top cls-2" points="45.14 23.4 55.19 13.09 44.06 1.54 34.41 14.28 45.14 23.4"></polygon>
<polygon class="trap-bottom cls-2" points="43.37 96.14 36.65 99.92 42.37 108.45 48.68 99.95 43.37 96.14"></polygon>
<line class="line-middle-bottom cls-2" x1="43.29" y1="78.29" x2="43.37" y2="96.14"></line>
<line class="line-middle-top cls-2" x1="44.77" y1="49.67" x2="45.14" y2="23.4"></line>
</g>
<path class="ring-top cls-1" d="M25.98,28.56c-12.37,4.24-20.55,10.63-19.48,16,1.37,6.81,17.13,9.4,35.21,5.78,5.57-1.12,10.72-2.69,15.13-4.54,9.89-4.14,16.09-9.64,15.14-14.36-.58-2.89-3.75-5.02-8.6-6.24"></path>
<path class="ring-bottom cls-1" d="M5.4,56.22c-3.36,3.65-4.96,7.5-4.23,11.13,2.07,10.31,22.09,14.99,44.73,10.45,3.23-.65,6.34-1.45,9.29-2.37,17.73-5.56,29.73-15.67,27.96-24.51-.55-2.73-2.35-5.06-5.12-6.93"></path>
<g class="arch-container" data-name="arch-container" clip-path="url(#clip-arch)">
<defs>
<clipPath id="clip-arch">
<rect x="6.3" y="0" width="66.7" height="110"></rect>
</clipPath>
</defs>
<g class="arches" data-name="arches">
<g><polygon points="3.2345359325408936 40.96485137939453 0.9968349933624268 47.49678039550781 0.9968349933624268 65.49678039550781 3.2345359325408936 58.96485137939453" class="cls-3"></polygon><path d="M3.2345359325408936,40.96485137939453 C3.2345359325408936,33.96485137939453 0.9968349933624268,40.49678039550781 0.9968349933624268,47.49678039550781" class="cls-3"></path></g>
<g><polygon points="1.4641879796981812 50.445709228515625 5.711980819702148 55.87122344970703 5.711980819702148 73.87122344970703 1.4641879796981812 68.44570922851562" class="cls-3"></polygon><path d="M1.4641879796981812,50.445709228515625 C1.4641879796981812,43.445709228515625 5.711980819702148,48.87122344970703 5.711980819702148,55.87122344970703" class="cls-3"></path></g>
<g><polygon points="8.26050090789795 57.448455810546875 14.804019927978516 59.8927001953125 14.804019927978516 77.8927001953125 8.26050090789795 75.44845581054688" class="cls-3"></polygon><path d="M8.26050090789795,57.448455810546875 C8.26050090789795,50.448455810546875 14.804019927978516,52.8927001953125 14.804019927978516,59.8927001953125" class="cls-3"></path></g>
<g><polygon points="17.731313705444336 60.546539306640625 24.67487907409668 61.39787292480469 24.67487907409668 79.39787292480469 17.731313705444336 78.54653930664062" class="cls-3"></polygon><path d="M17.731313705444336,60.546539306640625 C17.731313705444336,53.546539306640625 24.67487907409668,54.39787292480469 24.67487907409668,61.39787292480469" class="cls-3"></path></g>
<g><polygon points="27.672109603881836 61.52081298828125 34.66722869873047 61.332862854003906 34.66722869873047 79.3328628540039 27.672109603881836 79.52081298828125" class="cls-3"></polygon><path d="M27.672109603881836,61.52081298828125 C27.672109603881836,54.52081298828125 34.66722869873047,54.332862854003906 34.66722869873047,61.332862854003906" class="cls-3"></path></g>
<g><polygon points="37.65530776977539 61.06742858886719 44.57993698120117 60.054840087890625 44.57993698120117 78.05484008789062 37.65530776977539 79.06742858886719" class="cls-3"></polygon><path d="M37.65530776977539,61.06742858886719 C37.65530776977539,54.06742858886719 44.57993698120117,53.054840087890625 44.57993698120117,60.054840087890625" class="cls-3"></path></g>
<g><polygon points="47.52008819580078 59.45928192138672 54.29470443725586 57.703948974609375 54.29470443725586 75.70394897460938 47.52008819580078 77.45928192138672" class="cls-3"></polygon><path d="M47.52008819580078,59.45928192138672 C47.52008819580078,52.45928192138672 54.29470443725586,50.703948974609375 54.29470443725586,57.703948974609375" class="cls-3"></path></g>
<g><polygon points="57.15144729614258 56.78850555419922 63.668701171875 54.240638732910156 63.668701171875 72.24063873291016 57.15144729614258 74.78850555419922" class="cls-3"></polygon><path d="M57.15144729614258,56.78850555419922 C57.15144729614258,49.78850555419922 63.668701171875,47.240638732910156 63.668701171875,54.240638732910156" class="cls-3"></path></g>
<g><polygon points="66.3813247680664 52.959922790527344 72.44116973876953 49.46501922607422 72.44116973876953 67.46501922607422 66.3813247680664 70.95992279052734" class="cls-3"></polygon><path d="M66.3813247680664,52.959922790527344 C66.3813247680664,45.959922790527344 72.44116973876953,42.46501922607422 72.44116973876953,49.46501922607422" class="cls-3"></path></g>
<g><polygon points="74.87805938720703 47.716339111328125 79.9244384765625 42.88775634765625 79.9244384765625 60.88775634765625 74.87805938720703 65.71633911132812" class="cls-3"></polygon><path d="M74.87805938720703,47.716339111328125 C74.87805938720703,40.716339111328125 79.9244384765625,35.88775634765625 79.9244384765625,42.88775634765625" class="cls-3"></path></g>
<g><polygon points="81.62885284423828 40.422725677490234 83.27423858642578 33.737823486328125 83.27423858642578 51.737823486328125 81.62885284423828 58.422725677490234" class="cls-3"></polygon><path d="M81.62885284423828,40.422725677490234 C81.62885284423828,33.422725677490234 83.27423858642578,26.737823486328125 83.27423858642578,33.737823486328125" class="cls-3"></path></g>
<g><polygon points="82.46772003173828 30.862987518310547 78.03004455566406 25.990020751953125 78.03004455566406 43.990020751953125 82.46772003173828 48.86298751831055" class="cls-3"></polygon><path d="M82.46772003173828,30.862987518310547 C82.46772003173828,23.862987518310547 78.03004455566406,18.990020751953125 78.03004455566406,25.990020751953125" class="cls-3"></path></g>
</g>
</g>
</g>
</svg>
Let's break this problem down. Here are our goals
1. Animate a Circle on Bottom Ring Path
const container = document.getElementById('arches');
const path = document.querySelector('.ring-bottom');
function createCircle(x, y) {
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', x); // X center
circle.setAttribute('cy', y); // Y center
circle.setAttribute('r', 2); // radius
container.appendChild(circle);
}
const totalLength = path.getTotalLength();
let t = 0;
const runCircleExample = () => {
container.replaceChildren([]);
const { x, y } = path.getPointAtLength(t);
drawCircle(x, y);
t = (t + 1) % totalLength;
requestAnimationFrame(runCircleExample);
}
runCircleExample();
Click to Play/Pause
2. Replace the circle with an arch
const container = document.getElementById('arches');
const path = document.querySelector('.ring-bottom');
const HEIGHT = 18;
const WIDTH = 7;
function drawArch(x0, y0, x1, y1) {
const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
// Trapezoid
const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
const topLeft = [x0, y0 - HEIGHT], topRight = [x1, y1 - HEIGHT];
const bottomLeft = [x0, y0], bottomRight = [x1, y1];
polygon.setAttribute(
'points',
`${topLeft.join(' ')} ${topRight.join(' ')} ${bottomRight.join(' ')} ${bottomLeft.join(' ')}`
);
group.appendChild(polygon);
// ARCH
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
const control1X = x0, control1Y = y0 - HEIGHT - WIDTH;
const control2X = x1, control2Y = y1 - HEIGHT - WIDTH;
const d = `M${x0},${y0 - HEIGHT} C${control1X},${control1Y} ${control2X},${control2Y} ${x1},${y1 - HEIGHT}`;
path.setAttribute('d', d);
group.appendChild(path);
container.appendChild(group);
}
const totalLength = this.path.getTotalLength();
let t = 0;
const runArchExample = () => {
container.replaceChildren([]);
const { x: x0, y: y0 } = path.getPointAtLength(t);
const { x: x1, y: y1 } = path.getPointAtLength(t + WIDTH);
drawArch(x0, y0, x1, y1);
t = (t + 1) % totalLength;
requestAnimationFrame(runArchExample);
}
runArchExample();
Click to Play/Pause
3. Fill in arches for the whole length
const container = document.getElementById('arches');
const path = document.querySelector('.ring-bottom');
const HEIGHT = 18;
const WIDTH = 7;
const totalLength = this.path.getTotalLength(); // 120
// arch_width (7) + spacing (3) = 10 -> totalLength (120) / 10 = 12 arches
const startingPositions = [0,1,2,3,4,5,6,7,8,9,10,11].map(n => n * 10)
let t = startingPositions;
const runFullArchExample = () => {
container.replaceChildren([]);
for (let i = 0; i < t.length; i++) {
const { x: x0, y: y0 } = path.getPointAtLength(t[i]);
const { x: x1, y: y1 } = path.getPointAtLength(t[i] + WIDTH);
drawArch(x0, y0, x1, y1);
t[i] = (t[i] + 1) % totalLength;
}
requestAnimationFrame(container);
}
runFullArchExample();
Click to Play/Pause
4. Mask the arches to fit the length of the upper ring
<g class="arch-container" data-name="arch-container" clip-path="url(#clip-arch)">
<defs>
<clipPath id="clip-arch">
<rect x="6.3" y="0" width="66.7" height="110"></rect>
</clipPath>
</defs>
...
</g>
const totalLength = this.path.getTotalLength(); // 120
// arch_width (7) + spacing (3) = 10 -> totalLength (120) / 10 = 12 arches
const OFFSET = 3.5;
const startingPositions = [0,1,2,3,4,5,6,7,8,9,10,11].map(n => n * 10 + OFFSET)
let t = startingPositions;