7

I need to intersect 2 SVG paths and get path with thier intersection.

It should work in browser or in Node.js (any of them, not both).
I need intersection, usage of clip-path isn't ok.
If by some reason intersection will use transform it's ok (I'll remove it myself).

I think there is a library for that, but I found only


For example intersecting following paths:

M 24.379464,51.504463 23.434524,23.156249 38.742559,12.572916 c 0,0 29.860118,-9.0714281 17.00893,0.755953 -12.851191,9.82738 13.229166,19.465774 13.229166,19.465774 z
m 32.883928,0.28869028 c 0,0 -15.686011,1.51190452 -8.504463,7.18154712 7.181546,5.6696426 50.270836,30.0491076 26.458332,42.3333336 -23.8125,12.284226 47.058036,14.174107 47.058036,14.174107 z

I want to get something like:

M 43.943359 11.123047 C 40.995759 11.900151 38.742188 12.572266 38.742188 12.572266 L 35.236328 14.996094 C 44.091432999999995 21.21816 55.052161 29.822765 57.455078 37.628906 L 66.939453 33.650391 63.632812 30.410156 C 58.77426 27.95814 52.364322 23.85552 52.214844 19.224609 L 43.943359 11.123047 z

Here is interactive snippet with paths from the example (ignore colors - they are for clarity) - need to get Intersection from #path1 and #path2:

svg { width: 10em; width: 100vmin; outline: 1px dotted blue; display: none; }
input { display: none; }
label { width: 10em; float: left; clear: left; cursor: pointer; line-height: 2em; margin: 0 .5em .25em 0; padding: 0 .25em; border: 1px solid; }
:checked + * + * + label { background: antiquewhite; color: blue; }
:checked + * + * + * + * + * + svg { display: inline-block; }
<input type=radio name=svg id=in checked>
<input type=radio name=svg id=out>
<input type=radio name=svg id=cp>

<label for=in>Input</label>
<label for=out>Intersection</label>
<label for=cp>Clip</label>

<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="22 0 76 64">
  <path id="path1"
    style="fill:rgba(255,0,0,.5); stroke:red;stroke-width:0.26458332px;"
    d="M 24.379464,51.504463 23.434524,23.156249 38.742559,12.572916 c 0,0 29.860118,-9.0714281 17.00893,0.755953 -12.851191,9.82738 13.229166,19.465774 13.229166,19.465774 z"
  />
  <path id="path2"
    style="fill:rgba(0,255,0,.5);stroke:green;stroke-width:0.26458332px;"
    d="m 32.883928,0.28869028 c 0,0 -15.686011,1.51190452 -8.504463,7.18154712 7.181546,5.6696426 50.270836,30.0491076 26.458332,42.3333336 -23.8125,12.284226 47.058036,14.174107 47.058036,14.174107 z"
  />
</svg>

<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="22 0 76 64">
  <path
    style="fill:rgba(0,0,255,.5);stroke:blue;stroke-width:0.26458332px;"
    d="M 43.943359 11.123047 C 40.995759 11.900151 38.742188 12.572266 38.742188 12.572266 L 35.236328 14.996094 C 44.091432999999995 21.21816 55.052161 29.822765 57.455078 37.628906 L 66.939453 33.650391 63.632812 30.410156 C 58.77426 27.95814 52.364322 23.85552 52.214844 19.224609 L 43.943359 11.123047 z"
  />
</svg>

<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="22 0 76 64">
  <clipPath id="clip2">
    <use xlink:href="#path2" />
  </clipPath>
  <use xlink:href="#path1" clip-path="url(#clip2)" />
</svg>

Example with Snap.svg:

var p1 = "M 24.379464,51.504463 23.434524,23.156249 38.742559,12.572916 c 0,0 29.860118,-9.0714281 17.00893,0.755953 -12.851191,9.82738 13.229166,19.465774 13.229166,19.465774 z"
var p2 = "m 32.883928,0.28869028 c 0,0 -15.686011,1.51190452 -8.504463,7.18154712 7.181546,5.6696426 50.270836,30.0491076 26.458332,42.3333336 -23.8125,12.284226 47.058036,14.174107 47.058036,14.174107 z"

var intersection = Snap.path.intersection(p1, p2)

console.log(intersection)
.as-console-wrapper.as-console-wrapper { max-height: 100vh }
<script src=//cdnjs.cloudflare.com/ajax/libs/snap.svg/0.5.1/snap.svg-min.js></script>

PS: Same question in Russian.

Bharata
  • 13,509
  • 6
  • 36
  • 50
