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
4 Answers
Reset to default 5ok 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 longhandl
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 h
and 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
版权声明:本文标题:Converting svg path to polygon in javascript - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1741317068a2371977.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论