The Engineering Principles Behind Figma's Vector Grid
2024 年 2 月 3 日

Figma矢量网络背后的工程原理

Adobe Illustrator在1987年引入了钢笔工具,用于创建和修改路径。自那时以来,钢笔工具变得非常普及,以至于它已经成为了图形设计行业的事实标志。

钢笔工具的功能在其引入后的30年里并没有发生重大变化。只需单击并拖动即可创建平滑曲线。设计师们已经学会了使用它,并且了解了它的特点。

不过,Figma觉得他们可以改进钢笔工具的某些方面,因此他们试图对其进行重新设计。他们通过创建所谓的矢量网络来改进钢笔工具,而不是用于处理传统路径。

在本文中,我们将介绍什么是矢量网络以及它们试图解决的问题。在定义了矢量网络的概念之后,我们将探讨如果你尝试实现它们,你将面临的一些工程挑战。

本文可以被看作是一个介绍一个非常有趣的问题领域的文章,也可以作为一个资源,供对新概念和想法感兴趣的开发人员以及对他们熟悉和喜爱的工具更多了解的设计师使用。

我将首先阐述钢笔工具背后的核心概念,然后我们将转向Figma的矢量网络。

路径

钢笔工具用于创建和操作路径。

如果您以前使用过Illustrator等图形软件,那么您已经使用过路径。路径是一系列线条和曲线,可能形成一个闭环,也可能不形成。

左侧的路径是闭环的,而右侧的路径不是。这两个都是有效的路径。

路径的主要特点是它们形成了一个连续的不间断链。这意味着每个节点只能连接到一个或两个其他节点。

然而,如果您将它们正确地放在一起,您可以使用多个路径构建这些形状。

通过组合路径,您可以创建任何想象得到的形状。

例如,这个啤酒杯只是由五个不同路径的组合以特定的方式放置和缩放而成。

路径的构建块

路径由两部分组成 点和线

点被称为节点(或顶点),线被称为

它们一起构成了一条路径

任何路径都可以用一系列节点和边来描述。

这条路径可以描述为节点的序列(0, 1, 2, 3, 4)。它也可以被认为是构成它的边的序列。边的列表将是(0, 1)(1, 2)(2, 3)(3, 4)(4, 0)

你可以将其想象成你小时候做的连线游戏 (opens new window):按点的顺序画出路径的边。

但是,不同于孩童在纸上连接编号点的线条,冷酷的计算机在笛卡尔坐标系中完成此任务。

#

边是一对节点之间的连接。在视觉上,边是从节点a到节点b的一条线。

但是,这条线可以以许多不同的方式绘制。如何描述这些不同类型的线?

边分为两类,直线和曲线。

直线边就像它们看起来的那样简单,只是从ab的一条直线。但是曲线边是如何定义的呢?

贝塞尔曲线 #

曲线边是贝塞尔曲线 (opens new window)。贝塞尔曲线是由四个点定义的特殊类型的曲线。

我们边上的两个节点的位置构成了曲线的起点和终点。每个节点都有一个_控制点_。

在大多数应用中,这些控制点被显示为从各自节点延伸出来的_手柄_。这些手柄用于控制曲线的形状。

贝塞尔曲线可以链接在一起,以形成单个曲线无法绘制的更复杂的形状。它们还可以与直线结合,制作一些很酷的设计。

但是这些手柄到底在做什么?它们如何告诉计算机绘制曲线的方式?

计算机通过将曲线分割为直线段并绘制单独的线段来绘制曲线。

将曲线分割为的线段越多,曲线就越平滑。

因此,要绘制曲线,我们需要知道如何获得构成曲线的不同点。如果我们计算足够多的点,我们就会得到一条平滑的曲线。

计算贝塞尔曲线上的点 #

让我们来计算。 计算曲线的25%点。我们可以通过将控制点连接以形成一条蓝线来开始。

然后,对于每条蓝线,我们在其25%点处画一个蓝点。

接下来,在三个蓝点之间画两条绿线。

然后,我们重复与蓝点相同的步骤。在绿线的25%点处画出绿点。

然后,在新创建的绿点之间再画一条红线。

然后,在红线的25%点处添加一个红点。

就这样,我们计算出了曲线的25%点。

从现在开始,我们将通过一个t值来引用曲线上的点,其中t是从01的数。在上面的示例中,点将在t=0.25(25%)处。

计算点t的方式被称为De Casteljau's algorithm(德卡斯特利乌算法) (opens new window),并且还可以用于细分贝塞尔曲线。使用我们沿途创建的点,我们还可以将贝塞尔曲线细分为两个较小的贝塞尔曲线。

