Skip to content

Generating Font Data

The Tegaki generator processes fonts through a multi-stage pipeline to produce stroke data suitable for handwriting animation. Use the studio on the Tegaki website to generate font bundles directly in the browser.

The generator has two ways to find a glyph’s strokes. Both start the same way: opentype.js extracts the path commands and metrics, and adaptive de Casteljau subdivision flattens the bezier curves into polylines.

Works on the outline itself, with no bitmap:

  1. Mesh: a conforming Delaunay triangulation fills the glyph’s ink
  2. Ink graph: the triangles’ chordal axis gives each stroke’s centerline, with its width read from the chords
  3. Spurs and nibs: short offshoots at pointed ends and corners are pruned, and the ink they held is painted by an elliptic nib stamp as the pen passes
  4. Junctions: where strokes meet, the arms are paired so a pen runs straight through a crossing instead of turning
  5. Simplify: width-aware Ramer-Douglas-Peucker keeps the points the pen needs to cover the ink; leftover holes get stamps of their own
  6. Stroke order: ordered by the reference glyph that fits the ink best, from KanjiVG, Make Me a Hanzi, the Hershey fonts, Letterpaths and composed Hangul (pick which in the Studio’s Pipeline → References, or with --reference-datasets), or by your own drawing — draw a glyph’s strokes in the Studio’s Reference stage, Download the dataset, and pass it to the CLI with --reference-file; a letter whose shape misses them all still takes the pen direction a reference clearly prefers; otherwise top-to-bottom/left-to-right

Rasterizes the glyph and thins it to a skeleton:

  1. Rasterize: scanline fill with nonzero winding rule produces a binary bitmap
  2. Skeletonize: Zhang-Suen thinning reduces the bitmap to 1px-wide skeleton
  3. Trace: walks skeleton pixels into polylines, prunes short spurs, simplifies with Ramer-Douglas-Peucker
  4. Width: a distance transform computes stroke width at each skeleton point
  5. Stroke order: groups polylines into connected components, sorts top-to-bottom/left-to-right

Some fonts are already pen strokes: Hershey’s plotter fonts, the single-line SVG fonts made for pen plotters and engravers (Evil Mad Scientist’s Hershey and EMS faces, Relief SingleLine), or a stroke-order dataset you drew in the studio. Nothing is extracted from them: their strokes go into the bundle as they are, in the font’s own order and direction, drawn with a round pen of the width you pick. Tegaki then makes a font file from the strokes (each glyph’s outline is its strokes as the pen draws them, at the font’s advance widths), so the renderer lays out, wraps and clips the text as it does for any bundle, and every plugin works on them.

In the studio, pick one from Single-line fonts in the font picker (or upload an .svg font, a .jhf file or a .strokes.json dataset) and set the pen in Pipeline → Stroke font. From the CLI:

bun start generate --stroke-font EMSAllure.svg --pen-width 0.06 -o output/ems-allure

-c narrows the characters (default: every glyph in the font), and --drawing-speed / --stroke-pause time the strokes as for the pipelines. The font’s credits and license, which single-line fonts carry in their metadata, are written beside the bundle as LICENSE.txt.

A bundle can hold several weights of a font. Each weight is traced on its own, and the renderer draws the one the text’s font-weight picks (see font).

In the studio, with a Google font loaded, Style → Text → Weight picks the weight the studio loads, draws and inspects, and the chips in Pipeline → Weights pick the weights Export → Download bundle writes. The weight drawn is always one of them.

From the CLI, list the weights of a Google font, or add other files of a local font:

bun start generate Caveat --weights 400,700 -o output/caveat
bun start generate --font-file Foo-Regular.ttf --weight-file Foo-Bold.ttf -o output/foo

The first weight listed is the bundle’s own face, the one a renderer from before weights draws. Each other weight is listed in faces, with its files named after the first’s with its weight added (caveat-700.ttf, glyphData-700.json). A local file’s weight is read from its OS/2 table.

Option Default Description
Font Caveat Google Font family name
Characters A-Za-z0-9 + punctuation Characters to process
Pipeline geometry Stroke extraction: geometry or raster
Resolution 400 Bitmap resolution for rasterization (raster)
Skeleton method zhang-suen Skeletonization algorithm (raster)
Line cap round Stroke line cap style
Bezier tolerance 0.5 Flatness tolerance for bezier subdivision
RDP tolerance 1.0 Ramer-Douglas-Peucker simplification tolerance (raster)

The generator outputs a compressed format with short keys to minimize bundle size. Per-glyph data includes w (advance width), t (total animation duration), and s (strokes). Each stroke has p (points as [x, y, width] tuples), d (delay), a (animation duration), and, for geometry strokes that carry them, n (nib stamps as [pointIndex, dx, dy, major, minor, angle]). See the TegakiGlyphData type for full details.

A bundle of several weights also has weight, its own face’s weight, and faces: for each other weight, its weight, font URLs and glyph data (TegakiFace). The family name, metrics and OpenType features are the bundle’s and apply to every weight.

Use the studio to visualize each pipeline stage and fine-tune options.