Home > Software engineering >  How to create a new svg path to automatically scale to different heights / widths
How to create a new svg path to automatically scale to different heights / widths

Time:01-10

I have an svg path and I want to use it to create a new path in JS to automatically adjust to different heights etc.

let w = window.innerWidth;
let h = document.querySelector("main").offsetHeight;

const pathSpec = "M305.91,0c8.54,101.44,17.07,203.69,5.27,304.8s-45.5,202.26-112.37,279c-...";

const yScale = h / 8000;
const xScale = w / 200;

let newPath = ""; 

const pathBits = pathSpec.matchAll(/([MmLlHhVvCcSsQqTtAa])([\d\.,\se-]*)/g);

pathBits.forEach((bit) => {
    newPath  = ; //not sure what goes in here
});

CodePudding user response:

try this :

let newPath = ""; 

const pathBits = pathSpec.matchAll(/([MmLlHhVvCcSsQqTtAa])([\d\.,\se-]*)/g);

pathBits.forEach((bit) => {
  const command = bit[1];
  const coordinates = bit[2];

  newPath  = command;

  const coordArray = coordinates.split(/[,\s]/g);
  coordArray.forEach((coord) => {
    const coordComponents = coord.split(/[eE,]/g);
    const x = parseFloat(coordComponents[0]) * xScale;
    const y = parseFloat(coordComponents[1]) * yScale;
    newPath  = x   ","   y;
  });
});

This should create a new path string that is scaled according to the xScale and yScale variables.

CodePudding user response:

Auto scaling svg

You might not need any javaScript to scale your pathdata. preserveAspectRatio="none" might also do the trick.

.resize{
  overflow:auto;
    border:1px solid #ccc;
  padding: 1em;
  width:50%;
  resize:both;
}

.svgFluid{
  width:100%;
  height:100%;
}
<h3>Resize me</h3>
<div >
<svg id="svg"  viewBox="3 7 80 70" preserveAspectRatio="none">
        <path id="path" d="M3,7 L13,7 m-10,10 l10,0 V27 H23 v10 h10C 33,43 38,47 43,47 c 0,5 5,10 10,10S 63,67 63,67 s -10,10 10,10Q 50,50 73,57q 20,-5 0,-10T 70,40t 0,-15A 5,5 45 1 0 40,20 a5,5 20 0 1 -10,-10Z" fill="#cccccc" stroke="#000000" stroke-width="1" stroke-linecap="butt" vector-effect="non-scaling-stroke"></path>
</svg>
  </div>

Recalculate pathData / d attribute

I highly recommend using a more "battle proof" path parser like Jarek Foksa's pathData polyfill..

Path d strings allows different notations (comma or space as delimiter), horizontal or vertical shorthand commands (h,v) abbreviated coordinates (e.g. ".5.05" instead of ) etc.
So you might run into troubles using an overly simplified parsing function.

function scalePath(path, scaleX, scaleY) {
  let pathData = path.getPathData();
  let pathDataScaled = scalePathData(pathData, scaleX, scaleY);
  path.setPathData(pathDataScaled);
  adjustViewBox(svg)
}

/**
 * scale pathData
 */
function scalePathData(pathData, scaleX, scaleY) {
  let pathDataScaled = [];
  pathData.forEach((com) => {
    let [type, values] = [com.type, com.values];
    let typeL = type.toLowerCase();
    let valuesL = values.length;
    let valsScaled = [];

    switch (typeL) {
      case "a":
        pathDataScaled.push({
          type: type,
          values: [
            values[0] * scaleX,
            values[1] * scaleY,
            values[2],
            values[3],
            values[4],
            values[5] * scaleX,
            values[6] * scaleY
          ]
        });
        break;

      case "h":
        pathDataScaled.push({
          type: type,
          values: [values[0] * scaleX]
        });
        break;

      case "v":
        pathDataScaled.push({
          type: type,
          values: [values[0] * scaleY]
        });
        break;
      case "z":
        pathDataScaled.push(com);
        break;

      default:
        if (valuesL) {
          valsScaled = [];
          for (let i = 0; i < values.length; i  = 2) {
            let x = values[i] * scaleX;
            let y = values[i   1] * scaleY;
            valsScaled.push(x, y);
          }
          pathDataScaled.push({
            type: type,
            values: valsScaled
          });
        }
    }
  });
  return pathDataScaled;
}