贝塞尔曲线是非常了不起的东西。通过调整控制点来塑造曲线的感觉出奇地自然,将它们链接在一起可以创建出精细而复杂的形状。

对于计算机来说,它们稳定且计算成本低廉。因此,它们被用于从矢量图形 (opens new window)动画曲线 (opens new window)汽车车身 (opens new window)等各个领域。

您可以在Jason Davies的网站 (opens new window)上看到贝塞尔曲线的交互式演示。观察一系列直线轨迹形成平滑曲线的过程非常有趣。

来源:https://jasondavies.com/animated-bezier (opens new window)

路径的创造性约束 #

在本文的早些时候,路径被定义为可能形成或不形成循环的连续线段和曲线。

路径作为一个单一连续链的事实是一个相当大的限制。

这意味着使用单一路径无法创建三路交叉口。要创建三路交叉口,需要使用两条或多条路径。 矢量网络

2016年,Figma推出了矢量网络(Vector Networks)功能。它通过允许任意两个节点连接在一起而不受限制,打破了“单一连续”的限制。

“矢量网络通过允许在任意两个点之间创建线条和曲线,而不是要求它们都连接起来形成一个单一链条,改进了路径模型。”

源自:https://www.figma.com/blog/introducing-vector-networks/ (opens new window)

立方体是展示矢量网络的典型示例。

通过传统路径,你需要至少创建3条不同的路径来描述这个形状。

这在一个看似简单和常见的形状上产生了很多摩擦。要修改立方体,你需要修改两个或三个不同的路径。但是使用矢量网络,你只需抓住一个边缘并移动它,形状会按照你的期望行为。

所以,如果你想增加立方体的挤压效果,你只需抓住两个相应的边缘并将它们移动在一起。

这是Figma的矢量网络的卖点。易于使用。

矢量网络并不能让你创建其他工具无法创建的东西,但它确实消除了创建过程中的很多摩擦。

你甚至可以进一步操作。比如你想在立方体的侧面添加一个孔。

只需先选择并复制立方体的侧面。然后复制这些边缘并按照你想要的大小进行缩放。

就这样,你就得到了一个带孔的立方体。

为了使这个孔看起来更真实,你只需要内部边缘。

同样,矢量网络可能不会让你创建其他工具无法实现的东西。相反,它们提供了以前无法实现的工作流程。

创建矢量网络

通过了解 翻译如下的Markdown为中文,并删除一级标题,并删除其中的图片链接,同时尽可能删除Markdown格式错误和一些无用的段落,重新修饰整篇文章,使文章读起来更自然。同时,合理分配标题和段落,使文章读起来更有层次感:

关于什么是向量网络,我们现在来看一下如何实现它们。

向量网络背后的主要数据结构是图。图可以被看作是一组节点和边。

节点

图可以有任意数量的节点。对于我们的目的来说,节点有两个属性,一个唯一的“id”和一个“position”。

边是两个节点之间的连接。每个边由两个边部分组成。边部分包含一个节点的id和一个可选的控制点。

标签“n0”和“n1”将用于指代边的起点和终点的节点,控制点将被标记为“cp0”和“cp1”。

如果省略边的控制点,边将成为一条直线。

填充孔洞

来源:https://www.figma.com/blog/introducing-vector-networks/ (opens new window)

在使用向量网络时,填充工具允许您“切换”图的不同“区域”的填充。

这些区域可以被定义为一个节点id的序列,这些节点id形成一个循环,一个循环。

这个循环序列被称为一个“循环”。在上面的例子中,循环将包括节点“n0”,“n1”,“n3”,“n4”,“n5”,“n6”和“n7”。这些循环将被写成“(0, 1, 3, 4, 5, 6, 7)”。

如果您要计算循环的不同视觉上不同的“区域”的数量,您的答案可能是三个,但您可能会很容易地找到超过三个循环。

什么使这个正确或错误?

序列“(0, 1, 2, 3, 4, 5, 6, 7)”是一个循环,它循环,但这不是我们要找的。这个问题可以用你在Facebook上看到的“有多少个三角形”谜题来说明。

这个图中有多少个三角形?

根据您选择包括哪些区域,您应该能够数出24个不同的三角形。

但这不是我们想要的。我们需要找到16个小区域。

我们需要一种方法来找到图中的“小循环”。

最小循环基础

这篇关于最小循环基础的论文比大多数其他学术论文(并且有图片!)要简单一些。它的目标是:

计算形成图的循环基础的最小数量的循环。

“最小循环基础”是什么意思?

这只是一种花哨的说法,指的是形成图的所有的“循环基础”。 小区域的图形。您可以将这些视为图形的“视觉上不同的”区域。封闭区域。

左还是右?

