Ollin

Text

Ollin draws text in three kinds of font. All three use the same drawText / textFont / textSize / textAlign calls:

  • An outline font is a real TrueType/OpenType (.ttf/.otf) face. Each glyph is stored as vector contours and drawn as a Shape. One load draws crisp at any size. The type takes fill and any stroke you set, and the glyph geometry is yours to work with. The default font is an outline font, so text works with no setup.
  • A bitmap font stores each glyph as a small grid of pixels. Each lit pixel is stamped as one square on the same instanced-SDF path the shapes use, so there is no rasterizer and no separate pipeline.
  • A stroke font is a single-line (plotter) face. Its glyphs are open pen paths with no fill, drawn with the current stroke. This is the kind of letterform a pen plotter draws, and Hershey Sans comes bundled.

All three follow the transform stack, composite in draw order with everything else, and stay crisp at any size.

Three rows showing the same word: an outline font filled and stroked, a bitmap font built from visible squares, and a stroke font drawn as a single thin pen line

The default font is OutlineFont.systemMedium, the system UI face (San Francisco on macOS) at medium weight. That weight is a little sturdier than the regular one, so text holds up over busy canvases, and drawText works with no setup. For a pixel look, use the bundled BitmapFont.builtIn, which is Cozette. Cozette is a 13px pixel font that covers a wide range. It has Latin (including the Spanish accents á é í ó ú, ñ, ü, ¿, ¡), Cyrillic, Greek, and Japanese kana. Call these bare inside draw(), and they forward to the Drawer.

Contents

drawText

drawText(_ string: String, _ x: Double, _ y: Double)
drawText(_ string: String, at position: Vector2)
drawText(_ string: String, at position: Vector2,
         size: Double? = nil, color: Color? = nil,
         align horizontal: HorizontalTextAlign? = nil, _ vertical: VerticalTextAlign? = nil)

Draw string at a point, in the current fill color, using the active textFont / textSize / textAlign. A \n starts a new line. Glyphs are geometry, vector shapes for an outline font and SDF squares for a bitmap one. So text rotates and scales with the transform stack like everything else. noFill() draws nothing, and a character the font does not have advances the pen but draws nothing.

background(.black)
fill(.white)
textSize(120)
textAlign(.center, .middle)
drawText("ollin", width / 2, height / 2)

The styled form draws a label in one call. Any of size, color, and align that you give apply to this text alone, so the standing state comes back untouched. It replaces the four calls a label usually needs first:

drawText("Hello", at: center, size: 32, color: .white, align: .center, .middle)

align takes the same pair as textAlign(_:_:), and the vertical part defaults to .baseline. color paints the glyphs in exactly that color for every font kind. It sets the fill, or the stroke for a stroke font, and it suppresses the outline that a standing stroke would add. The scalar (x, y) and box forms take the same three arguments.

drawStatus & drawCaption

drawStatus(_ message: String, style: StatusStyle = .info, in container: Rectangle? = nil)
drawCaption(_ text: String, edge: CaptionEdge = .bottom)

Each of these draws a standard piece of text in a single call, so a state the sketch should report never stays silently blank:

  • drawStatus centers a standard notice in container, which is the whole canvas by default, and wraps the notice if it is long. An .info notice draws in quiet gray, as in "Waiting for camera…", which drawFrame does for you. A .warning notice draws in salmon, for a vision model that can't run here or a missing install.
  • drawCaption draws a small white label along the bottom edge, or along the top with edge: .top. Use it to say what the sketch is, what it is showing, and what to do with it. It draws over the finished frame, the way a withOverlay block does, so it stays sharp and white over a depth of field, a motion blur, a bloom, or a lens flare, and never joins an accumulating canvas.
if let reason = tracker.unavailableReason {
    return drawStatus(reason, style: .warning)
}
drawCaption("FaceTracking · \(faces.count) faces")
A dark canvas waiting on a camera feed, with the quiet gray drawStatus notice centered in it and the small white drawCaption label along the bottom edge, each pointed out by name

Both draw in their own standard typeface, size, and alignment. That state is scoped like a withState { }, so the sketch's own fill, font, and alignment are untouched afterward.

textFont

