function pathDataToRelative(pathData, decimals = -1) {
let M = pathData[0].values;
let x = M[0],
y = M[1],
mx = x,
my = y;
for (let i = 1; i < pathData.length; i++) {
let cmd = pathData[i];
let type = cmd.type;
let typeRel = type.toLowerCase();
let values = cmd.values;
if (type != typeRel) {
type = typeRel;
cmd.type = type;
switch (typeRel) {
case "a":
values[5] = +(values[5] - x);
values[6] = +(values[6] - y);
break;
case "v":
values[0] = +(values[0] - y);
break;
case "m":
mx = values[0];
my = values[1];
default:
if (values.length) {
for (let v = 0; v < values.length; v++) {
values[v] = values[v] - (v % 2 ? y : x);
}
}
}
}
else {
if (cmd.type == "m") {
mx = values[0] + x;
my = values[1] + y;
}
}
let vLen = values.length;
switch (type) {
case "z":
x = mx;
y = my;
break;
case "h":
x += values[vLen - 1];
break;
case "v":
y += values[vLen - 1];
break;
default:
x += values[vLen - 2];
y += values[vLen - 1];
}
if (decimals >= 0) {
cmd.values = values.map((val) => {
return +val.toFixed(decimals);
});
}
}
if (decimals >= 0) {
[M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];
}
return pathData;
}
function pathDataToAbsolute(pathData, decimals = -1) {
let M = pathData[0].values;
let x = M[0],
y = M[1],
mx = x,
my = y;
for (let i = 1; i < pathData.length; i++) {
let cmd = pathData[i];
let type = cmd.type;
let typeAbs = type.toUpperCase();
let values = cmd.values;
if (type != typeAbs) {
type = typeAbs;
cmd.type = type;
switch (typeAbs) {
case "A":
values[5] = +(values[5] + x);
values[6] = +(values[6] + y);
break;
case "V":
values[0] = +(values[0] + y);
break;
case "H":
values[0] = +(values[0] + x);
break;
case "M":
mx = +values[0] + x;
my = +values[1] + y;
default:
if (values.length) {
for (let v = 0; v < values.length; v++) {
values[v] = values[v] + (v % 2 ? y : x);
}
}
}
}
let vLen = values.length;
switch (type) {
case "Z":
x = +mx;
y = +my;
break;
case "H":
x = values[0];
break;
case "V":
y = values[0];
break;
case "M":
mx = values[vLen - 2];
my = values[vLen - 1];
default:
x = values[vLen - 2];
y = values[vLen - 1];
}
if (decimals >= 0) {
cmd.values = values.map((val) => {
return +val.toFixed(decimals);
});
}
}
if (decimals >= 0) {
[M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];
}
return pathData;
}
function roundPathData(pathData, decimals = -1) {
if (decimals >= 0) {
pathData.forEach(function (com, c) {
let values = com["values"];
values.forEach(function (val, v) {
pathData[c]["values"][v] = +val.toFixed(decimals);
});
});
}
return pathData;
}
function reversePathData(pathData) {
let M = pathData[0];
let newPathData = [M];
let subPathDataArr = splitSubpaths(pathData);
subPathDataArr.forEach((subPathData, s) => {
let subPathDataL = subPathData.length;
let closed = subPathData[subPathDataL - 1]["type"] == "Z" ? true : false;
let stripZ = false;
if (!closed) {
subPathData.push({
type: "Z",
values: []
});
subPathDataL++;
closed = true;
stripZ = true;
}
let subM = subPathData[0]["values"];
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) {
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
});
}
});
if (closed) {
newPathData.push({
type: "Z",
values: []
});
}
if (diff > 1 && stripZ) {
let firstL = newPathData[1]["values"];
newPathData[1] = {
type: "M",
values: [firstL[0], firstL[1]]
};
newPathData.shift();
newPathData.pop();
}
});
return newPathData;
}
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);
}
});
subPathMindex.forEach(function (index, i) {
let n = subPathMindex[i + 1];
let thisSeg = pathData.slice(index, n);
subPathArr.push(thisSeg);
});
return subPathArr;
}
body {
font: 100%/1.5 Helvetica Neue, sans-serif;
margin: 1em;
}
svg {
border: 1px solid #ccc;
max-height: 10em;
}
pre {
display: inline-block;
background: #eee;
margin: 0;
}
section {
flex: 1;
display: flex;
flex-flow: column;
}
textarea {
display: block;
font: inherit;
font-family: Consolas, monospace;
width: 100%;
height: 8em;
margin: 0.1em 0;
resize: vertical;
}
footer {
color: gray;
}
footer a {
color: inherit;
}
@media (min-width: 800px) {
.flex {
display: flex;
width: 100%;
gap: 1em;
}
svg {
max-width: 40vw;
}
}
<form>
<label>Round coordinates (-1 = no rounding)</label>
<input class="input" type="number" id="precision" min="-1" value="3">
<div class="flex">
<section>
<label>
Your path: <textarea class="input" id="origPathT">M46.8 34.9 L49.5 43.2 Q46.5 44.2 42.9 44.5 Q39.3 44.8 34.1 44.8 L34.1 44.8 Q43.4 49 43.4 58.1 L43.4 58.1 Q43.4 66 38 71 Q32.6 76 23.3 76 L23.3 76 Q19.7 76 16.6 75 L16.6 75 Q15.4 75.8 14.7 77.15 Q14 78.5 14 79.9 L14 79.9 Q14 84.2 20.9 84.2 L20.9 84.2 L29.3 84.2 Q34.6 84.2 38.7 86.1 Q42.8 88 45.05 91.3 Q47.3 94.6 47.3 98.8 L47.3 98.8 Q47.3 106.5 41 110.65 Q34.7 114.8 22.6 114.8 L22.6 114.8 Q14.1 114.8 9.15 113.05 Q4.2 111.3 2.1 107.8 Q0 104.3 0 98.8 L0 98.8 L8.3 98.8 Q8.3 102 9.5 103.85 Q10.7 105.7 13.8 106.65 Q16.9 107.6 22.6 107.6 L22.6 107.6 Q30.9 107.6 34.45 105.55 Q38 103.5 38 99.4 L38 99.4 Q38 95.7 35.2 93.8 Q32.4 91.9 27.4 91.9 L27.4 91.9 L19.1 91.9 Q12.4 91.9 8.95 89.05 Q5.5 86.2 5.5 81.9 L5.5 81.9 Q5.5 79.3 7 76.9 Q8.5 74.5 11.3 72.6 L11.3 72.6 Q6.7 70.2 4.55 66.65 Q2.4 63.1 2.4 58 L2.4 58 Q2.4 52.7 5.05 48.5 Q7.7 44.3 12.35 41.95 Q17 39.6 22.7 39.6 L22.7 39.6 Q28.9 39.7 33.1 39.15 Q37.3 38.6 40.05 37.65 Q42.8 36.7 46.8 34.9 L46.8 34.9 ZM22.7 46.2 Q17.5 46.2 14.65 49.45 Q11.8 52.7 11.8 58 L11.8 58 Q11.8 63.4 14.7 66.65 Q17.6 69.9 22.9 69.9 L22.9 69.9 Q28.3 69.9 31.15 66.75 Q34 63.6 34 57.9 L34 57.9 Q34 46.2 22.7 46.2 L22.7 46.2 Z </textarea></label>
<svg width="100%" height="100%">
<path id="origPath" fill="indianred" d="" />
</svg>
<p id="fileSizeOrig" class="fileSize"></p>
</section>
<section>
<label>
All-relative path: <textarea id="relPathT" readonly></textarea></label>
<svg width="100%" height="100%">
<path id="relativePath" fill="yellowgreen" d="" />
</svg>
<p id="fileSizeRel" class="fileSize"></p>
</section>
<section>
<label>
All-absolute path: <textarea id="absPathT" readonly></textarea></label>
<svg width="100%" height="100%">
<path id="absolutePath" fill="hsl(180,50%,50%)" d="" />
</svg>
<p id="fileSizeAbs" class="fileSize"></p>
</section>
</div>
</form>
<footer>
<p>Convert path commands to relative and absolute coordinates – using <a href="https://github.com/jarek-foksa/path-data-polyfill">path data polyfill by Jarek Foksa.</a></p>
<p>Forked from original codepen: by <a href="https://codepen.io/leaverou/pen/RmwzKv">Lea Verou</a>
</p> Described in this article: <a href="https://lea.verou.me/2019/05/utility-convert-svg-path-to-all-relative-or-all-absolute-commands/">Utility: Convert SVG path to all-relative or all-absolute commands </a></p>
</footer>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.4/path-data-polyfill.min.js"></script>
<script>
window.addEventListener('DOMContentLoaded', evt => {
let decimals = parseFloat(precision.value);
let inputs = document.querySelectorAll('.input');
let svgs = document.querySelectorAll('svg');
upDateSVG()
inputs.forEach(input => {
input.addEventListener('input', evt => {
upDateSVG()
})
})
function upDateSVG() {
decimals = parseFloat(precision.value);
let d = origPathT.value;
origPath.setAttribute('d', d);
let pathData = origPath.getPathData();
let sizeOrig = filesize(d);
fileSizeOrig.textContent = sizeOrig + ' KB';
let pathDataRel = pathDataToRelative(pathData, decimals);
relativePath.setPathData(pathDataRel);
relPathT.value = relativePath.getAttribute('d');
let sizeRel = filesize(relPathT.value);
fileSizeRel.textContent = sizeRel + ' KB';
let pathDataAbs = pathDataToAbsolute(pathData, decimals);
absolutePath.setPathData(pathDataAbs);
absPathT.value = absolutePath.getAttribute('d');
let sizeAbs = filesize(absPathT.value);
fileSizeAbs.textContent = sizeAbs + ' KB';
svgs.forEach(svg => {
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(" "));
}
function filesize(str) {
let size = new Blob([str]).size / Math.pow(1024, 1);
return +size.toFixed(3);
}
</script>