我的问题
我正在尝试使用 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 });
}
}
}
}
}
}
....
我创建的一些额外演示:
我感谢我得到的所有帮助。
免责声明:我根本不是 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>