Merging two bezier-based shapes into one to create a new outline
Asked Answered
S

1

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

Settles answered 11/2, 2022 at 8:38 Comment(1)
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?Roos
B
4

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/[email protected]/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

Barron answered 11/2, 2022 at 14:48 Comment(5)
The last link (Subtracting SVG paths programmatically) is brokenRoos
@Michael Rovinsky: pardon me - link is fixed!Barron
@Barron 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.Settles
@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?Barron
@herrstrietzel. That is exactly what I was looking for. Thank you!Settles

© 2022 - 2024 — McMap. All rights reserved.