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>