Search code examples
javascripthtmlcsssvg

How can I define new "d" attribute from a resized/scaled SVG path?


My issues

Currently, I have positioned the pathIcon along the 'd' path of the SVG path. However, if I change the size of my SVG, the 'd' attribute must also change.
How can I define a new 'd' that resembles the old SVG path with the new size? I don't know how to define a new 'd' in JavaScript.

document.addEventListener("DOMContentLoaded", function() {
  let svg        = document.querySelector(".svg-path");
  let mPath      = document.getElementById("Path_440");
  let strokePath = document.getElementById("theFill");
  let pathIcon   = document.getElementById("pathIcon");

  const svgRect = svg.getBBox();
  const width   = svgRect.width;
  const height  = svgRect.height;
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`)

  function defineNewOffsetPath()
    {
    /**my issues : defineNewOffsetPath 
      mPath.setAttribute("d", newPath) 
      how to define a path look like old path with new size
    **/
    pathIcon.style.offsetPath = `path('${mPath.getAttribute("d")}')`;
    }

   defineNewOffsetPath();
})
.svg-path {
  overflow: visible;
  width: 100%;
  height: auto;
}

#pathIcon {
  position: absolute;
  inset: 0;
  width: 100px;
  height: 200px;
  background-size: 25px;
  offset-rotate: 0rad;
  transition: 0.2s;
  offset-distance: 0%;
}

#Path_440 {
  stroke-width: 2;
  stroke: #001d36;
}
<div style="height: 175px"></div>
<div id="scrollDiv" style="position: relative">
  <svg class="svg-path" viewBox="0 0 0 0" fill="none">
    <defs>
      <path id="Path_440"
        d="M1293 2S1277 76.47 1197 93.5C1105.55 112.97 888.33 91.07 772.5 100.5 545.5 100.5 302.61 125.94 279 295.5 268 374.5 265.11 419.83 268 503S269.9 645.65 305 741C346.77 854.46 770 838.5 1094.5 832 1366 842.5 1676.02 792 1766 1011 1803.18 1101.5 1766 1457.5 1766 1493.5 1766 1561 1696 1613.5 1618 1627.5 1465 1627.5 1188.11 1632.5 1003.5 1632.5 732.5 1632.5 369.53 1605.69 312 1717.5 271.61 1796 276 1920 276 1982 277.12 2074.28 272.55 2144.17 312 2258 339.86 2338.39 721.15 2324.5 981 2324.5 1297 2324.5 1677.34 2307.5 1739.5 2403.5 1793.57 2487 1772.73 2616.18 1772.73 2765 1772.73 2893 1770.73 2997.5 1652 3032 1612.67 3043.43 1237 3032 893 3032 405.5 3032 2 3030 2 3030"
        stroke="#020878" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-dasharray="20 20" />
    </defs>
    <use href="#Path_440" stroke-width="10" stroke-dasharray="20 10"></use>
    <use id="theFill" href="#Path_440" stroke-dasharray="1991.82, 9259.88" stroke-width="10" stroke="#4cacff"></use>
  </svg>
  <svg id="pathIcon" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <rect width="100" height="200" fill="url(#pattern0)" />
    <defs>
      <pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
        <use xlink:href="#image0_873_8619" transform="scale(0.00353357 0.00176678)" />
      </pattern>
      <image id="image0_873_8619" width="283" height="566" xlink:href="data:image/png;base64,iVBORw0KGgoAAAA" />
    </defs>
  </svg>
</div>


Solution

  • Provided you only need to scale your path data proportional – it is pretty straight forward:

    • parse your stringified path data (from the d attribute)
    • scale all point related command values by a scaling factor (you need to exclude a arc command values like x-axis-rotation, largeArc, and sweep flag)
    • stringify the path data to a d attribute

    let svg = document.querySelector(".svg-path");
    let mPath = document.getElementById("Path_440");
    let strokePath = document.getElementById("theFill");
    let pathIcon = document.getElementById("pathIcon");
    
    document.addEventListener("DOMContentLoaded", function () {
      // auto adjust viewBox
      let svgRect = svg.getBBox();
      let width = svgRect.width;
      let height = svgRect.height;
      svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
    
      // update offset path
      defineNewOffsetPath();
    });
    
    
    function defineNewOffsetPath() {
      
      // retrieve the current scale from SVG transformation matrix
      let matrix = svg.getCTM();
      let scale = matrix.a;
      
      // parse path data
      let d = mPath.getAttribute("d");
      let pathData = parsePathData(d);
    
      //scale pathdata
      pathData = scalePathData(pathData, scale);
    
      // apply scaled pathdata as stringified d attribute value
      d = pathDataToD(pathData);
      pathIcon.style.offsetPath = `path('${d}')`;
    }
    
    // recalculate offset path on resize
    window.addEventListener("resize", (e) => {
      defineNewOffsetPath();
    });
    
    // just for illustration
    resizeObserver()
    function resizeObserver() {
      defineNewOffsetPath();
    }
    new ResizeObserver(resizeObserver).observe(scrollDiv)
    
    
    
    
    /**
    * sclae path data proportional
    */
    function scalePathData(pathData, scale = 1) {
      let pathDataScaled = [];
      pathData.forEach((com, i) => {
        let { type, values } = com;
        let comT = {
          type: type,
          values: []
        };
    
        switch (type.toLowerCase()) {
          // lineto shorthands
          case "h":
            comT.values = [values[0] * scale]; // horizontal - x-only
            break;
          case "v":
            comT.values = [values[0] * scale]; // vertical - x-only
            break;
    
          // arcto
          case "a":
            comT.values = [
              values[0] * scale, // rx: scale
              values[1] * scale, // ry: scale
              values[2], // x-axis-rotation: keep it 
              values[3], // largeArc: dito
              values[4], // sweep: dito
              values[5] * scale, // final x: scale
              values[6] * scale // final y: scale
            ];
            break;
    
          /**
          * Other point based commands: L, C, S, Q, T
          * scale all values
          */
          default:
            if (values.length) {
              comT.values = values.map((val, i) => {
                return val * scale;
              });
            }
        }
        pathDataScaled.push(comT);
      });
      return pathDataScaled;
    }
    
    /**
    * parse stringified path data used in d attribute
    * to an array of computable command data
    */
    function parsePathData(d) {
      d = d
        // remove new lines, tabs an comma with whitespace
        .replace(/[\n\r\t|,]/g, " ")
        // pre trim left and right whitespace
        .trim()
        // add space before minus sign
        .replace(/(\d)-/g, "$1 -")
        // decompose multiple adjacent decimal delimiters like 0.5.5.5 => 0.5 0.5 0.5
        .replace(/(\.)(?=(\d+\.\d+)+)(\d+)/g, "$1$3 ");
    
      let pathData = [];
      let cmdRegEx = /([mlcqazvhst])([^mlcqazvhst]*)/gi;
      let commands = d.match(cmdRegEx);
    
      // valid command value lengths
      let comLengths = {
        m: 2,
        a: 7,
        c: 6,
        h: 1,
        l: 2,
        q: 4,
        s: 4,
        t: 2,
        v: 1,
        z: 0
      };
      commands.forEach((com) => {
        let type = com.substring(0, 1);
        let typeRel = type.toLowerCase();
        let isRel = type === typeRel;
        let chunkSize = comLengths[typeRel];
    
        // split values to array
        let values = com.substring(1, com.length).trim().split(" ").filter(Boolean);
    
        /**
         * A - Arc commands
         * large arc and sweep flags
         * are boolean and can be concatenated like
         * 11 or 01
         * or be concatenated with the final on path points like
         * 1110 10 => 1 1 10 10
         */
        if (typeRel === "a" && values.length != comLengths.a) {
          let n = 0,
            arcValues = [];
          for (let i = 0; i < values.length; i++) {
            let value = values[i];
    
            // reset counter
            if (n >= chunkSize) {
              n = 0;
            }
            // if 3. or 4. parameter longer than 1
            if ((n === 3 || n === 4) && value.length > 1) {
              let largeArc = n === 3 ? value.substring(0, 1) : "";
              let sweep = n === 3 ? value.substring(1, 2) : value.substring(0, 1);
              let finalX = n === 3 ? value.substring(2) : value.substring(1);
              let comN = [largeArc, sweep, finalX].filter(Boolean);
              arcValues.push(comN);
              n += comN.length;
            } else {
              // regular
              arcValues.push(value);
              n++;
            }
          }
          values = arcValues.flat().filter(Boolean);
        }
    
        // string  to number
        values = values.map(Number);
    
        // if string contains repeated shorthand commands - split them
        let hasMultiple = values.length > chunkSize;
        let chunk = hasMultiple ? values.slice(0, chunkSize) : values;
        let comChunks = [
          {
            type: type,
            values: chunk
          }
        ];
    
        // has implicit or repeated commands – split into chunks
        if (hasMultiple) {
          let typeImplicit = typeRel === "m" ? (isRel ? "l" : "L") : type;
          for (let i = chunkSize; i < values.length; i += chunkSize) {
            let chunk = values.slice(i, i + chunkSize);
            comChunks.push({
              type: typeImplicit,
              values: chunk
            });
          }
        }
        comChunks.forEach((com) => {
          pathData.push(com);
        });
      });
    
      /**
       * first M is always absolute/uppercase -
       * unless it adds relative linetos
       * (facilitates d concatenating)
       */
      pathData[0].type = "M";
      return pathData;
    }
    
    /**
     * serialize pathData array to
     * d attribute string
     */
    function pathDataToD(pathData, decimals = 3) {
      let d = ``;
      pathData.forEach((com) => {
        d += `${com.type}${com.values
          .map((val) => {
            return +val.toFixed(decimals);
          })
          .join(" ")}`;
      });
      return d;
    }
    html{
      margin:0;
      padding:0;
    }
    
    
    .svg-path {
      overflow: visible;
      width: 100%;
    }
    
    #pathIcon {
      position: absolute;
      inset: 0;
      width: 5vw;
      height: 5vw;
      offset-rotate: 0deg;
      offset-distance: 10%;
    }
    
    
    #scrollDiv{
    resize:both;
    overflow:auto;
    border: 1px solid #ccc;
    margin:10px;
    }
    <div id="scrollDiv" style="position: relative">
      <svg class="svg-path" viewBox="0 0 0 0" fill="none">
        <defs>
          <path id="Path_440"
            d="M1293 2 s-16 74.47-96 91.5c-91.45 19.47-308.67-2.43-424.5 7-227 0-469.89 25.44-493.5 195-11 79-13.89 124.33-11 207.5s1.9 142.65 37 238c41.77 113.46 465 97.5 789.5 91 271.5 10.5 581.52-40 671.5 179 37.18 90.5 0 446.5 0 482.5 0 67.5-70 120-148 134-153 0-429.89 5-614.5 5-271 0-633.97-26.81-691.5 85-40.39 78.5-36 202.5-36 264.5 1.12 92.28-3.45 162.17 36 276 27.86 80.39 409.15 66.5 669 66.5 316 0 696.34-17 758.5 79 54.07 83.5 33.23 212.68 33.23 361.5 0 128-2 232.5-120.73 267-39.33 11.43-415 0-759 0-487.5 0-891-2-891-2"
             />
        </defs>
        <use class="stroke" href="#Path_440" stroke="#ccc" stroke-width="10" stroke-dasharray="20 10"></use>
        <use class="stroke" id="theFill" href="#Path_440" stroke-dasharray="925.988 9259.88" stroke-width="10" stroke="#4cacff"></use>
      </svg>
      <svg id="pathIcon" fill="none">
        <rect width="100" height="100" fill="red" fill-opacity="0.5"/>
      </svg>
    </div>