textFont(_ font: BitmapFont)
textFont(_ font: OutlineFont)
textFont(_ font: StrokeFont)

Set the active font. It can be a bitmap face on a pixel grid, an outline face from a .ttf or .otf file, or a stroke face drawn as single lines. The default is OutlineFont.systemMedium, and textFont(OutlineFont.systemMedium) switches back to it. Use textFont(BitmapFont.builtIn) for the bundled Cozette pixel font. Like the other drawing state, the active font is part of the push/pop stack, so withState { textFont(custom); … } restores the previous font automatically on exit. See BitmapFont to load or build a bitmap font, Outline fonts for a .ttf/.otf, and Stroke fonts for single-line type.

textSize

textSize(_ size: Double)

Set the rendered text height in points, the height one line of glyphs occupies on screen. A BitmapFont is authored at a native pixel height. textSize scales each of its pixels to a module of size / font.pixelHeight points, so textSize(140) makes one line 140 points tall whatever the font's grid. The default is 24.

textAlign

textAlign(_ horizontal: HorizontalTextAlign, _ vertical: VerticalTextAlign = .baseline)

Set how text is anchored to the drawText position.

  • Horizontal (HorizontalTextAlign): .left (default) starts the text at the x, .center centers it, .right ends it at the x.
  • Vertical (VerticalTextAlign): .baseline (default, like p5) sits the first line's baseline on the y. .top and .bottom align the block's top and bottom edge, and .middle centers the whole block. .center is accepted as an alias, so textAlign(.center, .center) compiles.
A three-by-three grid of the same two-line block with a dot marking the anchor point in every horizontal and vertical alignment combination: left starts the text at the x, center centers it, right ends it there, while top hangs the block below the y, middle centers it, and bottom stands it above; a strip below shows the default baseline anchor with the first line's baseline sitting on the y
textAlign(.center, .top)
drawText("two\nlines", width / 2, 100)   // centered, growing downward from y = 100

textWidth

textWidth(_ string: String) -> Double

The on-screen width of string's widest line, in points, at the current textFont / textSize. Use it to lay text out yourself, for centering by hand, wrapping, and marquees.

let size = 42.0; textSize(size)
let w = textWidth(label)
drawRect(center: Vector2(x, y), width: w + 24, height: size + 16)   // a padded backing

Outline fonts

textFont(_ font: OutlineFont)

An OutlineFont is a real TrueType/OpenType (.ttf/.otf) face. Unlike a bitmap font, it stores each glyph as vector contours. So one load draws crisp at any textSize, and you never reload for a new size. Set it with the same textFont, and drawText, textSize, textAlign, and textWidth all work exactly as before.

An outline glyph is rendered as a Shape, the same vector fill the triangulator draws. So outline text takes the current fill and composites in draw order with everything else. Once you have set a stroke, text takes that too. That gives you filled text, outlined text, and stroke-only text:

let display = OutlineFont(name: "Avenir Next") ?? .system
textFont(display)
textSize(160)
textAlign(.center, .middle)

fill(.white); noStroke()
drawText("filled", width / 2, 240)

noFill(); stroke(.white); strokeWeight(2)
drawText("outline", width / 2, 480)

[!NOTE] Text uses a stroke you have set. A leftover stroke(...) from earlier drawing will outline your glyphs, so call noStroke() when that is not the look you want. The one stroke that never applies to text is the initial default, the 1px black every fresh sketch starts with. A band around every contour would bold text on a light ground and eat thin light glyphs on a dark one. So glyphs take only a stroke the sketch asked for.

Loading a font, from anywhere:

OutlineFont(name: "Helvetica Neue")             // an installed family / PostScript name
(try? OutlineFont(path: "/path/to/Font.otf"))          // a file on disk
(try? OutlineFont(url: fileURL))                        // a file:// URL
(try? OutlineFont(data: bytes))                          // raw .ttf/.otf data
(try? OutlineFont(resource: "Font.ttf", in: .module))   // a font bundled beside your sketch

OutlineFont.system        // the system UI font (San Francisco on macOS)
OutlineFont.systemMedium  // medium weight, the default text font
OutlineFont.systemBold
OutlineFont.systemMono