找到“最小循环基”的主要工具将是根据左或右选择哪条边。

应该去a还是b

我们将以顺时针和逆时针的方式考虑这个问题。

从左边行进时,我们选择相对于前一条边最逆时针的边(CCW)。

从右边行进时,我们选择相对于前一条边最顺时针的边(CW)。

算法

我们将为之前看到的图形找到最小循环基。

第一步是选择图中最左边的节点。

从第一个节点出发时,我们想顺时针行进(CW)。但是相对于哪条边?

对于第一个节点,我们想象前一条边位于当前边的“下方”。然后我们选择相对于那个边的顺时针边。

在这种情况下,相对于prevab更顺时针,所以我们会走向a

第一次行走后,我们开始选择逆时针边。

在这种情况下,逆时针边是b。我们重复前面的步骤,并继续选择逆时针边,直到我们到达原始节点。

当我们再次到达原始节点时,找到一个循环。

现在我们在图中有了第一个小循环。

找到一个循环后,将删除循环的第一条边。

然后,删除循环中前两个节点的“细丝”。

在这种情况下,我们只有一个细丝。

细丝是只有一个相邻边的节点。将其视为死胡同。当找到一个细丝时,我们还要检查单个相邻节点是否是细丝。这确保了下一个循环的第一个节点有两个相邻节点。稍后我们将看到一个例子。

现在我们选择下一个循环中的第一个节点。在我们的图中,有两个同样最左边的节点。

当这种情况发生时,我们选择底部节点,本例中为n1

这时我们可以看到,找到的第一个小循环的第一条边,即(n0 n1)已被从图中删除。

然后,删除循环中前两个节点的“细丝”。

在这种情况下,我们只有一个细丝。

细丝是只有一个相邻边的节点。将其视为死胡同。当找到一个细丝时,我们还要检查单个相邻节点是否是细丝。这确保了下一个循环的第一个节点有两个相邻节点。稍后我们将看到一个例子。

现在我们选择下一个循环中的第一个节点。在我们的图中,有两个同样最左边的节点。

当这种情况发生时,我们选择底部节点,本例中为n1。 然后我们从之前的步骤重复这个过程。从底部逆时针遍历第一个节点,然后再从上一个节点逆时针遍历,直到找到第一个节点。

我们找到了循环 (1, 2, 3)

现在我们有循环 (0, 1, 3, 4, 5, 6, 7)(1, 2, 3)

然后我们删除循环的第一条边和之前的线。我们首先删除与循环的前两个节点连接的线。

我们继续进行,直到没有任何线剩下。

找到下一个循环是很明显的。

现在我们有了图的所有循环。

这是我们图的最小循环基!现在我们可以随意切换这些循环的填充。

数学 #

我想深入研究边缘与另一条边缘的顺时针关系是如何确定的。

理解本节的唯一先决条件是向量 (opens new window);指向2D坐标系统中另一个点的箭头。

i = (1, 0),j = (0, 1)

有了两个向量i和j,我们可以创建一个正方形。

对于单位向量 (1, 0)(0, 1),正方形的面积为1。

我们可以用任意两个向量做同样的事情。

这个形状被称为_平行四边形_。平行四边形有一个我们关心的特性,即它们的面积等于它们的行列式的绝对值。

这听起来可能像行话,但行列式对我们来说确实非常有用。看看当我们将一个个向量的正方形移到一起时会发生什么。

当向量靠近时,它们的面积变小。当向量平行时,行列式和面积变为0。

此时一个自然的问题是,当我们继续前进,蓝色箭头在绿色箭头的右边时会发生什么?

行列式变为负数!

当蓝色向量j在绿色向量i的左边时,平行四边形的行列式变为负数。当相反情况发生时,行列式变为正数。

对于我们的使用情况,这意味着我们可以检查行列式的正负来确定边缘的顺时针关系。 两个向量的行列式的正负决定了一个向量是否在另一个向量的左边或右边。

而且,无论方向如何,我们都可以这样做,因为创建平行四边形的向量的方向不会改变其面积。

行列式会在向量相对于彼此的方向改变时发生变化。

有了这个知识作为武器,我们可以创建一个函数det(i, j),它接受两个向量并返回行列式的值。

函数在ji的左侧(逆时针)时返回一个正值。

应用数学

假设我们正在寻找一个循环,并且正在决定是否移动到n0n1

让我们将其转化为坐标系。

我们将从a开始。

我们想要得到从curra的向量,可以通过从a中减去curr来实现。我们将这个新向量称为da

我们可以使用prevcurr进行相同的操作。

现在我们可以通过计算dadcurr形成的平行四边形的行列式来确定a是否在curr的左侧。

