Implementing Chinese Character Auto‑Trace (描红) in Flutter: Data Handling, SVG Conversion, Coordinate Transformation, and Animation
This article details a Flutter‑based solution for automatically tracing Chinese characters, covering data preprocessing, SVG‑to‑coordinate conversion, scaling, Y‑axis inversion, translation, centering correction, and smooth stroke animation with frame‑rate optimization.
Background – The product requires an automatic Chinese character tracing feature. Although a native SDK exists, the team chose Flutter to avoid heavy PlatformView usage and to enable easy integration across multiple UI screens.
Representing Chinese characters in code – The open‑source dataset provides a JSON object containing strokes (an array of SVG path strings) and medians (key points for each stroke). For example, the character “八” is represented as:
{ "strokes": [ "M 317 465 Q 318 338 155 190 ... Z", "M 446 687 Q 507 636 530 577 ... Z" ], "medians": [ [ [331,470],[358,421],... ], [ [452,695],[484,681],... ] ] }The strokes strings are SVG commands (M, Q, H, V, etc.) that can be parsed into a list of coordinate points, which together form the outline of the character.
Converting SVG commands to readable coordinates – By mapping each SVG command to its Canvas equivalent (e.g., M → moveto, Q → quadratic Bézier), we obtain a list of points that can be drawn as a Path on a Flutter canvas.
Transforming coordinates for the target canvas – The original data uses a 1024‑pixel square with the Y‑axis pointing upward. The app’s canvas uses a square of size width with Y pointing downward. The conversion consists of three steps:
Scale: final scale = width / 1024; final Point point = Point(point.x * scale, point.y * scale);
Flip Y‑axis: final point = Point(point.x, -point.y);
Translate: final Point point = Point(point.x, point.y + width);
These operations produce correctly sized and oriented points, but some characters still exceed the square.
Center correction – By computing the min/max X and Y of all points, we calculate the required offset to center the glyph within the square and apply a translation when the offset exceeds a small threshold.
Stroke animation (描红) – The animation mimics a brush moving along the stroke path. Since Flutter lacks a native strokeEnd property, the solution builds a thick brush (a circle of radius ~35) that follows the “bone” points from medians . The brush circles are unioned into a path, intersected with the original stroke path, and animated point‑by‑point.
Frame‑rate issues – Sparse bone points cause uneven motion and broken strokes. To achieve smooth 60 fps animation, the total length of each stroke polyline is measured, the desired per‑frame distance is computed, and additional intermediate points are inserted where the gap exceeds that distance.
Magic number 900 – After reviewing iOS source code, a translation of translate(0, -900) was discovered. Adding this constant after Y‑axis inversion yields correct positioning without the centering step. The final SVG snippet looks like:
<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 ... Z"></path>
<path d="M 529 400 Q 570 394 ... Z"></path>
</g>
</svg>Remaining challenges include handling rare characters, starting strokes from the correct top point, extra drawing for certain stroke shapes, and integrating handwriting verification.
ByteDance Dali Intelligent Technology Team
Technical practice sharing from the ByteDance Dali Intelligent Technology Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.