init(name:) returns nil if no installed font matches, so a typo fails loudly instead of quietly substituting another face. Pair it with a fallback, as in OutlineFont(name: "Futura") ?? .system. A remote https:// URL needs an asynchronous download, and that cannot run on the draw thread. Fetch it in setup(), or ahead of time, then pass the bytes to init(data:).

Ollin ships no .ttf/.otf of its own, so you bring the font yourself, installed, bundled, or fetched. Layout goes through Core Text, so kerning, ligatures, and fallback for missing glyphs come with it.

Rendering at volume

By default each outline glyph is filled as a vector shape. That is the highest quality, and it takes fill and stroke. It is the right choice for headlines and body text. But a paragraph of thousands of glyphs is re-tessellated every frame, and that gets expensive. textMode(.atlas) switches outline text to a signed-distance-field atlas instead. Each glyph is rasterized once into a shared texture and drawn as a single quad that samples it. The per-glyph cost then drops to a handful of vertex writes. The text stays crisp under magnification, and one raster serves every textSize.

textFont(OutlineFont.systemMono)
textSize(18)
textMode(.atlas)              // the default is .outline

for (i, line) in paragraph.enumerated() {
    drawText(line, margin, top + Double(i) * lineHeight)
}

textMode is drawing state, like textSize and textAlign. It persists until you change it, and it scopes with withState { }. So you can keep big headlines on the crisp .outline path and switch to .atlas for the block of small text:

textMode(.outline); textSize(120); drawText("Title", x, y)
textMode(.atlas);   textSize(16);  drawText(bodyText, x, y2)

Setting text one character at a time costs no more than one string. Calls in a row with nothing else drawn between them share one draw call, whatever their color and size, so a grid of characters placed cell by cell is as cheap as the same characters in lines.

There are two things to know. The atlas path is fill-only and ignores stroke, because the volume case is filled text, so use .outline when you want stroked glyphs. The single-channel field also rounds very sharp corners at extreme magnification, which you cannot see at the body and display sizes this path is for. textMode does nothing for bitmap and stroke fonts, which have no atlas. See the TextVolume example.

Variable fonts

A variable font holds a family's continuous design axes (weight, width, optical size, slant) in one file. OutlineFont exposes those axes. variation(_:) returns a copy with axes set, and variationAxes lists what a font offers and the range of each axis.

let skia = OutlineFont(name: "Skia")!
print(skia.variationAxes)   // [(tag: "wght", name: "Weight", min: 0.48, max: 3.2, default: 1), …]

textFont(skia.variation(["wght": 2.6, "wdth": 1.2]))   // heavy, a touch wide

Keys are the 4-character axis tags ("wght", "wdth", "opsz", "slnt"). Values are in the font's own axis units, and the ranges differ from font to font, so read them from variationAxes. The one-axis calls weight, width, opticalSize, and slant each set a single axis. Combine several of them in one variation(_:) call, and they do not reset each other.

A varied font is another OutlineFont, so you can animate the axes to move a word from thin to heavy:

textFont(skia.variation(["wght": map(sin(time), -1, 1, 0.5, 3.1)]))
drawText("ollin", width / 2, height / 2)

A non-variable font ignores variation and returns an empty variationAxes.

Stroke fonts

textFont(_ font: StrokeFont)

A StrokeFont is a single-line font, so each glyph is a set of open pen paths with no interior. A bitmap font stamps pixels and an outline font fills contours. A stroke font is stroked instead. It draws with the current stroke (weight, join, cap) and ignores fill, which is the mirror image of outline text. It is the letterform a pen plotter wants, and it suits thin, even line weights.

textFont(StrokeFont.builtIn)        // Hershey Sans, bundled
textSize(160)
textAlign(.center, .middle)
stroke(.white); strokeWeight(2)
strokeCap(.round); strokeJoin(.round)   // a softer, drawn line
drawText("ollin", width / 2, height / 2)

The bundled default is Hershey Sans, from the public-domain Hershey vector fonts. The glyphs are open paths, so textToShapes hands them back as open contours. Stroke or warp those rather than filling them. They also go through the same per-glyph and text-on-a-path calls as the other font kinds.