注意顺序很重要。如果我们使用da作为i,则面积为负。如果我们将其用作j,则变为正数。

我们可以对b做同样的操作。

有了这个信息,我们知道abcurr是在彼此的左侧还是右侧。

我们如何利用这个信息呢?

绿色区域

我们将重点确定相对于currda是否比db更逆时针。简单来说,da是否在db的左侧?

如果da相对于dcurr更逆时针,可以说dadb更好。

第一步是确定dcurrdb之间的角度是否是凸的。

如果角度是凸的,我们使用下面的表达式来检查da是否比db更好。

∨符号表示数学中的逻辑或运算符。

让我们来看一下这个表达式的各个部分。

da是否在dcurr的逆时针方向?

如果角度是凸的,我们可以使用下面的表达式来检查da是否比db更好。

这个是数学中的逻辑与运算符。

让我们来看看这个表达式的各个部分。

da是否在dcurr的逆时针方向?

如果是,则返回真,否则返回假。

dadb之间的角度是否是凸的?

如果是,则返回真,否则返回假。

通过判断这些条件,我们可以确定da是否比db更好。 da是否逆时针旋转到db?

我发现这在脑海中很难想象,所以我想到了两个不同的表达式创建了一个“绿色区域”,在这个区域中dadb更好。

对于表达式的第一部分(da是否在dcurr的左边),绿色区域如下所示。

第二部分的表达式询问了da是否在db的左边。绿色区域如下所示。

由于这是一个或表达式,只要这些子表达式中的任何一个为真,a就比b更好。因此,绿色区域如下所示。

a是否比b更好?

我们使用这个来确定当角度是凸的时候a的优越性。

但是如果从dcurrdb的角度是凹的呢?

那么表达式如下所示:

在这里唯一改变的是逻辑或运算符(∨)变为逻辑与运算符(∧)。

让我们看一下使用这个表达式时绿色区域发生了什么变化。

da是否在dcurr的左边?

da是否在db的左边?

由于这些子表达式由逻辑与连接,绿色区域如下所示:

使用这种方法,我们可以始终获得逆时针或顺时针的最佳节点。而且最好的是,这种方法不受旋转的影响,并且计算成本很低。

计算行列式 #

给定两个向量,可以使用以下公式计算行列式 (opens new window)

交叉点在图中

让我们回到我们的图表。

这个图表是最简单、最乐观的情况。这个图表只有直线,没有两条线相交。

这个盒子形状有一个交叉点。边缘 (0, 2) 与边缘 (1, 3) 相交。

通过交叉点,上面的区域看起来是可填充的。但是定义“填充区域”相当困难。

定义这个区域为什么那么困难?考虑这个矩形和直线。

边缘 (4, 5)(0, 1)(2, 3) 相交。

假设线的左侧区域是填充的。如果我们将线往左移动,会发生什么? 很明显,区域在收缩,但如果我们继续移动线条到矩形外面会怎样呢?下面的哪种结果应该是最终的结果,为什么?

在这种情况下,感觉矩形应该是空的。但如果我们把线条向右移动呢?

那么应该填充吗?当线条不再分隔两个部分时,如果将线条向上或向下移动呢?矩形应该填充还是空?

扩展图形

这就是我认为Figma解决这个问题的方式。我称之为"扩展图形",但Figma的工程师可能使用不同的词汇来描述它。

扩展图形意味着将每个交点创建为一个节点,并在相交点处分割相互相交的边。

这是原始的图形:

当线条扩展时,图形如下所示:

将在交点处添加一个新节点5。

边(0, 2)和(1, 3)已被删除,并由边(0, 5)、(5, 2)、(1, 5)和(5, 3)代替。

图形的结构已经发生了改变。

这里有一个图形可以更清楚地说明这一点。

扩展交点

多个交点

对于具有单个交点的线边,这些步骤非常简单。但是每条边可以有多个其他边相交,两个三次贝塞尔曲线可以创建9个交点。

这使事情变得有点复杂。让我们来看一个贝塞尔曲线和直线的相交。

最好的方法是将边的交点视为与相交边分开的独立边。

线条在t = 0.3t = 0.7处有两个交点。贝塞尔曲线也有两个交点,但在t = 0.25t = 0.75处。

在继续这个例子之前,我想介绍一种不同的边思考方式,因为我相信这将有助于对问题的整体理解。

重复边

两个节点可能由不同的边多次连接。

在这个图中,由节点对(2, 3)表示的边可以表示连接n2n3的任意两条边之一。

为了解决这个问题,我们可以 ### 交点地图

我们可以将边的交点数据结构化如下:

创建边交点处的节点