function adjustViewBox(svg) {
  let bb = svg.getBBox();
  let bbVals = [bb.x, bb.y, bb.width, bb.height].map((val) => {
    return  val.toFixed(2);
  });
  let maxBB = Math.max(...bbVals);
  let [x, y, width, height] = bbVals;
  svg.setAttribute("viewBox", [x, y, width, height].join(" "));
}
svg {
  width: 20em;
  border: 1px solid #ccc;
  overflow: visible
}
<!-- 
sample path y @phrogz:
https://stackoverflow.com/questions/9677885/convert-svg-path-to-absolute-commands
http://phrogz.net/svg/convert_path_to_absolute_commands.svg
-->

<p><button onclick="scalePath(path, 0.75, 0.5)">Scale path</button></p>

<svg id="svg"  viewBox="3 7 80 70">
        <path id="path" d="M3,7 L13,7 m-10,10 l10,0 V27 H23 v10 h10C 33,43 38,47 43,47 c 0,5 5,10 10,10S 63,67 63,67 s -10,10 10,10Q 50,50 73,57q 20,-5 0,-10T 70,40t 0,-15A 5,5 45 1 0 40,20 a5,5 20 0 1 -10,-10Z" fill="#cccccc" stroke="#000000" stroke-width="1" stroke-linecap="butt"></path>
</svg>

<script src="https://cdn.jsdelivr.net/npm/[email protected]/path-data-polyfill.min.js"></script>

