SVG 填色算法演进实现

参考:

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

比如:下方这两个图形,描述他们的形状是一样的,局别只在于填色规则是 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. 先找到这个<path>所描述的不规则图形的最小外切矩形
    2. 遍历这个矩形的所有点,找到属于这个 <path> 标签的所有点
      1. 拆分多个M为单个M
      2. 为每个M生成多边形,得到多边形点集
      3. 判断矩形点是否在某个M中的多边形中,如果在,那么就是属于后续需要判断是否需要填色的点
    3. 然后遍历这些点,执行下面步骤
  2. 从需要判定的点向任意方向发射线,然后计算图形与线段交点的处的走向
    1. 选择水平往右方向的射线
  3. 计算结果从0开始,每有一个交点处的线段是从左到右的,就加1;每有一个交点处的线段是从右到左的,就减1;
    1. 确定<path>不同M开始所画线段的走向,从左往右加1,从右往左减1
    2. 计算射线方向与<path>标签所描述图形(准换为多边形)的次数
  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>标签上存在相交关系,因为这会是不同色块,而不同颜色是会分成两条 path
    • 计算相交点,得出3个或多个多边形
    • 求出相交的所有交点
    • ...
    • 如果原始的两个多边形是方向相同
    • 依次对这些拆分后的按照步骤1去进行,最后合并得出一个多边形
  2. 如果两个多边形之间是相离的关系(应该没有吧)(原则上,填色游戏是不会出现一条<path>标签上存在相离关系,因为这会是不同色块,而不同色块是会分成两条 <path>

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

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

  1. 判断两个多边形包含关系,得出外部多边形以及内部多边形之分
  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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,200评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,526评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,321评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,601评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,446评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,345评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,753评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,405评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,712评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,743评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,529评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,369评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,770评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,026评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,301评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,732评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,927评论 2 336