当我们遇到一个交点时,我们创建一个节点,其位置位于交点处。然后,我们将交点添加到一个名为“交点地图”的数据结构中,其中包含每个边的交点,以及相应的t值和nodeId。这些交点按照t值进行排序。

对于具有最小t值的交点,我们创建一条边,第一个“边部分”具有原始边的第一个“边部分”的nodeId。第二个“边部分”应包含交点的nodeId。这样就创建了边(2, 4)

随后的边将使第一个“边部分”的nodeId成为前一个交点的nodeId,第二个“边部分”的nodeId成为当前交点的nodeId。在这个例子中,该边是(4, 5)

每个具有交点的边还会创建一条附加边。

第一个“边部分”的nodeId将是最后一个交点的nodeId,第二个“边部分”的nodeId将是原始边的第二个“边部分”的nodeId

这有点啰嗦,希望这个图能帮助理解这个字母汤。

将边的交点与创建这些交点的边分开,可以更容易地思考。这减轻了多个边相互交叉可能引起的一些复杂性。

自相交 #

立方贝塞尔曲线可能会自相交。

这意味着每条立方贝塞尔边都必须检查自相交。这是一个有趣的问题,涉及查找贝塞尔曲线自相交的两个不同t值,但我不会在这里讨论如何找到这些值。

一旦获得了t值,可以这样展开自相交的贝塞尔曲线:

蓝色节点对用户来说应该是不可见的

我们插入n3,因为在边的两端都有自身的边的节点会引起问题,但应该对用户隐藏。

交点 自交贝塞尔曲线的循环

删除第一级标题,并同时删除其中的图片链接,尽可能删除Markdown格式错误和一些无用的段落,重新修饰整篇文章,使文章读起来更自然。同时,合理分配标题和段落,使文章读起来更有层次感:自交贝塞尔曲线的循环

删除n3的第一个机会

弯曲边缘

早些时候,我们介绍了CW-CCW图遍历算法来找到最小循环基础(小区域)。

找到与“curr”相邻的更好的(逆时针最多)点

但是,论文中描述的算法是设计用于由不相交的直线连接的节点。引入由三次贝塞尔曲线定义的边会引入显著的复杂性。

选择哪条边,蓝色还是绿色?

在上面的示例中,我们可以通过使用行列式来确定蓝色边比绿色边更好。我们仍然将更好定义为逆时针最多的边。

在使用三次贝塞尔曲线时,天真的解决方案只是将贝塞尔曲线转换为由曲线起点和终点处的点定义的线。

但是,一旦一条边曲线超过另一条边,这个想法就会崩溃。

让我们重新审视贝塞尔曲线,并从那里开始思考。

观察到这一点,我们注意到曲线起点“n0”的切线与从“n0”到“cp0”的直线平行。因此,为了获得边的起始方向,我们可以使用线段“(n0,cp0)”。

为了清楚起见,我们的边的起点“n0”与“curr”节点相同。

因此,通过将由三次贝塞尔曲线定义的边转换为由“(n0,cp0)”定义的线,我们得到曲线的初始角度。

当我们考虑“曲线环绕”情况时,这似乎是一个不错的解决方案。

看起来我们解决了这个问题。对吗?

没有交叉点

在我们继续处理其他边缘情况之前,理解任何解决方案都假设在决定要遍历的边时,任何两条边都不会相交是有帮助的。

我们遍历的图的边缘在计算图的循环(最小循环基础)时不得有任何交叉点。

我们只能在一个“扩展图”上操作。

正如我们之前提到的,扩展图是一个用新节点和边替换了所有交叉点的图。因此,如果原始的用户定义图有任何交叉点,它们必须在我们找到图的循环之前进行扩展。

并行边

下一个边缘情况是两条边平行(指向相同方向) 如果线条朝着同一方向,没有更多信息的情况下无法确定哪个更好。

以下是几种可能的解决方案,适用于曲线的控制点平行的情况。

t处的点 #

如果我们只是在曲线上取点,例如在t = 0.1处会发生什么呢?

这对于长度相似的曲线可以得出正确的结果,但是如果其中一条曲线明显比另一条长,我们很容易破坏这个结果。

这实际上与我们之前看到的“围绕曲线”情况是相同的问题。

在长度处的点 #

与其在固定的t值处取点,我们可以在曲线上某个长度处取点。该长度将由较小曲线上的某个点决定,例如在t = 0.1处。

我没有尝试实现这个方法,因为我有另一个有效的解决方案,但是如果对所有边缘情况都有效的话,这可能是一个可行且高效的解决方案。

激光! #

下一个解决方案有点奇特,但可以得出正确的结果。这是我目前正在使用的解决方案。

我们从t = 0.05开始将每个贝塞尔曲线分割(上图夸大了)。然后我们将每个部分细分成n个点。

