Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Converting svg path to polygon in javascript

i am trying to convert an svg path to an svg polygon in javascript. i found this function to crawl along the path and extract its coordinates.

    var length = path.getTotalLength();
    var p=path.getPointAtLength(0);
    var stp=p.x+","+p.y;
    
    for(var i=1; i<length; i++){
    
        p=path.getPointAtLength(i);
        stp=stp+" "+p.x+","+p.y;
        
    }

this works but it returns some hundreds of points for a polygon that has only six points originally. how would i get only the necessary points (all paths are straight lines, no curves)

var path = $.find("path")[0];

var len = path.getTotalLength();
var p = path.getPointAtLength(0);
var stp = p.x + "," + p.y;
var newp;

for (var i = 1; i < len; i++) {
  newp = path.getPointAtLength(i);
  if (newp.x != p.x && newp.y != p.y) {
    p = newp;
  } else {
    continue;
  }
  stp = stp + " " + p.x + " " + p.y;

}

$('#poly').text(stp);
pre {
  display: block;
  white-space: pre-wrap;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="425.2px" height="303.31px" viewBox="0 0 425.2 303.31" enable-background="new 0 0 425.2 303.31" xml:space="preserve">
<g>
    <path id="path" fill="#FFFFFF" stroke="#000000" stroke-miterlimit="10" d="M256.768,227.211h-33.013l4.902-65.682l-40.357,59.06
        l-26.654-37.931l0.292-0.298L264.865,77.415L256.768,227.211z M224.833,226.211h30.987l7.903-146.205L162.942,182.764
        l25.346,36.069l41.643-60.94L224.833,226.211z"/>
</g>
</svg>

<code>
<pre id="poly">
</pre>
</code>
like image 499
aushilfe444 Avatar asked Apr 25 '26 10:04

aushilfe444


2 Answers

ok got it.. the function getPathSegAtLength() returns the number of the actual path segment. with that it's easy then.

    var len = path.getTotalLength();
    var p=path.getPointAtLength(0);
    var seg = path.getPathSegAtLength(0);
    var stp=p.x+","+p.y;

    for(var i=1; i<len; i++){

        p=path.getPointAtLength(i);

        if (path.getPathSegAtLength(i)>seg) {

        stp=stp+" "+p.x+","+p.y;
        seg = path.getPathSegAtLength(i);

        }

    }
like image 111
aushilfe444 Avatar answered Apr 28 '26 02:04

aushilfe444


how would i get only the necessary points (all paths are straight lines, no curves)

If your path actually contains only linetos you can take a shortcut to retrieve polygon vertices by parsing the path data.

1. Polygon vertices from path data

This approach requires to:

  • parse the path data from the d attribute
  • convert all commands to absolute values
  • convert shorthand commands h, v to their longhand l equivalents

Test if path data is a polygon

We can check whether a path could be represented as a polygon by inspecting its d attribute like so:

function pathIsPolygon(d) {
  // any beziers or arc commands?
  let isPolygon = /[csqta]/gi.test(d) ? false : true
  return isPolygon;
}

We're basically testing if the path data string contains any bézier or arc commands. If not (no c,s,q,t,a commands): we can proceed to retrieve vertices from the path data by getting the last couple of values representing the final on-path point. This check improves both performance and accuracy as we avoid unnecessary iterative pointAtLength() calculations and retain the original polygonal geometry of the path.

/**
 * 1. is polygon:
 */
let path = document.getElementById("path");
let poly = document.getElementById("poly");

// parse pathdata
let d = path.getAttribute("d");
let pathData = parsePathDataNormalized(d);

/**
 * check if path is already a polygon:
 * just return the final command points
 */
let vertices;
let isPolygon = pathIsPolygon(d);
if (isPolygon) {
  console.log(path.id, "is polygon");
  vertices = getPathDataVertices(pathData);
}

//apply
let ptAtt = vertices
  .map((pt) => {
    return Object.values(pt);
  })
  .flat()
  .join(" ");
poly.setAttribute("points", ptAtt);

//output vertices/point array
verticesOut.value = JSON.stringify(vertices, null, ' ');

function pathIsPolygon(d) {
  // any beziers or arc commands?
  let isPolygon = /[csqta]/gi.test(d) ? false : true;
  return isPolygon;
}

function getPathDataVertices(pathData) {
  let polyPoints = [];
  pathData.forEach((com) => {
    let values = com.values;
    // get final on path point from last 2 values
    if (values.length) {
      let pt = {
        x: values[values.length - 2],
        y: values[values.length - 1]
      };
      polyPoints.push(pt);
    }
  });
  return polyPoints;
}

/**
 * Standalone pathData parser
 * returns a pathData array compliant
 * with the w3C SVGPathData interface draft
 * https://svgwg.org/specs/paths/#InterfaceSVGPathData
 */

function parsePathDataNormalized(d) {
  d = d
    .replace(/[\n\r\t|,]/g, " ")
    .trim()
    .replace(/(\d)-/g, "$1 -")
    .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
  };

  // offsets for absolute conversion
  let offX, offY, lastX, lastY;

  for (let c = 0; c < commands.length; c++) {
    let com = commands[c];
    let type = com.substring(0, 1);
    let typeRel = type.toLowerCase();
    let typeAbs = type.toUpperCase();
    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
     */
    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 {
          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
        });
      }
    }

    // convert to absolute
    if (c === 0) {
      offX = values[0];
      offY = values[1];
      lastX = offX;
      lastY = offY;
    }

    let typeFirst = comChunks[0].type;
    typeAbs = typeFirst.toUpperCase();

    isRel =
      typeFirst.toLowerCase() === typeFirst && pathData.length ? true : false;

    for (let i = 0; i < comChunks.length; i++) {
      let com = comChunks[i];
      let type = com.type;
      let values = com.values;
      let valuesL = values.length;
      let comPrev = comChunks[i - 1] ?
        comChunks[i - 1] :
        c > 0 && pathData[pathData.length - 1] ?
        pathData[pathData.length - 1] :
        comChunks[i];

      let valuesPrev = comPrev.values;
      let valuesPrevL = valuesPrev.length;
      isRel =
        comChunks.length > 1 ?
        type.toLowerCase() === type && pathData.length :
        isRel;

      if (isRel) {
        com.type = comChunks.length > 1 ? type.toUpperCase() : typeAbs;

        switch (typeRel) {
          case "a":
            com.values = [
              values[0],
              values[1],
              values[2],
              values[3],
              values[4],
              values[5] + offX,
              values[6] + offY
            ];
            break;

          case "h":
          case "v":
            com.values = type === "h" ? [values[0] + offX] : [values[0] + offY];
            break;

          case "m":
          case "l":
          case "t":
            com.values = [values[0] + offX, values[1] + offY];
            break;

          case "c":
            com.values = [
              values[0] + offX,
              values[1] + offY,
              values[2] + offX,
              values[3] + offY,
              values[4] + offX,
              values[5] + offY
            ];
            break;

          case "q":
          case "s":
            com.values = [
              values[0] + offX,
              values[1] + offY,
              values[2] + offX,
              values[3] + offY
            ];
            break;
        }
      }
      // is absolute
      else {
        offX = 0;
        offY = 0;
      }

      // convert shorthands
      let shorthandTypes = ["H", "V", "S", "T"];

      if (shorthandTypes.includes(typeAbs)) {
        let cp1X, cp1Y, cpN1X, cpN1Y, cp2X, cp2Y;
        if (com.type === "H" || com.type === "V") {
          com.values =
            com.type === "H" ? [com.values[0], lastY] : [lastX, com.values[0]];
          com.type = "L";
        } else if (com.type === "T" || com.type === "S") {
          [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
          [cp2X, cp2Y] =
          valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];

          // new control point
          cpN1X = com.type === "T" ? lastX * 2 - cp1X : lastX * 2 - cp2X;
          cpN1Y = com.type === "T" ? lastY * 2 - cp1Y : lastY * 2 - cp2Y;

          com.values = [cpN1X, cpN1Y, com.values].flat();
          com.type = com.type === "T" ? "Q" : "C";
        }
      }

      pathData.push(com);

      lastX =
        valuesL > 1 ?
        values[valuesL - 2] + offX :
        typeRel === "h" ?
        values[0] + offX :
        lastX;
      lastY =
        valuesL > 1 ?
        values[valuesL - 1] + offY :
        typeRel === "v" ?
        values[0] + offY :
        lastY;
      offX = lastX;
      offY = lastY;
    }
  }
  pathData[0].type = "M";
  return pathData;
}
svg {
  overflow: visible;
  width:25%;
}