The above example

  • parses the path via getPathData()
  • scales the pathData array (we need exceptions for commands like a, h and v
  • apply the scaled pathData to the <path> element

Standalone solution

This version will directly parse any d data string (compatible with the official pathData spec).
So you can generate a scaled d string without the need of appending a <path> to your DOM.

let d = 'M3,7 L13,7 m-10,10 l10,0 V27 H23 v10 h10C 33,43 38,47 43,47 c 0,5 5,10 10,10S 63,67 63,67 s -10,10 10,10Q 50,50 73,57q 20,-5 0,-10T 70,40t 0,-15A 5,5 45 1 0 40,20 a5,5 20 0 1 -10,-10Z';
let pathData = parseDtoPathData(d);
let scaleX = 0.75;
let scaleY = 0.5;
let pathDataScaled = scalePathData(pathData, scaleX, scaleY);
let dScaled = pathDataToD(pathDataScaled);
path.setAttribute('d', dScaled);
adjustViewBox(svg)


function adjustViewBox(svg) {
  let bb = svg.getBBox();
  let bbVals = [bb.x, bb.y, bb.width, bb.height].map((val) => {
    return  val.toFixed(2);
  });
  let maxBB = Math.max(...bbVals);
  let [x, y, width, height] = bbVals;
  svg.setAttribute("viewBox", [x, y, width, height].join(" "));
}


/**
 * scale pathData
 */
function scalePathData(pathData, scaleX, scaleY) {
  let pathDataScaled = [];
  pathData.forEach((com) => {
    let [type, values] = [com.type, com.values];
    let typeL = type.toLowerCase();
    let valuesL = values.length;
    let valsScaled = [];

    switch (typeL) {
      case "a":
        pathDataScaled.push({
          type: type,
          values: [
            values[0] * scaleX,
            values[1] * scaleY,
            values[2],
            values[3],
            values[4],
            values[5] * scaleX,
            values[6] * scaleY
          ]
        });
        break;

      case "h":
        pathDataScaled.push({
          type: type,
          values: [values[0] * scaleX]
        });
        break;

      case "v":
        pathDataScaled.push({
          type: type,
          values: [values[0] * scaleY]
        });
        break;
      case "z":
        pathDataScaled.push(com);
        break;

      default:
        if (valuesL) {
          valsScaled = [];
          for (let i = 0; i < values.length; i  = 2) {
            let x = values[i] * scaleX;
            let y = values[i   1] * scaleY;
            valsScaled.push(x, y);
          }
          pathDataScaled.push({
            type: type,
            values: valsScaled
          });
        }
    }
  });
  return pathDataScaled;
}


/**
 * create pathData from d attribute
 **/
function parseDtoPathData(d, normalize = false) {
  // sanitize d string
  let commands = d
    .replace(/[\n\r\t]/g, "")
    .replace(/,/g, " ")
    .replace(/-/g, " -")
    .replace(/(\.)(\d )(\.)(\d )/g, "$1$2 $3$4")
    .replace(/( )(0)(\d )/g, "$1 $2 $3")
    .replace(/([a-z])/gi, "|$1 ")
    .replace(/\s{2,}/g, " ")
    .trim()
    .split("|")
    .filter(Boolean)
    .map((val) => {
      return val.trim();
    });

  // compile pathData
  let pathData = [];

  for (let i = 0; i < commands.length; i  ) {
    let com = commands[i].split(" ");
    let type = com.shift();
    let typeLc = type.toLowerCase();
    let isRelative = type === typeLc ? true : false;
    let values = com.map((val) => {
      return parseFloat(val);
    });

    // analyze repeated (shorthanded) commands
    let chunks = [];
    let repeatedType = type;
    // maximum values for a specific command type
    let maxValues = 2;
    switch (typeLc) {
      case "v":
      case "h":
        maxValues = 1;
        if (typeLc === "h") {
          repeatedType = isRelative ? "h" : "H";
        } else {
          repeatedType = isRelative ? "v" : "V";
        }
        break;
      case "m":
      case "l":
      case "t":
        maxValues = 2;
        repeatedType =
          typeLc !== "t" ? (isRelative ? "l" : "L") : isRelative ? "t" : "T";
        /**
         * first starting point should be absolute/uppercase -
         * unless it adds relative linetos
         * (facilitates d concatenating)
         */
        if (typeLc === "m") {
          if (i == 0) {
            type = "M";
          }
        }
        break;
      case "s":
      case "q":
        maxValues = 4;
        repeatedType =
          typeLc !== "q" ? (isRelative ? "s" : "S") : isRelative ? "q" : "Q";
        break;
      case "c":
        maxValues = 6;
        repeatedType = isRelative ? "c" : "C";
        break;
      case "a":
        maxValues = 7;
        repeatedType = isRelative ? "a" : "A";
        break;
        // z closepath
      default:
        maxValues = 0;
    }

    // if string contains repeated shorthand commands - split them
    const arrayChunks = (array, chunkSize = 2) => {
      let chunks = [];
      for (let i = 0; i < array.length; i  = chunkSize) {
        let chunk = array.slice(i, i   chunkSize);
        chunks.push(chunk);
      }
      return chunks;
    };

    chunks = arrayChunks(values, maxValues);
    // add 1st/regular command
    let chunk0 = chunks.length ? chunks[0] : [];
    pathData.push({
      type: type,
      values: chunk0
    });
    // add repeated commands
    if (chunks.length > 1) {
      for (let c = 1; c < chunks.length; c  ) {
        pathData.push({
          type: repeatedType,
          values: chunks[c]
        });
      }
    }
  }
  return pathData;
}


function pathDataToD(pathData, decimals = -1) {
  let d = "";
  pathData.forEach((com, c) => {
    if (decimals >= 0) {
      com.values.forEach(function(val, v) {
        pathData[c]["values"][v] =  val.toFixed(decimals);
      });
    }
    d  = `${com.type}${com.values.join(" ")}`;
  });
  d = d.replaceAll(",", " ").replaceAll(" -", "-");
  return d;
}
svg {
  width: 20em;
  border: 1px solid #ccc;
  overflow: visible
}
<!-- 
sample path y @phrogz:
https://stackoverflow.com/questions/9677885/convert-svg-path-to-absolute-commands
http://phrogz.net/svg/convert_path_to_absolute_commands.svg
-->

<svg id="svg"  viewBox="3 7 80 70">
        <path id="path" d="" fill="#cccccc" stroke="#000000" stroke-width="1" stroke-linecap="butt"></path>
</svg>

  • Related