Loading more single-line fonts: Ollin reads the Hershey .jhf format. You can drop in any of the many Hershey faces (serif, script, gothic, Cyrillic, Greek):

let serif = (try? StrokeFont(resource: "rowmans.jhf", in: .module)) ?? .builtIn
textFont(serif)

A StrokeFont measures itself in the font's own units, which unitsPerEm divides to turn into a fraction of the em: ascentUnits, descentUnits, lineGapUnits, and spaceAdvanceUnits are the raw numbers, and ascent, descent, and leading the same three as fractions, so multiplying by the rendered textSize gives points. advanceUnits(for:) is one character's pen advance and lineAdvanceUnits(of:) a whole line's, both in font units. Each glyph is a StrokeGlyph, whose polylines are the pen paths the stroke follows.

StrokeFont(resource:in:) loads a font bundled beside your sketch, and you pass .module for a swift run sketch's resources. StrokeFont(jhfContentsOf:) takes any file URL, and StrokeFont(jhf:) parses .jhf text you already have. The Hershey faces live in many public-domain mirrors, for example kamalmostafa/hershey-fonts. For the wider world of single-line type, Golan Levin's single-line-font resources is a good map, and each font there has its own license to check. Ollin ships only the parser and the one Hershey default.

Per-glyph drawText

drawText(_ string: String, _ x: Double, _ y: Double, perGlyph: (TextGlyph) -> Void)
drawText(_ string: String, at position: Vector2, perGlyph: (TextGlyph) -> Void)

This trailing-closure form of drawText hands you each glyph as a TextGlyph instead of drawing the string for you. Give each letter its own transform or color, then stamp it with g.draw(). Use it for per-letter waves, rainbows, and springs. It draws a single line.

textAlign(.center, .middle)
drawText("ollin", at: center) { g in
    fill(palette.color(at: g.progress))          // a hue per letter
    withState {
        translate(0, sin(time * 3 - Double(g.index) * 0.7) * 60)   // bob on a wave
        g.draw()
    }
}

The closure runs once per piece. A piece is one character in Latin, a whole syllable in Devanagari, or a ligature. See Every script.

Each piece's shapes are the outline the plain drawText fills: cleaned of the overlapping sub-contours some system faces build a glyph from, and simplified to a third of a pixel. A glyph stamped unchanged, or set along a path, is therefore the plain run's glyph to the pixel.

A TextGlyph carries:

  • text is the source characters of the piece, and character is the first of them.
  • index and count are its place in the run, and progress is the normalized position 0...1 across it.
  • The order runs left to right on the canvas, even for a right-to-left script that reads the other way.
  • position is the pen origin in canvas space, at the left edge and on the baseline. center is the natural pivot for rotating the glyph in place, and bounds is its advance box.
  • shapes is the glyph geometry in canvas space, for text-as-geometry per letter.
  • isPicture marks an emoji stored as a bitmap in the font. Its shapes are empty, but draw() still stamps it.
  • draw() stamps the glyph with the current fill / stroke, through your transform.

To rotate a glyph about its own center, pivot on center:

withState {
    translate(g.center); rotate(angle); translate(g.center * -1)
    g.draw()
}

Text on a path

drawText(_ string: String, along path: Path, offset: Double = 0)

Lay each glyph along a Path, rotated to the path's tangent so the baseline follows the curve. Each glyph is centered on the point offset plus its own distance along the run, measured as arc length from the path's start. A glyph that falls before the start or past the end is skipped, so animating offset flows the text on and off the ends. This form draws a single line, and it takes fill and stroke like drawText.

let path = Path { p in
    p.move(to: Vector2(0, height / 2))
    for i in 1...60 {
        let f = Double(i) / 60
        p.curve(to: Vector2(f * width, height / 2 + sin(f * .pi * 3) * 160))
    }
}
fill(.white)
drawText("text on a curve · ", along: path, offset: -time * 120)   // scrolling

Where a path bends tighter than the text is tall, the glyphs crowd on the concave side. Every text-on-path layout does that, so keep the curves broad relative to the text size.

Box layout

drawText(_ string: String, in rect: Rectangle)

