Animating Along SVG Path

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>

Click to Play

Let's break this problem down. Here are our goals

  1. Animate a circle on the bottom ring path
  2. Replace the circle with an arch
  3. Fill in arches for the whole length
  4. Mask the arches to fit the length of the upper ring.

1. Animate a Circle on Bottom Ring Path

Tools:

  1. <circle cx cy r > – (x, y, radius)
  2. SVGPathElement.getTotalLength() – returns the total length of a path
  3. SVGPathElement.getPointAtLength(t) – returns the position at length t
  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

Tools:

  1. <polygon points> – list of (x, y)
  2. <path d> – draw commands
    • M(x,y) – Move to point (x, y)
    • c(x1,y1, x2, y2, x3, y3) – make a bezier curve using current point and 3 others
  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

Tools:

  1. SVGPathElement.getTotalLength() – returns the total length of a path
  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

Tools:

  1. clip-path="url(path)" – hides everything outside of the clip space
<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;

Click to Play/Pause