剖析漢字描紅在flutter中的實現原理

SugarTurboS發表於2021-09-02

前言

筆者最近需要做一款基於Android平臺的《學漢字》App,碰巧部門Android開發的同學都沒有時間。筆者作為一名前端碼農,剛好學過Flutter,腦門一熱自告奮勇入坑Flutter App開發。。。

作為一款《學漢字》App,其中核心功能就包括==漢字描紅==與==手寫識別==,實現效果如下:

漢字描紅

image

手寫識別

image

本文重點講解漢字描紅的原理剖析。

漢字描紅

原理分析

如何進行漢字描紅呢?從上面動圖分析,有三個關鍵步驟。

第一步,背景漢字輪廓

image

第二步,背景漢字填充——描紅

第三步,漢字描紅加上過程動畫

image

如何實現以上三個步驟呢?接下來我們一一分析。

如何繪製漢字輪廓?

要繪製漢字輪廓,首先必須要有漢字輪廓資料。筆者想起來,之前做過拼音“abc”的繪製的功能。當時是視覺設計師給了“abc”26個字母的svg的資料,筆者通過svg path資料把字母輪廓繪製出來。

漢字繪製與字母繪製原理相同,但漢字多達幾萬個,讓視覺設計師給出所有漢字svg資料,工作量大的不可想象,顯然不現實。為此,筆者想到了開源。通過搜尋,最終找到了開源字型庫HanZiWriter。

下載所有字型資料,最終是一份.db檔案。通過資料庫工具開啟.db檔案,查詢漢字“三”資料,如下圖所示:

image

關鍵資料如下:

// 漢字名
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]]]

複製程式碼

依據筆畫輪廓資料和骨骼點資料,最終繪製效果如下圖所示:

==紅色點表示骨骼點座標==

image

通過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原始資料繪是這樣的,如下圖所示:

image

跟正常漢字相比翻轉了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;
  }
複製程式碼

轉換之後的效果如下圖所示:

image

漢字方向終於矯正了,但漢字相對田字格向下偏移。由於不知道漢字向下偏移係數,筆者也是除錯了很久,最終找到一個相對合理的係數。漢字預設寬高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;
  }
複製程式碼

最終效果如下圖所示:

image

到此,終於完成了繪製漢字輪廓。

依據漢字筆畫書寫順序描紅

如何漢字筆畫書寫順序描紅呢?

我們知道字型庫HanZiWriter提供了骨骼點資料median。可以通過以下步驟實現基本描紅:

  1. 通過把骨骼點用線連線起來形成一條漢字骨骼線。
  2. 再對骨骼線設定一定的寬度,使骨骼線寬度大於漢字輪廓最大寬度。
  3. 最後通過筆畫輪廓對骨骼線進行裁剪最終得到書寫描紅的效果。

最終效果如下圖所示:

實現程式碼如下:

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);
複製程式碼

完成以上關鍵步驟,基本上就完成了漢字描紅的效果。最後只需要加上動畫,讓漢字描紅的過程,自動繪製,最終如下圖所示。

image

手寫識別

由於時間關係,手寫識別的規則放到後續下一篇文章講解,敬請期待...

image

結尾

關於“漢字描紅”到此告一段落,還有不瞭解原理和實現的童鞋,歡迎留言交流~~~

相關文章