const svg = document.querySelector('svg');
const path = svg.querySelector('path');
function fixPath(path) {
// get pathData array
let pathData = path.getPathData({
normalize: true
});
// split sub paths
let pathDataSubArr = splitSubpaths(pathData);
let fixedPathData = fixInnerPathDirections(path, pathDataSubArr);
path.setPathData(fixedPathData);
}
/**
* helpers
*/
function fixInnerPathDirections(path, pathDataSubArr) {
let svg = path.closest('svg');
let fixedPathData = [];
let subPathEls = [];
let bbO = path.getBBox();
let [xO, yO, wO, hO, rO, bO] = [bbO.x, bbO.y, bbO.width, bbO.height, (bbO.x + bbO.width), (bbO.y + bbO
.height)];
let outerPathData = pathDataSubArr[0];
let outerPathTmp = document.createElementNS('http://www.w3.org/2000/svg', 'path');
outerPathTmp.setPathData(outerPathData);
outerPathTmp.classList.add('outer');
let outerClockwise = isClockwise(outerPathTmp)
pathDataSubArr.forEach(function(pathDataSub, i) {
// create temporary subpath elements for checking positions
let subPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
subPath.setPathData(pathDataSub);
subPath.setAttribute('stroke', 'red');
subPath.setAttribute('stroke-width', '1');
subPath.setAttribute('fill', 'none');
svg.appendChild(subPath);
subPathEls.push(subPath);
let bb = subPath.getBBox();
let [x, y, w, h, r, b] = [bb.x, bb.y, bb.width, bb.height, (bb.x + bb.width), (bb.y + bb
.height)];
// remove temporary subpaths
subPath.remove();
if (i > 0) {
// is subpath within outer path
if (x > xO && y > yO && r < rO && b < bO) {
let isClockwiseInner = isClockwise(subPath);
// if subpath has same direction as outer path: reverse direction
if (isClockwiseInner == outerClockwise) {
pathDataSub = reversePathData(pathDataSub);
}
}
}
fixedPathData = fixedPathData.concat(pathDataSub);
})
return fixedPathData;
}
function reversePathData(pathData) {
let M = pathData[0];
let newPathData = [M];
// split subpaths
let subPathDataArr = splitSubpaths(pathData);
subPathDataArr.forEach(function(subPathData, s) {
let subPathDataL = subPathData.length;
let closed = subPathData[subPathDataL - 1]['type'] == 'Z' ? true : false;
let subM = subPathData[0]['values'];
// insert Lineto if last path segment has created by z
let lastCom = closed ? subPathData[subPathDataL - 2] : subPathData[subPathDataL - 1];
let lastComL = lastCom['values'].length;
let lastXY = [lastCom['values'][lastComL - 2], lastCom['values'][lastComL - 1]];
let diff = Math.abs(subM[0] - lastXY[0]);
if (diff > 1 && closed) {
subPathData.pop();
subPathData.push({
'type': 'L',
'values': [subM[0], subM[1]]
});
subPathData.push({
'type': 'Z',
'values': []
});
}
subPathData.forEach(function(com, i) {
// reverse index
let subpathDataL = subPathData.length;
let indexR = subpathDataL - 1 - i;
let comR = subPathData[indexR];
let comF = subPathData[i];
let [typeR, valuesR] = [comR['type'], comR['values']];
let [typeF, valuesF] = [comF['type'], comF['values']];
if (typeF == 'M' && s > 0) {
newPathData.push(comF);
} else if (typeR != 'M' && typeR != 'Z') {
indexR--;
let prevCom = i > 0 ? subPathData[indexR] : subPathData[subpathDataL - 1 - i];
let prevVals = prevCom ? (prevCom['values'] ? prevCom['values'] : [0, 0]) : [];
prevVals = prevCom['values'];
let prevValsL = prevVals.length;
let newCoords = [];
if (typeR == 'C') {
newCoords = [
valuesR[2], valuesR[3],
valuesR[0], valuesR[1],
prevVals[prevValsL - 2], prevVals[prevValsL - 1]
];
if (!closed) {
let nextVals = i < subpathDataL - 1 ? subPathData[i + 1]['values'] : lastXY;
let lastCX = (i < subpathDataL - 2) ? nextVals[prevValsL - 2] : subM[0];
let lastCY = (i < subpathDataL - 2) ? nextVals[prevValsL - 1] : subM[1];
newCoords[4] = lastCX;
newCoords[5] = lastCY;
}
} else {
newCoords = [prevVals[prevValsL - 2], prevVals[prevValsL - 1]];
}
newPathData.push({
'type': typeR,
'values': newCoords
});
}
})
//use last coordinates as M values if path isn't closed
if (!closed) {
newPathData[0]['values'] = lastXY;
}
if (closed) {
newPathData.push({
'type': 'Z',
'values': []
});
}
});
return newPathData;
}
/**
* check path direction of polygon
* based on answer:
* @mpen: https://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order/1180256#answer-46613578
*/
function isClockwise(path, divisions = 50) {
var length = path.getTotalLength();
let p1 = path.getPointAtLength(0);
let A = [p1.x, p1.y];
let p2 = path.getPointAtLength(length * 0.25);
let B = [p2.x, p2.y];
let p3 = path.getPointAtLength(length * 0.75);
let C = [p3.x, p3.y];
let poly = [A, B, C];
let end = poly.length - 1;
let sum = poly[end][0] * poly[0][1] - poly[0][0] * poly[end][1];
for (let i = 0; i < end; ++i) {
const n = i + 1;
sum += poly[i][0] * poly[n][1] - poly[n][0] * poly[i][1];
}
let cw = sum > 0 ? true : false;
return cw;
}
function splitSubpaths(pathData) {
let pathDataL = pathData.length;
let subPathArr = [];
let subPathMindex = [];
pathData.forEach(function(com, i) {
let [type, values] = [com['type'], com['values']];
if (type == 'M') {
subPathMindex.push(i);
}
});
//split segments after M command
subPathMindex.forEach(function(index, i) {
let n = subPathMindex[i + 1];
let thisSeg = pathData.slice(index, n);
subPathArr.push(thisSeg)
})
return subPathArr;
}
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.3/path-data-polyfill.min.js"></script>
<p><button type="button" onclick="fixPath(path)">Fix path directions</button></p>
<svg width="325" height="350" viewBox="0 0 325 350" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="
M205.43 59.38C205.43 26.5853 232.015 0 264.81 0C297.605 0 324.19 26.5853 324.19 59.38C324.19 92.1747 297.605 118.76 264.81 118.76C246.525 118.76 230.17 110.495 219.277 97.4975L116.536 157.274C117.985 162.41 118.76 167.83 118.76 173.43C118.76 178.348 118.162 183.127 117.035 187.697L222.002 248.758C232.806 237.522 247.991 230.53 264.81 230.53C297.605 230.53 324.19 257.115 324.19 289.91C324.19 322.705 297.605 349.29 264.81 349.29C232.015 349.29 205.43 322.705 205.43 289.91C205.43 280.791 207.486 272.152 211.159 264.431L109.497 205.292C98.9566 221.836 80.4498 232.81 59.38 232.81C26.5853 232.81 0 206.225 0 173.43C0 140.635 26.5853 114.05 59.38 114.05C79.7735 114.05 97.7659 124.331 108.457 139.992L209.556 81.1716C206.893 74.4248 205.43 67.0733 205.43 59.38Z
M264.81 19C242.509 19 224.43 37.0787 224.43 59.38C224.43 81.6813 242.509 99.76 264.81 99.76C287.111 99.76 305.19 81.6813 305.19 59.38C305.19 37.0787 287.111 19 264.81 19Z
M59.38 133.05C37.0787 133.05 19 151.129 19 173.43C19 195.731 37.0787 213.81 59.38 213.81C81.6813 213.81 99.76 195.731 99.76 173.43C99.76 151.129 81.6813 133.05 59.38 133.05Z
M224.43 289.91C224.43 267.609 242.509 249.53 264.81 249.53C287.111 249.53 305.19 267.609 305.19 289.91C305.19 312.211 287.111 330.29 264.81 330.29C242.509 330.29 224.43 312.211 224.43 289.91Z
" fill="#0D0F13" />
</svg>