Paper.js 不规则形状的元球效果

问题描述 投票:0回答:1

我的问题

我正在尝试使用 Paper.js 创建元球效果。虽然 Paper.js 网站Metaball demo)上有一个元球效果示例,但它侧重于圆形。然而,我的形状是不规则。我不确定如何用这些形状实现这种效果,或者是否可行。我的数学不够好,我可以编写一个逻辑函数来计算对象之间的形状。

虽然我知道我可以使用 SVG 滤镜来获得所需的结果,但这会光栅化效果,并且导出为 SVG将不再是一个选项。

这是我创建的 CodePen 演示。在此演示中,您可以移动形状,当满足阈值时,它们会以直线连接。

感兴趣的主要函数是

drawConnections

,它在两个分段点之间建立连接。分段的 Paper.js 文档可在
此处获取。其中一些方法和属性可能有助于实现元球效果。

demo的结构是这样的:

  • init()
     设置 Paper.js 并将 SVG 导入到画布上。
  • onSvgLoad(item)
     此函数在加载 SVG 时执行初始设置任务,例如缩放、添加到画布、片段提取和设置事件侦听器
  • drawLine(points, size)
    使用提供的点创建并返回简单路径(线)。
  • drawConnections(segments, threshold)
    :检查附近的线段并根据阈值距离绘制它们之间的连接。
  • extractSegments(item, includeCounterClockwise)
    从提供的 SVG 项目中提取段(点)。它还可以配置为排除逆时针路径段。
  • getAllObjects(item)
    从提供的项目中检索所有 SVG 对象(如路径、复合路径)。
  • addSegmentsToPath(item, add, includeCounterClockwise)
    根据指定的数量增加给定路径或复合路径中的段数(添加)。
  • onDragStart(item, event)
    确定单击了 SVG 的哪个子路径。
  • onDrag(item, event)
    根据拖动事件移动选定的路径并重绘连接。
  • onDragEnd(item, event)
    完成拖动操作,取消选择路径并更新视图。
这里是主函数

drawConnections

这个函数在
onDrag
函数中被调用:

.... function drawConnections(segments, threshold = 40) { for (const segment of segments) { for (const otherSegment of segments) { if (segment === otherSegment) continue; for (const seg of segment) { for (const otherSeg of otherSegment) { if (seg.point.getDistance(otherSeg.point) < threshold) { const path = drawLine([seg.point, otherSeg.point], 4); path.sendToBack(); path.removeOn({ drag: true, up: true }); } } } } } } ....

我创建的一些额外演示:

    在此演示中,每个段只能有一个连接
  1. 演示
  2. 元球效果是使用 SVG 滤镜创建的
  3. 演示
我感谢我得到的所有帮助。

javascript math canvas paperjs
1个回答
0
投票

免责声明:我根本不是 paper.js 方面的专家,所以可能有一个更简单的解决方案。


虽然我知道我可以使用 SVG 滤镜来获得所需的结果,但这会光栅化效果,并且导出为 SVG 将不再是一种选择。

是什么让您认为不能将 SVG 滤镜与 SVG 一起使用?这确实是它们被设计出来的目的。

由于您的过滤器是通过画布上的 CSS 添加的,因此 paper.js 无法导出它并不奇怪,但您完全可以自己将其添加到导出的
<svg>