svg path {
  stroke-width: 2%;
  stroke: #ccc;
}

svg polygon {
  stroke-width: 0.75%;
  marker-start: url(#markerStart);
  marker-mid: url(#markerRound);
}

textarea {
  width: 100%;
  display: block;
  min-height: 10em;
}
<h3>Path is already a polygon</h3>
<svg id="svg" viewBox="0 0 425.2 303.31" enable-background="new 0 0 425.2 303.31" xml:space="preserve">
<g>
    <path id="path" fill="#FFFFFF" stroke="#000000" stroke-miterlimit="10" d="M256.768,227.211h-33.013l4.902-65.682l-40.357,59.06
        l-26.654-37.931l0.292-0.298L264.865,77.415L256.768,227.211z M224.833,226.211h30.987l7.903-146.205L162.942,182.764
        l25.346,36.069l41.643-60.94L224.833,226.211z"/>
</g>

<polygon id="poly" />
</svg>


<h3>Point/vertices data</h3>
<textarea id="verticesOut"></textarea>


<!-- markers to show commands -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;opacity:0">
<defs>
<marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth"
markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="5" fill="green"></circle>
<marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5"
markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="2.5" fill="red"></circle>
</marker>
</defs>
</svg>

Parsing path data

By parsing the path data we can easily retrieve x and y coordinates from each command to create an array of point objects.
The parsing is done via a custom parsing script trying to be compliant with the suggested W3C pathdata interface spec draft. This draft is intended to replace the (mostly) deprecated/unsupported SVGPathSeg interface (although I won't hold my breath ... this concept is around since 2016).

Worth noting: there is still a SVGPathSeg polyfill.

You can actually use any path data parser (as long as it provides a way to convert commands to all absolute and "unshort" commands like hand v to l) for instance Jarek Foksa's path-data-polyfill setting the normalize parameter like so path.getPathData({normalize:true}).

2. Polygon retaining path shape

If a path also contains curves, getPointAtlength() may not be ideal as it wont't retain the basic geometry – respecting command final points.

pointAtLength() vertices
Despite points are calculated based on equal path length intervals the visual result appears to be rather arbitrary. We lose the expected symmetry of the original shape.

Add points per segment

We can fix this issue by adding vertices according to each segment's length.

let decimals = 2;
let split = 32;

/**
 * 1. is polygon:
 */
let path = document.getElementById('path')
let poly = document.getElementById('poly')
let vertices = getPathPolygonVertices(path, split, decimals);

//apply
let ptAtt = vertices.map(pt => {
  return Object.values(pt)
}).flat().join(' ')
poly.setAttribute('points', ptAtt)


/**
 * 2. has curves:
 */
let path1 = document.getElementById('path1')
let poly1 = document.getElementById('poly1')
let vertices1 = getPathPolygonVertices(path1, split, decimals);

//apply
let ptAtt1 = vertices1.map(pt => {
  return Object.values(pt)
}).flat().join(' ')
poly1.setAttribute('points', ptAtt1)


function getPathPolygonVertices(path, split = 16, decimals = 3) {

  let pts = []
  let ns = 'http://www.w3.org/2000/svg'

  // parse pathdata
  let d = path.getAttribute('d')
  let pathData = parsePathDataNormalized(d)

  /**
   * check if path is already polygon:
   * just return the final command points
   */
  let isPolygon = pathIsPolygon(d);
  if (isPolygon) {
    console.log(path.id, 'is polygon');
    pts = getPathDataVertices(pathData)
    return pts
  }

  // target side length
  let totalLength = path.getTotalLength();
  let step = totalLength / split;
  let lastLength = 0;
  let Mvalues = pathData[0].values;
  let M = {
    x: Mvalues[Mvalues.length - 2],
    y: Mvalues[Mvalues.length - 1]
  };

  for (let i = 1; i < pathData.length; i++) {
    let com = pathData[i];
    let comPrev = pathData[i - 1];
    let type = com.type.toLowerCase();
    let [values, valuesPrev] = [com.values, comPrev.values];

    //previous commands final point
    let p0 = {
      x: valuesPrev[valuesPrev.length - 2],
      y: valuesPrev[valuesPrev.length - 1]
    };
    let p = values.length ? {
      x: values[values.length - 2],
      y: values[values.length - 1]
    } : p0;

    if (values.length) {
      // create temporary path to get segment length
      let pathSeg = document.createElementNS(ns, 'path')
      pathSeg.setAttribute('d', `M ${p0.x} ${p0.y} ${com.type} ${com.values.join(' ')}`)
      let segLength = pathSeg.getTotalLength()

      // fit to segment length – keep command end points to better retain shape
      let segSplits = Math.ceil(segLength / step);

      // if lineto: no need to calculate points
      if (type === 'l') {
        pts.push(p0);
        pts.push(p);
      } else {

        for (let s = 0; s < segSplits; s++) {
          let len = lastLength + (segLength / segSplits) * s;
          // get point
          let pt = path.getPointAtLength(len);
          pts.push(pt);
        }
      }
      //remove temorary path
      pathSeg.remove()
      lastLength += segLength;
    }

    // is Z/closepath: add previous end point
    else {
      pts.push(p0);
    }
  }

  //round coordinates
  pts = Array.from(pts).map(pt => {
    return {
      x: +pt.x.toFixed(decimals),
      y: y = +pt.y.toFixed(decimals)
    }
  });

  return pts
}



function pathIsPolygon(d) {
  // any beziers or arc commands?
  let isPolygon = /[csqta]/gi.test(d) ? false : true
  return isPolygon;
}

function getPathDataVertices(pathData) {
  let polyPoints = [];
  pathData.forEach(com => {
    let values = com.values;
    // get final on path point from last 2 values
    if (values.length) {
      let pt = {
        x: values[values.length - 2],
        y: values[values.length - 1]
      }
      polyPoints.push(pt)
    }
  })
  return polyPoints
}



/**
 * Standalone pathData parser
 */
function parsePathDataNormalized(d) {
  d = d
    .replace(/[\n\r\t|,]/g, " ")
    .trim()
    .replace(/(\d)-/g, "$1 -")
    .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
  };

  // offsets for absolute conversion
  let offX, offY, lastX, lastY;

  for (let c = 0; c < commands.length; c++) {
    let com = commands[c];
    let type = com.substring(0, 1);
    let typeRel = type.toLowerCase();
    let typeAbs = type.toUpperCase();
    let isRel = type === typeRel;
    let chunkSize = comLengths[typeRel];

    // split values to array
    let values = com.substring(1, com.length).trim().split(" ").filter(Boolean);

    /**
     * fix A - Arc commands
     */
    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);
    let hasMultiple = values.length > chunkSize;
    let chunk = hasMultiple ? values.slice(0, chunkSize) : values;
    let comChunks = [{
      type: type,
      values: chunk
    }];

    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
        });
      }
    }

    /**
     * convert to absolute
     */
    if (c === 0) {
      offX = values[0];
      offY = values[1];
      lastX = offX;
      lastY = offY;
    }

    let typeFirst = comChunks[0].type;
    typeAbs = typeFirst.toUpperCase();

    isRel =
      typeFirst.toLowerCase() === typeFirst && pathData.length ? true : false;

    for (let i = 0; i < comChunks.length; i++) {
      let com = comChunks[i];
      let type = com.type;
      let values = com.values;
      let valuesL = values.length;
      let comPrev = comChunks[i - 1] ?
        comChunks[i - 1] :
        c > 0 && pathData[pathData.length - 1] ?
        pathData[pathData.length - 1] :
        comChunks[i];

      let valuesPrev = comPrev.values;
      let valuesPrevL = valuesPrev.length;
      isRel =
        comChunks.length > 1 ?
        type.toLowerCase() === type && pathData.length :
        isRel;

      if (isRel) {
        com.type = comChunks.length > 1 ? type.toUpperCase() : typeAbs;

        switch (typeRel) {
          case "a":
            com.values = [
              values[0],
              values[1],
              values[2],
              values[3],
              values[4],
              values[5] + offX,
              values[6] + offY
            ];
            break;

          case "h":
          case "v":
            com.values = type === "h" ? [values[0] + offX] : [values[0] + offY];
            break;

          case "m":
          case "l":
          case "t":
            com.values = [values[0] + offX, values[1] + offY];
            break;

          case "c":
            com.values = [
              values[0] + offX,
              values[1] + offY,
              values[2] + offX,
              values[3] + offY,
              values[4] + offX,
              values[5] + offY
            ];
            break;

          case "q":
          case "s":
            com.values = [
              values[0] + offX,
              values[1] + offY,
              values[2] + offX,
              values[3] + offY
            ];
            break;
        }
      }
      // is absolute
      else {
        offX = 0;
        offY = 0;
      }

      // convert shorthands
      let shorthandTypes = ["H", "V", "S", "T"];

      if (shorthandTypes.includes(typeAbs)) {
        let cp1X, cp1Y, cpN1X, cpN1Y, cp2X, cp2Y;
        if (com.type === "H" || com.type === "V") {
          com.values =
            com.type === "H" ? [com.values[0], lastY] : [lastX, com.values[0]];
          com.type = "L";
        } else if (com.type === "T" || com.type === "S") {
          [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
          [cp2X, cp2Y] =
          valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];

          // new control point
          cpN1X = com.type === "T" ? lastX * 2 - cp1X : lastX * 2 - cp2X;
          cpN1Y = com.type === "T" ? lastY * 2 - cp1Y : lastY * 2 - cp2Y;

          com.values = [cpN1X, cpN1Y, com.values].flat();
          com.type = com.type === "T" ? "Q" : "C";
        }
      }

      // add to pathData array
      pathData.push(com);

      // update offsets
      lastX =
        valuesL > 1 ?
        values[valuesL - 2] + offX :
        typeRel === "h" ?
        values[0] + offX :
        lastX;
      lastY =
        valuesL > 1 ?
        values[valuesL - 1] + offY :
        typeRel === "v" ?
        values[0] + offY :
        lastY;
      offX = lastX;
      offY = lastY;
    }
  }

  pathData[0].type = "M";
  return pathData;
}
svg {
  border: 1px solid #ccc;
  overflow: visible;
  padding: 1em
}