Wrap string into a Rectangle. Words break to the next line at the box width, and textAlign positions the wrapped block within the box. The horizontal .left/.center/.right align against the box edges, as do the vertical .top/.middle/.bottom. An explicit \n starts a new paragraph. This works for every font kind.

textSize(42); textAlign(.left, .top)
let box = Rectangle(center: center, width: 600, height: 700)
fill(.white)
drawText(paragraph, in: box)

Measuring and wrapping happen every frame, so the text reflows live if the box or the size changes. Text taller than the box overflows below it, since there is no vertical clip yet.

The system's own rules decide where a line may break, not the spaces in the string. So a Japanese paragraph breaks between characters and a Thai one between words, and neither is written with spaces at all. See Every script.

Example: Examples/Text/TextBox. Its box changes size while a grid of rules spaced by textLeading() holds still under the rewrapping lines.

textToShapes

textToShapes(_ string: String, _ x: Double, _ y: Double) -> [Shape]
textToShapes(_ string: String, at position: Vector2) -> [Shape]

The glyphs of string as vector Shapes, positioned exactly where drawText would place them, at the current textFont / textSize / textAlign. This gives you text as first-class geometry. Warp it, sample points along it, scatter particles on it, or animate the contours, then fill or stroke the result like any other shape. An outline font returns one Shape per glyph, so a letter with a counter (o, e, a) keeps its hole. A bitmap font returns its lit pixels as squares.

textSize(300); textAlign(.center, .middle)
for shape in textToShapes("ollin", width / 2, height / 2) {
    // Ripple each letterform with a bounded, smooth field.
    let rippled = Shape(contours: shape.contours.map { c in
        Contour(c.points.map { p in
            p + Vector2(signedNoise(p.x * 0.004, p.y * 0.004, time),
                        signedNoise(p.x * 0.004 + 40, p.y * 0.004, time)) * 14
        }, closed: c.isClosed)
    })
    drawShape(rippled)
}

[!WARNING] Displacing outline points by a large or uneven amount can fold a contour over itself. The fill renders that fold as a spike. Keep warps bounded and smooth, for example signedNoise, which stays in -1...1. Do not use raw curlNoise, whose magnitude is unbounded. See the TypeAsGeometry example.

An emoji has no geometry to hand back, because the font stores it as a bitmap. It is left out, and a note is printed once. drawText still draws it. See Every script.

One more thing is worth knowing about the returned geometry. The outline points come back unevenly spaced, as the raw layout vertices, dense on curves and sparse on straights. That is fine for warping and filling, but marks placed one per point (dots, dashes, particles) would clump. Respace a glyph first with resampled(spacing:), as shape.resampled(spacing: 8) or per contour, and the marks spread evenly. The GlyphContours and TypeAsGeometry examples do exactly this.

Type as a solid

Glyphs are shapes, and a shape extrudes, so a word can be a 3D solid that takes a material and throws a shadow:

drawText3D("Ollin", size: 2, depth: 0.4)

size is measured in world units, not the canvas points textSize uses, and only the font comes from the text state. Mesh.text(...) builds the same word once for a string that does not change. Mesh.textGlyphs(...) returns it a letter at a time, and each letter knows where it sits. Two traps live under those calls. Canvas y runs down while the world's y runs up, and a glyph is simplified against the size it is asked for. That is why this is a call rather than an extrusion you write yourself. See 3D for the whole story, and the 3D/SolidType example.

Metrics

textAscent() -> Double      // baseline up to the top of the tallest glyphs
textDescent() -> Double     // baseline down to the bottom of the lowest descenders
textLeading() -> Double     // baseline-to-baseline distance (what `\n` advances by)
textBounds(_ string: String, _ x: Double, _ y: Double) -> Rectangle
textBounds(_ string: String, at position: Vector2) -> Rectangle
A large sample line with the ascender line, baseline, and descender line ruled through it, textAscent and textDescent bracketed from the baseline at the right, a ghosted next line one textLeading below, and the baseline labeled as drawText's y

All of these are in points at the current textFont / textSize, and they work for every font kind. textBounds returns the box string would occupy if drawn at (x, y) with the current alignment. Use it for backings, layout, and hit-testing.

Every script

