最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 汉字描红在Flutter侧的落地

    正文概述 掘金(大力智能技术)   2021-04-09   756

    背景

    业务需要

    汉字自动描红是 大力爱辅导-语文字词专项 中的重要功能部分。前期技术调研阶段发现,公司内部已经存在能够满足需求效果的Native SDK,但是考虑到以下原因:

    1. 字词专项的技术选型是Flutter作为主要实现
    2. 汉字绘制或者描红的需求在多功能、多界面中出现
    3. 大量使用PlatformView对Flutter界面的性能有较大伤害 最终决定,使用Flutter实现一套汉字自动描红的方案,便于集成和后续相似功能迭代。

    Flutter的实现则经过:

    1. 数据可操作性处理
    2. 数据校准
    3. 动画绘制
    4. 动画补帧 等操作,最终实现整个汉字描红功能的落地。

    汉字描红在Flutter侧的落地

    站在巨人的肩膀

    无论是已经实现的Native SDK或者Flutter的落地,都不是一蹴而就的,两套方案皆是基于一套开源方案。其中较为重要的汉字点阵数据来自Github。这部分资源的录入是完全自动化的,但是其原理还有待发掘,目前是Js实现的一套开源方案。

    如何将汉字用代码表现出来

    这个话题听起来非常的“代码智能”,代码是很难智能的。如果没有前人的成果,这个问题确实让人抓不着头脑。而上面提到的文字资源提供了我们可行的方案。我们以“八”字的数据为例子,这一节中涉及最主要的字段是strokes:

    { "strokes": [ "M 317 465 Q 318 338 155 190 Q 136 178 79 126 Q 67 110 85 113 Q 110 114 146 137 Q 258 209 325 305 Q 368 363 406 409 Q 419 422 404 441 Q 355 486 329 484 Q 316 483 317 465 Z", "M 446 687 Q 507 636 530 577 Q 608 343 711 190 Q 732 163 846 151 Q 892 147 958 141 Q 983 140 984 146 Q 984 152 963 163 Q 756 269 675 396 Q 621 480 551 644 Q 530 690 483 702 Q 449 709 445 702 Q 438 692 446 687 Z" ], "medians": [ [ [ 331, 470 ], [ 358, 421 ], [ 291, 303 ], [ 204, 206 ], [ 143, 156 ], [ 89, 123 ] ], [ [ 452, 695 ], [ 484, 681 ], [ 525, 640 ], [ 645, 378 ], [ 693, 302 ], [ 755, 227 ], [ 839, 190 ], [ 978, 147 ] ] ]}
    

    strokes字段是一个数组,数组的长度表示当前字的笔画数。数组元素是一串字符串,查阅资料得知这是一串SVG指令,按照一定的格式可以转化成可读的坐标点,而这么坐标点连接起来则围成了汉字的笔画,所有的笔画叠加起来,就有了字的轮廓。

    转化SVG指令到可读坐标点

    遇到这种麻烦的样板代码,我们一般都是Google解决。我们可以简单了解基本原理。像这样一串SVG指令,其中的M Q H V其实都是一系列指令,和大多数UI SDK中Path的方法可以对照理解。

    "M 317 465 Q 318 338 155 190 Q 136 178 79 126 Q 67 110 85 113 Q 110 114 146 137 Q 258 209 325 305 Q 368 363 406 409 Q 419 422 404 441 Q 355 486 329 484 Q 316 483 317 465 Z"
    

    贴一段资料:

    我们按照上述指定进行操作,最终就能得到一个List对象(在实际代码中得到的是Path对象,这里方便理解用坐标点数组代替)。而这些坐标点连起来组成Path,使用Canvas绘制出来,就是我们想要的汉字轮廓。上文提到“八”字的轮廓绘制下来如下图:

    汉字描红在Flutter侧的落地

    但是SVG坐标转化出的坐标值,并不能直接绘制出如上图的形状。

    转化原始坐标点到可用坐标点

    我们在上一步得到了文字的坐标点是基于如下的一个坐标系:

    • 田字格大小: 1024- Y轴朝上

    而我们在业务中需要指定的田字格区域,则是一个随机的正方形,而且我们的canvas坐标系是Y轴朝下的。我们记业务中田字格的实际宽度为width,那么如何把基于1024大小且Y轴向上的坐标点,转化到width大小且Y轴向下的画布上呢,我们需要经过以下步骤:

    1. 按按比例缩小(scale):将 1024 大小的坐标点转化成基于 width 的坐标点
    2. 翻转y轴(rotation):将坐标点转化成基于 y 轴朝下的坐标点
    3. 平移(translate):将翻转后的坐标点平移到“原来的”位置

    按比例缩小

    首先我们对坐标点进行比例缩小:

    final scale = width / 1024;
    final Point point = Point(point.x * scale, point.y * scale); 
    

    比例缩小之后的坐标点绘制出来看下效果:

    汉字描红在Flutter侧的落地汉字描红在Flutter侧的落地

    可以看到,本来很大的文字轮廓,比例缩小之后则能够在指定区域内显示,只是方向不太正确。

    翻转Y轴

    这一步我们需要针对坐标点,以x轴为中心翻转y坐标:

    final point = Point(point.x, -point.y)
    

    经过坐标轴翻转之后得到的绘制结果如下:

    汉字描红在Flutter侧的落地汉字描红在Flutter侧的落地

    左右图的对比可以看出,翻转的坐标系x轴其实就是正方形区域的上边,经过旋转之后的坐标点不出所料的出现在了田字格的上边,而其显示方向已经符合预期了。

    平移

    针对翻转后文字绘制超出了田字格的问题,我们需要对坐标点沿Y轴进行平移,而平移的距离就是田字格的宽度(宽度、高度相等)。

    final Point point = Point(point.x, point.y + width);
    

    这一步得出的绘制结果如下:

    汉字描红在Flutter侧的落地汉字描红在Flutter侧的落地

    经过缩小 + 翻转 + 平移的三步操作之后,得到的坐标点已经是比较理想的形态,如果所有的汉字都如示例一样听话,则完全可以作为线上方案发布。随着case的逐渐增加,发现了经过三步操作之后,依然无法正常绘制的文字:

    汉字描红在Flutter侧的落地

    很明显,上面的“薅”字下边超出了田字格。经过长期地Review上述三步操作,貌似并不能找出明显漏洞所在。

    居中矫正

    有上面步骤我们得出了最终的可用坐标点,但是还有一些瑕疵。随着case的增加,我发现在有些字上,最终绘制出的字超出了指定范围。经过在三步操作中打log排查,发现在scale的过程中,一些坐标点的值变成负值。这是一个明显的问题,我们的坐标点无论是在y轴向上的坐标系,还是y轴向下的坐标系,始终都完全保留在第一象限,坐标值变为负值,也就意味着最终绘制势必超出田字格。因为SVG指令到坐标点的转化过程式样板代码,不会只针对我们的场景有问题,如果要追根究底是不是上步操作的顺序和数值有问题,则需要研读开源方案,找出其原理才能找出正确的处理顺序,而最快的解决方案则是对不正确的坐标点进行纠正。考虑到我们汉字的书写习惯,大部分的字都是居中显示的,所以我们可以对经过三步操作的坐标点进行居中矫正。居中矫正的步骤如下:

    1. 找出所有坐标点中(minX, maxX) 和 (minY, maxY)四个值,计算出使文字居中需要在 x 轴 和 y 轴分别需要的偏移值
    2. 如果偏移值 > 0.5 则对相应坐标轴上的坐标值进行平移。太小的偏移值没有必要去浪费计算量。

    最终我们得到的绘制结果如下:

    汉字描红在Flutter侧的落地

    经过比较多的case验证,经过居中矫正的方案可以用于生产。

    如何描红

    描红动画则如一开始动图中所示,是“顺滑的写出一笔”,“顺滑的”则势必就要使用到动画。参考了iOS上的实现:

    汉字描红在Flutter侧的落地

    Flutter则找不到strokeEnd类似的动画属性。对于描红动画,我们能想到最理想的状态则是:

    1. 在足够小的笔画走势区间内,应有一个合适宽度的刷子,刷过这个区间
    2. 合适的宽度的计算:经过此区间内笔画的中心点,并且和两侧的笔画边缘尽量垂直

    对于以上“合适的宽度”这个计算规则,一个普通开发者在短时间内用代码实现出来是很难完成的任务。那我们只能换一种思路。当我看到最终绘制出来的字体是楷体时,我想到了大一的书法课。我们可以想象毛笔在宣纸上写出一个笔画的时候,正符合我们所描绘的理想状态,只是这里合适的宽度的计算过程,变成了毛笔的受力程度。当把毛笔的书写过程剖面解析时,他的场景是这样的:一个圆在二维平面上不断的移动,圆的走势和大小决定了笔画的形状。这里有两个比较重要的点:

    1. 走势
    2. 大小

    笔画走势

    笔画的走势要引入字体数据的第二个字段:medians。从上面的数据结构可以看出,medians 字段的数据是一个二维数组,而维度2的数组其实可以看成一个Point结构,数组的两个元素分别存储了x坐标值和y坐标值。那么整个medians就是一个 List,而这个坐标数组就是笔画走势中比较关键的坐标点,我们叫他骨骼点。连接这些骨骼点就可以看出整个笔画的走势。如下图:

    汉字描红在Flutter侧的落地

    走势我们已经可以知道,骨骼点的连线,则是毛笔的走位。而剩下的大小则让我们绕回了原来的问题。

    圆的大小

    通过计算的方式得到圆的半径,依然是短时间难以完成的任务。但是另一个书法工具-凹版书法贴给了我灵感。我们无需知道圆的半径应该是多少,我们可以给圆指定一个固定的半径,按照上述的笔画走势写出一个很粗的笔画(圆的半径要足够大,要保证圆形区域覆盖笔画的边界,我们取一个经验数值35,如果田字格范围扩大,则等比扩大半径),我们再利用字体的边界Path,对齐进行框选,则能实现工整的笔画。刚好Path也提供了我们这样的方法,我们使用 Path.combine 来模拟书发帖的效果。一个笔画的绘制步骤如下:

    1. 按照骨骼点的顺序,依次以骨骼点绘制出半径为radius的圆,取所有圆的并集得到一个Path。此时的Path代表了一个很粗的笔画走势
    2. 给笔画加上凹版书法帖:和笔画的边界Path取交集得到Path2,这一步可以剔除超出边界的部分,只保留边界内的区域。此时的Path2经过书法帖的纠正,已经是很工整的笔画了
    3. 假设当前笔画的骨骼点的数目为N,我们把0 -> N-1的绘制过程做成动画,自动描红就出来了

    汉字描红在Flutter侧的落地

    解决文字帧数不够的问题

    虽然描红动画如期出现,但是这样的描红动画还是一个不理想的状态,出现了跳帧和笔画断裂的情况。

    我们把上述笔画的骨骼点可视化之后得到如下图的样子:

    汉字描红在Flutter侧的落地

    可以看到整体的骨骼点非常的稀疏,这就导致上面的两个问题:

    1. 在较短时间内,描红动画前进的距离较大,且不均匀
    2. 右侧笔画中第三个到第四个骨骼点之间的距离较大,而我们取的画笔半径,在这两点上绘制的圆形区域是无法取到交集的,于是就出现了断裂笔画

    从取点方的视角来看,这样取点也是比较合理的,因为中间笔画的走势基本保持的同一个方向,前后两个点则是关键点,中间的区域无需取关键点。基于这样的现状,如果我们想让描红动画是连续的,那就需要再次补录一些骨骼点。考虑到动画 60帧/s时比较流畅,也就是每秒钟向前走60个间距较小的骨骼点,才会显得比较流畅。所以补点的时候也应该遵循这样的原则。补帧步骤如下:

    1. 计算出一个笔画上所有骨骼点折线的长度
    2. 经过上步的计算,已知绘制一个笔画的时长(业务自定义),笔画的长度,则计算出每秒应该前进的笔画长度
    3. 到这里,已知每秒前进的距离,除以理想帧数,得到每帧应当前进的距离
    4. 在长度超过每帧前进距离的骨骼点之间,以每帧前进的距离为单位长度,补点

    最终我们可以得到这样的骨骼点:

    汉字描红在Flutter侧的落地

    经过补帧之后,得到的描红动画就比较流畅了。到这里,整个笔画描红的动画经过更多的case验证都无误的话,就可以用于生产了。

    汉字描红在Flutter侧的落地

    迟到的正义

    上面我们说到,排查了很久,都没找到三步操作的漏洞。直到梳理这期文档时候,为了有据可循,我重新查看了iOS的源码,不小心看到了魔法数字:900。于是我花了一个小时把这个参数加入到三步操作中,奇迹般地,所有的字在不经过居中校准的情况下,正常显示。加入魔法数字之后的步骤如下:

    1. 翻转y轴坐标
    2. 延y轴方向平移900
    3. 等比缩小

    绘制结果如下图:(左边是魔法900,右边的居中纠正)

    汉字描红在Flutter侧的落地汉字描红在Flutter侧的落地

    可以说两者的结果别无二致。

    这是“卜”字的SVG代码:

    <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
      <g transform="scale(1, -1) translate(0, -900)">
          <path d="M 519 53 Q 517 149 517 156 L 529 400 L 530 439 Q 531 469 533 518 Q 546 730 559 778 Q 563 793 539 808 Q 508 829 464 837 Q 445 841 433 830 Q 429 825 429 821 Q 428 812 443 790 Q 465 757 466 733 Q 470 664 470 600 L 465 397 Q 461 363 457 296 Q 455 262 443 216 Q 439 171 439 129 Q 437 25 447 -3 Q 462 -58 490 -75 Q 498 -76 502 -71 Q 517 -56 519 53 Z"> </path>
          <path d="M 529 400 Q 570 394 663 410 Q 784 435 791 441 Q 797 447 798 453 Q 798 470 753 483 Q 725 489 622 457 L 530 439 C 501 433 499 403 529 400 Z" ></path>
      </g>
    </svg>
    

    可以看到这里有一个translate(0, -900)的标志,猜测是和上述的900保持对应,当然为什么这么做有待深挖开源库的实现。

    遗留问题

    • 部分生僻字还无法支持
    • 开始绘制不是从笔画最顶端开始
    • 绘制竖勾、横折钩时存在额外绘制
    • 手写校验尚未支持

    起源地下载网 » 汉字描红在Flutter侧的落地

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元