Compare two closed SVG paths in javascript - javascript

I have two all black SVG paths, consisting of [M]ove, [C]urve, [Q]uad, and [Z]close commands.
I'd like to figure out how similar they are, or what percentage of one curve overlaps with the other.
I know there's lots of tools for svg that can 'subtract' paths - if I can find the area of the SVG in a given plane, I can compare it to the area of the subtracted SVG, and compare the two. This would be sufficient for my purposes.
However, I can't seem to figure out how to compare SVG paths or subtract the paths in JavaScript.
Is there an easy way to do this, or is my best bet just to rasterize the paths and compare the pixel difference?

There are different qualities of similarity
100% congruent e.g. if pathData d attribute is identical
visually equal: path shapes appear visually identical, but have different layout properties e.g. x/y offsets or sizes
visually quite similar: caused by sub-ideal optimizing e.g. coordinate rounding – so you would need to define a certain threshold
Fortunately we can make some assumptions. However, we always need some kind of threshold value to compensate rounding errors etc.
Basic requirements for similar paths
must have the same aspect ratio (rounded)
as well as the same pathLength (as calculated via getTotalLength()
same number of sub paths – checked via simple regex like path.getAttribute('d').match(/m/gi).length
We can use these simple requirements to define exclusion criteria to avoid more expensive processing/calculation steps.
let compare1 = compareElements(g0, g1);
console.log(compare1);
let compare2 = compareElements(g0, g2);
console.log(compare2);
let compare3 = compareElements(g0, g3);
console.log(compare3);
let compare4 = compareElements(g0, g4);
console.log(compare4);
function compareElements(el1, el2, tolerance = 1.5, checkPoints = 24) {
let svg = el2.closest('svg');
let similarity = 0;
let style = window.getComputedStyle(el1);
let strokeWidth = style.strokeWidth;
let stroke = style.stroke;
let d1 = el1.getAttribute('d');
let d2 = el2.getAttribute('d');
/**
* optional:
* compare the total number of commands
let commandCount1 = d1.match(/[macsqtlhvz]/gi).length;
let commandCount2 = d2.match(/[macsqtlhvz]/gi).length;
*/
let subPathCount1 = d1.match(/m/gi).length;
let subPathCount2 = d2.match(/m/gi).length;
let same = {
size: false,
sizeRel: false,
identical: false,
aspect: false,
pathLength: false,
pathLengthRel: false,
pointOnPath: false,
pointOnPathRel: false,
subPathCount: false,
pos: false,
score: 0
}
/**
* 0. Compare d attribute - worth a try ;)
* if identical we can stop here
* */
if (d1 === d2) {
same.size = true,
same.sizeRel = true,
same.identical = true,
same.aspect = true,
same.pathLength = true,
same.pathLengthRel = true,
same.pointOnPath = true,
same.pointOnPathRel = true,
subPathCount = true,
pos = true,
same.score = 10;
colorSimilarity(el2, same);
return same;
}
/**
* 0.2 different number of subpaths
* probably we should stop here
*/
if (subPathCount1 !== subPathCount2) {
colorSimilarity(el2, same);
return same;
} else {
similarity++
same.subPathCount = true;
}
/**
* 1. compare sizes
*/
let bb1 = el1.getBBox();
let bb2 = el2.getBBox();
let scale = bb1.width / bb2.width;
let simWidth = 1 / bb1.width * bb2.width;
let simWidthRel = 1 / bb1.width * (bb2.width * scale);
let simHeight = 1 / bb1.height * bb2.height;
let simHeightRel = 1 / bb1.height * (bb2.height * scale);
/**
* 1.1 offsets: sizes might be equal
* but compared elements might have different positions in layout
*/
let offsetX = bb1.x - bb2.x;
let offsetY = bb1.y - bb2.y;
if (simWidth > 0.9 && simHeight > 0.9) {
same.size = true;
same.sizeRel = true
similarity += 2
} else if (simWidthRel > 0.9 && simHeightRel > 0.9) {
same.sizeRel = true
similarity++
}
if (Math.abs(offsetX) < 0.1 && Math.abs(offsetY) < 0.1) {
same.pos = true;
similarity++
}
/**
* 2. Compare aspect ratios
* visually similar elements must have
* similar aspect ratios
*/
let aspect1 = bb1.width / bb1.height;
let aspect2 = bb2.width / bb2.height;
let simAspect = 1 / aspect1 * aspect2;
/**
* 3. compare pathLength
* visually similar elements must have a similar path length
*/
if (simAspect > 0.9) {
same.aspect = true;
similarity++
let pathLength1 = el1.getTotalLength();
let pathLength2 = el2.getTotalLength();
let pathLength2Rel = pathLength2 * scale;
let simPathLength = 1 / pathLength1 * pathLength2;
let simPathLengthRel = 1 / pathLength1 * pathLength2Rel;
if (simPathLength > 0.9) {
same.pathLength = true;
similarity++
}
if (simPathLengthRel > 0.9) {
same.pathLengthRel = true;
similarity++
}
let intersects = false;
let pointsInStroke = 0;
let pointsInStrokeRel = 0;
// 4. points on stroke
// increase stroke width temporarily for better tolerance
el1.style.strokeWidth = tolerance + '%';
el1.style.stroke = '#ccc';
for (let i = 0; i < checkPoints; i++) {
let p = el2.getPointAtLength(pathLength2 / checkPoints * i);
let pO = p;
// 4.1 direct intersection
if (same.size && same.pos) {
intersects = el1.isPointInStroke(p)
if (intersects) {
pointsInStroke++;
renderPoint(svg, p, 'green', '0.5%')
}
}
// 4.2 same shape but different position or scale
else {
let matrix = svg.createSVGMatrix();
matrix = matrix.translate(bb2.x, bb2.y);
matrix = matrix.scale(scale);
matrix = matrix.translate(bb2.x * -1, bb2.y * -1);
matrix = matrix.translate(offsetX / scale, offsetY / scale);
p = p.matrixTransform(matrix);
intersects = el1.isPointInStroke(p);
if (intersects) {
renderPoint(svg, pO, 'orange', '0.5%')
pointsInStrokeRel++
} else {
renderPoint(svg, pO, 'red', '0.5%')
}
}
}
let pointsInStrokeRat = 1 / checkPoints * pointsInStroke;
let pointsInStrokeRelRat = 1 / checkPoints * pointsInStrokeRel;
if (pointsInStrokeRat > 0.75) {
same.pointOnPath = true;
same.pointOnPathRel = true;
similarity += pointsInStrokeRat * 2;
} else if (pointsInStrokeRelRat > 0.75) {
same.pointOnPathRel = true;
similarity += pointsInStrokeRelRat
}
// reset stroke
el1.style.strokeWidth = strokeWidth;
el1.style.stroke = stroke;
// set score
same.score = similarity;
/**
* just for display
*/
colorSimilarity(el2, same);
}
return same;
}
function colorSimilarity(el, same) {
if (same.score >= 9) {
el.setAttribute('fill', 'hsl(100deg 100% 25%)');
} else if (same.score >= 5) {
el.setAttribute('fill', 'hsl(75deg 100% 25%)');
} else if (same.score >= 3) {
el.setAttribute('fill', 'hsl(50deg 100% 25%)');
} else {
el.setAttribute('fill', 'red');
}
}
function renderPoint(
svg,
coords,
fill = "red",
r = "2",
opacity = "1",
id = "",
className = ""
) {
if (Array.isArray(coords)) {
coords = {
x: coords[0],
y: coords[1]
};
}
let marker = `<circle class="${className}" opacity="${opacity}" id="${id}" cx="${coords.x}" cy="${coords.y}" r="${r}" fill="${fill}">
<title>${coords.x} ${coords.y}</title></circle>`;
svg.insertAdjacentHTML("beforeend", marker);
}
svg {
width: auto;
height: 20em;
border: 1px solid #ccc;
}
text,
tspan {
font-family: sans-serif;
text-anchor: middle;
}
<svg class="svgText" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 120">
<text x="50%" y="5%" font-size="2.5">#g0
<tspan x="50%" dy="1.2em"></tspan>
</text>
<path id="g0"
d="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.2 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.1 91.3 Q47.3 94.6 47.3 98.8 L47.3 98.8 Q47.3 106.5 41 110.7 Q34.7 114.8 22.6 114.8 L22.6 114.8 Q14.1 114.8 9.2 113 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.8 Q10.7 105.7 13.8 106.7 Q16.9 107.6 22.6 107.6 L22.6 107.6 Q30.9 107.6 34.5 105.5 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 9 89 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.6 66.7 Q2.4 63.1 2.4 58 L2.4 58 Q2.4 52.7 5.1 48.5 Q7.7 44.3 12.4 41.9 Q17 39.6 22.7 39.6 L22.7 39.6 Q28.9 39.7 33.1 39.1 Q37.3 38.6 40.1 37.6 Q42.8 36.7 46.8 34.9 L46.8 34.9 ZM22.7 46.2 Q17.5 46.2 14.7 49.4 Q11.8 52.7 11.8 58 L11.8 58 Q11.8 63.4 14.7 66.7 Q17.6 69.9 22.9 69.9 L22.9 69.9 Q28.3 69.9 31.1 66.8 Q34 63.6 34 57.9 L34 57.9 Q34 46.2 22.7 46.2 L22.7 46.2 Z ">
</path>
</svg>
<svg class="svgText" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 120">
<text x="50%" y="5%" font-size="2.5">#g1
<tspan x="50%" dy="1.2em">Identical to #p0</tspan>
</text>
<path id="g1"
d="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.2 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.1 91.3 Q47.3 94.6 47.3 98.8 L47.3 98.8 Q47.3 106.5 41 110.7 Q34.7 114.8 22.6 114.8 L22.6 114.8 Q14.1 114.8 9.2 113 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.8 Q10.7 105.7 13.8 106.7 Q16.9 107.6 22.6 107.6 L22.6 107.6 Q30.9 107.6 34.5 105.5 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 9 89 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.6 66.7 Q2.4 63.1 2.4 58 L2.4 58 Q2.4 52.7 5.1 48.5 Q7.7 44.3 12.4 41.9 Q17 39.6 22.7 39.6 L22.7 39.6 Q28.9 39.7 33.1 39.1 Q37.3 38.6 40.1 37.6 Q42.8 36.7 46.8 34.9 L46.8 34.9 ZM22.7 46.2 Q17.5 46.2 14.7 49.4 Q11.8 52.7 11.8 58 L11.8 58 Q11.8 63.4 14.7 66.7 Q17.6 69.9 22.9 69.9 L22.9 69.9 Q28.3 69.9 31.1 66.8 Q34 63.6 34 57.9 L34 57.9 Q34 46.2 22.7 46.2 L22.7 46.2 Z ">
</path>
</svg>
<svg class="svgText" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 495 1200" data-desc="24.083">
<text x="50%" y="5%" font-size="25">#g2
<tspan x="50%" dy="1.2em">X/Y Offsets; scaled; optimized</tspan>
<tspan x="50%" dy="1.2em">visually equal</tspan>
</text>
<path id="g2"
d="M568 149l27 83q-30 10-66 13t-88 3l0 0q93 42 93 133l0 0q0 79-54 129t-147 50l0 0q-36 0-67-10l0 0q-12 8-19 21.5t-7 27.5l0 0q0 43 69 43l0 0h84q53 0 94 19t63.5 52t22.5 75l0 0q0 77-63 118.5t-184 41.5l0 0q-85 0-134.5-17.5t-70.5-52.5t-21-90l0 0h83q0 32 12 50.5t43 28t88 9.5l0 0q83 0 118.5-20.5t35.5-61.5l0 0q0-37-28-56t-78-19l0 0h-83q-67 0-101.5-28.5t-34.5-71.5l0 0q0-26 15-50t43-43l0 0q-46-24-67.5-59.5t-21.5-86.5l0 0q0-53 26.5-95t73-65.5t103.5-23.5l0 0q62 1 104-4.5t69.5-15t67.5-27.5l0 0zm-241 113q-52 0-80.5 32.5t-28.5 85.5l0 0q0 54 29 86.5t82 32.5l0 0q54 0 82.5-31.5t28.5-88.5l0 0q0-117-113-117l0 0z">
</path>
</svg>
<svg class="svgText" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 12" data-desc="24.083">
<text x="50%" y="5%" font-size="0.25">#g3
<tspan x="50%" dy="1.2em">Downscaled; badly optimized</tspan>
<tspan x="50%" dy="1.2em">... still quite similar</tspan>
</text>
<path id="g3"
d="M4.7 3.5l0.3 0.8c-0.2 0.1-0.4 0.1-0.7 0.2s-0.6 0-0.9 0l0 0c0.6 0.3 0.9 0.7 0.9 1.3l0 0c0 0.5-0.2 1-0.5 1.3s-0.8 0.5-1.5 0.5l0 0c-0.2 0-0.4 0-0.6-0.1l0 0s-0.2 0.1-0.2 0.2c-0.1 0.1-0.1 0.2-0.1 0.3l0 0c0 0.3 0.2 0.4 0.7 0.4l0 0h0.8c0.4 0 0.7 0.1 1 0.2s0.5 0.3 0.6 0.5s0.2 0.5 0.2 0.8l0 0c0 0.5-0.2 0.9-0.6 1.2s-1 0.4-1.8 0.4l0 0c-0.6 0-1.1-0.1-1.4-0.2s-0.6-0.3-0.7-0.5s-0.2-0.6-0.2-0.9l0 0h0.8s0 0.4 0.1 0.5s0.3 0.2 0.5 0.3s0.5 0.1 0.9 0.1l0 0c0.5 0 0.9-0.1 1.1-0.2s0.4-0.4 0.4-0.7l0 0c0-0.2-0.1-0.4-0.3-0.5s-0.5-0.2-0.8-0.2l0 0h-0.8c-0.5 0-0.8-0.1-1-0.3s-0.3-0.4-0.3-0.7l0 0c0-0.2 0-0.4 0.1-0.5c0.1-0.1 0.3-0.3 0.4-0.4l0 0c-0.3-0.2-0.5-0.4-0.6-0.6c-0.2-0.3-0.3-0.6-0.3-0.9l0 0c0-0.3 0.1-0.6 0.3-0.9s0.4-0.6 0.7-0.7c0.3-0.1 0.7-0.2 1.1-0.2l0 0c0.4 0 0.7 0 1-0.1s0.5 0 0.7-0.1s0.4-0.2 0.7-0.3l0 0zm-2.4 1.1c-0.3 0-0.6 0.1-0.8 0.3c-0.2 0.3-0.3 0.6-0.3 0.9l0 0c0 0.3 0.1 0.6 0.3 0.9s0.5 0.3 0.8 0.3l0 0c0.3 0 0.6-0.1 0.8-0.3s0.3-0.5 0.3-0.9l0 0c0-0.8-0.4-1.2-1.1-1.2l0 0z">
</path>
</svg>
<svg class="svgText" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 495 1200" data-desc="24.083">
<text x="50%" y="5%" font-size="25">#g4
<tspan x="50%" dy="1.2em">X/Y Offsets; scaled; optimized</tspan>
<tspan x="50%" dy="1.2em">Not same number of sub paths</tspan>
<tspan x="50%" dy="1.2em">Disqualified!</tspan>
</text>
<path id="g4"
d="M468 349l27 83q-30 10-66 13t-88 3l0 0q93 42 93 133l0 0q0 79-54 129t-147 50l0 0q-36 0-67-10l0 0q-12 8-19 21.5t-7 27.5l0 0q0 43 69 43l0 0h84q53 0 94 19t63.5 52t22.5 75l0 0q0 77-63 118.5t-184 41.5l0 0q-85 0-134.5-17.5t-70.5-52.5t-21-90l0 0h83q0 32 12 50.5t43 28t88 9.5l0 0q83 0 118.5-20.5t35.5-61.5l0 0q0-37-28-56t-78-19l0 0h-83q-67 0-101.5-28.5t-34.5-71.5l0 0q0-26 15-50t43-43l0 0q-46-24-67.5-59.5t-21.5-86.5l0 0q0-53 26.5-95t73-65.5t103.5-23.5l0 0q62 1 104-4.5t69.5-15t67.5-27.5l0 0">
</path>
</svg>
The above example will return an object containing several details about the compared similarity:
let same = {
size: false, // 1
sizeRel: false, // 2
identical: false, // 3
aspect: false, // 4
pathLength: false, // 5
pathLengthRel: false, // 6
pointOnPath: false, // 7
pointOnPathRel: false, // 8
subPathCount: false, // 9
pos: false, // 10
score: 0 // 11
}
1–2. exactly the same size or same size according to scaling
3. perfectly identical, due to identical d attribute
4. aspect ratio
5–6. pathlength or scaled pathlength is the same
7–8. points intersecting (directly or transformed)
9. number of subpaths
10. exact same x/y position
11. summarized score
The most expensive step is caused by the actual intersection check:
We're basically testing if a limited number of points in path2 (retrieved via getPointAtlength() is also intersecting with path1's stroke via isPointInStroke() method.
The benefit of using isPointInStroke() – we can also tweak the tolerance/precision threshold by temporarily increasing/decreasing the base element's stroke-width.

Related

Merge multiple SVG elements into one SVG and download it with React

I have a logo creator in my app.
User has to type smthn in input field (for example "TESTING") and then it renders in preview block.
Every letter in preview is the svg element:
(Each letter can be a glyph letter or regular letter, it means that there are 2 different fonts)
const C = ({ glyph }) => {
if (!glyph) {
return (
<svg width="93" height="132" viewBox="0 0 93 132" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M75 41.76L87.6 34.92C82.2 21.6 67.2 10.8 49.08 10.8C19.92 10.8 4.8 30.12 4.8 54C4.8 77.88 19.2 97.2 49.08 97.2C67.2 97.2 82.2 86.4 87.6 73.08L75 66.12C71.04 75.72 61.92 84 48.48 84C30.72 84 19.68 71.76 19.68 54C19.68 36.24 30.72 24 48.48 24C61.92 24 71.04 32.28 75 41.76Z"
fill="currentColor"
/>
</svg>
);
} else {
return (
<svg width="96" height="132" viewBox="0 0 96 132" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M20.4 51.24H33.6C33.6 31.92 42.24 24 54.36 24C64.92 24 72.36 29.16 76.2 40.56L88.8 33.6C83.4 19.2 71.64 10.8 54.48 10.8C32.04 10.8 18.72 25.08 18.72 51.24H4.8C4.8 80.52 24.48 97.08 49.8 97.08C70.68 97.08 83.04 85.8 89.16 74.04L77.64 67.44C71.88 77.16 64.2 83.64 50.4 83.64C33.12 83.64 21.12 71.16 20.4 51.24Z"
fill="currentColor"
/>
</svg>
);
}
};
I change glyph state by clicking on letter (this is a hover view):
How it looks in devtools:
What i expect:
On download click:
I need to download this logo ("TESTING") as .svg file.
const svgHtml = Array.from(logoRef.current.querySelectorAll('span'))
.map((el) => el.innerHTML)
.join('\n');
const svg = `
<svg xmlns="http://www.w3.org/2000/svg">
${svgHtml}
</svg>
`;
const blob = new Blob([svg], { type: 'image/svg+xml' });
createDownloadLink(blob, `${inputText}.svg`);
Actual behavior:
All letters have no relative positioning as in preview:
Do you have any ideas how can i do this?
Or maybe there is a better solution you can purpose?
Thanks in advance
You need to calculate x offset values for each letter svg before merging them, since they have no idea how they are positioned in the HTML DOM context (i.e as children of <span> elements).
This offset value would be incremented by the viewBox width
(3. viewBox argument: viewBox="0 0 93 132" => width=93):
JavaScript example
let spans = document.querySelectorAll('span');
let svgMarkup = '';
let xOffset = 0;
let viewBoxHeight = 132;
let letterSpacing = 5;
spans.forEach((span,i)=>{
let svg = span.querySelector('svg');
let style = window.getComputedStyle(span)
let color = style.color;
let viewBox = svg.getAttribute('viewBox').split(' ');
let width = +viewBox[2];
let glyph = svg.querySelector('path');
// clone path to apply offset
let glyphCloned = glyph.cloneNode(true);
// apply x offset by translate()
glyphCloned.setAttribute('transform', `translate(${xOffset} 0)`);
glyphCloned.style.color=color;
let glyphMarkup = glyphCloned.outerHTML;
glyphCloned.remove();
svgMarkup+=glyphMarkup;
// increment offset for next letter
xOffset += i==spans.length-1 ? width : width+letterSpacing;
})
let svgCombined = `
<svg id="combined" xmlns="http://www.w3.org/2000/svg" width="${xOffset}" height="${viewBoxHeight}" viewBox="0 0 ${xOffset} ${viewBoxHeight}">
${svgMarkup}
</svg>`;
document.body.insertAdjacentHTML('beforeend', svgCombined);
svg{
height:10em;
width:auto;
border:1px solid #ccc;
}
.blue{
color:blue
}
<span><svg width="93" height="132" viewBox="0 0 93 132" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M75 41.76L87.6 34.92C82.2 21.6 67.2 10.8 49.08 10.8C19.92 10.8 4.8 30.12 4.8 54C4.8 77.88 19.2 97.2 49.08 97.2C67.2 97.2 82.2 86.4 87.6 73.08L75 66.12C71.04 75.72 61.92 84 48.48 84C30.72 84 19.68 71.76 19.68 54C19.68 36.24 30.72 24 48.48 24C61.92 24 71.04 32.28 75 41.76Z" fill="currentColor" />
</svg>
</span>
<span class="blue">
<svg width="96" height="132" viewBox="0 0 96 132" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.4 51.24H33.6C33.6 31.92 42.24 24 54.36 24C64.92 24 72.36 29.16 76.2 40.56L88.8 33.6C83.4 19.2 71.64 10.8 54.48 10.8C32.04 10.8 18.72 25.08 18.72 51.24H4.8C4.8 80.52 24.48 97.08 49.8 97.08C70.68 97.08 83.04 85.8 89.16 74.04L77.64 67.44C71.88 77.16 64.2 83.64 50.4 83.64C33.12 83.64 21.12 71.16 20.4 51.24Z" fill="currentColor" />
</svg>
</span>
<span><svg width="93" height="132" viewBox="0 0 93 132" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M75 41.76L87.6 34.92C82.2 21.6 67.2 10.8 49.08 10.8C19.92 10.8 4.8 30.12 4.8 54C4.8 77.88 19.2 97.2 49.08 97.2C67.2 97.2 82.2 86.4 87.6 73.08L75 66.12C71.04 75.72 61.92 84 48.48 84C30.72 84 19.68 71.76 19.68 54C19.68 36.24 30.72 24 48.48 24C61.92 24 71.04 32.28 75 41.76Z" fill="currentColor" />
</svg>
</span>
<p>Combined Svg</p>
The example above will also select only the <path> elements instead of their parent <svg>s.
In fact, you could also set an offset via x attribute.
However, a lot of graphic applications struggle with nested svgs - so copying only paths is usually a more robust solution for standalone svgs.
Besides, we're adding a viewBox based to the total width and height of all combined letter svgs.

Rounded corners in SVG path semi circle

I have a path which is a semi-circle. How do I make the corners rounded? I do not want to use stroke-linecap: round as I need it to be rounded only on these corners:
<svg>
<g>
<!--background -->
<path fill="none" stroke-dasharray="" stroke-width="16" stroke="#607985" d="M30 100 A 40 40 0 0 1 170 100"></path>
<!-- strokes -->
<path id="meter-back" fill="none" stroke-width="15" stroke="white" d="M30 100 A 40 40 0 0 1 170 100"></path>
<!--progress -->
<path id="meter-fill" fill="none" stroke-dashoffset="219.942" stroke-dasharray="109.971, 109.971" stroke="rgba(96,121,133,0.7)" stroke-width="15" d="M30 100 A 40 40 0 0 1 170 100" stroke="#607985"></path>
</g>
</svg>
Here is a fixed solution. dividerPos can be in range from 0 to 1:
const getPath = (outerRadius, innerRadius, cornerRadius, dividerPos) => {
const angle = Math.PI * (1 - dividerPos);
const outerPointX = outerRadius * Math.cos(angle);
const outerPointY = outerRadius * -Math.sin(angle);
const innerPointX = innerRadius * Math.cos(angle);
const innerPointY = innerRadius * -Math.sin(angle);
const left = `M ${-outerRadius},0
A ${outerRadius},${outerRadius} 0 0 1
${outerPointX},${outerPointY}
L ${innerPointX},${innerPointY}
A ${innerRadius},${innerRadius} 0 0 0 ${-innerRadius},0
Q ${-innerRadius},${cornerRadius}
${-innerRadius-cornerRadius},${cornerRadius}
H ${-outerRadius+cornerRadius}
Q ${-outerRadius},${cornerRadius}
${-outerRadius},0
Z`;
const right = `M ${outerPointX},${outerPointY}
A ${outerRadius},${outerRadius} 0 0 1
${outerRadius},0
Q ${outerRadius},${cornerRadius}
${outerRadius-cornerRadius},${cornerRadius}
H ${innerRadius+cornerRadius}
Q ${innerRadius},${cornerRadius}
${innerRadius},0
A ${innerRadius},${innerRadius} 0 0 0
${innerPointX},${innerPointY}
Z`;
return {left, right};
};
const {left, right} = getPath(120, 90, 15, 0.5);
d3.select('.left').attr('d', left);
d3.select('.right').attr('d', right);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width='300' height='200'>
<g transform='translate(150,150)'>
<path stroke='grey' fill='grey' class='left'/>
<path stroke='grey' fill='none' class='right'/>
</g>
</svg>
Use getPath routine to compute the desired path
(The 0,0 point in the center of the semi-circle):
const getPath = (outerRadius, innerRadius, cornerRadius) => {
return `M ${-outerRadius},0
A ${outerRadius},${outerRadius} 1 1 1 ${outerRadius},0
Q ${outerRadius},${cornerRadius}
${outerRadius-cornerRadius},${cornerRadius}
H ${innerRadius+cornerRadius}
Q ${innerRadius},${cornerRadius}
${innerRadius},0
A ${innerRadius},${innerRadius} 0 0 0
${-innerRadius},0
Q ${-innerRadius},${cornerRadius}
${-innerRadius-cornerRadius},${cornerRadius}
H ${-outerRadius+cornerRadius}
Q ${-outerRadius},${cornerRadius}
${-outerRadius},0
Z`;
};
d3.select('path').attr('d', getPath(120, 90, 12));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width='300' height='200'>
<g transform='translate(150,150)'>
<path stroke='red' fill='none'/>
<circle fill='red' r='5' cx='0' cy='0'/>
</g>
</svg>

SVG: get arc segment length

I'm looking into ways to calculate path.getTotalLength() in Node.js and it seems that a polyfill is not available. So far I managed to compute all other pathCommands except A.
For instance, knowing last X and Y from M segment, and also having all the A segment values, how to determine the length of this path?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="M8 15 A7 7 0 1 0 8 1">
</svg>
Thank you
MDN has some great documentation in regards to the various path commands.
MDN > SVG Tutorial > Paths
Here is how you decompose the provided path commands:
M 8 15 = Move To (Absolute)
x = 8
y = 15
A 7 7 0 1 0 8 1 = Arc (Absolute)
rx = 7
ry = 7
x-axis-rotation = 0
large-arc-flag = 1
sweep-flag = 0
x = 8
y = 1
I followed this Mathematics Exchange post to compute the arc length, given the states path commands. Since the x and y radii of your arc are equal, this is a bit easier.
Note: I am not sure what you would need to do if they are different.
const x1 = 8, y1 = 15;
const x2 = 8, y2 = 1;
const r = 7; // Since rx === ry, this is a bit easier
const d = Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
const theta = Math.acos(1 - (Math.pow(d, 2) / (2 * Math.pow(r, 2))));
const arcLength = theta * r;
console.log(arcLength); // Arc Length = 21.9911
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<path d="
M 8 15
A 7 7 0 1 0 8 1
" fill="none" stroke="black" stroke-width="2" />
<!-- Move to and begin arc at (8, 15) -->
<circle cx="8" cy="15" r="1" stroke="none" fill="red" />
<!-- End arc at (8, 1) -->
<circle cx="8" cy="1" r="1" stroke="none" fill="cyan" />
<!-- Radius of (7, 7) -->
<circle cx="15" cy="7.5" r="1" stroke="none" fill="lime" />
<!-- Center -->
<circle cx="8" cy="7.5" r="1" stroke="none" fill="gold" />
</svg>

Pretty displaying SVG math Latex equation into a canvas

From this link, I would like to put a math equation (generated from Latex with tex2svg tool) into a canvas.
You can see here [this SVG formula][2] (fill color is black).
Now, I include this SVG into canvas like this :
<body>
<canvas id="textbox"></canvas>
<script>
// Draw SVG formula of absolute differential
var textCanvas = document.getElementById('textbox');
var contextTextBox = textCanvas.getContext('2d');
var img = new Image;
img.onload = function(){ contextTextBox.drawImage(img,0,0); };
img.src = "./formula.svg";
</script>
</body>
You can see the result on [this link][3] (fill color of SVG is white).
The issue is that rendering is not pretty, formula is tight and blurred.
Anyone could give me clues to improve the quality of equation rendering (I don't know if it is possible to have the same quality as MathJax equation).
Thanks for your help
Math latex on canvas with subpixel rendering.
The image you provided is not of the SVG image you have linked to, so i do not know why you have the image split into 3 and squashed?
Anyways see this answer for a way to improve text rendering (like Latex equations) by re-rendering the image at the (physical) sub pixel level.
Note that the image must have a non opaque background colour.
The following snippet uses the core function from that answer to do the same to the SVG image.
I am using the SVG image you provided which is black on white but this method will work for all colours except for transparent pixels. If you use transparent pixels you will have to access the quality for yourself as it will depend on the background the image is drawn onto.
Note may not work on outdated browsers.
// the SVG image
var svg = `<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="21.371ex" height="3.676ex" style="vertical-align: -1.338ex;" viewBox="0 -1006.6 9201.2 1582.7" role="img" focusable="false" xmlns="http://www.w3.org/2000/svg">
<defs>
<path stroke-width="1" id="E1-MJMAIN-44" d="M130 622Q123 629 119 631T103 634T60 637H27V683H228Q399 682 419 682T461 676Q504 667 546 641T626 573T685 470T708 336Q708 210 634 116T442 3Q429 1 228 0H27V46H60Q102 47 111 49T130 61V622ZM593 338Q593 439 571 501T493 602Q439 637 355 637H322H294Q238 637 234 628Q231 624 231 344Q231 62 232 59Q233 49 248 48T339 46H350Q456 46 515 95Q561 133 577 191T593 338Z"></path>
<path stroke-width="1" id="E1-MJMATHI-76" d="M173 380Q173 405 154 405Q130 405 104 376T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Q21 294 29 316T53 368T97 419T160 441Q202 441 225 417T249 361Q249 344 246 335Q246 329 231 291T200 202T182 113Q182 86 187 69Q200 26 250 26Q287 26 319 60T369 139T398 222T409 277Q409 300 401 317T383 343T365 361T357 383Q357 405 376 424T417 443Q436 443 451 425T467 367Q467 340 455 284T418 159T347 40T241 -11Q177 -11 139 22Q102 54 102 117Q102 148 110 181T151 298Q173 362 173 380Z"></path>
<path stroke-width="1" id="E1-MJMATHI-69" d="M184 600Q184 624 203 642T247 661Q265 661 277 649T290 619Q290 596 270 577T226 557Q211 557 198 567T184 600ZM21 287Q21 295 30 318T54 369T98 420T158 442Q197 442 223 419T250 357Q250 340 236 301T196 196T154 83Q149 61 149 51Q149 26 166 26Q175 26 185 29T208 43T235 78T260 137Q263 149 265 151T282 153Q302 153 302 143Q302 135 293 112T268 61T223 11T161 -11Q129 -11 102 10T74 74Q74 91 79 106T122 220Q160 321 166 341T173 380Q173 404 156 404H154Q124 404 99 371T61 287Q60 286 59 284T58 281T56 279T53 278T49 278T41 278H27Q21 284 21 287Z"></path>
<path stroke-width="1" id="E1-MJMAIN-3D" d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z"></path>
<path stroke-width="1" id="E1-MJMAIN-64" d="M376 495Q376 511 376 535T377 568Q377 613 367 624T316 637H298V660Q298 683 300 683L310 684Q320 685 339 686T376 688Q393 689 413 690T443 693T454 694H457V390Q457 84 458 81Q461 61 472 55T517 46H535V0Q533 0 459 -5T380 -11H373V44L365 37Q307 -11 235 -11Q158 -11 96 50T34 215Q34 315 97 378T244 442Q319 442 376 393V495ZM373 342Q328 405 260 405Q211 405 173 369Q146 341 139 305T131 211Q131 155 138 120T173 59Q203 26 251 26Q322 26 373 103V342Z"></path>
<path stroke-width="1" id="E1-MJMAIN-2212" d="M84 237T84 250T98 270H679Q694 262 694 250T679 230H98Q84 237 84 250Z"></path>
<path stroke-width="1" id="E1-MJMATHI-6B" d="M121 647Q121 657 125 670T137 683Q138 683 209 688T282 694Q294 694 294 686Q294 679 244 477Q194 279 194 272Q213 282 223 291Q247 309 292 354T362 415Q402 442 438 442Q468 442 485 423T503 369Q503 344 496 327T477 302T456 291T438 288Q418 288 406 299T394 328Q394 353 410 369T442 390L458 393Q446 405 434 405H430Q398 402 367 380T294 316T228 255Q230 254 243 252T267 246T293 238T320 224T342 206T359 180T365 147Q365 130 360 106T354 66Q354 26 381 26Q429 26 459 145Q461 153 479 153H483Q499 153 499 144Q499 139 496 130Q455 -11 378 -11Q333 -11 305 15T277 90Q277 108 280 121T283 145Q283 167 269 183T234 206T200 217T182 220H180Q168 178 159 139T145 81T136 44T129 20T122 7T111 -2Q98 -11 83 -11Q66 -11 57 -1T48 16Q48 26 85 176T158 471L195 616Q196 629 188 632T149 637H144Q134 637 131 637T124 640T121 647Z"></path>
<path stroke-width="1" id="E1-MJMAIN-393" d="M128 619Q121 626 117 628T101 631T58 634H25V680H554V676Q556 670 568 560T582 444V440H542V444Q542 445 538 478T523 545T492 598Q454 634 349 634H334Q264 634 249 633T233 621Q232 618 232 339L233 61Q240 54 245 52T270 48T333 46H360V0H348Q324 3 182 3Q51 3 36 0H25V46H58Q100 47 109 49T128 61V619Z"></path>
<path stroke-width="1" id="E1-MJMATHI-6A" d="M297 596Q297 627 318 644T361 661Q378 661 389 651T403 623Q403 595 384 576T340 557Q322 557 310 567T297 596ZM288 376Q288 405 262 405Q240 405 220 393T185 362T161 325T144 293L137 279Q135 278 121 278H107Q101 284 101 286T105 299Q126 348 164 391T252 441Q253 441 260 441T272 442Q296 441 316 432Q341 418 354 401T367 348V332L318 133Q267 -67 264 -75Q246 -125 194 -164T75 -204Q25 -204 7 -183T-12 -137Q-12 -110 7 -91T53 -71Q70 -71 82 -81T95 -112Q95 -148 63 -167Q69 -168 77 -168Q111 -168 139 -140T182 -74L193 -32Q204 11 219 72T251 197T278 308T289 365Q289 372 288 376Z"></path>
<path stroke-width="1" id="E1-MJMATHI-79" d="M21 287Q21 301 36 335T84 406T158 442Q199 442 224 419T250 355Q248 336 247 334Q247 331 231 288T198 191T182 105Q182 62 196 45T238 27Q261 27 281 38T312 61T339 94Q339 95 344 114T358 173T377 247Q415 397 419 404Q432 431 462 431Q475 431 483 424T494 412T496 403Q496 390 447 193T391 -23Q363 -106 294 -155T156 -205Q111 -205 77 -183T43 -117Q43 -95 50 -80T69 -58T89 -48T106 -45Q150 -45 150 -87Q150 -107 138 -122T115 -142T102 -147L99 -148Q101 -153 118 -160T152 -167H160Q177 -167 186 -165Q219 -156 247 -127T290 -65T313 -9T321 21L315 17Q309 13 296 6T270 -6Q250 -11 231 -11Q185 -11 150 11T104 82Q103 89 103 113Q103 170 138 262T173 379Q173 380 173 381Q173 390 173 393T169 400T158 404H154Q131 404 112 385T82 344T65 302T57 280Q55 278 41 278H27Q21 284 21 287Z"></path>
</defs>
<g stroke="currentColor" fill="black" stroke-width="0" transform="matrix(1 0 0 -1 0 0)">
<use xlink:href="#E1-MJMAIN-44" x="0" y="0"></use>
<g transform="translate(764,0)">
<use xlink:href="#E1-MJMATHI-76" x="0" y="0"></use>
<use transform="scale(0.707)" xlink:href="#E1-MJMATHI-69" x="686" y="-213"></use>
</g>
<use xlink:href="#E1-MJMAIN-3D" x="1872" y="0"></use>
<use xlink:href="#E1-MJMAIN-64" x="2928" y="0"></use>
<g transform="translate(3484,0)">
<use xlink:href="#E1-MJMATHI-76" x="0" y="0"></use>
<use transform="scale(0.707)" xlink:href="#E1-MJMATHI-69" x="686" y="-213"></use>
</g>
<use xlink:href="#E1-MJMAIN-2212" x="4536" y="0"></use>
<g transform="translate(5537,0)">
<use xlink:href="#E1-MJMATHI-76" x="0" y="0"></use>
<use transform="scale(0.707)" xlink:href="#E1-MJMATHI-6B" x="686" y="-213"></use>
</g>
<g transform="translate(6491,0)">
<use xlink:href="#E1-MJMAIN-393" x="0" y="0"></use>
<use transform="scale(0.707)" xlink:href="#E1-MJMATHI-6B" x="884" y="499"></use>
<g transform="translate(625,-304)">
<use transform="scale(0.707)" xlink:href="#E1-MJMATHI-69" x="0" y="0"></use>
<use transform="scale(0.707)" xlink:href="#E1-MJMATHI-6A" x="345" y="0"></use>
</g>
</g>
<use xlink:href="#E1-MJMAIN-64" x="7753" y="0"></use>
<g transform="translate(8309,0)">
<use xlink:href="#E1-MJMATHI-79" x="0" y="0"></use>
<use transform="scale(0.707)" xlink:href="#E1-MJMATHI-6A" x="706" y="583"></use>
</g>
</g>
</svg>`;
const backgroundColour = "White";
// creates a blank image with 2d context
var createImage=function(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
// helper function
function createInfoDiv(text){ var div = document.createElement("div"); div.textContent = text; return div;}
// create a canvas and add to the dom
var canvas = createImage(350,100);
canvas.style.border = "1px red solid";
var ctx = canvas.ctx;
document.body.appendChild(canvas);
// double canvas resolution if pixels are 2 times CSS pixel size. (might be hires or retina display)
if(devicePixelRatio === 2){
var w = canvas.width;
var h = canvas.height;
canvas.style.width = w + "px;"
canvas.style.height = h + "px;"
canvas.width = w * 2;
canvas.height = h * 2;
}
// This function uses subpixels to render hi quality image.
// Note that the image must have a non opaque background colour
// returns the same imgData with 1st 1/3rd containing new image.
var subPixelBitmap = function(imgData){
var spR,spG,spB; // sub pixels
var id,id1; // pixel indexes
var w = imgData.width;
var h = imgData.height;
var d = imgData.data;
var x,y;
var ww = w*4;
var ww4 = ww+4;
for(y = 0; y < h; y+=1){
for(x = 0; x < w; x+=3){
var id = y*ww+x*4;
var id1 = Math.floor(y)*ww+Math.floor(x/3)*4;
spR = Math.sqrt(d[id + 0] * d[id + 0] * 0.2126 + d[id + 1] * d[id + 1] * 0.7152 + d[id + 2] * d[id + 2] * 0.0722);
id += 4;
spG = Math.sqrt(d[id + 0] * d[id + 0] * 0.2126 + d[id + 1] * d[id + 1] * 0.7152 + d[id + 2] * d[id + 2] * 0.0722);
id += 4;
spB = Math.sqrt(d[id + 0] * d[id + 0] * 0.2126 + d[id + 1] * d[id + 1] * 0.7152 + d[id + 2] * d[id + 2] * 0.0722);
d[id1++] = spR;
d[id1++] = spG;
d[id1++] = spB;
d[id1++] = 255; // alpha always 255
}
}
return imgData;
}
// clear the canvas
ctx.clearRect(0,0,canvas.width,canvas.height);
// Create the SVG image
var SVGImage = new Image(155*3,27*3);
SVGImage.src = "data:image/svg+xml;base64," + btoa(('<?xml version="1.0"?>'+svg))
// When the image has been parsed create the hiquality subpixel rendered image.
SVGImage.onload = function(){
try{
var image3;
var scale = 1;
if(devicePixelRatio === 2){ // only for 2 as this is guessing that a Hires display is being used
scale = 2;
}
image3 = createImage(this.width * scale, this.height * scale);
var h = Math.ceil(image3.height/ 3);
image3.ctx.fillStyle = backgroundColour;
image3.ctx.fillRect(0,0,image3.width,image3.height);
image3.ctx.drawImage(this,0,0,image3.width,image3.height);
image3.ctx.drawImage(this,0,0,image3.width,image3.height+1);
image3.ctx.drawImage(this,0,0,image3.width+0.5,image3.height);
image3.ctx.drawImage(this,0,0,image3.width+0.5,image3.height+1);
image3.ctx.drawImage(image3,0,0,image3.width,h)
var dat = subPixelBitmap(image3.ctx.getImageData(0,0,image3.width,h));
image3.width /= 3;
image3.height = h;
this.width /= 3;
this.height = h;
//================================================================
// image3 now has the hi quality image
image3.ctx.putImageData(dat,0,0);
ctx.fillStyle = backgroundColour;
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "black";
// draw high qual image
ctx.font = (16 * scale) + "px arial";
ctx.drawImage(image3,0,20 * scale);
ctx.fillText("SVG plus (Sub pixel) render.", 5,16 * scale)
// draw normal SVG on canvas
ctx.drawImage(this,0,image3.height + 40 * scale, this.width * scale, this.height * scale);
ctx.fillText("SVG as is rendered on canvas",5,image3.height + (20 + 16) * scale)
document.body.appendChild(createInfoDiv("Original SVG as HTML <svg> object"));
document.body.appendChild(this); // show original SVG
}catch(e){
document.body.innerHTML = "<h1>So sorry but you need to upgrade to Firefox, Edge, or Chrome!</h1>"
}
}

How to detect which segment of a svg path is clicked in javascript?

SVG path element for example:
<path id="path1"
d="M 160 180 C 60 140 230 20 200 170 C 290 120 270 300 200 240 C 160 390 50 240 233 196"
stroke="#009900" stroke-width="4" fill="none"/>
It has 4 svg segments (3 curve segments in human eyes):
M 160 180
C 60 140 230 20 200 170
C 290 120 270 300 200 240
C 160 390 50 240 233 196
when click on the path, I get the x and y of mouse position, then how to detect which curve segment is clicked?
function isInWhichSegment(pathElement,x,y){
//var segs = pathElement.pathSegList; //all segments
//
//return the index of which segment is clicked
//
}
There are a few methods for SVGPathElements that you can use. Not really straighforward, but you could get the total length of your path, then check at every point of length the coordinates with getPointAtLength and compare it with coordinates of the click. Once you figure the click was at which length, you get the segment at that length with getPathSegAtLength. like that for example:
var pathElement = document.getElementById('path1')
var len = pathElement.getTotalLength();
pathElement.onclick = function(e) {
console.log('The index of the clicked segment is', isInWhichSegment(pathElement, e.offsetX, e.offsetY))
}
function isInWhichSegment(pathElement, x, y) {
var seg;
// You get get the coordinates at the length of the path, so you
// check at all length point to see if it matches
// the coordinates of the click
for (var i = 0; i < len; i++) {
var pt = pathElement.getPointAtLength(i);
// you need to take into account the stroke width, hence the +- 2
if ((pt.x < (x + 2) && pt.x > (x - 2)) && (pt.y > (y - 2) && pt.y < (y + 2))) {
seg = pathElement.getPathSegAtLength(i);
break;
}
}
return seg;
}
<svg>
<path id="path1" d="M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80" stroke="#009900" stroke-width="4" fill="none" />
</svg>

Categories