Search code examples
javascriptsvgcss-animationsdrawingimage-optimization

Manual animation for each path command for an SVG


Is it possible to do the animation on the drawing attribute of an SVG manually (with a button) for each path command instead of continuously?

.path {stroke-dasharray: 1000;
       stroke-dashoffset: 1000;
       animation: dash 5s linear alternate infinite;}
@keyframes dash {from {stroke-dashoffset: 822;}
                 to   {stroke-dashoffset: 0;}
                 }
<svg width="200" height="200" viewBox="50 50 240 270">
  <path class="path" fill="white" stroke="black" stroke-width="4" 
        d="M66.039,133.545c0,0-21-57,18-67s49-4,65,8 s30,41,53,27s66,4,58,32s-5,44,18,57s22,46,0,
           45s-54-40-68-16s-40,88-83,48s11-61-11-80s-79-7-70-41 C46.039,146.545,53.039,128.545,66.039,133.545z"/>
</svg>

That could help one merge two or more path commands and reduce the number of drawing path commands in an SVG.


Solution

  • You will need to get the length for each path segment, since you're animating/transitioning stroke-dasharray/stroke-dashoffset values.

    Here's a simplified example using a path showing a perfect circle with a circumference of 100 (diameter=31,832 * π). The path consists of 4 segments that should have a segment length of 25 units.

    The helper function to get all segments lengths is based on @bez997's answer

    let svg = document.querySelector("svg");
    let path = svg.querySelector(".path");
    let pathLength = path.getTotalLength().toFixed(1) * 1;
    
    /**
     * save segments' path lengths to array
     **/
    
    let segments = getSegmentLengths(path);
    let segmentJson = JSON.stringify(segments).replaceAll('"', "");
    
    /**
    * @bez997
    https://stackoverflow.com/questions/21564905/svg-api-length-of-a-single-segment#answer-45793408
    original pen: https://codepen.io/bez997/pen/dzJemZ?editors=1010
    **/
    
    function getSegmentLengths(path) {
      let segList = path.pathSegList;
      // temporary path for computing segments
      let lengthPath = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "path"
      );
      let lastLength = 0;
      let segLengths = [];
      let segSteps = [];
      let segOffset = 0;
    
      for (let i = 0; i < segList.numberOfItems; i += 1) {
        let segObj = segList.getItem(i);
        lengthPath.pathSegList.appendItem(segObj);
        // rounding numbers
        let currentLength = lengthPath.getTotalLength().toFixed(1) * 1;
        let segmentLength = (currentLength - lastLength).toFixed(1) * 1;
    
        // strip M and Z commands since, they don't have any length
        if (segmentLength) {
          lastLength = currentLength;
          segSteps.push({
            offset: i == 1 ? 0 : -(lastLength - segmentLength).toFixed(1) * 1,
            dash: segmentLength,
            currentLength: currentLength
          });
        }
      }
      return segSteps;
    }
    
    /**
     * change stroke dash attributes for animation
     **/
    function strokeTo(path, pathLength, offset, dash) {
      let gap = pathLength - dash;
      path.setAttribute("stroke-dashoffset", offset);
      path.setAttribute("stroke-dasharray", dash + " " + gap);
    }
    
    /**
     * create navigation for each segment
     **/
    function getStrokeNav(svg, segments, singleSegment = true, label = "") {
      let strokeNav = label;
      let index = 0;
      segments.forEach(function (el, i) {
        index++;
        let thisOffset = el.offset;
        let thisDash = el.dash;
    
        if (!singleSegment) {
          thisOffset = 0;
          thisDash = el.currentLength;
        }
        strokeNav +=
          '<button type="button" onclick="strokeTo(path,' +
          pathLength +
          ", " +
          thisOffset +
          ", " +
          thisDash +
          ')">' +
          index +
          "</button>";
      });
    
      return strokeNav;
    }
    
    // nav html output
    let strokeToNav =
      "<p>" +
      getStrokeNav(svg, segments, false, "Animate to stroke segment ") +
      "</p><p>" +
      getStrokeNav(svg, segments, true, "Animate single segment ") +
      "</p>";
    document.body.insertAdjacentHTML("afterBegin", strokeToNav);
    svg {
      border: 1px solid #ccc;
      display: inline-block;
      font-size: calc( ( 25vw + 25vh )/ 2) ;
      width: 1em;
    }
    
    .path{
      transition:0.5s
    }
    <script src="https://cdn.rawgit.com/progers/pathseg/a1072a7b/pathseg.js"></script>
    <svg  id="svg" xmlns="http://www.w3.org/2000/svg" 
          viewBox="0 0 50 50" >
    <path class="path" fill="none" stroke="#000"  d="
    M40.916,25
    c0,8.79-7.125,15.916-15.916,15.916
    S9.084,33.79,9.084,25
    S16.21,9.084,25,9.084
    S40.916,16.21,40.916,25
    z" />
    </svg>

    The expected segment array would be:

    let segments = [
      {offset:0, dash:25, currentLength:25},
      {offset:-25, dash:25, currentLength:50},
      {offset:-50, dash:25, currentLength:75},
      {offset:-75, dash:25, currentLength:100}
    ];
    

    By also saving an offset value, we can transition to a segment (including previous segments) but also animate a single segment.

    stroke-dashoffset="0" stroke-dasharray="25 75"
    

    Would show the first (bottom right) segment stroke.

    Once you got all segments' you could store this data to an array or json statically. Since the length calculation creates a temporary DOM element – so you might experience some performance hit, depending on the svg's complexity.

    let svg = document.querySelector("svg");
    let path = svg.querySelector(".path");
    let pathLength = path.getTotalLength().toFixed(1) * 1;
    
    /**
     * save segments' path lengths to array
     **/
    let segments = [
      { offset: 0, dash: 78.6, currentLength: 78.6 },
      { offset: -78.6, dash: 68.8, currentLength: 147.4 },
      { offset: -147.4, dash: 65.1, currentLength: 212.5 },
      { offset: -212.5, dash: 81.9, currentLength: 294.4 },
      { offset: -294.4, dash: 66.3, currentLength: 360.7 },
      { offset: -360.7, dash: 61.4, currentLength: 422.1 },
      { offset: -422.1, dash: 76.5, currentLength: 498.6 },
      { offset: -498.6, dash: 116.9, currentLength: 615.5 },
      { offset: -615.5, dash: 89.3, currentLength: 704.8 },
      { offset: -704.8, dash: 90.2, currentLength: 795 },
      { offset: -795, dash: 26.6, currentLength: 821.6 }
    ];
    
    
    /**
     * change stroke dash attributes for animation
     **/
    function strokeTo(path, pathLength, offset, dash) {
      let gap = pathLength - dash;
      path.setAttribute("stroke-dashoffset", offset);
      path.setAttribute("stroke-dasharray", dash + " " + gap);
    }
    svg {
      border: 1px solid #ccc;
      display: inline-block;
      font-size: calc( ( 50vw + 50vh )/ 2) ;
      width: 1em;
    }
    
    .path{
      transition:0.5s
    }
    <svg width="200" height="200" viewBox="50 50 240 270">
      <path class="path" fill="white" stroke="black" stroke-width="4" 
            d="M66.039,133.545c0,0-21-57,18-67s49-4,65,8 s30,41,53,27s66,4,58,32s-5,44,18,57s22,46,0,
               45s-54-40-68-16s-40,88-83,48s11-61-11-80s-79-7-70-41 C46.039,146.545,53.039,128.545,66.039,133.545z"/>
    </svg>
    
    <p>Animate to stroke segment <button type="button" onclick="strokeTo(path,821.6, 0, 78.6)">1</button><button type="button" onclick="strokeTo(path,821.6, 0, 147.4)">2</button><button type="button" onclick="strokeTo(path,821.6, 0, 212.5)">3</button><button type="button" onclick="strokeTo(path,821.6, 0, 294.4)">4</button><button type="button" onclick="strokeTo(path,821.6, 0, 360.7)">5</button><button type="button" onclick="strokeTo(path,821.6, 0, 422.1)">6</button><button type="button" onclick="strokeTo(path,821.6, 0, 498.6)">7</button><button type="button" onclick="strokeTo(path,821.6, 0, 615.5)">8</button><button type="button" onclick="strokeTo(path,821.6, 0, 704.8)">9</button><button type="button" onclick="strokeTo(path,821.6, 0, 795)">10</button><button type="button" onclick="strokeTo(path,821.6, 0, 821.6)">11</button></p>
    
    <p>Animate single segment <button type="button" onclick="strokeTo(path,821.6, 0, 78.6)">1</button><button type="button" onclick="strokeTo(path,821.6, -78.6, 68.8)">2</button><button type="button" onclick="strokeTo(path,821.6, -147.4, 65.1)">3</button><button type="button" onclick="strokeTo(path,821.6, -212.5, 81.9)">4</button><button type="button" onclick="strokeTo(path,821.6, -294.4, 66.3)">5</button><button type="button" onclick="strokeTo(path,821.6, -360.7, 61.4)">6</button><button type="button" onclick="strokeTo(path,821.6, -422.1, 76.5)">7</button><button type="button" onclick="strokeTo(path,821.6, -498.6, 116.9)">8</button><button type="button" onclick="strokeTo(path,821.6, -615.5, 89.3)">9</button><button type="button" onclick="strokeTo(path,821.6, -704.8, 90.2)">10</button><button type="button" onclick="strokeTo(path,821.6, -795, 26.6)">11</button></p>

    As an alternative you might also store your segments in a svg data attribute - albeit data-attributes are still not valid by specs.
    However, they shouldn't introduce rendering issues.

    A benefit of this approach would be, your segment data could directly be saved in your svg markup/file while reducing your js file.

    let svg = document.querySelector("svg");
    let path = svg.querySelector(".path");
    let pathLength = path.getTotalLength().toFixed(1)*1;
    
    
    /**
    * change stroke dash attributes for animation
    **/
    function strokeTo(path, pathLength, offset, dash) {
      let gap = pathLength - dash;
      path.setAttribute("stroke-dashoffset", offset);
      path.setAttribute("stroke-dasharray", dash + " " + gap);
    }
    svg {
      border: 1px solid #ccc;
      display: inline-block;
      font-size: calc( ( 50vw + 50vh )/ 2) ;
      width: 1em;
    }
    
    .path{
      transition:0.3s
    }
    <svg width="200" height="200" viewBox="50 50 240 270" data-segments='[{"offset":0,"dash":78.6,"currentLength":78.6},{"offset":-78.6,"dash":68.8,"currentLength":147.4},{"offset":-147.4,"dash":65.1,"currentLength":212.5},{"offset":-212.5,"dash":81.9,"currentLength":294.4},{"offset":-294.4,"dash":66.3,"currentLength":360.7},{"offset":-360.7,"dash":61.4,"currentLength":422.1},{"offset":-422.1,"dash":76.5,"currentLength":498.6},{"offset":-498.6,"dash":116.9,"currentLength":615.5},{"offset":-615.5,"dash":89.3,"currentLength":704.8},{"offset":-704.8,"dash":90.2,"currentLength":795},{"offset":-795,"dash":26.6,"currentLength":821.6}]'>
      <path class="path" fill="white" stroke="black" stroke-width="4" d="M66.039,133.545c0,0-21-57,18-67s49-4,65,8 s30,41,53,27s66,4,58,32s-5,44,18,57s22,46,0,
               45s-54-40-68-16s-40,88-83,48s11-61-11-80s-79-7-70-41 C46.039,146.545,53.039,128.545,66.039,133.545z"></path>
    </svg>
    
    
    <p>Animate single segment <button type="button" onclick="strokeTo(path,821.6, 0, 78.6)">1</button><button type="button" onclick="strokeTo(path,821.6, -78.6, 68.8)">2</button><button type="button" onclick="strokeTo(path,821.6, -147.4, 65.1)">3</button><button type="button" onclick="strokeTo(path,821.6, -212.5, 81.9)">4</button><button type="button" onclick="strokeTo(path,821.6, -294.4, 66.3)">5</button><button type="button" onclick="strokeTo(path,821.6, -360.7, 61.4)">6</button><button type="button" onclick="strokeTo(path,821.6, -422.1, 76.5)">7</button><button type="button" onclick="strokeTo(path,821.6, -498.6, 116.9)">8</button><button type="button" onclick="strokeTo(path,821.6, -615.5, 89.3)">9</button><button type="button" onclick="strokeTo(path,821.6, -704.8, 90.2)">10</button><button type="button" onclick="strokeTo(path,821.6, -795, 26.6)">11</button></p>