Metaballs
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/。
如需转载,烦请注明出处:https://www.fedev.cn/svg/metaballs.htmljordan retro 11 mens zappos