SVG 填色算法演进实现

参考:

  • https://www.w3cplus.com/svg/svg-fill-features.html
  • https://www.w3.org/TR/SVG11/painting.html#FillProperty

SVG 有一个 标签,里面有很多个图形指令,而如果由多个M(Moveto)构成的,那么这个 描述的图形可能会很复杂,比如构成一个圈包围一个圈,那么这个时候来了,填色的时候如何填?

比如:下方这两个图形,描述他们的形状是一样的,局别只在于填色规则是 nonzero 还是 evenodd

nonzero.png
evenodd.png
  • nonzero 填色规则如下:
    1. 找到待判断的点
    2. 从需要判定的点向任意方向发射线,然后计算图形与线段交点的处的走向;
    3. 计算结果从0开始,每有一个交点处的线段是从左到右的,就加1;每有一个交点处的线段是从右到左的,就减1;
    4. 这样计算完所有交点后,如果这个计算的结果不等于0,则该点在图形内,需要填充;如果该值等于0,则在图形外,不需要填充
  • evenodd 填色规则如下:
    1. 找到待判断的点
    2. 从需要判定的点向任意方向发射线,然后计算图形与线段交点的个数
    3. 个数为奇数则该点在图形内,则需要填充;个数为偶数,则该点在图形外,不需要填充

恩,看起来好像比较容易理解,但是怎么实现呢???

针对 nonzero 填色规则,目前我采用的是下方这个动图所示的填色方案,嗯,在到达这个动图的填色方案之前,自己瞎搞了好几个算法方案,此文主要记录这些算法的演进过程:

填色算法动图-算法4-2.gif

方案一:直白地按照描述细分点执行射线操作判断是否为填色区域

  1. 找到待判断的点
    1. 先找到这个所描述的不规则图形的最小外切矩形
    2. 遍历这个矩形的所有点,找到属于这个 标签的所有点
      1. 拆分多个M为单个M
      2. 为每个M生成多边形,得到多边形点集
      3. 判断矩形点是否在某个M中的多边形中,如果在,那么就是属于后续需要判断是否需要填色的点
    3. 然后遍历这些点,执行下面步骤
  2. 从需要判定的点向任意方向发射线,然后计算图形与线段交点的处的走向
    1. 选择水平往右方向的射线
  3. 计算结果从0开始,每有一个交点处的线段是从左到右的,就加1;每有一个交点处的线段是从右到左的,就减1;
    1. 确定不同M开始所画线段的走向,从左往右加1,从右往左减1
    2. 计算射线方向与标签所描述图形(准换为多边形)的次数
  4. 这样计算完所有交点后,如果这个计算的结果不等于0,则该点在图形内,需要填充;如果该值等于0,则在图形外,不需要填充
    1. 找到点后,每个点用rect去填充(TODO:看起来可能有锯齿,下面的想法可能会解决这个问题)
    2. 一个想法(此方案好像也能用来实现渐进填色效果,及颜色慢慢往中心点填色):
      1. 从左往右,从上至下,先找到第一个带填充的点,设置任意一个初始方向(其实是8个中的任意一个)
        • (-1,0)
        • (-1,1)
        • (0,1)
        • (1,1)
        • (1,0)
        • (1,-1)
        • (0,-1)
        • (-1,-1)
      2. 然后按照顺时针更新方向,寻找下一个点
        1. 当某个方向上存在一个新的点,则进行步骤3
        2. 当所有方向都找不到一个新的点时,则设置当前点为上一个点,然后重新重新执行步骤2
      3. 设置刚刚找到的点为当前点,设置当前点的初始方向为当前点到上一个点的方向,执行步骤2
      4. 当重新找回步骤1中的点时,结束,得出当前多边形的边线,用 stroke 描边,并将这些边点都为已填充状态
      5. 重新执行步骤1,寻找下一个待填充的点,一次循环下去,直到所有点都变为(多边形并且)已填充
    3. 或者从点击位置开始慢慢扩散出去填色

碰撞检测

将待判断点与每一个填色点(区域,再小的点,加上宽高也是一个区域)进行坐标对比

恩,实际测试是不太行,问题在哪也不太确定,而且会很耗计算量,因此此方案可行

方案二:图像混合

也尝试过图像混合,但是这明显要多个 Graphics 进行处理,DC 会升高,而且好像还比较难实现,因此不可取

方案三:拆分与合并多边形填色算法

