Takumi

Text

Draw shaped runs or collect values for a text exporter.

A paragraph's text is one TextNode, and node.runs holds its runs in visual order. A run is text shaped with one font and size, so a <b> starts a new run.

Read a run

Hover a run to see its fields. The dot marks run.x and run.y, where the baseline starts.

text
"Text arrives as "
element
none
line
0
x, y
24, 46
width
135.44
ascent
20.1
descent
5.9
fontSize
20
lineHeight
26
font
Geist 400
paragraph.tsx
import {  } from "takumi-paint";

export const  = await (
  < ={{ : 260, : 24, : 0, : 20, : "30px" }}>
    Text arrives as <>shaped runs</> with their baselines.
  </>,
  { : 308 },
);
FieldValue
x, yBaseline start, in the text node's local space
widthAdvance
ascent, descentTypographic distances from the baseline
lineIndex of the line, from 0
elementThe inline element the run came from, such as <b>

In tree.nodes, the text node comes before the inline boxes in its paragraph. Its runs can sit on both sides of those boxes.

run.transform is set when text-fit scales the line. Apply it before placing the run at x, y.

Draw glyphs

run.outlines() returns each glyph's outline as SVG path data, in the order of run.glyphs. The paths are relative to the baseline start, and run.outlines().join("") draws the whole run. Drawing them needs no font engine.

An outline is an empty string for a glyph with no contours, such as a space. It is null for a glyph the run's color does not fill. Color font layers arrive as their own drawables, and bitmap glyphs, such as some emoji, are left out.

DrawableDraw
glyphsFill the outline with paint, blurred by blur
glyphs with strokeStroke the outline, as -webkit-text-stroke does
masked under background-clip: textDraw the background layers, masked by the glyphs

A native text API reads run.glyphs instead. Each glyph has an id in run.font, its x and y offset from the baseline start, and usually a cluster, which is where its cluster starts in run.text, in UTF-16 code units. run.font.data() returns the font file. Runs in the same face share one Font object.

Lay text out again

A slide writer can rebuild a text box from the paragraph box and the run fields.

slides.tsx
import {  } from "takumi-paint";

const  = await (
  < ={{ : 240, : 12, : "center", : "30px" }}>Title</>,
  { : 320 },
);

const  = ..(() =>
  . === "text"
    ? [
        {
          : .?., 
          : .,
          : .,
          : ..(() => ({
            : .,
            : .,
            : .,
            : .,
            : .,
            : [., .], 
          })),
        },
      ]
    : [],
);

.();
ValueField
Text boxnode.parent.contentBox
Alignmentnode.textAlign
Linerun.line
Text and facerun.text, run.font
Font sizerun.fontSize
Line heightrun.lineHeight
Letter spacingrun.letterSpacing
Baselinerun.x, run.y

run.letterSpacing is already applied to the glyph positions, so do not add it again when you place glyphs. Under line-height: normal, a fallback face can make run.lineHeight taller.

Last updated on