Qwertiy
  • 19,681
  • 15
  • 61
  • 128
  • The second lib states its dependency to [tag:Snap.svg]. Maybe you have more luck with the original [Snap.path.intersection](http://snapsvg.io/docs/#Snap.path.intersection). – ccprog Apr 25 '18 at 01:07
  • 1
    May this question has an answer for you: https://stackoverflow.com/questions/11880739/javascript-curve-library-with-boolean-operations – philipp Apr 25 '18 at 08:01
  • @ccprog, seems like it returns point with some additional information. Can't find a way to get a `path` from them. – Qwertiy Apr 25 '18 at 08:26
  • @philipp, I don't want to convert paths to polygons as second answer suggests and paperjs from the other answer seem to give points, not path - isn't it? – Qwertiy Apr 25 '18 at 08:31
  • @ccprog, https://stackoverflow.com/q/43179268/4928642 – Qwertiy Apr 25 '18 at 08:35
  • @Qwertiy I think `paper.js` wont give points only. And it should also work on node… – philipp Apr 25 '18 at 08:39

2 Answers2

8

We can do it with PaperJS boolean operations, which can operate with SVG paths.

PaperJS has 5 different boolean operations: exclude, subtract, unite, intersect, divide and we will use one from them with the name intersect. This operations are also functions with the same name and they return item object, which has the function exportSVG(). It returns true SVG Path element, which has a new shape of both paths intersection.

Example of correct solution

paper.install(window);
window.onload = function()
{
    paper.setup('canvas');

    var p1 = 'M 24.379464,51.504463 23.434524,23.156249 38.742559,12.572916 c 0,0 29.860118,-9.0714281 17.00893,0.755953 -12.851191,9.82738 13.229166,19.465774 13.229166,19.465774 z',
        p2 = 'm 32.883928,0.28869028 c 0,0 -15.686011,1.51190452 -8.504463,7.18154712 7.181546,5.6696426 50.270836,30.0491076 26.458332,42.3333336 -23.8125,12.284226 47.058036,14.174107 47.058036,14.174107 z',
        path1 = new Path(p1),
        path2 = new Path(p2);

    path1.fillColor = 'rgba(255,0,0,.5)';
    path1.position = new Point(25, 25);

    path2.fillColor = 'rgba(0,255,0,.5)';
    path2.position = new Point(40, 25);
    
    var result = path2.intersect(path1);
    result.selected = true;
    result.fillColor = '#77f';

    //exportSVG() docu: http://paperjs.org/reference/item/#exportsvg
    var svgPathElement = result.exportSVG(),
        dPath = svgPathElement.getAttribute('d');
    
    document.querySelector('path').setAttribute('d', dPath);

    var output = document.querySelector('#output');
    output.innerHTML = '<pre>' + dPath + '</pre>';
    output.innerHTML += '<xmp>' + svgPathElement.outerHTML + '</xmp>';
};
table
{
    margin-left:14px;
    padding-left:14px;
    border-left:1px solid gray;
    display:inline-block
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.0/paper-full.min.js"></script>

<canvas id="canvas" width="75" height="75" resize></canvas>
<table><tr><td><b>Our new shape of both paths intersection in separate SVG:</b></td></tr>
<tr><td>
    <svg width="75" height="75" viewBox="0 0 75 75">
    <path fill="rgba(0,0,255,.5)" d=""/>
    </svg>
</td></tr></table>

<div id="output"></div>

Useful links:

Bharata
  • 13,509
  • 6
  • 36
  • 50
1

Snap.svg seems to have the basic functions needed to do this. Some of them are be a bit akward, but this might be the direction to go. Please take this as pseudocode and not as working as-is:

// returns something like [ [ "M", 1, 2 ], ["C", 3, 4, 5, 6, 7, 8 ], ... ]
// and has a custom .toString() method
segList1 = Snap.path.toCubic(p1)
segList2 = Snap.path.toCubic(p2)

intersections = Snap.path.intersection(p1, p2)

// handle the subpaths between neighbouring intersections
intersectionPaths = intersections.map((point, id) => {
    from = point
    to = intersections[id + 1 < intersections.length ? id + 1 : 0]
    return pathBetweenIntersections(from, to)
})

// return the two paths between two neighbouring intersection points
function pathBetweenIntersections (from, to) {
    // list the segments between the intersection points on first path
    // TODO: handle cases when from.segment1 >= to.segment1
    subSegList1 = segList1.slice(from.segment1 + 1, to.segment1)

    // first segment has the intersection point
    startSegments = [ from.bez1.slice(0, 2).unshift('M'), from.bez1.slice(2, 8).unshift('C') ]
    startString1 = Snap.path.toCubic(startSegments).toString()
    // construct a path element from the segment
    startPath1 = Paper.path(startString1)
    startPathLength1 = startPath1.getTotalLength()
    // get the relevant subpath from intersection point to end
    startPathString1 = startPath1.getSubpath(from.t1 * startPathLength1, startPathLength1)
    subSegList1 = Snap.path.toCubic(startPathString1).concat(subSegList1)

    // and the same for the last segment
    endSegments = [ to.bez1.slice(0, 2).unshift('M'), to.bez1.slice(2, 8).unshift('C') ]
    endString1 = Snap.path.toCubic(endSegments).toString()
    endPath1 = Paper.path(endString1)
    endPathLength1 = endPath1.getTotalLength()
    endPathString1 = endPath1.getSubpath(0, to.t1 * endPathString1)
    subSegList1.push(Snap.path.toCubic(endPathString1)[1])

    subSegList2 = // do the same for segList2

    return {
        p1: Snap.path.toCubic(subSegList1).toString(),
        p2: Snap.path.toCubic(subSegList2).toString()
    }
}
ccprog
  • 20,308
  • 4
  • 27
  • 44
  • What's the purpoise of using `toCubic`? – Qwertiy Apr 25 '18 at 16:34
  • It takes as argument: either a path string, or an array of segments. It returns: an array of segments, but the array has a custom `.toString()` method that returns a correctly formatted path string. (That last fact is not [documented](http://snapsvg.io/docs/#Snap.path.toCubic), but I found it works.) – ccprog Apr 25 '18 at 17:32
  • In addition, I suspect you might need to prepend the starting point in the form of `['M', ?, ?]` to newly-constructed segment lists. – ccprog Apr 25 '18 at 18:00