admin管理员组

文章数量:1287647

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=".7.1/jquery.min.js"></script>
<svg version="1.1" id="Ebene_1" xmlns="" xmlns: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>

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./ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3/2000/svg" xmlns:xlink="http://www.w3/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>

Share Improve this question edited Mar 19, 2024 at 23:21 herrstrietzel 17.3k2 gold badges27 silver badges53 bronze badges asked Apr 12, 2013 at 13:40 aushilfe444aushilfe444 1451 silver badge10 bronze badges 4
  • I would bet that you need to determine when one of the x or y values changes since the last iteration, meaning that the direction has changed. Only then should you grab that point. – Ian Commented Apr 12, 2013 at 13:44
  • that reduces the number of points but it's still around 1000 points.. i need 6. – aushilfe444 Commented Apr 12, 2013 at 14:02
  • Would you be able to provide a jsFiddle with the SVG and this code? – Ian Commented Apr 12, 2013 at 14:03
  • sure.. here u go.. jsfiddle/AudEh – aushilfe444 Commented Apr 12, 2013 at 14:36
Add a ment  | 

4 Answers 4

Reset to default 5

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

        }

    }

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 mands to absolute values
  • convert shorthand mands 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 mands?
  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 mands. If not (no c,s,q,t,a mands): 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 mand 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 mands?
  let isPolygon = /[csqta]/gi.test(d) ? false : true;
  return isPolygon;
}

function getPathDataVertices(pathData) {
  let polyPoints = [];
  pathData.forEach(() => {
    let values = .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 pliant
 * with the w3C SVGPathData interface draft
 * https://svgwg/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 mands = d.match(cmdRegEx);

  // valid mand value lengths
  let Lengths = {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 < mands.length; c++) {
    let  = mands[c];
    let type = .substring(0, 1);
    let typeRel = type.toLowerCase();
    let typeAbs = type.toUpperCase();
    let isRel = type === typeRel;
    let chunkSize = Lengths[typeRel];

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

    /**
     * A - Arc mands
     * large arc and sweep flags
     * are boolean and can be concatenated like
     */
    if (typeRel === "a" && values.length != Lengths.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 N = [largeArc, sweep, finalX].filter(Boolean);
          arcValues.push(N);
          n += N.length;
        } else {
          arcValues.push(value);
          n++;
        }
      }
      values = arcValues.flat().filter(Boolean);
    }

    // string  to number
    values = values.map(Number);

    // if string contains repeated shorthand mands - split them
    let hasMultiple = values.length > chunkSize;
    let chunk = hasMultiple ? values.slice(0, chunkSize) : values;
    let Chunks = [{
      type: type,
      values: chunk
    }];

    // has implicit or repeated mands – 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);
        Chunks.push({
          type: typeImplicit,
          values: chunk
        });
      }
    }

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

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

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

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

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

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

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

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

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

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

          case "q":
          case "s":
            .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 (.type === "H" || .type === "V") {
          .values =
            .type === "H" ? [.values[0], lastY] : [lastX, .values[0]];
          .type = "L";
        } else if (.type === "T" || .type === "S") {
          [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
          [cp2X, cp2Y] =
          valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];

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

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

      pathData.push();

      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 mands -->
<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 mand to create an array of point objects.
The parsing is done via a custom parsing script trying to be pliant 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 mands to all absolute and "unshort" mands 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 mand final points.


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/2000/svg'

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

  /**
   * check if path is already polygon:
   * just return the final mand 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  = pathData[i];
    let Prev = pathData[i - 1];
    let type = .type.toLowerCase();
    let [values, valuesPrev] = [.values, Prev.values];

    //previous mands 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} ${.type} ${.values.join(' ')}`)
      let segLength = pathSeg.getTotalLength()

      // fit to segment length – keep mand 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 mands?
  let isPolygon = /[csqta]/gi.test(d) ? false : true
  return isPolygon;
}

function getPathDataVertices(pathData) {
  let polyPoints = [];
  pathData.forEach( => {
    let values = .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 mands = d.match(cmdRegEx);

  // valid mand value lengths
  let Lengths = {
    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 < mands.length; c++) {
    let  = mands[c];
    let type = .substring(0, 1);
    let typeRel = type.toLowerCase();
    let typeAbs = type.toUpperCase();
    let isRel = type === typeRel;
    let chunkSize = Lengths[typeRel];

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

    /**
     * fix A - Arc mands
     */
    if (typeRel === "a" && values.length != Lengths.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 N = [largeArc, sweep, finalX].filter(Boolean);
          arcValues.push(N);
          n += N.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 Chunks = [{
      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);
        Chunks.push({
          type: typeImplicit,
          values: chunk
        });
      }
    }

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

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

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

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

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

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

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

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

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

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

          case "q":
          case "s":
            .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 (.type === "H" || .type === "V") {
          .values =
            .type === "H" ? [.values[0], lastY] : [lastX, .values[0]];
          .type = "L";
        } else if (.type === "T" || .type === "S") {
          [cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
          [cp2X, cp2Y] =
          valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];

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

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

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

      // 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 mand 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/npm/[email protected]/getPointAtLengthLookup.js"></script>
<script src="https://cdn.jsdelivr/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>

path.getPointAtLength() is good for rough purposes where you don't need both speed and quality. If you get every pixel, you get thousands of points, but still the quality is low, because SVG path can have decimal values, eg. 0.1, 0.2.

If you want more precision by calling eg. path.getPointAtLength(0.1) you get easily tens of thousands of points in plex paths and the process last seconds or tens of seconds. And after that you have to reduce the count of point (https://stackoverflow./a/15976155/1691517), which last again seconds. But still the quality can be low, if wrong points are removed.

Better techique is to first convert all path segments to cubic curves eg. using Raphael's path2curve() and then use some adaptive method (http://antigrain./research/adaptive_bezier/) to convert cubic segments to points and you get at the same time both the speed and quality. And after that there is no need to reduce points because the adaptive process itself has parameters to adjust the quality.

I have made a function that does all that and I'm going to publish it when it is enough optimized for speed. The quality and reliability seems to be 100% after testing with thousands of random paths and the speed is yet significantly faster than with path.getPointAtLength().

To iterate over the segments, use something like this:

var segList = path.normalizedPathSegList; // or .pathSegList

for(var i=1; i<segList.numberOfSegments; i++){
    var seg = segList.getItem(i);
}

If you want to reduce the number of vertices, then you can use Simplify.js as described here.

本文标签: Converting svg path to polygon in javascriptStack Overflow