SVG奇淫巧技(四):放之四海而皆准的fill-rule,不来了解一下吗?

1,702 阅读7分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第4篇文章。

要搞懂SVG,那么strokefill这两种把SVG压的死死的绘制形式是绕不开的话题,之前聊过了stroke,今天就趁热打铁把fill也一并给扒一扒。

fill的常识总结

相较于stroke的花里胡哨,fill显然要单纯的多,除了可以通过CSS来实现继承,甚至可以偷懒的设置currentColor ,不过,以上这些stroke也可以,所以并不能算是fill的特色。

除此之外呢,能想到的就是SVGtext标签中的文字颜色是通过fill填充而不是color来指定的,所以文字在SVG中更像是被当做一个图形来渲染的。

另外,fill的默认值是黑色,也就是说用SVG画一个rect在不设置fillstroke时,会渲染为一块黑色的矩形。

以上都属于常识性的总结,如果只说这些那fill确实配不上为它单独开一篇,不过,咱也别不拿豆包当干粮,人家还有个在图形学领域放之四海而皆准的属性fill-rule

fill-rule

老规矩,先上MDN定义:

fill-rule :一个外观属性,它定义了用来确定一个多边形内部区域的算法。只会在以下8种元素中生效:<altGlyph><path><polygon><polyline><text><textPath><tref> 和 <tspan>

这里要注意,作为一个外观属性,fill-rule 是可以被用于 CSS,好,让我们继续。

fill-rule 有两个可选值,分别为 nonzeroevenodd ,虽然只有两个值,但这两种填充规则几乎是图形学中路径填充的普世知识,无论是SVGCanvas或其他什么技术,只要是路径填充相关就离不开这两种填充规则。

下面,我们来看看这两种规则到底是什么?

如果是一个简单的图形填充,这两种规则下几乎没有任何区别:

67.png

但是,一旦路径发生重叠(注意这里的重叠是指同一个元素路径发生重叠,并不是多个元素形成的图形发生上下覆盖),差异就体现出来了:

68.png

那这两个规则到底是什么意思呢?还是来看看MDN上的定义:

nonzero(非0规则):从该点向任意方向的无限远处绘制射线,然后检测形状与射线相交的位置。从 0 开始统计,路径上每一条从左到右(顺时针)跨过射线的线段都会让结果加 1,每条从右向左(逆时针)跨过射线的线段都会让结果减 1。当统计结束后,如果结果为 0,则点在外部;如果结果不为 0,则点在内部。

evenodd(奇偶规则):从该点向任意方向无限远处绘制射线,并统计这个形状所有的路径段中,与射线相交的路径段的数量。如果有奇数个路径段与射线相交,则点在内部;如果有偶数个,则点在外部。

是不是看的七荤八素了?没关系,我们还是直接上图:

69.png

在将每段路径的绘制顺序和方向标识出来后,让我们使用射线法 来分别验证这两个规则。

nonzero(非0规则)

70.png

点A发出一条射线,结果经过了路径5路径2,我们顺着路径前进方向和射线前进方向,可以看到,合并后的运动方向都是逆时针,逆时针方向-1,因此,最后计算值是-2,不是0,因此,是内部fill时候可以被填充。

71.png

同样的,从点B再发出一条射线,经过两条路径片段,为路径2路径3,我们顺着路径前进方向和射线前进方向,可以看到,合并后的运动方向一个是逆时针-1,一个是顺时针+1,因此,最后的计算值是0,是外部,因此,不被填充。

怎么样,看懂了吗?我猜这时候你一定还在琢磨,怎么那条线就是逆时针,为什么那条线就是顺时针,对吗?如果猜对了,麻烦把“正是在下”打在公屏上。

没关系,我看到这里时也有一样的困惑,不过,在我苦心钻研了一番之后总结出了一个很简单的判断规律,现在分享给大家,仔细看下面这张图:

72.png

有没有发现,我们只需要判断整个路径与射线的位置即可,判断规则就是面向路径方向,如果射线在路径方向的左边,那么就是逆时针,如果射线在路径方向的右边,那么就是顺时针,怎么样是不是一下就豁然开朗了。

那再来一个验证下:

73.png

射线在向下方向路径的右边,在斜上方向路径的左边,所以一个顺时针,一个逆时针,累加为0,所以是外部,OK,依旧适用验证成功。

学会了射线与路径的顺时针还是逆时针的判断之后,判断内外就很简单了,只需要选中一个点然后画一条射线,接着逆-1顺+1的进行累加,最后看结果是否为0就能知道,这个点所在的区域是内部还是外部啦,是不是超级简单。

了解了nonzer(非0规则)后,我们再来看下另一个evenodd(奇偶规则)

evenodd(奇偶规则)

相较于nonzero判断的复杂性来讲,evenodd的规则简直就是简单粗暴,总结一下:

evenodd: 起始值为0,射线会和路径相交,每交叉一条路径,我们计数就+1,最后看我们的总计算数值,如果是奇数,则认为是路径内部,如果是偶数,则认为是路径外部

直接看图:

74.png

点A发出的射线,与路径5路径2相交,相交数为2偶数,所以是外部

再来看一个:

75.png

点C发出的射线,与路径5路径3路径2相交,相交数为3奇数,所以是内部

是不是很简单,我想看到这里,一定会有大聪明产生疑问,如果发出的射线经过的是两条路径的交点那应该怎么算呢?

76.jpg

如图所示,点C发出的上方射线经过路径1路径5的交点,理论上相交数是2偶数应该算外部,但是下方射线的途经路径6路径2路径3的交点,理论上相交数是3奇数又应该算内部,这种时候应该怎么算呢?

确实,如果考虑到路径交点的话,情况就要复杂的多了,所以填充规则里直接就不考虑交点的问题,就好像我们知道投掷硬币的时候正反面的概率是50%,这是因为硬币竖着的情况压根不在考虑范围一样,因此,MDN关于fill-rule的填充规范上有这么一条提示:

The above descriptions do not specify what to do if a path segment coincides with or is tangent to the ray. Since any ray will do, one may simply choose a different ray that does not have such problem intersections.

翻译过来就是,既然射线怎么走都可以,那就选择一条简单纯粹且没有交叉点的路径,别老给自己找不痛快。

fill-rule的应用场景

好了,现在fill-rule这个看似高大上的规则,已经被扒的赤裸裸了,那么这两种规则的实际应用场景又是啥呢?

个人觉得,了解 fill-rule 规则并不能知道他们的应用场景,而是在面对实际需求时知道选择哪种填充方式来实现,所以不能本末倒置,不过,硬要举例的话,也许下面的例子会给你一个相对直观的认识。

77.png

<svg viewBox="-10 -10 320 120" xmlns="http://www.w3.org/2000/svg">
    <polygon fill-rule="nonzero" stroke="red" points="50,0 21,90 98,35 2,35 79,90"/>

    <polygon fill-rule="evenodd" stroke="red" points="150,0 121,90 198,35 102,35 179,90"/>
</svg>

你看两种规则下的五角星呈现了截然不同的效果,再结合我们之前聊过的《SVG交互那点事儿》,应该会更加深刻:

78.gif

好了,至此关于SVG仅有的两种呈现方式我们就都聊完啦,但此种玄奥远不止于此,明天我们再一起来看看SVG的坐标变换吧。

参考文档

SVG/Canvas中nonzero和evenodd填充规则

MDN-windingRule