// http://paperjs.org/reference/segment/ function init() { // Setup Paper paper.setup(document.getElementById("canvas")); // Load SVG const svg = document.getElementById("svg"); paper.project.importSVG(svg, { expandShapes: true, onLoad: onSvgLoad }); svg.style.display = "none"; } function onSvgLoad(item) { paper.project.clear(); // Show the anchors item.selected = true; // Scale the imported SVG to fit the view item.fitBounds(paper.view.bounds); // Scale item by 50% item.scale(0.5); // Add the imported SVG to the canvas paper.project.activeLayer.addChild(item); // Collect all objects const objects = getAllObjects(item); // Add more segments to all objects for (const object of objects) { addSegmentsToPath(object, 15, false); } // Extract segments (points) from the imported SVG item const segments = extractSegments(item, false); item.allCollectedSegments = segments; // add event listeners item.onMouseDown = (event) => onDragStart(item, event); item.onMouseDrag = (event) => onDrag(item, event); item.onMouseUp = (event) => onDragEnd(item, event); // Refresh the view paper.view.draw(); // Update view paper.view.update(); } function drawLine(points, size = 3){ const path = new paper.Path({ segments: points, strokeColor: "red", strokeWidth: size, closed: false, strokeCap: "round", strokeJoin: "round", opacity: 1, type: "connection", // Custom property }); return path; } // Check for close segments and draw the connection function drawConnections(segments, threshold = 40) { // First, clear existing connections clearExistingConnections(segments); for (const segmentGroup of segments) { for (const seg of segmentGroup) { if (seg.isConnected) continue; const closestSegment = findClosestSegment(seg, segments, segmentGroup); if (closestSegment && seg.point.getDistance(closestSegment.point) < threshold) { createConnection(seg, closestSegment); } } } } function clearExistingConnections(segments) { for (const segmentGroup of segments) { for (const seg of segmentGroup) { if (seg.connection) { if (seg.connection.segment2) { seg.connection.segment2.isConnected = false; seg.connection.segment2.connection = null; } seg.connection.remove(); seg.connection = null; seg.isConnected = false; } } } } function createConnection(seg1, seg2, steps = 40) { if (seg1.connection) { seg1.connection.segment2.isConnected = false; seg1.connection.remove(); } if (seg2.connection) { seg2.connection.segment1.isConnected = false; seg2.connection.remove(); } const path = drawLine([seg1.point, seg2.point], 9); path.sendToBack({ insert: true }); path.removeOn({ drag: true, up: false, down: true, move: false }); const midPoint = seg1.point.add(seg2.point).divide(2); const totalDistance = seg1.point.getDistance(seg2.point); const connectionGroup = new paper.Group(); seg1.connection = connectionGroup; seg2.connection = connectionGroup; seg1.isConnected = true; seg2.isConnected = true; connectionGroup.segment1 = seg1; connectionGroup.segment2 = seg2; } function findClosestSegment(targetSeg, segments, currentSegmentGroup) { let closestSegment = null; let closestDistance = Infinity; for (const segmentGroup of segments) { if (segmentGroup === currentSegmentGroup) continue; for (const seg of segmentGroup) { if (seg.isConnected) continue; const distance = targetSeg.point.getDistance(seg.point); if (distance < closestDistance) { closestDistance = distance; closestSegment = seg; } } } return closestSegment; } // Get all segments (points) from svg function extractSegments(item, includeCounterClockwise = true) { let segmentsList = []; // If the item itself is a direct path, return its segments in an array if (item instanceof paper.Path && item.segments) return [item.segments.slice()]; // If the item is a compound path, gather its path children's segments else if (item instanceof paper.CompoundPath && item.children) { let compoundSegments = []; for (let child of item.children) { if (child.clockwise === true && includeCounterClockwise === false) continue; if (child instanceof paper.Path && child.segments) compoundSegments.push(child.segments.slice()); } if (compoundSegments.length > 0) segmentsList.push(compoundSegments.flat()); } // If the item is a group and has children, recursively extract segments from each child else if (item instanceof paper.Group && item.children) { for (let child of item.children) { if (child.clockwise === true && includeCounterClockwise === false) continue; let childSegments = extractSegments(child, includeCounterClockwise); segmentsList = segmentsList.concat(childSegments); } } return segmentsList; } // Get all svg objects function getAllObjects(item) { let objects = []; // If the item itself is of the desired type, collect it if (item instanceof paper.Path) objects.push(item); // If the item is a compound path, gather it else if (item instanceof paper.CompoundPath) objects.push(item); // If the item is a group else if (item instanceof paper.Group && item.children) { for (let child of item.children) { const childObjects = getAllObjects(child); objects = objects.concat(childObjects); } } return objects; } // Add more segments (points) to svg function addSegmentsToPath(item, add = 10, includeCounterClockwise = false) { if (item instanceof paper.Path) { if (item.clockwise === true && includeCounterClockwise === false) return; let pathLength = item.length; let step = pathLength / (add + item.segments.length - 1); let iterations = Math.floor(pathLength / step); for (let i = 1; i <= iterations; i++) { let offset = i * step; item.divideAt(offset); } } else if (item instanceof paper.CompoundPath && item.children) { // Recursively process children of the compound path for (const child of item.children) { addSegmentsToPath(child, add, includeCounterClockwise); } } } // Event listeners function onDragStart(item, event) { for (const child of item.children) { if (child.hitTest(event.point)) { item.selectedPath = child; item.selectedPath.selected = true; break; } } } function onDrag(item, event) { if (item.selectedPath) { item.selectedPath.position = item.selectedPath.position.add(event.delta); } // Draw the connections drawConnections(item.allCollectedSegments, 60); // Refresh the view paper.view.draw(); } function onDragEnd(item, event) { if (item.selectedPath) { item.selectedPath.selected = true; item.selectedPath = null; } // Refresh the view paper.view.draw(); // Update view paper.view.update(); } // Init project window.onload = init; document.querySelector("button").onclick = (evt) => { // get the project as <svg> const exp = paper.project.exportSVG(); // add a clone of the <filter> in that <svg> exp.prepend(document.querySelector("#goo").cloneNode(true)); // set up the filter on the main <g> exp.querySelector("g").setAttribute("filter", "url(#goo)"); // do whatever with the exported <svg>, here we'll display it in an img. const markup = new XMLSerializer().serializeToString(exp); const blob = new Blob([markup], { type: "image/svg+xml" }); const img = document.querySelector("img").src = URL.createObjectURL(blob); };
canvas{ 
  border: solid 1px #1D1E22; 
  width: 800px; 
  height: 400px;
  -webkit-filter: url("#goo");
  filter: url("#goo");
}
#svg{ visibility: hidden; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/paper.js/0.12.17/paper-full.min.js"></script>
<button id="btn">export</button>
<svg id="svg" xmlns="http://www.w3.org/2000/svg">
<path d="M175.138 1.36292H156.138C152.538 1.36292 149.738 4.26291 149.738 7.76291V57.7629C149.738 61.2629 152.638 64.1629 156.138 64.1629H175.338C197.838 64.1629 208.938 51.3629 208.938 32.8629C208.938 14.3629 197.638 1.36292 175.138 1.36292ZM174.138 48.6629H166.738V17.5629H174.038C189.138 17.5629 191.538 24.5629 191.538 32.8629C191.538 41.1629 189.138 48.6629 174.138 48.6629Z" />
<path d="M104.138 0.462952C84.9378 0.462952 69.5378 15.063 69.5378 32.963C69.5378 50.863 84.9378 65.363 104.138 65.363C123.338 65.363 138.738 50.863 138.738 32.963C138.738 15.063 123.438 0.462952 104.138 0.462952ZM104.138 50.063C94.7378 50.063 87.0378 42.563 87.0378 32.963C87.0378 23.363 94.7378 15.763 104.138 15.763C113.538 15.763 121.338 23.763 121.338 32.963C121.338 42.163 113.538 50.063 104.138 50.063Z" />
<path d="M51.7258 1.36292H50.8258C44.4258 1.36292 43.7258 9.46291 43.2258 14.3629C42.8258 18.8629 42.4258 25.4629 37.6258 25.4629C28.5258 25.4629 20.8258 2.1629 8.42583 2.1629H8.22583C3.12583 2.1629 0.72583 4.66291 0.72583 11.4629V57.7629C0.72583 61.9629 3.32582 64.7629 7.62582 64.7629H8.42583C19.5258 64.7629 11.8258 39.2629 21.7258 39.2629C31.6258 39.2629 37.9258 64.0629 51.3258 64.0629H51.5258C56.0258 64.0629 58.5258 61.4629 58.5258 56.4629V8.76291C58.7258 4.46291 56.4258 1.36292 51.7258 1.36292Z" />
</svg>

<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
  <defs>
    <filter id="goo">
      <feGaussianBlur in="SourceGraphic" stdDeviation="4" result="blur" />
      <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 16 -8" result="goo" />
      <feComposite in="SourceGraphic" in2="goo" operator="atop"/>
    </filter>
  </defs>
</svg>

<canvas id="canvas" resize></canvas><br>
Exported SVG:<br>
<img>

© www.soinside.com 2019 - 2024. All rights reserved.