以编程方式减去 SVG 路径

Tud*_*scu 7 javascript math svg

我正在尝试找到一种从另一个 SVG 路径中减去 SVG 路径的方法,类似于反向剪辑蒙版。我无法使用过滤器,因为我需要找到复合路径与其他路径的交点。Illustrator 使用“减号前端”探路工具来实现此目的,如下所示:

减去前面之前

减去前面之后

红方块相减前的路径:

<rect class="cls-1" x="0.5" y="0.5" width="184.93" height="178.08"/>
Run Code Online (Sandbox Code Playgroud)

减法后:

<polygon class="cls-1" points="112.83 52.55 185.43 52.55 185.43 0.5 0.5 0.5 0.5 178.58 112.83 178.58 112.83 52.55"/>
Run Code Online (Sandbox Code Playgroud)

我需要它可以处理所有类型的形状,包括曲线。如果重要的话,输入的 SVG 将全部转换为通用路径。

her*_*zel 6

您可以使用paper.js来完成此任务。
\n以下示例还使用了Jarek Foksa 的 pathData polyfill。

\n

paper.js 示例

\n

\r\n
\r\n
var svg = document.querySelector("#svgSubtract");\n// set auto ids for processing\nfunction setAutoIDs(svg) {\n  let svgtEls = svg.querySelectorAll(\n    "path, polygon, rect, circle, line, text, g"\n  );\n  svgtEls.forEach(function(el, i) {\n    if (!el.getAttribute("id")) {\n      el.id = el.nodeName + "-" + i;\n    }\n  });\n}\nsetAutoIDs(svg);\n\n\nfunction shapesToPath(svg) {\n  let els = svg.querySelectorAll(\'rect, circle, polygon\');\n  els.forEach(function(el, i) {\n    let className = el.getAttribute(\'class\');\n    let id = el.id;\n    let d = el.getAttribute(\'d\');\n    let fill = el.getAttribute(\'fill\');\n    let pathData = el.getPathData({\n      normalize: true\n    });\n    let pathTmp = document.createElementNS("http://www.w3.org/2000/svg", \'path\');\n    pathTmp.id = id;\n    pathTmp.setAttribute(\'class\', className);\n    pathTmp.setAttribute(\'fill\', fill);\n    pathTmp.setPathData(pathData);\n    svg.insertBefore(pathTmp, el);\n    el.remove();\n\n  })\n};\n\nshapesToPath(svg);\n\n\nfunction subtract(svg) {\n  // init paper.js and add mandatory canvas\n  canvas = document.createElement(\'canvas\');\n  canvas.id = "canvasPaper";\n  canvas.setAttribute(\'style\', \'display:none\')\n  document.body.appendChild(canvas);\n  paper.setup("canvasPaper");\n\n  let all = paper.project.importSVG(svg, function(item, i) {\n    let items = item.getItems();\n    // remove first item not containing path data\n    items.shift();\n    // get id names for selecting svg elements after processing\n    let ids = Object.keys(item._namedChildren);\n\n    if (items.length) {\n      let lastEl = items[items.length - 1];\n      // subtract paper.js objects\n      let subtracted = items[0].subtract(lastEl);\n      // convert subtracted paper.js object to svg pathData\n      let subtractedData = subtracted\n        .exportSVG({\n          precision: 3\n        })\n        .getAttribute("d");\n      let svgElFirst = svg.querySelector(\'#\' + ids[0]);\n      let svgElLast = svg.querySelector(\'#\' + ids[ids.length - 1]);\n      // overwrite original svg path\n      svgElFirst.setAttribute("d", subtractedData);\n      // delete subtracted svg path\n      svgElLast.remove();\n    }\n  });\n}
Run Code Online (Sandbox Code Playgroud)\r\n
svg {\n  display: inline-block;\n  width: 25%;\n  border: 1px solid #ccc\n}
Run Code Online (Sandbox Code Playgroud)\r\n
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.3/path-data-polyfill.min.js"></script>\n<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.0/paper-full.min.js"></script>\n\n<p>\n  <button type="button" onclick="subtract(svg)">Subtract Path </button>\n</p>\n<svg id="svgSubtract" viewBox="0 0 100 100">\n        <rect class="cls-1" x="0" y="0" width="80" height="80" fill="red" />\n        <path d="M87.9,78.7C87.9,84,86,88,82.2,91c-3.8,2.9-8.9,4.4-15.4,4.4c-7,0-12.5-0.9-16.2-2.7v-6.7c2.4,1,5.1,1.8,8,2.4\n        c2.9,0.6,5.7,0.9,8.5,0.9c4.6,0,8.1-0.9,10.4-2.6c2.3-1.7,3.5-4.2,3.5-7.3c0-2.1-0.4-3.7-1.2-5.1c-0.8-1.3-2.2-2.5-4.1-3.6\n        c-1.9-1.1-4.9-2.4-8.8-3.8c-5.5-2-9.5-4.3-11.8-7c-2.4-2.7-3.6-6.2-3.6-10.6c0-4.6,1.7-8.2,5.2-10.9c3.4-2.7,8-4.1,13.6-4.1\n        c5.9,0,11.3,1.1,16.3,3.2l-2.2,6c-4.9-2.1-9.7-3.1-14.3-3.1c-3.7,0-6.5,0.8-8.6,2.4c-2.1,1.6-3.1,3.8-3.1,6.6\n        c0,2.1,0.4,3.7,1.1,5.1c0.8,1.3,2,2.5,3.8,3.6c1.8,1.1,4.6,2.3,8.3,3.6c6.2,2.2,10.5,4.6,12.9,7.1C86.7,71.4,87.9,74.7,87.9,78.7z"\n        />\n</svg>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

路径标准化(使用 getPathData() polyfill)

\n

至少在使用 paper.js布尔运算时,我们需要将 svg 原语 ( <rect>, <circle>, <polygon>)转换
为元素 \xe2\x80\x93 。\n对于本机创建为 paper.js 对象的形状,不需要此步骤。<path>

\n

pathData polyfill 提供了一种标准化 svg 元素的方法。
\n此标准化将输出一个d属性(对于每个选定的 svg 子元素),仅包含一组缩减的立方路径命令(M、C、L、Z) \xe2\x80\x93,全部基于绝对坐标。

\n

示例2(多个元素相减)

\n

\r\n
\r\n
const svg = document.querySelector("#svgSubtract");\nconst btnDownload = document.querySelector("#btnDownload");\nconst decimals = 1;\n// set auto ids for processing\nfunction setAutoIDs(svg) {\n  let svgtEls = svg.querySelectorAll(\n    "path, polygon, rect, circle, line, text, g"\n  );\n  svgtEls.forEach(function(el, i) {\n    if (!el.getAttribute("id")) {\n      el.id = el.nodeName + "-" + i;\n    }\n  });\n}\nsetAutoIDs(svg);\n\n\nfunction shapesToPathMerged(svg) {\n  let els = svg.querySelectorAll(\'path, rect, circle, polygon, ellipse \');\n  let pathsCombinedData = \'\';\n  let className = els[1].getAttribute(\'class\');\n  let id = els[1].id;\n  let d = els[1].getAttribute(\'d\');\n  let fill = els[1].getAttribute(\'fill\');\n\n  els.forEach(function(el, i) {\n    let pathData = el.getPathData({\n      normalize: true\n    });\n    if (i == 0 && el.nodeName.toLowerCase() != \'path\') {\n      let firstTmp = document.createElementNS("http://www.w3.org/2000/svg", \'path\');\n      let firstClassName = els[1].getAttribute(\'class\');\n      let firstId = el.id;\n      let firstFill = el.getAttribute(\'fill\');\n      firstTmp.setPathData(pathData);\n      firstTmp.id = firstId;\n      firstTmp.setAttribute(\'class\', firstClassName);\n      firstTmp.setAttribute(\'fill\', firstFill);\n      svg.insertBefore(firstTmp, el);\n      el.remove();\n    }\n    if (i > 0) {\n      pathData.forEach(function(command, c) {\n        pathsCombinedData += \' \' + command[\'type\'] + \'\' + command[\'values\'].join(\' \');\n      });\n      el.remove();\n    }\n  })\n  let pathTmp = document.createElementNS("http://www.w3.org/2000/svg", \'path\');\n  pathTmp.id = id;\n  pathTmp.setAttribute(\'class\', className);\n  pathTmp.setAttribute(\'fill\', fill);\n  pathTmp.setAttribute(\'d\', pathsCombinedData);\n  svg.insertBefore(pathTmp, els[0].nextElementSibling);\n};\n\nshapesToPathMerged(svg);\n\n\nfunction subtract(svg) {\n  // init paper.js and add mandatory canvas\n  canvas = document.createElement(\'canvas\');\n  canvas.id = "canvasPaper";\n  canvas.setAttribute(\'style\', \'display:none\')\n  document.body.appendChild(canvas);\n  paper.setup("canvasPaper");\n\n  let all = paper.project.importSVG(svg, function(item, i) {\n    let items = item.getItems();\n    // remove first item not containing path data\n    items.shift();\n    // get id names for selecting svg elements after processing\n    let ids = Object.keys(item._namedChildren);\n\n    if (items.length) {\n      let lastEl = items[items.length - 1];\n      // subtract paper.js objects\n      let subtracted = items[0].subtract(lastEl);\n      // convert subtracted paper.js object to svg pathData\n      let subtractedData = subtracted\n        .exportSVG({\n          precision: decimals\n        })\n        .getAttribute("d");\n      let svgElFirst = svg.querySelector(\'#\' + ids[0]);\n      let svgElLast = svg.querySelector(\'#\' + ids[ids.length - 1]);\n      // overwrite original svg path\n      svgElFirst.setAttribute("d", subtractedData);\n      // delete subtracted svg path\n      svgElLast.remove();\n    }\n  });\n  // get data URL\n  getdataURL(svg)\n\n}\n\nfunction getdataURL(svg) {\n  let markup = svg.outerHTML;\n  markupOpt = \'data:image/svg+xml;utf8,\' + markup.replaceAll(\'"\', \'\\\'\').\n  replaceAll(\'\\t\', \'\').\n  replaceAll(\'\\n\', \'\').\n  replaceAll(\'\\r\', \'\').\n  replaceAll(\'></path>\', \'/>\').\n  replaceAll(\'<\', \'%3C\').\n  replaceAll(\'>\', \'%3E\').\n  replaceAll(\'#\', \'%23\').\n  replaceAll(\',\', \' \').\n  replaceAll(\' -\', \'-\').\n  replace(/ +(?= )/g, \'\');\n\n  let btn = document.createElement(\'a\');\n  btn.href = markupOpt;\n  btn.innerText = \'Download Svg\';\n  btn.setAttribute(\'download\', \'subtracted.svg\');\n  document.body.insertAdjacentElement(\'afterbegin\', btn);\n  return markupOpt;\n}
Run Code Online (Sandbox Code Playgroud)\r\n
<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.0/paper-full.min.js"></script>\n<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.3/path-data-polyfill.min.js"></script>\n\n\n<p>\n  <button type="button" onclick="subtract(svg)">Subtract Path </button>\n</p>\n<svg id="svgSubtract" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">\n        <rect class="cls-1" x="0" y="0" width="80" height="80" fill="red" />\n        <path id="s"\n            d="M87.9,78.7C87.9,84,86,88,82.2,91c-3.8,2.9-8.9,4.4-15.4,4.4c-7,0-12.5-0.9-16.2-2.7v-6.7c2.4,1,5.1,1.8,8,2.4\n        c2.9,0.6,5.7,0.9,8.5,0.9c4.6,0,8.1-0.9,10.4-2.6c2.3-1.7,3.5-4.2,3.5-7.3c0-2.1-0.4-3.7-1.2-5.1c-0.8-1.3-2.2-2.5-4.1-3.6\n        c-1.9-1.1-4.9-2.4-8.8-3.8c-5.5-2-9.5-4.3-11.8-7c-2.4-2.7-3.6-6.2-3.6-10.6c0-4.6,1.7-8.2,5.2-10.9c3.4-2.7,8-4.1,13.6-4.1\n        c5.9,0,11.3,1.1,16.3,3.2l-2.2,6c-4.9-2.1-9.7-3.1-14.3-3.1c-3.7,0-6.5,0.8-8.6,2.4c-2.1,1.6-3.1,3.8-3.1,6.6\n        c0,2.1,0.4,3.7,1.1,5.1c0.8,1.3,2,2.5,3.8,3.6c1.8,1.1,4.6,2.3,8.3,3.6c6.2,2.2,10.5,4.6,12.9,7.1C86.7,71.4,87.9,74.7,87.9,78.7z" />\n\n        <path id="o" d="M30.2,22.4c0,8.3-5.8,12-11.2,12c-6.1,0-10.8-4.5-10.8-11.6c0-7.5,4.9-12,11.2-12C25.9,10.8,30.2,15.5,30.2,22.4z\n        M12.4,22.6c0,4.9,2.8,8.7,6.8,8.7c3.9,0,6.8-3.7,6.8-8.7c0-3.8-1.9-8.7-6.7-8.7C14.5,13.8,12.4,18.3,12.4,22.6z" />\n        <circle cx="50%" cy="50%" r="10%"></circle>\n    </svg>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

  • @Tudor Popescu:不客气!paper.js 有一些非常棒的功能...老实说,当涉及到文档和示例片段时,它不是我最喜欢的。然而,它的几何精度确实令人印象深刻(与人工智能相当)。您可能还对这个通过获取相交路径进行剪切的示例感兴趣:[根据clipPath元素更改要剪切的SVG路径](/sf/ask/4918920051/根据剪辑路径元素进行剪辑/70278985#70278985) (2认同)

650*_*502 5

一般来说,这是一个不平凡的问题。

如果您可以接受将形状光栅化为像素,在那里执行布尔运算,然后使用行进方块+简化将结果矢量化回结果,则可以轻松解决(少量代码)。

计算某种精确*几何结果的已知算法相当复杂,并且难以在保持快速的同时正确实现。

Clipper是一个易于使用的库,可以在 C++ 中执行此类计算,并可移植到 Javascript。

请注意,困难的是正确处理边缘情况(例如,当输入线部分重叠或顶点恰好落在线上以及结果包含零面积部分时)。

仅推理交叉明确情况的代码编写起来要容易得多,但不幸的是,当这些边缘情况确实发生时,可能会产生宏观错误的结果。

浮点数学太难以预测,无法用于这些计算...请参阅https://hal.inria.fr/inria-00344310/document来详细讨论使用浮点时会出现的问题类型精确几何计算的数学。

即使是一个“简单”的方程,比如判断三个点是否共线、顺时针还是逆时针的方程,在用浮点数学计算时也会表现得很疯狂……(来自论文的图片)

三点方向的颜色编码图


(*) 即使在理论上使用浮点数时,真正精确的解也是不可能的:具有整数坐标的两个线段的交点的坐标通常不能用浮点数精确表示;因此,任何浮点结果(无论如何计算)都将是近似值,因为正确的结果需要精确的有理数。例如,Clipper 提供的是快速计算和保证“接近”精确结果的结果(即避免宏观误差;它们仍然可能包含小的不准确度,甚至小的拓扑误差)。