Metaballs是有机的黏糊糊的黏糊糊的东西。从数学的角度来看,它们是一个等值面。可以用一个数学公式来表示: f(x,y,z) = r / ((x - x0)2 + (y - y0)2 + (z - z0)2)
。@Jamie Wong写了一篇非常 优秀的教程 ,介绍了怎么使用Canvas来渲染Metaballs。
我们可以在一个元素中使用模糊和滤镜在CSS和SVG中复制Metaball效果。比如@chris Gannon写的一个泡泡滑块的效果:
SVG Metaball
我发现了另一种方法,使用 Paper.js 可以实现这种效果。在编写代码的时代, @Hiroyuki Sato 通过一个脚本和Adobe Illustrator生成一个Gooey Blobs的效果。与以前的技术不同的是,这并没有像素的渲染或依赖于过滤器特性。相反,它将两个圆与个膜(membrane)相连。也就是说,我们可以将整个块作为路径生成。比如@Amoebal在Codepen上写的这个示例,就采用了这种技术。
在这篇文章中,我将分解Metaball效果实现所需要的步骤。我将通过一个叫做 metaball
的函数来生成下面所看到的黑色阴影路径。这包括连接器加上第二个圆的一部分。
创建Metaball
要想弄清楚连接器触到两个圆的位置,我们先定位两个接触圆的切线。这是连接器的最宽处。顺便说一下,当圆圈没有重叠时,就集中在这个例子上:
我们可以使用Spread计算出最大的角度:
const maxSpread = Math.acos((radius1 - radius2) / d);
为什么是这样呢?我花了一段时间才弄明白。我试着解释一下,但是你可能看到 这个外部切线的分步说明 ,会更易理解。
这是连接器可以拥有的最大可能的扩展。我们可以通过将其与一个叫做 v
的因子相乘来控制传播量。JavaScript的代码是 v=0.5
。这样似乎更有效。
小圆的传播(Spread)是 (Math.PI - maxSpread) * v
。这主要是因为一个多边形的对角的和总是 180°
。
接下来咱们需要找到这四个点的位置。我们知道圆的中心( center1
和 center2
)和半径( radius1
和 radius2
)。因此,我们只需要处理角度,然后使用极坐标将其转换为 (x,y)
值。
const angleBetweenCenters = angle(center2, center1); const maxSpread = Math.acos((radius1 - radius2) / d); // 圆1(左) const angle1 = angleBetweenCenters + maxSpread * v; const angle2 = angleBetweenCenters - maxSpread * v; // 圆2(右) const angle3 = angleBetweenCenters + (Math.PI - (Math.PI - maxSpread) * v); const angle4 = angleBetweenCenters - (Math.PI - (Math.PI - maxSpread) * v);
角度需要顺时针测量。因此,对于第二个圆圈,需要把它从 Math.PI
中减去。我们添加了 angleBetweenCenters
,然后将极坐标转换为笛卡尔坐标。
// 点 const p1 = getVector(center1, angle1, radius1); const p2 = getVector(center1, angle2, radius1); const p3 = getVector(center2, angle3, radius2); const p4 = getVector(center2, angle4, radius2);
要将梯形形状的连接器转换成弯曲的连接器,需要将手柄添加到所有的四个点上。这个过程的下一部分是计算手柄的位置。
一个特定的句柄应该对齐到那个点上的圆切七。再次使用极坐标来定位手柄。但这一次,它将与这一点本身有关。
AB
和 BC
是垂直的,因为 AB
是一个圆的半径,而 BC
是该圆的切线。因此 handle1
的角度是 angle1 - Math.PI / 2
。同样,我们可以计算出其他三个手柄的角度值。
把手的长度是相对于圆的半径而言的。例如, handle1
的长度是 radius1 * d2
。现在我们可以这样计算手柄的位置。
const totalRadius = radius1 + radius2; // 处理手柄长度的因子 const d2 = Math.min(v * handleSize, dist(p1, p3) / totalRadius); // 手柄长度 const r1 = radius1 * d2; const r2 = radius2 * d2; const h1 = getVector(p1, angle1 - HALF_PI, r1); const h2 = getVector(p2, angle2 + HALF_PI, r1); const h3 = getVector(p3, angle3 + HALF_PI, r2); const h4 = getVector(p4, angle4 - HALF_PI, r2);
我们拥有根建SVG的 path
的所有点。 path
由三部分组成:从 point1
到 point3
的曲线,从 point3
到 point4
的弧线和 point4
到 point2
的曲线。
function metaballToPath(p1, p2, p3, p4, h1, h2, h3, h4, escaped, r) { return [ 'M', p1, 'C', h1, h3, p3, 'A', r, r, 0, escaped ? 1 : 0, 0, p4, 'C', h4, h3, p4, ].join(' '); }
圆的重叠
我们有一个胶粘的Metaball!但你会注意到,当圆圈开始重叠时, path
会变得很怪异。我们可以通过扩大这个比例来解决这个问题,这个比例要与圆圈重叠的程度成比例。
可以利用 u1
和 u2
来控制扩容。可以使用余弦定理来计算。
u1 = Math.acos( (radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d), ); u2 = Math.acos( (radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d), );
但是要怎么处理这些,说实话,我也不知道如何。我所知道的就是,随着圆圈越来越近,它会扩展开来,一旦 circle2
完全在 circle1
内时,它就会坍塌。
const angle1 = angleBetweenCenters + u1 + (maxSpread - u1) * v; const angle2 = angleBetweenCenters - (u1 + (maxSpread - u1) * v); const angle3 = angleBetweenCenters + Math.PI - u2 - (Math.PI - u2 - maxSpread) * v; const angle4 = angleBetweenCenters - (Math.PI - u2 - (Math.PI - u2 - maxSpread) * v);
最后一个关键就是重叠的圆。手柄的长度也与圆的距离成比例。
// 通过曲线两端之间的距离来定义手柄长度 const totalRadius = radius1 + radius2; const d2Base = Math.min(v * handleSize, dist(p1, p3) / totalRadius); // 圆圈重叠时 const d2 = d2Base * Math.min(1, d * 2 / (radius1 + radius2)); const r1 = radius1 * d2; const r2 = radius2 * d2;
总结
这是最终的结果和Metaball的整个代码。试着用不同的手形和不同的值来处理它,看看它是如何影响连器的形状的。在代码第70行中有许多令人惊讶的小细节。在@Hiroyuki Sato的作品中,我学到了很多东西。
/** * Based on Metaball script by Hiroyuki Sato * http://shspage.com/aijs/en/#metaball */ function metaball(radius1, radius2, center1, center2, handleSize = 2.4, v = 0.5) { const HALF_PI = Math.PI / 2; const d = dist(center1, center2); const maxDist = radius1 + radius2 * 2.5; let u1, u2; // No blob if a radius is 0 // or if distance between the circles is larger than max-dist // or if circle2 is completely inside circle1 if (radius1 === 0 || radius2 === 0 || d > maxDist || d <= Math.abs(radius1 - radius2)) { return ''; } // Calculate u1 and u2 if the circles are overlapping if (d < radius1 + radius2) { u1 = Math.acos( (radius1 * radius1 + d * d - radius2 * radius2) / (2 * radius1 * d), ); u2 = Math.acos( (radius2 * radius2 + d * d - radius1 * radius1) / (2 * radius2 * d), ); } else { // Else set u1 and u2 to zero u1 = 0; u2 = 0; } // Calculate the max spread const angleBetweenCenters = angle(center2, center1); const maxSpread = Math.acos((radius1 - radius2) / d); // Angles for the points const angle1 = angleBetweenCenters + u1 + (maxSpread - u1) * v; const angle2 = angleBetweenCenters - u1 - (maxSpread - u1) * v; const angle3 = angleBetweenCenters + Math.PI - u2 - (Math.PI - u2 - maxSpread) * v; const angle4 = angleBetweenCenters - Math.PI + u2 + (Math.PI - u2 - maxSpread) * v; // Point locations const p1 = getVector(center1, angle1, radius1); const p2 = getVector(center1, angle2, radius1); const p3 = getVector(center2, angle3, radius2); const p4 = getVector(center2, angle4, radius2); // Define handle length by the distance between both ends of the curve const totalRadius = radius1 + radius2; const d2Base = Math.min(v * handleSize, dist(p1, p3) / totalRadius); // Take into account when circles are overlapping const d2 = d2Base * Math.min(1, d * 2 / (radius1 + radius2)); // Length of the handles const r1 = radius1 * d2; const r2 = radius2 * d2; // Handle locations const h1 = getVector(p1, angle1 - HALF_PI, r1); const h2 = getVector(p2, angle2 + HALF_PI, r1); const h3 = getVector(p3, angle3 + HALF_PI, r2); const h4 = getVector(p4, angle4 - HALF_PI, r2); // Generate the connector path return metaballToPath( p1, p2, p3, p4, h1, h2, h3, h4, d > radius1, radius2, ); } function metaballToPath(p1, p2, p3, p4, h1, h2, h3, h4, escaped, r) { return [ 'M', p1, 'C', h1, h3, p3, 'A', r, r, 0, escaped ? 1 : 0, 0, p4, 'C', h4, h3, p4, ].join(' '); }
本文根据 @winkerVSbecks 的《 Metaballs 》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处: http://varun.ca/metaballs/ 。

大漠
常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《 图解CSS3:核心技术与案例实战 》。
如需转载,烦请注明出处: https://www.w3cplus.com/svg/metaballs.html
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。