An outline font lays text out through the system's own shaping engine, so a string in any script draws with no setup:

drawText("مرحبا بالعالم", 80, 200)      // Arabic, right to left, letters joined
drawText("क्षि नमस्ते", 80, 300)          // Devanagari
drawText("ที่นี่มีคนอยู่", 80, 400)          // Thai, marks stacked
drawText("日本語のテキスト", 80, 500)     // Japanese
drawText("hi 👋", 80, 100)              // and a picture the font carries
Four panels. A Devanagari syllable inside one box, labeled one call of the closure. An Arabic word with its pieces numbered zero to four from the left, noting that zero is the last letter read. The letter O and a waving-hand emoji drawn twice: once filled, once as outlines where only the O has any. A Japanese paragraph wrapped inside a thin box

Four assumptions that English lets you make stop holding in other scripts:

One glyph is not one letter. A Devanagari syllable is several glyphs. The shaper moves one of them to the left of the letter it follows. An Arabic letter carrying a vowel mark is two glyphs at almost the same place. A ligature works the other way, with one glyph standing for two characters. So per-glyph drawText hands you a piece a reader would point at, not a glyph. TextGlyph.text is a String for exactly that reason, and character is still there for the one-character case.

A line does not always run left to right. Arabic and Hebrew run right to left, and a line can hold both directions at once. The pieces still come out left to right on the canvas, so g.index sweeps across the drawing rather than through the reading. Where the base direction matters, set it with textDirection.

Not every script marks its word ends with a space. Japanese and Chinese write without spaces, and they may break between almost any two characters. Thai writes without spaces too, and it breaks only between words. Rather than splitting on spaces, box layout asks the system where a line may break, so a paragraph in any script fits its box. The Japanese rules about line edges come with that break set. A full stop, a comma, a closing bracket and a small kana all arrive joined to the character before them. So none of them can open a line, and an opening bracket cannot close one.

An emoji is a picture, not an outline. The color emoji font stores each one as bitmaps, so there are no contours to fill. drawText rasterizes it and places it as an image. Three things follow from that. It carries its own colors and takes neither fill nor stroke. textToShapes leaves it out and prints a note once, because there is no geometry to hand back. An SVG or PDF export skips it like any other image. In a per-glyph closure, TextGlyph.isPicture marks those pieces. Their shapes are empty, and draw() still works.

The picture is rasterized once at the largest size the font actually carries. For Apple Color Emoji that is 160 pixels per em, read from the font's own table. So one raster serves every textSize, and an emoji drawn much larger softens. That is the format's limit rather than Ollin's.

Font fallback happens whether you ask or not. Asking a Latin font for Japanese does not fail. The system borrows a face that has the letters. OutlineFont.fontsUsed(for:) shows which faces a line really used.

print(OutlineFont.system.fontsUsed(for: "Ollin 日本語 👋"))
// ["System Font Regular", ".PingFang UI Text SC Regular Text", ".Apple Color Emoji UI"]

Bitmap and stroke fonts have no shaping engine and no fallback. They lay out character by character, left to right, from the glyphs in their own file. That is the right model for a pixel font or a plotter pen, and it is why textMissingCharacters matters more there.

Worked example: Examples/Text/Scripts.

textDirection

textDirection(_ direction: TextDirection)     // .automatic (default) / .leftToRight / .rightToLeft

Set the base direction of a line, the direction the line as a whole runs in. It decides where the neutral characters (spaces, brackets, digits) land, and which end the line starts at.

.automatic reads the direction off the text, taking it from the first letter that has one. That is right for a paragraph in one language, and wrong when the line opens with something neutral:

textDirection(.leftToRight)
drawText("(1) مرحبا", 80, 100)      // (1) on the left

textDirection(.rightToLeft)
drawText("(1) مرحبا", 80, 200)      // (1) on the right

Direction reorders a line, and it never changes how wide the line is. It is drawing state, so it scopes with withState { } like fill and textSize. It applies to outline fonts only.

Writing in columns

textDirection(.topToBottom)

Japanese and Chinese can be written down the page instead of across it, and the columns fill right to left. This TextDirection turns the writing onto its other axis, which changes what the other text settings measure.

