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 将全部转换为通用路径。
您可以使用paper.js来完成此任务。
\n以下示例还使用了Jarek Foksa 的 pathData polyfill。
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\nsvg {\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至少在使用 paper.js布尔运算时,我们需要将 svg 原语 ( <rect>, <circle>, <polygon>)转换
为元素 \xe2\x80\x93 。\n对于本机创建为 paper.js 对象的形状,不需要此步骤。<path>
pathData polyfill 提供了一种标准化 svg 元素的方法。
\n此标准化将输出一个d属性(对于每个选定的 svg 子元素),仅包含一组缩减的立方路径命令(M、C、L、Z) \xe2\x80\x93,全部基于绝对坐标。
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一般来说,这是一个不平凡的问题。
如果您可以接受将形状光栅化为像素,在那里执行布尔运算,然后使用行进方块+简化将结果矢量化回结果,则可以轻松解决(少量代码)。
计算某种精确*几何结果的已知算法相当复杂,并且难以在保持快速的同时正确实现。
Clipper是一个易于使用的库,可以在 C++ 中执行此类计算,并可移植到 Javascript。
请注意,困难的是正确处理边缘情况(例如,当输入线部分重叠或顶点恰好落在线上以及结果包含零面积部分时)。
仅推理交叉明确情况的代码编写起来要容易得多,但不幸的是,当这些边缘情况确实发生时,可能会产生宏观错误的结果。
浮点数学太难以预测,无法用于这些计算...请参阅https://hal.inria.fr/inria-00344310/document来详细讨论使用浮点时会出现的问题类型精确几何计算的数学。
即使是一个“简单”的方程,比如判断三个点是否共线、顺时针还是逆时针的方程,在用浮点数学计算时也会表现得很疯狂……(来自论文的图片)
(*) 即使在理论上使用浮点数时,真正精确的解也是不可能的:具有整数坐标的两个线段的交点的坐标通常不能用浮点数精确表示;因此,任何浮点结果(无论如何计算)都将是近似值,因为正确的结果需要精确的有理数。例如,Clipper 提供的是快速计算和保证“接近”精确结果的结果(即避免宏观误差;它们仍然可能包含小的不准确度,甚至小的拓扑误差)。