然后,对于每个细分的贝塞尔曲线的点,我们检查从n0到该点的线是否与另一条边相交。

在这个比例下很难看清楚正在发生什么,所以让我们放大一点。

当一个点与另一条边相交时,我们使用它之前的点。

发现了一个交点

让我们再放大一点。

交点特写

对于另一条边,我们没有交点。

在这种情况下,我们只需使用边的末尾作为方向线。

通过这种方法,我们生成了似乎表示各自曲线的线条。

这对于“围绕曲线”情况也适用。

但是对于“曲线后方”情况则失败了。

这将产生绿色边作为更逆时针边缘,这是错误的。

我解决这个问题的方法是向前一个边缘的方向发射一束无限激光。

然后我们检查细分的贝塞尔曲线的点是否与激光相交。 ## zier与激光的交点

但是从n0到这些点的线永远不会与激光相交。

相反,我们可以从当前点到前一个点创建一条线,并将其用于交点测试。

当我们与激光相交时,我们使用前一个点。前一个点将始终位于激光的正确侧。

我们使用的点

就像这样,我们有了一个解决方案。

平行,但反向! #

也可能是蓝色或绿色边缘(分别为a和b)与从curr到prev的边平行。

a与prev平行

寻找更好的边缘的过程类似于上面描述的过程,因此我们将快速涵盖这个问题。

有两种情况:

A或B与Prev平行,但不是两者都平行 #

如果a或b中的任何一个,但不是两者都平行于prev,我们可以简单地将平行边与prev进行比较。

如果平行边是prev的顺时针方向,那么平行边更好。

如果平行边是prev的逆时针方向,那么另一条边更好。

想一想为什么会是这样。

如果一条边与prev平行并形成顺时针曲线,而另一条边不与prev平行,则平行边是逆时针方向的。这意味着另一条边的绿色区域完全为空。

如果平行边形成逆时针曲线,则相反是正确的,因为它将是顺时针方向。这意味着另一条边的绿色区域是整个圆。

A和B都与Prev平行 #

使用与前面相同的激光解决方案,此情况已得到解决。

嵌套的循环 #

现在我们要稍微了解一下填充。

让我们看一个带有嵌套循环的基本图形的示例。

您预期图形的区域定义如下:

但是,如果您悬停在外部区域上,您将得到一个不同的、令人不满意的结果。

但这是有道理的。让我们看一下图形的节点。

循环(0, 1, 2, 3)描述了我们想要的区域的外边界,但我们尚未描述区域的“内边界”。

让我们看看我们如何做到这一点。

奇偶规则 #

告诉计算机绘制2D形状的轮廓很简单。但是,如果您想要填充该形状,您如何告诉计算机 什么是“内部”和“外部”?

一种确定一个点是否在形状内部的方法是从该点以任意方向发射无限激光,并计算它穿过了多少个“墙”。

如果激光与奇数个墙相交,则点在形状内部。否则,点在形状外部。

相交一个墙,我们在形状内部。

相交四个墙,我们在形状外部。

这对于任何2D形状都适用,无论选择哪个点以及以哪个方向发射激光。

这也适用于嵌套路径的情况。

这给了我们关于如何定义形状“内部边界”的一个想法。

减少闭合路径 #

让我们看一个图形,其中一个循环嵌套在另一个循环内,但有一条边连接两个循环的节点。

这将引导我们思考嵌套循环,并更深入地理解如何思考它们。

让我们找到这些循环。我们使用与往常一样的顺时针和逆时针方法。

用这种方法,我们似乎在内部循环周围绕了一个小的绕路。

当我们到达起始节点时,循环就是这个样子。

这是我们见到的第一个循环,我们在其中一个节点(n3n4)经过两次。当我们观察循环在图中的方向时,会出现一些有趣的现象。

我们一开始是逆时针旋转,但当我们穿越从外部循环到内部循环的边时,我们旋转的方向似乎发生了变化。

目前,我要声明我们希望将外部循环与内部循环分开,并将它们之间的边视为不存在。稍后我会解释为什么这样做,并在此处解释如何实现。

我们移除所有重复的节点,例如n3,并将它们从循环中删除。我们还删除两个重复节点之间的任何节点。

你可能会注意到n4也是重复的,但由于它位于n3删除的循环部分的“内部”,我们可以忽略它。

我们只保留一个重复节点的实例,然后得到将在不存在“穿越”时找到的循环。

然后,我们标记连接外部循环和内部循环的边。我将这些标记的边称为“穿越”。

