How “AI Shodo” Works

AI Shodo does not use an image-generation AI. It writes calligraphy with a language model.

A typical image-generation AI outputs a finished image directly. Calligraphy, however, is an art that values the process: where the brush is set down, how hard it is pressed, how it is moved and how it is stopped. An image-generation AI has no such process, so it cannot express the essence of calligraphy.
A language model, on the other hand, cannot output images, but it can generate the process of the brushwork as data. By using a language model, we can simulate the very act of “writing calligraphy” and may be able to reach an expression closer to the essence of the art.

Language model × calligraphy engine

The language model is given the following information as input.

From these two pieces of information, the language model outputs the actual movement of the brush as data. The program that turns this data into an actual image is the calligraphy engine sho.js. sho.js is an original program that reproduces the characteristics of the calligraphy brush, simulating the physics of brush and paper to complete the work.

  1. System promptEngine specs and calligraphy know-how
    Task text(e.g.) Write “春風” in regular script
  2. Language modelPlans the brushwork
  3. Brush-movement data{"p":[[377,327,0.06,null,0.8],[418,352,1],…
  4. Calligraphy enginesho.js
  5. Work

The character of each model

sho.js is responsible only for the physical reproduction of brush and paper. Every expressive decision — how to move the brush, what shape to give each character, how to compose the sheet — is left entirely to the language model. As a result, each model’s characteristics show clearly in its works. Comparing the works, you can observe differences between models in their depth of understanding of calligraphy, the correctness of stroke order, technical skill and the quirks of their letterforms.

sho.js specification

This section explains the shape of the brush-movement data, the meaning of its values and how sho.js turns the data into lines, with working examples. The same content is also included in the system prompt given to the language model. (The full system prompt is here; it is written in Japanese.)

Data format

The data is in JSON Lines format: one JSON object per line. sho.js processes it from top to bottom.

{"t":"brush","size":58}
{"c":"永"}
{"p":[[377,327,0.06,null,0.8],[418,352,1],[434,376,2.0,null,0.3,0.12],[430,372,0.06,null,0.4]]}
{"p":[[311,528,0.1,null,0.5],[317,534,1.1,null,0.4], …]}
…
{"comment":"I extended the foot of the right sweep a little."}
LineMeaning
{"t":"brush","size":58}Declares the brush to use. size is the line width at pressure 1.ink_tone (ink darkness) and ink_load (ink amount) can also be specified. When drawing an emoji, the brush can be switched once.
{"c":"永"}Declares the character about to be written. The brush takes up ink at this point (re-inking). To skip it, write {"c":"永","no_dip":true}.
{"p":[[x,y,…],…]}The data of one stroke: a list of points that the brush passes through in order.
{"dip":0.6}The amount of ink taken up when re-inking.
{"comment":"…"}The model’s remarks after finishing the work.

The paper is a portrait-format hanshi, 1000 wide × 1376 high. The top left is (0, 0) and the bottom right is (1000, 1376).

How a stroke is described

A stroke is a series of points, and each point is written as [x, y, pressure, angle, speed, pause]. Any value can be left out by omitting it or writing null. Adding "k" makes that point a corner.

ValueRangeMeaningIf omitted
x, yPaper coordinatesPosition of the brush tip(cannot be omitted)
pressure0–2How deep the brush is pressed into the paper. At 1 the line is as wide as the brush size, at 2 twice as wide. Near 0, only the tip touches and the line is pointedInterpolated from the neighboring points (1 if all are omitted)
angleDegreesDirection from the tip toward the belly of the brushKeeps the previous point’s angle (42 at first)
speed0.05–6How fast the brush moves. 1 is “brush size × 20” per secondInterpolated from the neighboring points (1 if all are omitted)
pauseSecondsHow long the brush rests at that point. The longer it rests, the darker the ink and the rounder and fuller the outline0
"k"—Makes the point a corner instead of connecting smoothly (turning corner)Smooth
A single horizontal stroke. The tip is set down and pressed in right away (exposed tip), the middle of the stroke keeps a constant pace, and at the end the brush slows, presses, rests, then returns slightly and lifts (stop). The red dots are the data points (positions of the brush tip) and the light-blue shapes are the brush head at each point. The head extends from the tip in the direction of the angle, with a length of pressure × size. Use “Points” and “Brush” to toggle the display, and “Draw” to replay the brush movement.

The calligraphy engine joins the points with a smooth curve (centripetal Catmull-Rom), and pressure, angle and speed are also interpolated between points.

Brush: tip and belly

A brush is a bundle of several hundred hairs (about 300 for a size-60 brush; thicker brushes have more), shaped like a drop: thin at the tip and full at the belly. The position of a point is the tip, and the line spreads from the tip in the direction of the angle, with a width of pressure × brush size. The harder the brush is pressed, the more of the belly lies on the paper and the thicker the line; when it is lifted, the line shrinks back to the tip and becomes thin and pointed.

Three lines that differ only in pressure (from the top: 0.4, 1, 1.8). Even with the same brush, the line width changes with how deep it is pressed.

The angle is the tilt of the brush. At the default of 42, the belly lies to the lower right of the tip (oblique brush), so the thick part lies to the lower right of the point. If the angle is aligned with the direction of travel, the tip runs through the center of the line (centered tip) and the brush moves along its axis, so the line is thin. When the brush moves across its axis, the line is at its thickest.

Three lines at pressure 1.2 that differ only in angle (from the top: 42, 0, 90). For a line moving to the right, angle 0 follows the direction of travel and is thin; 90 crosses it and is thick.

Ink: darkness and amount

Ink is determined by two values, darkness and amount, both specified in the brush declaration. If omitted, both are 1: a brush well loaded with dark ink. The two are independent and are used in combination.

Ink darkness ink_tone (0–1) sets the shade of the color and how easily it bleeds. 1 is dark black ink, and lowering it gives a lighter gray. Light ink contains a lot of water, bleeds beyond the line, and leaves a slightly darker edge on the outer rim. The bleeding spreads more where the writing is slow and does not appear in dry-brush areas.

Darkness 1
Darkness 0.5
Darkness 0.2

Ink amount ink_load (0.2–1.5) is the amount of ink the brush holds. It does not change the color; it only decides how easily the brush goes dry.

Amount 1
Amount 0.4

Ink: dry brush and pauses

Each hair holds ink and loses it as it travels over the paper. While ink remains, the line stays connected; where it runs out, the line breaks up along the grain of the paper, producing streaky dry brush. The less ink, the faster the movement, and the later the stroke within the same character, the more easily the brush goes dry. Ink is re-loaded for every character (at each {"c":…} line). The ink on the paper has cloud-like unevenness, so dry brush is not uniform but appears in patches.

Three lines that differ only in speed (from the top: 0.5, 2, 5). The faster the movement, the more streaky the dry brush.

Where the brush rests (pause), ink pools and darkens, and the outline bleeds roundly. The longer the pause, the larger the bleed. The lighter the ink, the sooner it bleeds.

Lines in light ink (darkness 0.5), differing only in the pause at the middle point (from the top: 0 s, 0.15 s, 1.2 s). The longer the pause, the rounder the pool of ink.

Corners: turning corner

At a "k" point the curve is not joined smoothly but forms a corner. Where a stroke turns from horizontal to vertical, slow down before the corner, press in and pause at the corner, then ease the pressure a little after changing direction and bring it back; this leaves a “shoulder” at the corner.

A stroke that turns from horizontal to vertical. A "k" at the corner point leaves a “shoulder” at the corner.

Calligraphic techniques

sho.js has no built-in routines for techniques such as hooks and sweeps. As with a human brush, exposed tip, hidden tip, stop, hook, sweep, hanging dew and hanging needle are all expressed only through the combination of each point’s position, pressure, angle, speed and pause. For example, a left sweep goes down while the brush angle turns clockwise, sinks once at the lower left, then raises speed and releases pressure as it flows out to the left. Below is the example of “永” (the Eight Principles of Yong) given in the system prompt.

“永”. The eight brush techniques — 側・勒・努・趯・策・掠・啄・磔 — are all written with point data alone.

What the language model has to do

Simply lining up coordinates to trace the shape of a character does not make good calligraphy. For every stroke, the language model has to keep the following in mind and design the work as a whole.

Random numbers are determined by the brush position, so the same data always produces the same calligraphy. All differences between works come from differences in the numbers the language models wrote.

GitHub

Coming soon

References

Published: 2026-11-02 12:00 JST
AI Shodo Arena Committee (@enpitsu)