剖析汉字描红在flutter中的实现原理前言 笔者最近需要做一款基于Android平台的《学汉字》App,碰巧部门And
前言
笔者最近需要做一款基于Android平台的《学汉字》App,碰巧部门Android开发的同学都没有时间。笔者作为一名前端码农,刚好学过Flutter,脑门一热自告奋勇入坑Flutter App开发。。。
作为一款《学汉字》App,其中核心功能就包括==汉字描红==与==手写识别==,实现效果如下:
汉字描红
手写识别
本文重点讲解汉字描红的原理剖析。
汉字描红
原理分析
如何进行汉字描红呢?从上面动图分析,有三个关键步骤。
第一步,背景汉字轮廓
第二步,背景汉字填充——描红
第三步,汉字描红加上过程动画
如何实现以上三个步骤呢?接下来我们一一分析。
如何绘制汉字轮廓?
要绘制汉字轮廓,首先必须要有汉字轮廓数据。笔者想起来,之前做过拼音“abc”的绘制的功能。当时是视觉设计师给了“abc”26个字母的svg的数据,笔者通过svg path数据把字母轮廓绘制出来。
汉字绘制与字母绘制原理相同,但汉字多达几万个,让视觉设计师给出所有汉字svg数据,工作量大的不可想象,显然不现实。为此,笔者想到了开源。通过搜索,最终找到了开源字体库HanZiWriter。
下载所有字体数据,最终是一份.db文件。通过数据库工具打开.db文件,查询汉字“三”数据,如下图所示:
关键数据如下:
// 汉字名
character: "三"
// 笔画轮廓数据
stroke: ["M 326 667 Q 283 663 312 640 Q 369 610 428 623 Q 543 641 665 661 Q 720 671 729 678 Q 739 688 735 698 Q 728 711 693 722 Q 660 731 561 701 Q 420 673 326 667 Z","M 329 421 Q 304 417 332 392 Q 348 379 385 383 Q 557 405 685 416 Q 721 420 709 440 Q 694 462 657 472 Q 621 479 558 466 Q 435 441 329 421 Z","M 130 165 Q 102 162 122 139 Q 140 120 163 113 Q 191 104 212 110 Q 515 179 929 157 Q 930 158 933 157 Q 960 156 967 167 Q 974 183 953 201 Q 884 255 835 246 Q 643 210 130 165 Z"]
// 骨骼点数据
median: [[[316,655],[367,645],[416,648],[660,692],[722,692]],[[331,407],[375,405],[628,443],[657,443],[700,432]],[[127,152],[158,142],[195,139],[500,178],[846,204],[881,200],[955,174]]]
依据笔画轮廓数据和骨骼点数据,最终绘制效果如下图所示:
==红色点表示骨骼点坐标==
通过Flutter画板组件CustomPainter绘制关键代码如下:
void paint(Canvas canvas, Size size) async {
Paint paint = new Paint()..color = this.color;
chinese.stroke.forEach((element) {
// parseSvgPathData是flutter开源库path_drawing的接口,主要是把svg path转换Flutter Path对象。
Path path = parseSvgPathData(element);
canvas.drawPath(path, paint);
});
Paint paint1 = new Paint()..color = Colors.red;
chinese.median.forEach((element) {
element.forEach((element) {
canvas.drawCircle(element, 2, paint1);
});
});
}
通过以上步骤完成了关键步骤“如何绘制汉字轮廓?”。然而上面步骤看似简单,实际上实现过程中也是踩了一些坑。
填坑之旅
比如字体库HanZiWriter原始数据绘是这样的,如下图所示:
跟正常汉字相比翻转了180度,第一个坑还是很简单处理的,直接把svg path路径所有坐标以中线为x轴翻转180度,代码实现如下:
List<String> transformStroke(List<String> stroke, {double scale = 1}) {
String splitChar = ' ';
bool isY = false; // 是不是y坐标
for (int i = 0; i < stroke.length; i++) {
String path = stroke[i];
List<String> pathItems = path.split(splitChar);
for (int i = 0; i < pathItems.length; i++) {
try {
double num = double.parse(pathItems[i]);
// path数据坐标数据是成对出现,奇数是x坐标,偶数是y坐标。只需要转换y坐标。
if (isY) {
isY = false;
num = height - num;
} else {
isY = true;
}
pathItems[i] = (num * scale).toString();
} catch (e) {}
}
stroke[i] = pathItems.join(splitChar);
}
return stroke;
}
转换之后的效果如下图所示:
汉字方向终于矫正了,但汉字相对田字格向下偏移。由于不知道汉字向下偏移系数,笔者也是调试了很久,最终找到一个相对合理的系数。汉字默认宽高1040px的情况下,y偏移系数120px。最终代码实现如下:
List<String> transformStroke(List<String> stroke, {double scale = 1}) {
const double height = 1040;
const double deviationY = 120;
String splitChar = ' ';
bool isY = false; // 是不是y坐标
for (int i = 0; i < stroke.length; i++) {
String path = stroke[i];
List<String> pathItems = path.split(splitChar);
for (int i = 0; i < pathItems.length; i++) {
try {
double num = double.parse(pathItems[i]);
if (isY) {
isY = false;
num = height - num - deviationY;
} else {
isY = true;
}
pathItems[i] = (num * scale).toString();
} catch (e) {}
}
stroke[i] = pathItems.join(splitChar);
}
return stroke;
}
最终效果如下图所示:
到此,终于完成了绘制汉字轮廓。
依据汉字笔画书写顺序描红
如何汉字笔画书写顺序描红呢?
我们知道字体库HanZiWriter提供了骨骼点数据median。可以通过以下步骤实现基本描红:
- 通过把骨骼点用线连接起来形成一条汉字骨骼线。
- 再对骨骼线设置一定的宽度,使骨骼线宽度大于汉字轮廓最大宽度。
- 最后通过笔画轮廓对骨骼线进行裁剪最终得到书写描红的效果。
最终效果如下图所示:
实现代码如下:
1、绘制汉字骨骼线
Path _strokePath = Path();
_strokePath.reset();
// index 当前笔画数组索引
List<Offset> points = chinese.median[index];
for (int i = 0; i < points.length; i++) {
Offset point = points[i];
if (i == 0) {
_strokePath.moveTo(point.dx, point.dy);
} else {
_strokePath.lineTo(point.dx, point.dy);
}
}
2、骨骼线设置一定的宽度
要实现骨骼线设置一点的宽度,可以收集一条线密集的点绘制一个一个圆串起来就是一条有宽度的线,代码实现如下:
double strokeWidth = 0.05 * width; // 半径
Path fillPath = Path(); // 骨骼线设置一定的宽度
PathMetrics pms = _strokePath.computeMetrics();
PathMetric pm = pms.elementAt(0);
double len = pm.length / 10; // 除以10,避免点太密集,裁剪卡顿。
for (int i = 0; i < len; i++) {
Tangent? t = pm.getTangentForOffset(i.toDouble() * 10);
Offset point = t!.position;
double x = point.dx;
double y = point.dy;
fillPath.addOval(Rect.fromLTRB(
x - strokeWidth, y - strokeWidth, x + strokeWidth, y + strokeWidth));
}
3、裁剪
Path _cutPath = parseSvgPathData(chinese.stroke[index]);
Path _endPath = Path.combine(PathOperation.intersect, fillPath, _cutPath);
完成以上关键步骤,基本上就完成了汉字描红的效果。最后只需要加上动画,让汉字描红的过程,自动绘制,最终如下图所示。
手写识别
由于时间关系,手写识别的规则放到后续下一篇文章讲解,敬请期待...
结尾
关于“汉字描红”到此告一段落,还有不了解原理和实现的童鞋,欢迎留言交流~~~
转载自:https://juejin.cn/post/7003163739529117703