textDirection(.topToBottom)
textAlign(.right, .top)
drawText("春はあけぼの。やうやう白くなりゆく山ぎは、", 980, 120)
Top: one Japanese sentence set across a line, then its opening set down a column. Middle right: an opening bracket, a comma and an opening parenthesis shown upright above their turned forms. Bottom: the same passage set in two identical boxes, justified on the left where every full column reaches a red rule, ragged on the right where each stops short of it

Turned characters come from the font. A font carries a second shape for each character that turns, and vertical setting picks those shapes. A bracket lies down, and a comma moves to the top right of its square.

The face therefore decides what happens to Latin in a column. A Japanese face carries turned Latin forms, so an English word reads sideways among the kana. A Latin face has none, so its letters stack upright.

across a linedown a column
\nthe next line, belowthe next column, to the left
the vertical textAlignplaces the block of linessays where each column starts
the horizontal textAlignsays where each line startsplaces the block of columns
textWidthhow wide the line ishow long the column is
drawText(_:in:) wraps againstthe box widththe box height

Columns are one em wide, so a block of columns keeps its width as the text changes. Column spacing is textLeading.

Two things stay out of this. Text on a path keeps running along the path, because the curve already says which way the text travels. Bitmap and stroke fonts stay horizontal and print a note once, as they do for .rightToLeft.

Example: Examples/Text/Columns.

Columns that fill the other way

textDirection(.topToBottomLeftToRight)

Traditional Mongolian is vertical too. Its columns fill left to right, so \n starts the next column to the right.

textFont(OutlineFont(name: "Noto Sans Mongolian")!)
textDirection(.topToBottomLeftToRight)
textAlign(.left, .top)
drawText("ᠮᠣᠩᠭᠣᠯ ᠪᠢᠴᠢᠭ", 200, 120)

These letters join into one connected stroke. Each letter is as wide as its own shape, so an em square cannot hold it.

Ollin shapes the line horizontally, which keeps the joins and the widths. It then turns that line a quarter turn clockwise.

Top: one Mongolian phrase set across a line, an arrow curving a quarter turn clockwise, and the same phrase standing as a column. Bottom: three identical Mongolian columns with an arrow running left to right and the first column marked at the left, beside three Japanese columns with an arrow running right to left and the first marked at the right

A column otherwise behaves as it does under .topToBottom, including the textAlign axes.

A column is as wide as the face's ascent and descent together, because the turn puts the line's height across the column. The face must also have the script, because a column takes its width from the face you gave it. The system font lacks this script, so ask for one that has it.

Use .topToBottom for Japanese, because this mode would lay every character on its side.

Example: Examples/Text/MongolianColumns.

textJustify

textJustify(_ on: Bool = true)     // default off
noTextJustify()

Make wrapped text reach both edges of its box.

textJustify()
drawText(paragraph, in: Rectangle(x: 80, y: 80, width: 400, height: 600))

Only a box says how far a line should run, so this applies to box layout and nothing else. Text at a plain position is never stretched.

The last line of each paragraph keeps its natural width. That line is short because the writing ended there, and the box had nothing to do with it. textAlign decides which way it sits.

The layout engine decides where the extra room goes. English opens the spaces between words. Japanese has no such spaces, so it opens the gaps between characters. A column justifies like a line and reaches the bottom of its box.

Justification never squeezes a line. A line already at or past the box is left alone.

textHangingPunctuation

textHangingPunctuation(_ on: Bool = true)     // default off
noTextHangingPunctuation()

Let a full stop or comma at the end of a line sit past that end.

textHangingPunctuation()
drawText(passage, in: box)

A stop may not open a line. So a stop that will not fit would normally take the character it follows onto the next line. That leaves a hole at the edge where both used to be. Hanging leaves the pair where it is and lets the stop cross the edge. Japanese calls it ぶら下げ, and it is the same move a Latin typesetter makes to keep a right margin looking straight.

The same Japanese passage in two identical boxes, each with a red rule down its right edge. On the left every character stays inside the rule and the passage runs to six lines. On the right three full stops sit across the rule and the passage fits in five

