let svg = document.querySelector("svg");
let paths = svg.querySelectorAll("path");
function check() {
// reset results
gInter2.innerHTML = '';
gInter.innerHTML = '';
time.textContent = '';
/**
* Boolean check
*/
perfStart();
let intersections = checkPathIntersections(p0, p1, 24);
time.textContent += '1. stroke intersection: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
renderPoint(svg, intersections[0]);
perfStart();
let intersections1 = checkPathIntersections(p2, p3, 24);
time.textContent += '2. fill intersection: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
renderPoint(svg, intersections1[0])
/**
* Precise check
*/
perfStart();
let intersections3 = checkIntersectionPrecise(p4, p5, 100, 1);
time.textContent += '3. multiple intersections: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
if (intersections3.length) {
intersections3.forEach(p => {
renderPoint(svg, p, 'red')
})
}
// no bbox intersection
perfStart();
let intersections4 = checkPathIntersections(p5, p6, 24);
time.textContent += '4. no bbBox intersection: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
perfStart();
let intersections5 = checkIntersectionPrecise(p8, p9, 1200, 0);
time.textContent += '5. multiple intersections: ' + perfEnd().toFixed(3) * 1 + ' ms; \n ';
if (intersections5.length) {
intersections5.forEach(p => {
renderPoint(gInter2, p, 'green', '0.25%');
})
}
}
function checkIntersectionPrecise(path0, path1, split = 1000, decimals = 0) {
/**
* 0. check bbox intersection
* to skip sample point checks
*/
let bb = path0.getBBox();
let [left, top, right, bottom] = [bb.x, bb.y, bb.x + bb.width, bb.y + bb.height];
let bb1 = path1.getBBox();
let [left1, top1, right1, bottom1] = [bb1.x, bb1.y, bb1.x + bb1.width, bb1.y + bb1.height];
let bboxIntersection =
left <= right1 &&
top <= bottom1 &&
bottom >= top1 &&
right >= left1 ?
true :
false;
if (!bboxIntersection) {
console.log('no intersections at all');
return false;
}
// path0
let pathData0 = path0.getPathData({
normalize: true
})
let points0 = pathDataToPolygonPoints(pathData0, true, split);
let points0Strings = points0.map(val => {
return val.x.toFixed(decimals) + '_' + val.y.toFixed(decimals)
});
// filter duplicates
points0Strings = [...new Set(points0Strings)];
// path1
let pathLength1 = path1.getTotalLength();
let pathData1 = path1.getPathData({
normalize: true
})
let points1 = pathDataToPolygonPoints(pathData1, true, split);
let points1Strings = points1.map(val => {
return val.x.toFixed(decimals) + '_' + val.y.toFixed(decimals)
});
points1Strings = [...new Set(points1Strings)];
// 1. compare
let intersections = [];
let intersectionsFilter = [];
for (let i = 0; i < points0Strings.length; i++) {
let p0Str = points0Strings[i];
let index = points1Strings.indexOf(p0Str);
if (index !== -1) {
let p1 = p0Str.split('_');
intersections.push({
x: +p1[0],
y: +p1[1]
});
}
}
// filter nearby points
if (intersections.length) {
intersectionsFilter = [intersections[0]];
let length = intersections.length;
for (let i = 1; i < length; i += 1) {
let p = intersections[i];
let pPrev = intersections[i - 1];
let diffX = Math.abs(pPrev.x - p.x);
let diffY = Math.abs(pPrev.y - p.y);
let diff = diffX + diffY;
if (diff > 1) {
intersectionsFilter.push(p)
}
}
} else {
return false
}
return intersectionsFilter;
}
/**
* convert path d to polygon point array
*/
function pathDataToPolygonPoints(pathData, addControlPointsMid = false, splitNtimes = 0, splitLines = false) {
let points = [];
pathData.forEach((com, c) => {
let type = com.type;
let values = com.values;
let valL = values.length;
// optional splitting
let splitStep = splitNtimes ? (0.5 / splitNtimes) : (addControlPointsMid ? 0.5 : 0);
let split = splitStep;
// M
if (c === 0) {
let M = {
x: pathData[0].values[valL - 2],
y: pathData[0].values[valL - 1]
};
points.push(M);
}
if (valL && c > 0) {
let prev = pathData[c - 1];
let prevVal = prev.values;
let prevValL = prevVal.length;
let p0 = {
x: prevVal[prevValL - 2],
y: prevVal[prevValL - 1]
};
// cubic curves
if (type === "C") {
if (prevValL) {
let cp1 = {
x: values[valL - 6],
y: values[valL - 5]
};
let cp2 = {
x: values[valL - 4],
y: values[valL - 3]
};
let p = {
x: values[valL - 2],
y: values[valL - 1]
};
if (addControlPointsMid && split) {
// split cubic curves
for (let s = 0; split < 1 && s < 9999; s++) {
let midPoint = getPointAtCubicSegmentLength(p0, cp1, cp2, p, split);
points.push(midPoint);
split += splitStep
}
}
points.push({
x: values[valL - 2],
y: values[valL - 1]
});
}
}
// linetos
else if (type === "L") {
if (splitLines) {
//let prevCoords = [prevVal[prevValL - 2], prevVal[prevValL - 1]];
let p1 = {
x: prevVal[prevValL - 2],
y: prevVal[prevValL - 1]
}
let p2 = {
x: values[valL - 2],
y: values[valL - 1]
}
if (addControlPointsMid && split) {
for (let s = 0; split < 1; s++) {
let midPoint = interpolatedPoint(p1, p2, split);
points.push(midPoint);
split += splitStep
}
}
}
points.push({
x: values[valL - 2],
y: values[valL - 1]
});
}
}
});
return points;
}
/**
* Linear interpolation (LERP) helper
*/
function interpolatedPoint(p1, p2, t = 0.5) {
//t: 0.5 - point in the middle
if (Array.isArray(p1)) {
p1.x = p1[0];
p1.y = p1[1];
}
if (Array.isArray(p2)) {
p2.x = p2[0];
p2.y = p2[1];
}
let [x, y] = [(p2.x - p1.x) * t + p1.x, (p2.y - p1.y) * t + p1.y];
return {
x: x,
y: y
};
}
/**
* calculate single points on segments
*/
function getPointAtCubicSegmentLength(p0, cp1, cp2, p, t=0.5) {
let t1 = 1 - t;
return {
x: t1 ** 3 * p0.x + 3 * t1 ** 2 * t * cp1.x + 3 * t1 * t ** 2 * cp2.x + t ** 3 * p.x,
y: t1 ** 3 * p0.y + 3 * t1 ** 2 * t * cp1.y + 3 * t1 * t ** 2 * cp2.y + t ** 3 * p.y
}
}
function checkPathIntersections(path0, path1, checksPerPath = 24, threshold = 2) {
/**
* 0. check bbox intersection
* to skip sample point checks
*/
let bb = path0.getBBox();
let [left, top, right, bottom] = [bb.x, bb.y, bb.x + bb.width, bb.y + bb.height];
let bb1 = path1.getBBox();
let [left1, top1, right1, bottom1] = [bb1.x, bb1.y, bb1.x + bb1.width, bb1.y + bb1.height];
let bboxIntersection =
left <= right1 - threshold &&
top <= bottom1 - threshold &&
bottom >= top1 - threshold &&
right >= left1 - threshold ?
true :
false;
if (!bboxIntersection) {
return false;
}
// path0
let pathLength0 = path0.getTotalLength();
// set temporary stroke
let style0 = window.getComputedStyle(path0);
let fill0 = style0.fill;
let strokeWidth0 = style0.strokeWidth;
path0.style.strokeWidth = threshold;
// path1
let pathLength1 = path1.getTotalLength();
// set temporary stroke
let style1 = window.getComputedStyle(path1);
let fill1 = style1.fill;
let strokeWidth1 = style1.strokeWidth;
path1.style.strokeWidth = threshold;
/**
* 1. check sample point intersections
*/
let checks = 0;
let intersections = [];
/**
* 1.1 compare path0 against path1
*/
for (let c = 0; c < checksPerPath && !intersections.length; c++) {
let pt = path1.getPointAtLength((pathLength1 / checksPerPath) * c);
let inStroke = path0.isPointInStroke(pt);
let inFill = path0.isPointInFill(pt);
// check path 1 against path 2
if (inStroke || inFill) {
intersections.push(pt)
} else {
/**
* no intersections found:
* check path1 sample points against path0
*/
let pt1 = path0.getPointAtLength(
(pathLength0 / checksPerPath) * c
);
let inStroke1 = path1.isPointInStroke(pt1);
let inFill1 = path1.isPointInFill(pt1);
if (inStroke1 || inFill1) {
intersections.push(pt1)
}
}
// just for benchmarking
checks++;
}
// reset styles
path0.style.fill = fill0;
path0.style.strokeWidth = strokeWidth0;
path1.style.fill = fill1;
path1.style.strokeWidth = strokeWidth1;
console.log('sample point checks:', checks);
return intersections;
}
/**
* simple performance test
*/
function perfStart() {
t0 = performance.now();
}
function perfEnd(text = "") {
t1 = performance.now();
total = t1 - t0;
console.log(`excecution time ${text}: ${total} ms`);
return total;
}
function renderPoint(
svg,
coords,
fill = "red",
r = "2",
opacity = "1",
id = "",
className = ""
) {
//console.log(coords);
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);
}
body {
font-family: sans-serif;
}
svg {
width: 100%;
}
path {
fill: none;
stroke: #000;
stroke-width: 1px;
}
p {
white-space: pre-line;
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 150">
<path id="p0" d="M27.357,21.433c13.373,3.432,21.433,17.056,18.001,30.43
c-3.432,13.374-17.057,21.434-30.43,18.002" />
<path id="p1" d="M80.652,80.414c-12.205,6.457-27.332,1.8-33.791-10.403
c-6.458-12.204-1.8-27.333,10.404-33.791" />
<path id="p2"
d="M159.28 40.26c6.73 12.06 2.41 27.29-9.65 34.01s-27.29 2.41-34.01-9.65s-2.41-27.29 9.65-34.01c12.06-6.73 27.29-2.41 34.01 9.65z" />
<path id="p3"
d="M191.27 53.72c-0.7 13.79-12.45 24.4-26.24 23.7s-24.4-12.45-23.7-26.24s12.45-24.4 26.24-23.7s24.4 12.45 23.7 26.24z" />
<path id="p4"
d="M259.28 40.26c6.73 12.06 2.41 27.29-9.65 34.01s-27.29 2.41-34.01-9.65s-2.41-27.29 9.65-34.01c12.06-6.73 27.29-2.41 34.01 9.65z" />
<path id="p5"
d="M291.27 53.72c-0.7 13.79-12.45 24.4-26.24 23.7s-24.4-12.45-23.7-26.24s12.45-24.4 26.24-23.7s24.4 12.45 23.7 26.24z" />
<path id="p6"
d="M359.28 40.26c6.73 12.06 2.41 27.29-9.65 34.01s-27.29 2.41-34.01-9.65s-2.41-27.29 9.65-34.01c12.06-6.73 27.29-2.41 34.01 9.65z" />
<path id="p7"
d="M420 53.72c-0.7 13.79-12.45 24.4-26.24 23.7s-24.4-12.45-23.7-26.24s12.45-24.4 26.24-23.7s24.4 12.45 23.7 26.24z" />
<g id="gInter"></g>
</svg>
<p>Based on @Netsi1964's codepen:
https://codepen.io/netsi1964/pen/yKagwx/</p>
<svg id="svg2" viewBox="0 0 2000 700">
<path d=" M 529 664 C 93 290 616 93 1942 385 C 1014 330 147 720 2059 70 C 1307 400 278 713 1686 691 " style="stroke:orange!important" stroke="orange" id="p8"/>
<path d=" M 1711 363 C 847 15 1797 638 1230 169 C 1198 443 1931 146 383 13 C 1103 286 1063 514 521 566 " id="p9"/>
<g id="gInter2"></g>
</svg>
<p><button onclick="check()">Check intersection</button></p>
<p id="time"></p>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.4/path-data-polyfill.min.js"></script>