总体思路:将Path中所有图形一个一个处理,每次处理两个多边形图形,然后得到一个新的多边形图形和它的绘制方向,然后将这个新的多边形作为新的输入与下一个图形进行处理,依次循环,最后得到一个多边形,对这个多边形进行填色即可

  1. 如果两个多边形之间是相交的关系(暂时没想好)(原则上,填色游戏是不会存在一条标签上存在相交关系,因为这会是不同色块,而不同颜色是会分成两条 path
    • 计算相交点,得出3个或多个多边形
    • 求出相交的所有交点
    • ...
    • 如果原始的两个多边形是方向相同
    • 依次对这些拆分后的按照步骤1去进行,最后合并得出一个多边形
  2. 如果两个多边形之间是相离的关系(应该没有吧)(原则上,填色游戏是不会出现一条标签上存在相离关系,因为这会是不同色块,而不同色块是会分成两条

经过上面的分析,我们的基本只需要考虑多边形包含填色处理

多个包含关系的多边形填色算法-1

  1. 判断两个多边形包含关系,得出外部多边形以及内部多边形之分
    • 多边形包含关系算法:https://www.zhihu.com/question/26593501
  2. 如果两个多边形方向相同,则取最外面多边形和他原来的绘制方向作为新的多边形输出
  3. 如果两个多边形方向相反:
    • 求两个多边形之间一条连线,使得这条连线不会和后续待合并的多边形相交
      • 连线在在内部多边形上的点为A1
      • 连线在外部多边形上的点为A2
    • 通过 A1 A2 连接两个多边形,输出一个多边形,过程如下:
      • 从A1点开始,按照A1点所在多边形的方向,环绕一圈该多边形回到 A1
      • 从A1连接到A2
      • 以A2点开始,按照A2点所在多边形的方向,环绕一圈该多边形回到 A2
      • 从A2连接到A1
      • 最后得到一个方向为外部多边形方向,环绕两个多边形空白地方的新多边形
填色算法动图-算法1.gif

多个包含关系的多边形填色算法-2

在上面的算法1会出现一种问题

新生成的多边形会不断闭合空间,因此当多边形数量很多的时候,极有可能会出现空间已经很闭合,从而出现找不到一条连线去连接后续的多边形

因此我们需要改进算法1:当遇到不能找到连线的时候,往前回滚异步,生成另外一个新的多边形,然后重新寻找,知道找到,或者深度遍历完所有情况都找不到。差不多为下面的步骤

  • 每次生成多边形,都记录当前多边形和下个多边形是哪两个点连接(记录下标即可)
  • 生成的多边形不合适时,从上次记录的下个多边形连点下标开始完后继续寻找
  • 如果都找完也不合适,那么置空后面那条子路径的下标缩影,同时在往上一层去寻找
  • 当已经到达最上层并且最上层也已经遍历完毕了,也没有找到,那么就真的是找不到了

多个包含关系的多边形填色算法-3

即便在算法2的补充下,也有情况连不上,像下图,基本完全闭合的两个多边形,基本只有这两个多边形相连才能完成所有多边形相连的目标

image.png

很明显,上面算法1、2我们好好像走了一条错路,按照算法1、2,基本hold不足这种情况的。因此,我们要修正一下我们的思路:

从算法2中,我们了解到特殊情况下,基本只有相邻的多边形才能连接,其他多边形基本无法连接过去。

基于相邻 这个特征,我们提出下面算法:

按照广度遍历求出所有相邻(直线可达且不会和其他多边形相交的)多边形,按照深度遍历连接多边形

具体差不多是下图:

image.png
  • A、B、C、D...等字母代表不同深度
  • A1,A2,A3...等字母下标代表该深度下按照顺时针排序的节点
    • 实际运用时,理论上可以忽略按照顺时针排序这个操作,无序排序也是可以的
  • 特别地,只有A是需要特殊计算:找出一个离外边最近的多边形,即为A

按照广度遍历求出所有相邻多边形具体操作:

  1. 寻找到最大的多边形,使之能包含剩下的所有多边形
  2. 遍历出了最大的多边形外的剩余其他多边形,找到离最大多边形最近的一个多边形,将作为第一层深度的代表
    • 假设第一层为A,第二层为B,每层不同的多边形用下标[0, n]表示
    • 此时标记当前层(curLv)为第一层
  3. 遍历当前层所有节点(节点遍历顺序可以随意),为每个节点找到其子节点,使得子节点符合下面条件
    • 子节点从剩余没有归层的多边形中寻找
    • 和最大多边形方向相同的子节点:
      • 跳过,因为肯定是填色的
    • 和最大多边形方向相反的子节点:
      • 当前节点边上必须有一个点,使之能连接到子节点边上的某个点,同时该条连线不能和任何多边形相交
        • 找到时需要记下来这两个点,方便后续连线处理
      • 找到的自己点标记为当前层的下一层
  4. 当步骤3的遍历完毕时,即得出所有节点的父子关系
    • 遍历完毕定义,当前没有节点是没有归类到父子关系中

按照深度遍历连接多边形具体操作:

  1. 从最大多边形连接到第一层多边形(通过两者的最短路径,因为此路径不会和其他多边形相交)
  2. 从第一层多边形开始,依次遍历每个子节点,对每个子节点进行深度遍历,完成所有连接(需要对同一个节点下的子节点进行顺时针或者逆时针排序)

实际一顿操作下来,结果是准确的,但是计算量也是十分庞大,时间复杂度巨大

填色算法动图-算法3.gif

三角剖分-算法4

将填色区域转换成三角形,三角剖分(基于耳朵,嘴巴,单调多边形)

填色算法动图-算法4.gif

你可能感兴趣的:(SVG 填色算法演进实现)