.grd {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1em;
}

path {
  stroke: #ccc;
  stroke-width: 1.5%;
}

polygon {
  marker-start: url(#markerStart);
  marker-mid: url(#markerRound);
  stroke-width: 0.5%;
}
<div class="grd">
  <div class="col">
    <h3>Path is already a polygon – skip more expensive calulations</h3>
    <svg id="svg" viewBox="10 10 94 80">
      <path id="path" fill="none" stroke="black" d="m104 33.4-6.9-16.6-16.6-6.8-16.6 6.8-6.9 16.6-6.9-16.6-16.6-6.8-16.6 6.8-6.9 16.6 9.4 23 17.5 18.3 20.1 15.3 20.1-15.3 17.5-18.3z" />
      <polygon id="poly" points="" fill="none" stroke="red" />
     </svg>
  </div>
  <div class="col">
    <h3>Path has curves – retain on-path final points</h3>
    <svg id="svg1" viewBox="0 0 100 85">
      <path id="path1" fill="none" stroke="black" d="m50 85.2 33.4-27.8c9.9-9.9 16.6-17.9 16.6-32.4 0-13.7-11.2-24.9-25-24.9s-25 11.2-25 24.9c0-13.7-11.2-24.9-25-24.9s-25 11.2-25 24.9c0 16.6 8.7 24.5 16.6 32.4z" />
      <polygon id="poly1" points="" fill="none" stroke="red" />
    </svg>
  </div>
</div>

<!-- markers -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; opacity:0">
  <defs>
 <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
      <circle cx="5" cy="5" r="5" fill="green" />
 </marker>
      <marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
        <circle cx="5" cy="5" r="2.5" fill="red" />
    </marker>
  </defs>
</svg>

How it works

  • we're calculating each path segment's length (by creating a temporary path element)
  • the desired path length division is adjusted to split the segment length evenly – so we retain the final on path points
  • if a command is of type l lineto we omit any splitting

3. Close approximation

Sometimes you may need a close approximation so a highly detailed polygon mimicking a path's curvature (e.g for CSS clip-paths using polygon()).

When working on my custom pathLength library I came up with a helper that can auto-detect a suitable number of vertices based on a maximum length difference between path and polygon:

See codepen path to polygon converter

The vertice calculation is also based on parsing the path data so we can retain the original geometry of a path and skip additional point calculations for line segments

let d = path.getAttribute("d");
let options = {
  decimals: 3,
  adaptive: true,
  retainPoly: true,
  // polygon length can deviate 0.1 length units
  tolerance: 0.1
};
let vertices = polygonFromPathData(d, options);

// show output
renderPolygon(poly, vertices);
polyPointsOut.value = "let points=" + JSON.stringify(vertices);

function renderPolygon(poly, pts) {
  let polyAtt = pts
    .map((pt) => {
      return `${pt.x} ${pt.y}`;
    })
    .join(" ");
  poly.setAttribute("points", polyAtt);
}
svg{
  overflow:visible
}
svg path {
  stroke-width: 2%;
  stroke: #ccc;
}

svg polygon {
  stroke-width: 0.75%;
  marker-start: url(#markerStart);
  marker-mid: url(#markerRound);
}

textarea {
  width: 100%;
  display: block;
  min-height: 10em;
}
<script src="https://cdn.jsdelivr.net/npm/[email protected]/getPointAtLengthLookup.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/getPointAtLengthLookup_getPolygon.js"></script>
<svg id="svg" viewBox="0 0 100 85">
  <defs>
    <marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
      <circle cx="5" cy="5" r="5" fill="green" />
      < <marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
        <circle cx="5" cy="5" r="2.5" fill="red" />
    </marker>
  </defs>
  <path id="path" fill="none" stroke="black" d=" m51 86.2 33.4-27.8c9.9-9.9 16.6-17.9 16.6-32.4 0-13.7-11.2-24.9-25-24.9s-25 11.2-25 24.9c0-13.7-11.2-24.9-25-24.9s-25 11.2-25 24.9c0 16.6 8.7 24.5 16.6 32.4z" />
  <polygon id="poly" points="" fill="none" stroke="red" />
  </svg>
  
<h3>Polygon vertices</h3>
<textarea id="polyPointsOut">
</textarea>
like image 29
herrstrietzel Avatar answered Apr 28 '26 02:04

herrstrietzel