svg-parse-path-normalized
Version:
Parse svg path data from string and normalize it to calculable value arrays
541 lines (433 loc) • 22.2 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv='cache-control' content='no-cache'>
<meta http-equiv='expires' content='0'>
<meta http-equiv='pragma' content='no-cache'>
<title>Convert svg pathdata</title>
<link rel="stylesheet" href="style.css">
<style>
</style>
</head>
<body>
<div class="grd">
<fieldset>
<legend>Settings</legend>
<header>
<h1>Convert svg pathdata</h1>
</header>
<!-- samples -->
<p>
<select id="inputExample" class="">
</select>
</p>
<fieldset>
<legend>Absolute/relative</legend>
<label>
<input class="inputs" name="relativeAbsolute" type="radio" value="absolute"> All Absolute
</label>
<label><input class="inputs" name="relativeAbsolute" type="radio" value="relative" checked> All
Relative</label>
<label><input class="inputs" name="relativeAbsolute" type="radio" value="unchanged">
Unchanged</label>
</fieldset>
<fieldset>
<legend>Cubic/Quadratic conversions</legend>
<label><input class="inputs" name="quadraticCubic" type="radio" value="cubic"> Quadratic
to cubic</label>
<label><input class="inputs" name="quadraticCubic" type="radio" value="quadratic"> Cubic to
quadratic</label>
<label><input class="inputs" name="quadraticCubic" type="radio" value="unchanged" checked>
unchanged</label>
<label><input class="inputs" name="arcToCubic" type="checkbox"> Arcs to cubic</label>
<label><input class="inputs" name="linesTobezier" type="checkbox"> Lines to Bézier (handy for
morphing animations)</label>
<label><input class="inputs" name="convertShapes" type="checkbox" checked> Shapes to paths</label>
</fieldset>
<fieldset>
<legend>Shorthand/longhand conversions</legend>
<label>
<input class="inputs" name="toShorthands" type="radio" value="shorthands" checked> Apply
shorthands
</label>
<label><input class="inputs" name="toShorthands" type="radio" value="longhands"> All
longhands</label>
<label><input class="inputs" name="toShorthands" type="radio" value="unchanged">
Unchanged</label>
</fieldset>
<fieldset>
<legend>Rounding/Accuracy</legend>
<label><input class="inputs" name="decimals" type="number" value="2" min="-1"> Precision</label>
<label><input class="inputs" name="scale" type="number" value="1" min="0.01" step="0.1">
Scale</label>
<label><input class="inputs" name="arcAccuracy" type="number" value="1" min="1" max="10"> Arc
accuracy (add more segments)</label>
<label><input class="inputs" id="cubicToQuadraticPrecision" type="number" value="0.1" min="0.001"
step="0.1"> Cubic to quadratic accuracy</label>
</fieldset>
<fieldset>
<legend>Output optimizations</legend>
<label><input class="inputs" name="minify" value="minify" type="radio" checked> Minify</label>
<label><input class="inputs" name="minify" value="prettify" type="radio"> Prettify</label>
<label><input class="inputs" name="minify" value="unchanged" type="radio">
unchanged</label>
<label><input class="inputs" name="crop" type="checkbox"> Crop and center</label>
<label><input class="inputs" name="showMarkers" type="checkbox"> Show markers</label>
</fieldset>
<footer>
<p class="p-link">
<a href="https://herrstrietzel.github.io/getPathData2/">
<svg class="icn-svg" viewBox="0 0 96 100">
<use href="icons.svg#icn-github-solid" />
</svg> Back to main page
</a>
</p>
</footer>
</fieldset>
<fieldset>
<legend>Input</legend>
<p>Paste a svg or a path d string</p>
<textarea class="inputs" id="svgInput" name="svgInputVal" spellcheck="false">
</textarea>
</fieldset>
<fieldset>
<legend>Output</legend>
<p>
<span id="spanFilesize"></span>
</p>
<textarea id="output" spellcheck="false"></textarea>
<p>
<a class="btn-default btn-ddownload brd-rad" id="btnDownload" download="scaled.svg" href="">
<svg class="icn-svg icn-stroke" viewBox="0 0 100 100">
<use href="icons.svg#icn-download" />
</svg>
Download</a>
</p>
</fieldset>
<fieldset>
<legend>Preview</legend>
<div id="preview" class="preview bck-wht"></div>
</fieldset>
</div>
<!-- markers to show commands -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;">
<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>
<script src="../js/pathDataParseNormalized.js"></script>
<script src="../js/pathDataConvert.js"></script>
<script src="../js/pathDataConvert_prototypes.js"></script>
<script>
window.addEventListener('DOMContentLoaded', (e) => {
// load examples
for (key in samples) {
let option = document.createElement('option')
option.value = key
option.innerHTML = key
inputExample.append(option)
}
let first = inputExample.options[0];
first.selected = true
svgInput.value = samples[first.value]
let iV = getInputvars()
inputExample.addEventListener("input", (e) => {
// get svg markup – either single path or svg
let example = samples[inputExample.value]
svgInput.value = example
svgInput.dispatchEvent(new Event('input'))
})
let inputs = document.querySelectorAll(".inputs");
inputs.forEach((inp) => {
inp.addEventListener("input", async (e) => {
let svg;
// conversion variables
iV = getInputvars()
//extract(inputVars)
if (iV.showMarkers) {
preview.classList.add('showMarkers')
} else {
preview.classList.remove('showMarkers')
}
//let decimals = +inputPrecision.value;
let cropAndCenter = iV.crop;
let markup = iV.svgInputVal;
let minify = iV.minify
let decimals = iV.decimals
let singlePath = false;
// if is svg or just element or d
if (!markup.includes("<svg")) {
let ns = 'http://www.w3.org/2000/svg'
svg = document.createElementNS(ns, "svg");
if (!/<[a-z][\s\S]*>/i.test(markup)) {
singlePath = true;
path = document.createElementNS(ns, "path");
path.setAttribute("d", markup);
svg.append(path);
}
}
// parse complete svg
else {
svg = new DOMParser()
.parseFromString(markup, "text/html")
.querySelector("svg");
}
// reset preview
preview.innerHTML = "";
if (svg) {
preview.appendChild(svg);
// set auto viewBox
if (singlePath) {
let { x, y, width, height } = svg.getBBox()
svg.setAttribute('viewBox', [x, y, width, height].join(' '))
}
}
/**
* process paths
*/
// convert shapes
let shapes = svg.querySelectorAll('polygon, polyline, line, circle, ellipse, rect')
if (iV.convertShapes && shapes.length) {
shapes.forEach(shape => {
shape.convertShapeToPath()
})
}
let paths = svg.querySelectorAll('path')
paths.forEach(path => {
// parse path
//let pathData = path.getPathData2()
let d = path.getAttribute('d')
//let pathData = parsePathDataNormalized(d)
let pathData = parseD(d)
//scale
let scale = iV.scale
if (scale !== 1) {
pathData = scalePathData(pathData, scale, scale)
}
// crop and center
if (singlePath && cropAndCenter) {
let { x, y, width, height } = svg.viewBox.baseVal
let bb = svg.getBBox();
width = +width.toFixed(decimals) * scale
height = +height.toFixed(decimals) * scale
// adjust viewBox
svg.setAttribute('viewBox', [0, 0, width, height].join(' '))
//convert to Relative to shift starting point
pathData = pathDataToRelative(pathData, decimals);
let xN = pathData[0].values[0] - bb.x * scale;
let yN = pathData[0].values[1] - bb.y * scale;
let offsetX = (width - bb.width * scale) / 2;
let offsetY = (height - bb.height * scale) / 2;
pathData[0].values = [xN + offsetX, yN + offsetY]
}
// set optimized path
let options = {
toRelative: iV.relativeAbsolute === 'relative' ? true : false,
toAbsolute: iV.relativeAbsolute === 'absolute' ? true : false,
toShorthands: iV.toShorthands === 'shorthands' ? true : false,
toLonghands: iV.toShorthands === 'longhands' ? true : false,
quadraticToCubic: iV.quadraticCubic === 'cubic' ? true : false,
cubicToQuadratic: iV.quadraticCubic === 'quadratic' ? true : false,
cubicToQuadraticPrecision: iV.cubicToQuadraticPrecision,
lineToCubic: iV.linesTobezier,
arcToCubic: iV.arcToCubic,
arcAccuracy: iV.arcAccuracy,
decimals: iV.decimals,
minify: iV.minify === 'minify' ? true : false,
}
/**
* convert path data
*/
pathData = convertPathData(pathData, options)
/**
* stringify pathdata to attribute string
*/
let minifyPathData = minify === 'minify' ? true : false;
//let dNew = pathDataToD(pathData, iV.decimals, minifyPathData)
let dNew = pathData.toD(iV.decimals, minifyPathData)
// prettify
if (minify === 'prettify') {
dNew = pathData.toD(decimals, false)
.replace(/([mlcsqtahvz])([\s\s]*)/gi, "\n$1 $2").
replace(/(\d+)(\-)/g, "$1 $2").
replace(/(\.)(?=(\d+\.\d+)+)(\d+)/g, "$1$3 ").
replace(/\s{2,}/g, ' ').
replace(/([mlcsqtahvz])([\s\s]*)([mlcsqtahvz])/gi, "$1\n$3").
trim();
}
path.setAttribute('d', dNew)
})
/**
* return output
*/
let markupNew
if (singlePath) {
markupNew = svg.querySelector('path').getAttribute('d');
} else {
markupNew = new XMLSerializer().serializeToString(svg);
}
output.value = markupNew.replaceAll(' ', '\n');
// original size
let fileSizeOriginal = +(new Blob([svgInput.value]).size / 1024).toFixed(3)
let blob = new Blob([markupNew]);
btnDownload.href = URL.createObjectURL(blob);
let fileSizeNew = +(blob.size / 1024).toFixed(3);
spanFilesize.textContent = fileSizeNew + " KB" + ' – ' + ' saved: ' + (fileSizeOriginal - fileSizeNew).toFixed(3) + '/' + fileSizeOriginal;
})
})
svgInput.dispatchEvent(new Event('input'))
});
let samples = {
'path_from_hell3':`m 50,0 .00001.0001.001 10e-10 Q 36.4 0 24.8 6.8t-18 18 t-6.8 25.2 C 0 63.8 5.6 76.3 14.65 85.35 s 21.55 14.65 35.35 14.65 a 50 25-45 0115.8-34.3 50 25 -45 0134.2 -25.7h -20-.5 v -12.5-12.5 H 75 50 V 0 z M 40 60 h-20 V 20h 18 .5.5.5.5z m-5-35 c0 10 0 15 0 30 q-5 0 -10 0v-30zh-10 v20 h 5`,
'compound path from hell': `
<svg viewBox="0 0 100 100" overflow="visible"><path id="path" fill-rule="evenodd" d="
M50 0a50 50 0 110 100 50 50 0 110-100m0 30a20 20 0 110 40 20 20 0 110-40m0-10q12.42 0 21.21 8.79t8.79 21.21-8.79 21.21-21.21 8.79-21.21-8.79-8.79-21.21 8.79-21.21 21.21-8.79m-40-20a5 5 0 110 10 5 5 0 110-10m80 0a5 5 0 110 10 5 5 0 110-10m0 2.5a2.5 2.5 0 110 5 2.5 2.5 0 110-5m-45 42.5h10v10h-10zm-5-5h20v20h-20zm10-30c22.07 0 40 17.93 40 40s-17.93 40-40 40-40-17.93-40-40 17.93-40 40-40"/></svg>"
`,
'cross': `M11 2
H5
V5
L2 5
V11
H5
V14
H11
V11
H14
V5
H11
V2
Z`,
'path_face': `M328.4 393.5C318.7 402.6 303.5 402.1 294.5 392.4C287.1 384.5 274.4 376 256 376C237.6 376 224.9 384.5 217.5 392.4C208.5 402.1 193.3 402.6 183.6 393.5C173.9 384.5 173.4 369.3 182.5 359.6C196.7 344.3 221.4 328 256 328C290.6 328 315.3 344.3 329.5 359.6C338.6 369.3 338.1 384.5 328.4 393.5zM144.4 240C144.4 231.2 147.9 223.2 153.7 217.4L122.9 207.2C114.6 204.4 110 195.3 112.8 186.9C115.6 178.6 124.7 174 133.1 176.8L229.1 208.8C237.4 211.6 241.1 220.7 239.2 229.1C236.4 237.4 227.3 241.1 218.9 239.2L208.1 235.6C208.3 237 208.4 238.5 208.4 240C208.4 257.7 194 272 176.4 272C158.7 272 144.4 257.7 144.4 240V240zM368.4 240C368.4 257.7 354 272 336.4 272C318.7 272 304.4 257.7 304.4 240C304.4 238.4 304.5 236.8 304.7 235.3L293.1 239.2C284.7 241.1 275.6 237.4 272.8 229.1C270 220.7 274.6 211.6 282.9 208.8L378.9 176.8C387.3 174 396.4 178.6 399.2 186.9C401.1 195.3 397.4 204.4 389.1 207.2L358.9 217.2C364.7 223 368.4 231.1 368.4 240H368.4zM0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM256 464C370.9 464 464 370.9 464 256C464 141.1 370.9 48 256 48C141.1 48 48 141.1 48 256C48 370.9 141.1 464 256 464z`,
'svg_pathfrom_hell1': `<svg viewBox="0 0 100 100" overflow="visible">
<path id="path" d="
M3,7
L13,7
m-10,10
l10,0
V27
H23
v10
h10
C 33,43 38,47 43,47
c 0,5 5,10 10,10
S 63,67 63,67
s -10,10 10,10
Q 50,50 73,57
q 20,-5 0,-10
T 70,40
t 0,-15
A 5, 10 45 1040,20
a5,5 20 01 -10,-10
Z " />
</svg>`,
'svg_pathfrom_hell2': `<svg viewBox="0 0 100 100" overflow="visible">
<path id="path" d="
m 50 0 .00001.0001.001 10e-10
Q 36.4 0 24.8 6.8
t -18 18
t -6.8 25.2
C 0 63.8 5.6 76.3 14.65 85.35
s 21.55 14.65 35.35 14.65
a 50 25 -45 01 15.8 -34.3
50 25 -45 0134.2 -25.7
h -20-.5
v -12.5-12.5
H 75 50
V 0
z
M 40 60
h-20
V 20
h 18 .5.5.5.5
z" />
</svg>`,
'svg_shapes': ` <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 30">
<g class="gPrimitives">
<polygon id="polygon" fill="#CCCCCC" stroke="#E3000F"
points="7.9,22.8 3,14.3 7.9,5.8 17.6,5.8 22.5,14.3 17.6,22.8 " />
<polyline id="polyline" fill="none" stroke="#E3000F"
points="40.9,22.8 31.1,22.8 26.2,14.3 31.1,5.8 40.9,5.8 45.8,14.3 " />
<rect id="rect" x="37.5%" y="20%" rx="5%" ry="6%" fill="none" stroke="#E3000F" width="6%" height="56%" />
<line id="line" fill="none" stroke="#E3000F" x1="50.5" y1="22.8" x2="52.5" y2="5.8" />
<circle id="circle" fill="none" stroke="#E3000F" cx="52%" cy="49%" r="8%" />
<ellipse id="ellipse" fill="none" stroke="#E3000F" cx="68%" cy="49%" rx="7%" ry="25%" />
<path id="piechart" transform="scale(0.9) translate(130, 6)" d="M 10 0 A 10 10 0 1 1 1.34 15 L 10 10 z"
fill="red" class="segment segment-1 segment-class" id="segment-01" />
</g>
</svg>`,
'path_random': `M 529 664 C 93 290 616 93 1942 385 C 1014 330 147 720 2059 70 C 1307 400 278 713 1686 691`
}
/**
* scale pathData
*/
function scalePathData(pathData, scaleX, scaleY) {
//console.log('scalePathData', pathData);
pathData.forEach((com, i) => {
let { type, values } = com;
let typeRel = type.toLowerCase();
switch (typeRel) {
case "a":
com.values = [
values[0] * scaleX,
values[1] * scaleY,
values[2],
values[3],
values[4],
values[5] * scaleX,
values[6] * scaleY
];
break;
case "h":
com.values = [values[0] * scaleX];
break;
case "v":
com.values = [values[0] * scaleY];
break;
default:
if (values.length) {
for (let i = 0; i < values.length; i += 2) {
com.values[i] *= scaleX;
com.values[i + 1] *= scaleY;
}
}
}
});
return pathData;
}
function getInputvars(selector = ".inputs") {
let inputFields = document.querySelectorAll(selector);
let inputVals = {}
inputFields.forEach((inp, i) => {
let type = inp.type ? inp.type : inp.nodeName;
let varName = inp.name ? inp.name : (inp.id ? inp.id : `var_${i}`);
//let val = type==='checkbox' ? (inp.checked ? true : false) :
let val
switch (type) {
case 'checkbox':
val = inp.checked ? true : false;
break;
case 'radio':
val = document.querySelector(`input[name="${varName}"]:checked`).value;
break;
case 'number':
case 'range':
val = +inp.value;
break;
default:
val = inp.value;
}
inputVals[varName] = val
})
return inputVals;
}
</script>
</body>
</html>