![](https: 可能还有情况是外部-内部的组合有多个交叉点。

在这种情况下,我们将所有与连接到外部循环的节点相邻的边标记为交叉点。

完成所有这些后,我们的循环看起来是这样的:

子循环

我们将不再称之为“内部”和“外部”循环,而是称之为子循环和父循环。这样可以更容易地思考多个循环相对于彼此的关系。

话虽如此,让我们引入第三个循环。

当我们悬停在最外层循环上时,你期望会发生什么?

由于偶奇规则,最内层循环也被填充了!

为了解决这个问题,我们可以引入“直接子循环”的概念。

父循环(蓝色)和它们的直接子循环(绿色)

一个父循环可能有多个直接子循环。但是由于非交叉规则,一个子循环只能有一个父循环。

让我们看看这是如何工作的。

这个图有一个矩形,也就是我们的最外层循环,它有两个直接子循环:一个菱形和一个沙漏。这个菱形有两个自己的直接子循环,而沙漏有三个直接子循环。

我们将从矩形及其直接子循环开始。我们将它们命名为c0c1c2

用户已经决定填充其中一些循环,而将其中一些循环保留为空。

c0c1被填充,而c2为空

让我们用灰色填充而不使用描边来绘制图形。在绘制这个图形时,我们从最外层循环c0开始。

由于c0被填充,我们将绘制它。如果它没有被填充,我们可以跳过绘制它。我们可以从矩形中射出一束激光,并看到它与矩形的墙壁相交一次,所以根据偶奇规则,我们可以期望它被填充。

这可能看起来非常明显,但在我们继续之前,有必要清楚地说明游戏规则。

接下来,我们要绘制图形中的菱形c1。它与矩形一样被填充,所以我们也应该绘制它。但如果我们尝试同时绘制菱形,我们会得到错误的结果。

我们的激光与菱形相交 交叉两面墙壁时,如果形状的填充设置相同,则不会绘制这两个形状。

我们相交了偶数个墙壁,所以我们处于形状的“外部”。

因此,为了绘制用户想要的图像,我们可以简单地跳过绘制菱形,因为父循环隐式地绘制了具有相同填充设置的直接子循环。

沙漏c2应该是空的。在这种情况下,不绘制它似乎是一个合理的结论。但是由于父循环(矩形)已经将沙漏绘制为填充,我们需要通过绘制沙漏来“翻转”填充。

如果尝试使用激光交叉方法,我们可以看到交叉点的数量是2,是一个偶数。根据奇偶规则,偶数个墙壁意味着你在形状的“外部”。

现在,我们已经绘制了矩形及其直接子循环,我们可以继续处理这些直接子循环的直接子循环。

当处理c1的直接子循环c3c4时,我们可以将它们视为c0的直接子循环,因为它们具有相同的填充设置。

对于c3,我们希望“翻转”填充设置,所以我们绘制它。但是c4具有与其父循环相同的填充设置,所以我们不绘制它。

我们只需要绘制循环,如果它们的父循环的“填充设置”与它们自身相反。如果它们具有相同的填充设置,我们不需要绘制它们。

这意味着在绘制循环时,首先绘制最外层的“填充”循环,然后查看该循环的子循环。如果子循环具有与其父循环相同的填充设置,则不应绘制它们。

连续循环 #

一个图可能有多个循环的“群集”。

我使用短语“连续循环”来描述这些循环的“连续性”。我经常将这些连续的循环群体看作是不同颜色的。

找到这些连续循环可以通过深度优先遍历来完成:

从循环的第一个节点开始

对找到的每个节点进行着色

但是还记得那些“交叉点”吗?在搜索中,你可能会 这里是我们最终的颜色效果:

拿这组嵌套在另一组连续循环中的循环为例:

由于非交叉规则,我们知道如果连续循环组中的一个节点在不属于该组的循环内部,那么所有节点都在其中。

这个“连续循环”概念可能在表面上并不是这篇文章中最有趣的部分,但我发现在处理矢量网络时它很有用。

部分展开 #

当悬停在由交叉定义的区域上时,我们展示了一个扩展图的循环。

以这个三角形为例。

如果我们悬停在它的某个区域上,我们会看到一个由尚不存在的节点定义的区域。

蓝色条纹区域所代表的是如果用户点击鼠标左键,其填充状态将被“切换”的区域。这个区域在图形中并不存在,因为用户没有定义它。它存在于原始图形的扩展版本中的循环中。

扩展图形

当用户点击以切换区域的填充状态时,我们首先需要展开图形,使组成该区域的节点和边存在。

扩展图形

但通过这样做,我们扩展了两个不需要扩展来描述该区域的交叉点。这些扩展是破坏性的,应尽量避免。

相反,我们可以通过只展开定义所选循环的交叉点来进行部分展开。

部分展开的图形

这样可以在尽可能保留原始图形的同时定义填充。

实现部分展开 #

基本实现方法相当简单。在创建扩展图时,只需为每个扩展节点添加一些元数据,告诉您用于创建该节点的原始图形的两个边以及这些交叉点发生的t值。

然后,当点击循环时,迭代每个节点。如果节点存在于扩展图中但不存在于原始图形中,请将其添加到一个新的部分展开图中。

还有一些边缘情况,但这里不涉及它们。

省略的主题 #

以下是我决定在本文中省略的一些主题。你可以自己试试看!

连接点 #

Figma提供了三种类型的连接点:圆形、尖角和方形。这些不同的连接点如何 如何实现连接类型?

笔画对齐 #

Figma还提供了三种对齐图形笔画的方式:居中、内部和外部。

如何确定内部或外部的位置,并且当图形没有循环时会发生什么?

布尔运算 #

Figma和大多数矢量图形工具一样,提供了布尔运算 (opens new window)。如何实现这些运算?

Paper.js (opens new window)是开源的,并且具有路径的布尔运算,也许你可以从那里开始?

未来的主题 #

以下是我未来想要探索的一些开放性特性和想法。

与填充方式不同的工作方式 #

Figma允许用户以不同的方式处理填充,还有其他选择。

我有一个可能的解决方案,我有兴趣探索多个不同的“填充层”,这些层使用一个矢量对象作为参考。这将解决“一个图形,多种颜色”的问题,而无需复制图层并保持多个矢量对象同步,如果您以后想进行更改。

图形动画 #

使用类似于After Effects的基于表达式和基于参考的系统,结合Vector Networks,你可以实现什么?

或者,我们可以使用类似于Blender的着色器编辑器 (opens new window)Fusion的节点化工作流 (opens new window)的节点编辑器。

在这方面有很多探索可以做,我非常兴奋地深入探讨这个主题。

结语 #

感谢阅读本文!我希望它作为对我认为非常有趣的问题空间的一个很好的介绍。我已经在学校和工作之余花了很长时间来解决这个问题。它是我正在开发的Web动画编辑器及其运行时的一部分。我打算对Vector Networks进行修改,使其成为一些功能的核心。

我已经开始实现Vector Networks超过半年了。在创建、修改和扩展图形方面,矢量编辑器非常强大。但是在修改填充状态时出现的边缘情况一直困扰着我。

在发布本文之前,我希望有一个完全可用的演示版本,但需要几个月的时间才能稳定到足以供非我使用的人使用。

这个项目的重要思想是成为一个专门用于在Web上创建和运行动态动画的动画软件。我将在以后的日期分享更多关于这个项目的信息。

我也认为Figma的Vector Networks非常酷,但很难在网上找到相关材料。希望这篇文章有所帮助。 以下是关于Vector Networks的一些信息:

什么是Vector Networks? #

Vector Networks是一家软件公司,致力于提供IT资产和服务管理解决方案。他们的解决方案帮助组织更好地管理和优化其IT资产和服务,以实现最佳业务结果。

Vector Networks的产品和服务 #

Vector Networks提供一系列产品和服务,包括:

  • IT资产管理:Vector Networks的资产管理解决方案帮助组织跟踪和管理其IT资产的生命周期。这包括硬件设备、软件许可证、合同和保修等。

  • 服务管理:他们的服务管理解决方案帮助组织有效管理和提供IT服务。这包括故障管理、变更管理和配置管理等。

  • 软件许可证管理:Vector Networks的软件许可证管理解决方案帮助组织合规管理其软件许可证。这有助于降低风险并避免不必要的许可证费用。

  • 采购管理:他们的采购管理解决方案帮助组织更好地管理采购流程,包括供应商选择、采购订单和供应商合同等。

Vector Networks的优势 #

Vector Networks的解决方案有以下优势:

  • 综合性:他们的解决方案提供了一个综合平台,集成了资产管理、服务管理、许可证管理和采购管理等功能,帮助组织实现整体的IT资产和服务管理。

  • 可定制性:他们的解决方案可以根据组织的需求进行定制,以适应不同的业务流程和要求。

  • 易于使用:他们的解决方案提供直观的用户界面和易于使用的工具,使组织能够快速上手并提高工作效率。

  • 数据可视化:他们的解决方案提供了丰富的数据可视化功能,帮助组织更好地理解和分析其IT资产和服务的相关数据。

总结 #

Vector Networks是一家提供IT资产和服务管理解决方案的软件公司。他们的综合解决方案包括资产管理、服务管理、许可证管理和采购管理等功能。这些解决方案具有可定制性、易于使用和数据可视化的优势,帮助组织更好地管理其IT资产和服务,实现最佳业务结果。