Only stops and commas hang: 。 and 、, their full-width and half-width forms, and the Latin . and ,. A closing bracket may not open a line either, but hanging one would leave the bracket outside the thing it closes.

This happens at the wrap, so it applies to box layout and nothing else, exactly as justification does. A hung character is left out of how far its line counts as running, so alignment and justification measure the rest of the line. That part is outline-font work. A bitmap or stroke font still wraps this way, and it still aligns on the whole line.

It shows only where a stop would not otherwise fit, so change the box width and it comes and goes.

Example: Examples/Text/HangingStops.

textMissingCharacters

textMissingCharacters(_ string: String) -> [Character]

The characters the current font cannot draw, in the order they appear. OutlineFont.missingCharacters(in:) asks one loaded face the same question without making it the active font.

For an outline font this asks the whole system. The result is empty unless no installed face has the character at all, and then the character draws as a box rather than vanishing. For a bitmap or stroke font this is the question that matters. Those fonts have only the glyphs in their own file, and anything else advances the pen and draws nothing.

textFont(BitmapFont.builtIn)
print(textMissingCharacters("日本語"))     // [] - Cozette has the kanji
print(textMissingCharacters("नमस्ते"))      // the Devanagari, which it does not

Example: Examples/Text/Scripts. It carries a private-use code point that no installed face has, and it names that code point before drawing the line.

BitmapFont

A BitmapFont is a value type. It holds a table of BitmapGlyphs, each a small bit grid placed by its own xOffset and yOffset from the pen, plus the layout metrics pixelHeight, baseline, and lineHeight. spaceAdvance is the pen advance for a space when the font has no glyph for one, and kerning is the pair table a .fnt brings, which kerning(between:_:) reads for two characters. inkWidth(of:) measures a string's lit pixels rather than its advance, which is what centering ASCII art wants. The built-in font is Cozette, described above.

Load your own pixel font from a BDF file, the standard bitmap-font format that the X11 catalog and Cozette ship in:

if let mine = try? BitmapFont(bdfContentsOf: url) {
    textFont(mine)
}

BDF fonts are easy to come by. The classic X11 bitmap fonts (Adobe/DEC, permissively licensed) and the large u8g2 font collection are good places to look. Check each font's own license. Drop the .bdf beside your sketch and load it with BitmapFont(resource:in:).

You can also write a fixed-cell font by hand from ASCII art, using the grid initializer, where # marks a lit pixel:

let blocks = BitmapFont(grid: [
    "A": [".##.", "#..#", "####", "#..#", "#..#"],
    "I": ["###", ".#.", ".#.", ".#.", "###"],
    // …
], pixelHeight: 5)

textFont(blocks)
drawText("AI", width / 2, height / 2)

Loading a Playdate .fnt

Ollin also reads the Playdate .fnt format. That is a line-oriented metrics file, holding per-glyph widths, tracking, and kerning pairs, paired with a 1-bit glyph strike. The strike is either embedded in the file as base64 or sits beside it as a <name>-table-<width>-<height>.png. It is a common pixel-font format, with a large pool of free community fonts to draw from. Kerning pairs in the file are applied automatically during layout.

let font = (try? BitmapFont(resource: "MyFont.fnt", in: .module)) ?? .builtIn
textFont(font)

BitmapFont(resource:in:) is the short path for a font bundled beside your sketch. Pass the filename, and the loader picks BDF or .fnt from the extension. Pass the bundle it lives in too, .module for a swift run sketch's own resources, or the default .main for an app. It throws a FileError if the resource is missing or can't be parsed, so (try? …) ?? .builtIn falls back to the default font.

Under it, BitmapFont(fntContentsOf:) takes a file URL and handles both strike forms. It finds the sibling -table PNG when the strike is external. If you already have the text, say from a network fetch, BitmapFont(fnt:) parses the self-contained embedded form directly:

let text = try String(contentsOf: someURL, encoding: .utf8)
if let font = try? BitmapFont(fnt: text) { textFont(font) }

Ollin bundles only the loader, not a library of .fnt fonts, so drop your own beside your sketch. The PlaydateFont example does exactly this. Free, redistributable pixel fonts are easy to find, and the public-domain set at playdate-arcade-fonts is one source.

↑↓ move ↩ open