算法 · 简单矢量图形的自旋方向判断

1,662 阅读7分钟

这个,众所周知啊,【矢量】(或者称【向量】)是有方向性的。

当多个矢量头尾相连起来,就构成了一个矢量图形。

这个时候,这个图形就被赋予了另一种“方向性”:旋转方向。

也就是所谓的“顺时针”和“逆时针”。

我们这里只讨论简单的矢量图形,路径只把整个平面分割成了内外两个区域,而不是三个及以上的分割。

意味着图形各边没有交叉或者类似“8”字拧麻花的样式出现(如下图)。毕竟大麻花的扭转方向是另一个维度下的东西,不是我们这里要讨论的。

image.png

① 为什么要判断旋转方向

先说背景需求,可能有人会好奇,平面矢量图形只要看出来是那个样子就好了,它旋转朝哪个方向跟我又有什么关系?

确实,对于纯静态平面设计来说,还真不太需要判断矢量图形的旋转方向。

但不可否认,作为矢量图形的一大重要原生属性,方向性有它发挥独特作用的地方。

其中一个领域就是动画。

以 Adobe illustrator 为例,如果你使用一些自带的工具生成一些图形,它可能会自动生成不同旋转方向的路径。

以下图为例,为方便理解,这里通过渐变色彩渲染出了旋转路径的区别。这两个图形的外观布局一模一样,如果填充颜色和描边色一致,人们根本不会注意到他们的旋转区别。

image.png  

<polygon id="left"  fill="none" stroke-width="4" stroke-miterlimit="10" points="125.5,64 75.7,100.8 90.1,166.5 209.7,203 241.8,133.5 215.9,64 "/>
<polygon id="right" fill="none" stroke-width="4" stroke-miterlimit="10" points="519.2,65.7 545.1,135.2 513,204.7 393.4,168.2 379,102.6 428.8,65.7 "/>

这两个图形的伪码描述附上,从矢量描述上可以很清楚地看出旋转方向的不同。

当我们制作一些路径动画时,旋转方向就成为路径运动趋势的一个必要参考条件,甚至会成为主要依靠。

特别是当图形数量庞大到需要批处理的时候,混乱的旋转方向自然不会成为有效的素材。

这个时候,通过路径推导旋转方向并适时做出调整,对于工作效率和准确度的提高十分有益。

甚至在一些场合下,还可以利用旋转方向作为图形分类的一个依据。

② 直觉判断方法

直觉判断下,取图形路径任意点,与其紧邻的前后点,也就是非共线的三点,组成一个三角子图形。由于旋转方向的不变性,我们便可以从这个三角形的旋转方向推出原始图形的旋转性。

image.png

但是,这种方法只适用于上图这种普通的凸多边形,遇到下图这种,就会出问题。

image.png

如图所示,点 P_center 所在点及其紧邻前后点构成的三角形旋转方向为顺时针,而原始图形则为逆时针,方向相反。

原因就在于,这是一个凹多边形。

③ 适用于任意多边形的判断方法

我们从初等几何学中就已经知道,简单多边形又有凸多边形和凹多边形的区分,凹多边形至少有一个内角大于180度,小于360度。

正如下图所示,左边为凸多边形,右边为凹多边形。

image.png   当我们任取一个点时,这个点有可能位于这个图形的“内部”。这种情况下,我们就无法通过任意点及前后点来构造“旋转子三角”来判断图形的旋转方向。

不过,既然我们无法选取任意点,这就给解决方案提供了思路。我们可以通过选定一些满足特殊条件下的点,来完成判断。

这里的条件便是:避开凹点。

3.1 求极值

凹多边形的凹点有一个特点,就是它的所处位置,可以假设为在图形“内部”。想要避开它,只要选取图形所谓“最靠外”的点就好了。

通过图形的矢量描述,我们可以很容易获得任意图形的极值点。

通常我们会计算出四个值,分别是 XY 的最大最小值:x_min/x_max/y_min/y_max

当然了,这四个值不一定对应固定个点。比如一个点可以是 Y 的最大值,但不一定是 X 的最大值。相反也可以是一致的。

而我们要做的只有一件事,就是选取任意一个极值所在点,然后取这个点紧邻的前后两点,构造三角判断旋转方向。

极值所在点必为“凸点”,因为不可能存在比它还靠外的点,意味着这个点一定不会是凹点。

3.2 求向量的叉积

叉积,或者称为“叉乘”“外积”“向量积”,是在三维空间中,对两个向量的二元运算,通常用符号“×”表示。

运算结果为向量,是向量 a 和向量 b 所在平面的法线向量。

右手坐标系中的向量积示意如图,左手为反向(摘自wiki):

image.png

抛开向量积的模具体大小数值,利用向量积本身的方向性,就可以判断两个向量所在平面的法线方向,而向量积的正负,也就是所谓旋转方向的“正负”。

a=(Pcenter.xPpre.x,Pcenter.yPpre.y)b=(Psucc.xPcenter.x,Psucc.yPcenter.y)c=a×b\begin{aligned} & \vec{a} = (P_{center}.x-P_{pre}.x, P_{center}.y-P_{pre}.y) \\ & \vec{b} = (P_{succ}.x-P_{center}.x, P_{succ}.y-P_{center}.y) \\ & \vec{c} = \vec{a} \times \vec{b} \end{aligned}

由于方向判定与使用的左右手坐标系有关,不是一个固定答案,所以他们的外积也被称作为“赝向量”。

这样我们就可以根据实际情况,自定义方向。

比如,我们设定逆时针朝下,顺时针朝上,Z轴方向从下到上。

实现起来其实很简单,无须重复造轮子,使用 numpy 自带的 cross 函数即可。

def func(v1: list, v2: list) -> int:
    t = np.cross(v1, v2)
    if t > 0:
        print("顺时针")
        return 1
    elif t < 0:
        print("逆时针")
        return -1
    else:
        return 0

④ 补充一些潜在的问题

4.1 关于极值点的选取

可能有人会更进一步想,既然我们取了一个极值点和它的两个相邻点。那么能不能索性更快一点,不取相邻点,直接取任意三个极值点计算?也省下了遍历前后点的时间。

这个想法很“优化”,但是实际情况,却是不可以。

举个简单的栗子:

image.png

在这个平行四边形中,四个极值实际映射为两个点,这样不管怎么选取,最终三个点一定共线,那么此时就没有旋转方向的意义了,最终结果也为0。

4.2 关于路径多锚点的问题

所谓路径多锚点,是指存在一种状态,同一路径上存在冗余点。

比如一条线段本可以用两个端点来描述,结果在路径中间,添加了无数个点。看起来效果没什么不同,但是底层的矢量描述却大不相同。

因而不免会有人担心,如果我选取的点为冗余点,那么有没有可能导致最终用来判定的三点共线?

事实上,这种顾虑,可以不用担忧。

毋庸置疑,如果极值点刚好在“极值边”上,那么确实会有很多个共线的点共享同一个极值。

比如矩形的四边就是如此。

image.png

但是得益于矢量路径的顺序性,以及我们常规求最大最小值的算法。

计算得出的极值点一定是共享这个极值的序列里的第一个或者最后一个点。

换言之,极值点,必为拐点(当然也一定是凸点)。

反证一下,如果这个点不是拐点,那么一定存在比它还靠前的点。但是这不符合算法和矢量描述的顺序性,所以它一定是拐点。

拐点相邻的两点必然不共线,由此便可放心构造“旋转子三角”,求得答案。

⑤ 一句话概括

取图形最靠外拐点,与其相邻前后两点,通过这三点构造向量求向量积,来判断旋转方向。