metaball公式_Metaballs

本文介绍了Metaball的数学公式,以及使用Canvas、CSS、SVG、Paper.js等实现Metaball效果的方法。详细分解了实现Metaball效果的步骤,包括定位切线、计算角度和点的位置、添加手柄等,还探讨了圆重叠时的处理方法,并给出了最终代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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);

为什么是这样呢?我花了一段时间才弄明白。我试着解释一下,但是你可能看到这个外部切线的分步说明,会更易理解。

max-spread

这是连接器可以拥有的最大可能的扩展。我们可以通过将其与一个叫做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);

要将梯形形状的连接器转换成弯曲的连接器,需要将手柄添加到所有的四个点上。这个过程的下一部分是计算手柄的位置。

一个特定的句柄应该对齐到那个点上的圆切七。再次使用极坐标来定位手柄。但这一次,它将与这一点本身有关。

A B C angle 1

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来控制扩容。可以使用余弦定理来计算。

radius1 d radius2 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:核心技术与案例实战》。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值