4

Lets say I have the data to render two overlaying bezier-based shapes, that are overlapping, displayed in a svg or on the canvas (doesn't really matter where). I would like to calculate the outline of the shape resulting from the merge of the two shapes, so that I have a clean (new) outline and as few nodes and handles as possible. I would like to achieve the effect that vector programs like adobe illustrator offers with Pathfinder > Add or the font program glyphs with Remove Overlap. Example: https://helpx.adobe.com/illustrator/using/combining-objects.html

Is there possible a library or a concept for that task? I am working with javascript in the browser, but any other source for how to make such a calculation would help as well.

It is also important, that this calculation happens before the rendering an agnostic to the rendered result (be it svg/canvas).

In the illustration bellow, on the left side is the input shape. and on the right side the expected result. I have the data, meaning all the nodes and handles (from the bezier curve) and I would like to calculate the coordinates of the red (nodes) and green dots (handles) on the right side.

Illustrating expected outcome

Philipp
  • 105
  • 6
  • To be more specific, do you need a function that finds the intersection point between a line and a quadratic bezier curve (like in the picture) or something else? – Michael Rovinsky Feb 11 '22 at 13:56

1 Answers1

3

Paper.js might be the perfect library for this task:
In particular it's Boolean operations – like unite() to merge path elements. The syntax looks something like this:

let unitedPath = path1.unite(path2);  

The following example also employs Jarek Foksa's pathData polyfill.

Example: unite paths:

/**
 * merge paths
 */
function unite(svg, decimals = 3) {
  let paths = svg.querySelectorAll("path");
  let path0 = paths[0];
  let d0 = path0.getAttribute("d");
  // create new paper.js path object
  let paperPath0 = new Path(d0);

  for (let i = 1; i < paths.length; i++) {
    let pathI = paths[i];
    let dI = pathI.getAttribute("d");
    // create new paper.js path object for all children
    let paperPathI = new Path(dI);
    paperPath0 = paperPath0.unite(paperPathI);
    pathI.remove();
  }

  let dUnited = paperPath0
    .exportSVG({
      precision: 3
    })
    .getAttribute("d");
  path0.setAttribute("d", dUnited);
}

// init paper.js
window.addEventListener("DOMContentLoaded", (e) => {
  initPaper();
});

// init paper.js and add mandatory canvas
function initPaper() {
  canvas = document.createElement("canvas");
  canvas.id = "canvasPaper";
  canvas.setAttribute("style", "display:none");
  document.body.appendChild(canvas);
  paper.install(window);
  paper.setup("canvasPaper");
}
svg {
  display: inline-block;
  width: 10em
}

svg * {
  fill: none;
  stroke: red;
  stroke-width: 0.25%;
}
<p>
  <button type="button" onclick="unite(svg, 3)">Unite Path </button>
</p>

<svg class="svgunite" id="svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" stroke-width="1" stroke="#000">
<path fill="none" d="M50.05 23.21l-19.83 61.51h-9.27l23.6-69.44h10.82l23.7 69.44h-9.58l-20.44-61.51h1z"/>
<rect fill="none" x="35.49" y="52.75" width="28.5" height="6.17">
</rect>
</svg>


<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.0/paper-full.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.4/path-data-polyfill.min.js"></script>


<script>
  /**
   * convert all primitives to paths
   * like <rect>, <circle> etc
   */
  convertPrimitives(svg);

  function convertPrimitives(svg) {
    let els = svg.querySelectorAll("path, rect, circle, polygon, ellipse ");
    let pathDataCombined = [];
    let className = els[0].getAttribute("class") ?
      els[0].getAttribute("class") :
      "";
    let id = els[0].id;
    let fill = els[0].getAttribute("fill");
    els.forEach(function(el, i) {
      let pathData = el.getPathData({
        normalize: true
      });
      // create path for conversion
      let pathTmp = document.createElementNS(
        "http://www.w3.org/2000/svg",
        "path"
      );
      pathTmp.id = id;
      pathTmp.setAttribute("class", className);
      pathTmp.setAttribute("fill", fill);
      pathTmp.setPathData(pathData);
      el.replaceWith(pathTmp);
    });
  }
</script>

Optional: Path normalization (using getPathData() polyfill)

You might also need to convert svg primitives (<rect>, <circle>, <polygon>) like the horizontal stroke in the capital A .

The pathData polyfill provides a method of normalizing svg elements.
This normalization will output a d attribute (for every selected svg child element) containing only a reduced set of cubic path commands (M, C, L, Z) – all based on absolute coordinates.

Little downer:
I won't say paper.js can boast of a plethora of tutorials or detailed examples. But you might check the reference for pathItem to see all options.

See also: Subtracting SVG paths programmatically

herrstrietzel
  • 11,541
  • 2
  • 12
  • 34
  • The last link (Subtracting SVG paths programmatically) is broken – Michael Rovinsky Feb 11 '22 at 15:12
  • @Michael Rovinsky: pardon me - link is fixed! – herrstrietzel Feb 11 '22 at 15:14
  • @herrstrietzel Thank you for your example. Appreciate you took the time to even illustrate my example. That is definitely the right path. Only one slight difference. The initial shapes in my example are not supposed to be rendered, but should be transformed directly and then rendered, therefore I never have it available as an svg. (FYI: The input is generated as data through other processes). But if I can find out how I turn the shapes data into the format of whatever `let items = item.getItems(); ` is in your example, I should be able to get the same output! I will test it over the weekend. – Philipp Feb 11 '22 at 20:57
  • 1
    @Philipp: Actually paper.js is rather focused on creating vector objects from scratch. Therefore you could create elements supposed to be merged in a "headless" non-rendered mode. This answer might give some good hints how to achieve this: [How to find a new path (shape) from intersection of SVG paths?](https://stackoverflow.com/questions/50011373/how-to-find-a-new-path-shape-from-intersection-of-svg-paths) – herrstrietzel Feb 12 '22 at 01:01
  • @herrstrietzel. That is exactly what I was looking for. Thank you! – Philipp Feb 12 '22 at 10:18