Text layers
Text layers store styled text, not pixels. The engine renders them on demand and keeps the result as derived data.
Data
interface TextLayerData {
transform: Affine2D; // text space to document pixels, kept non-destructively
frame: { kind: 'point' } | { kind: 'area'; width: number; height: number };
writingMode: 'horizontal-tb' | 'vertical-rl';
paragraph: { align: 'start' | 'center' | 'end'; lineHeight: number };
runs: readonly TextRun[]; // { text, style } with font, size, weight, italic, color, spacing
}Point text grows with its content; area text wraps inside its frame. Vertical writing keeps CJK characters upright, rotates other text, and moves the punctuation marks 、。,. to the upper right of their cell. Other full-width marks, such as brackets and the long vowel mark, are drawn upright without vertical forms.
Creating and editing
import { applyStyle, ops, plainText, replaceText } from 'peinten';
const text = ops.addTextLayer({ text: { writingMode: 'vertical-rl' } });
await doc.dispatch(text);
const { runs } = doc.getSnapshot().nodes[text.nodeId]!.text!;
const edited = replaceText(runs, 0, plainText(runs).length, 'こんにちは');
await doc.dispatch(ops.updateText({ nodeId: text.nodeId, text: { runs: applyStyle(edited, 0, 2, { fontWeight: 700 }) } }));replaceText(runs, start, end, text) and applyStyle(runs, start, end, patch) work on UTF-16 ranges and merge
neighbouring runs with equal styles, so any UI can edit rich text without a dependency on a framework. The
TextTool creates layers on the canvas: click for point text, drag for area text, click existing text to select it.
Rendering
The default CanvasTextEngine measures and draws with Canvas2D through OffscreenCanvas, so it works on whichever
thread hosts the runtime, the worker included. The
TextEngine interface separates layout (glyph positions and bounds) from rasterization, the same split fontkit uses,
so a fontkit-based engine can replace it:
interface TextEngine {
readonly id: string;
layout(text: TextLayerData): Promise<TextLayout>;
rasterize(text: TextLayerData, layout: TextLayout, transform: Affine2D): TextRasterImage | null;
}Canvas text differs slightly between platforms. That is accepted for display, but operations that read text pixels
embed them: rasterizeNode renders on the issuing client and sends the tiles, and the magic wand only samples paint
layers. Brushes cannot paint on text layers; rasterize first.