Ollin

8. Words

A word is a shape that also means something, and it comes with centuries of craft attached. Here you learn to set words and to take them apart, down to outlines you can warp and respace. The specimen above beads one word along its own outline, over the three kinds of letter and a passage set flush on both edges. Each letter of that word floats on its own beat. After it, letters come one at a time, and scripts that do not run like English keep their shaping, their direction, and their columns.

Saying something: drawText

Everything in this chapter is a variation on one call, so that call comes first:

import Ollin

final class Hello: Sketch {
    override func draw() {
        background(Color(hex: 0x0E1116))
        noStroke()
        fill(.white)
        textSize(140)
        textAlign(.center, .middle)
        drawText("hola", width / 2, height / 2)
    }
}

drawText puts a string at a point, and three pieces of state decide how it lands. fill is the ink, because text is geometry and uses the same fill as your shapes. textSize is the height of a line in points. textAlign says how the string hangs on the point you gave. The pair .center, .middle centers it both ways. The default is .left, .baseline, which starts the text at your x and sits it on the baseline, the line the letters sit on. Glyphs honor a stroke you have set, so a leftover stroke(...) from earlier drawing outlines every letter. Call noStroke() for plain text, or keep the stroke on purpose as a look. The untouched default stroke is the one exception. It never applies to text, so a fresh sketch's first drawText comes out plain.

When one label wants its own look for one call, say it in the call. For example, drawText("hello", at: center, size: 32, color: .white, align: .center, .middle) styles that string alone. Afterward it puts every piece of state back, leftover stroke included. The state calls above are still the way to set a look that several strings share.

A string can hold more than one line (\n starts the next one). Everything follows the transform stack from Chapter 6, so you can translate to a point, rotate, and the words rotate with the paper.

Three kinds of letters: outline, bitmap, and stroke fonts

Everything above used the default font, which is one of three kinds Ollin draws. They all answer to the same calls, and what differs is what a glyph is:

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
  • An outline font is a .ttf or .otf face, each glyph stored as vector contours. It is the default (the system font, at medium weight), and it stays crisp at any size. A glyph renders as a shape, so it takes fill and stroke.
  • A bitmap font is a small grid of pixels per glyph, scaled up square by square. The bundled one is Cozette, a 13-pixel face with wide coverage (the Spanish accents are all there). It gives pixel-art flavor and stays visibly pixels at any size.
  • A stroke font is a single pen line per glyph, with no interior. It draws with stroke and ignores fill, and it is the letterform a pen plotter wants. Hershey Sans comes bundled.

Switching kinds is the same textFont call:

textFont(BitmapFont.builtIn)               // Cozette, the bundled pixel font
textFont(StrokeFont.builtIn)               // Hershey Sans, the bundled pen font
textFont(OutlineFont.systemMedium)         // back to the default

For outline fonts, anything installed on your Mac is a name away. OutlineFont(name:) is an initializer, a call named after a type that makes a new value of it. It returns nothing when the name does not match, so a typo fails where you can see it. Pair it with a fallback:

textFont(OutlineFont(name: "Avenir Next") ?? .system)

Swift note. ?? means "or, if that was nothing, use this instead". OutlineFont(name:) returns an optional like Chapter 2's Color(hex: String), and ?? .system unwraps it with a default in one step.

The same call has two more doors. If the font file is a variable font, variation(_:) sets its design axes. So a word can change from thin to heavy while the sketch runs. With your font held as f, textFont(f.variation(["wght": map(sin(time), -1, 1, 0.5, 3)])) does it. The square brackets are a small table that pairs an axis tag, a four-letter code like wght, with its value. The values are in the font's own units. The 0.5 to 3 above suits Skia, which comes with macOS, and any font's variationAxes lists its ranges. And you can bundle a font file beside your sketch and load it with try? OutlineFont(resource:in:), the try? from Chapter 2, since the file may be missing. The Text reference covers both, plus loading more bitmap and stroke faces.

Text as geometry: textToShapes

The fonts so far drew for you. The next move is to ask for the same glyphs back as shapes, positioned where drawText would have put them, and textToShapes does that. From that moment they are geometry like everything else in this guide. You can warp the points, stroke the contours, or scatter marks along them.

The word warp three times: crisp, gently bent by a small noise field, and strongly bent until the letters read as liquid
textSize(150)
textAlign(.center, .middle)
for shape in textToShapes("warp", width / 2, height / 2) {
    let warped = Shape(contours: shape.contours.map { c in
        Contour(c.points.map { p in
            p + Vector2(signedNoise(p.x * 0.006, p.y * 0.006),
                        signedNoise(p.x * 0.006 + 40, p.y * 0.006)) * 14
        }, closed: c.isClosed)
    })
    drawShape(warped)
}

Swift note. .map { p in ... } is the .map from Chapter 6, with the element given a name, p, instead of $0. It builds a new list by computing one new element from each old one.

Each glyph comes back as a Shape, a set of closed outlines, so a letter with a hole, like o or a, keeps its hole. Each outline is a Contour, the list of points Chapter 7 stroked as strands. The initializer Contour(points, closed:) builds one back from a list and keeps whether the outline was closed. The loop rebuilds each glyph with every outline point nudged by signedNoise, Chapter 5's smooth field in its swing-both-ways form. The field is sampled at the point's own position, so neighboring points move together and the letter bends instead of shattering. The nudge is a Vector2, and multiplying it by 14 scales both of its parts, so 14 is the strength. The figure shows 0, 7, and 26. Feed time as a third coordinate and the letters ripple live.

Keep warps bounded and smooth. signedNoise never leaves -1...1, so scaling it gives you a hard ceiling on how far any point moves. An unbounded push can fold an outline over itself, which the fill renders as a spike.

Marks along the letters need one more step. The outline points come back unevenly spaced, dense on curves and sparse on straights, so dots placed one per point clump. Respace a glyph first, shape.resampled(spacing: 8), and every point lands a steady 8 apart along the outline, ready for beads, dashes, or particles. The GlyphContours example builds shimmering dotted type in its bottom row from those two calls.

Both edges flush: the box form and justification

So far every string has been one line hung from a point. A passage wants a box. A Rectangle is a corner plus a width and a height. When you hand drawText one, it breaks the text to fit inside, line by line. It wraps where the system says a line may break. By default each line stops where its last word ends. textJustify() opens the spaces instead, until every full line reaches the box's right edge. In the listing, passage is a string holding a few sentences:

let box = Rectangle(x: 120, y: 758, width: 840, height: 280)
textAlign(.left, .top)
textJustify()
drawText(passage, in: box)
The same short passage set in two identical gray boxes side by side, each with a thin red rule down its right edge. On the left, labeled ragged right, every line stops where its last word ends, short of the rule. On the right, labeled justified, the spaces between words open until every full line touches the rule, and only the last line keeps its natural length.

Justification needs to know how far a line should run, and only a box says that. So it applies to the box form of drawText and to nothing else. The last line of each paragraph keeps its natural length, because the writing ended there. noTextJustify() turns it back off, and textAlign still decides which corner of the box the passage opens from.

Where the extra room goes is the business of the layout engine, the system code that places each glyph on its line. English opens the spaces between words. A script with no spaces opens the gaps between its characters instead, which the scripts family after the sketch comes back to. Either way you ask for the same thing.

The box has a second setting. The call textHangingPunctuation() lets a full stop or comma that will not fit sit past the box's edge. Without the setting, the mark would take the character before it to the next line. Hanging the mark keeps the right margin looking straight. In the reference, textHangingPunctuation has the rule, and Text/HangingStops sets one passage both ways.

Putting it together: a type specimen

Now you can build the sheet at the top. A specimen is what type designers make to show a face off, which makes it the right shape for this sketch. It composes every step above. The headline word comes from Text as geometry, and the three kinds of letter from Three kinds of letters. The passage set flush comes from Both edges flush. The headline moves with time, from Chapter 3, one letter at a time. Make a new file, MySketches/Specimen.swift:

import Ollin

final class Specimen: Sketch {
    @Param("Bead spacing", 6.0...26.0) var beadSpacing = 13.0
    @Param("Bead size", 1.0...6.0) var beadSize = 2.6
    @Param("Drift", 0.0...24.0) var drift = 8.0
    @Param("Justify") var justified = true

    let paper = Color(hex: 0xF4EFE6)
    let ink = Color(hex: 0x1E1B18)
    let accent = Color(hex: 0xC1442E)

    let passage = """
        A letter is a shape before it is a sound. Ask for it as geometry and \
        the whole chapter opens up: outlines you can warp, contours you can \
        stroke, and points you can respace until marks sit evenly along the \
        edge of an O. Everything here is one word, three kinds of letter, and one passage.
        """

    override func draw() {
        background(paper)

        // The headline is not text. It is a set of outlines, filled in ink,
        // then respaced so the beads sit an even distance apart along them.
        // Each letter floats on its own beat: it rises and sinks a little and
        // leans as it goes, about its own center, with a head start per letter
        // so the five never move as one. The beads come along, because they
        // are computed from the moved outline.
        textFont(OutlineFont(name: "Avenir Next Heavy") ?? .systemBold)
        textSize(210)
        textAlign(.center, .middle)
        for (i, letter) in textToShapes("Ollin", width / 2, 250).enumerated() {
            let beat = time * 1.3 + Double(i) * 1.1
            let center = letter.centroid
            let glyph = letter.mapPoints { p in
                p.rotated(by: cos(beat) * 0.03, around: center) + Vector2(0, sin(beat) * drift)
            }

            noStroke()
            fill(ink)
            drawShape(glyph)

            fill(accent)
            for contour in glyph.resampled(spacing: beadSpacing).contours {
                for p in contour.points {
                    drawCircle(center: p, radius: beadSize)
                }
            }
        }

        // The same line in each of the three kinds of letter. They take three
        // separate calls because each kind is its own type, and the pen font
        // takes a stroke rather than a fill, which is the whole point of it.
        textAlign(.left, .middle)

        noStroke()
        fill(ink)
        textFont(OutlineFont.systemMedium)
        textSize(34)
        drawText("outline, from the system", 120, 512)

        noFill()
        stroke(ink)
        strokeWeight(1.6)
        textFont(StrokeFont.builtIn)
        textSize(34)
        drawText("stroke, drawn by a pen", 120, 587)

        noStroke()
        fill(ink)
        textFont(BitmapFont.builtIn)
        textSize(24)
        drawText("bitmap, one pixel at a time", 120, 662)

        stroke(accent)
        strokeWeight(2)
        drawLine(120, 718, 960, 718)

        // The passage, set flush on both edges inside its box.
        noStroke()
        fill(ink)
        textFont(OutlineFont.systemMedium)
        textSize(28)
        textAlign(.left, .top)
        if justified { textJustify() } else { noTextJustify() }
        drawText(passage, in: Rectangle(x: 120, y: 758, width: 840, height: 280))
    }
}

Swift note. """ opens a string that runs over several lines, and closes it the same way. A \ at the end of a line joins the next line onto it without a line break. The passage uses it to stay one paragraph.

Run it, then drag the bead spacing. What each part contributes:

  • textToShapes is what the sketch turns on. The headline is drawn twice from one call: filled as a shape, then beaded along the same outline. Both need the outlines, which only that call hands you.
  • resampled(spacing:) is what makes the beads even. Outline points come back dense on curves and sparse on straights. So beads placed one per raw point would clump around the O and thin out along the l. Respacing first puts every point a fixed distance from the last one.
  • mapPoints is what moves a letter. It passes every point of the glyph through one closure and keeps the holes and the winding. Each letter gets a beat of its own from its place in the word, so the five never rise together. The move is a small rise and a lean about the letter's own centroid, and Drift sets how far. The beads are computed after the move, which is why they come along.
  • The three font blocks are three calls because each kind of font is its own type. The pen font draws with stroke, so leaving noStroke set from the headline makes it disappear with no error.
  • drawText(_:in:) wraps the passage inside a rectangle, and textJustify() opens the spaces so both edges line up. Turn the Justify parameter off and the right edge goes ragged, which is the same passage doing less work.

When a specimen is a keeper, export one frame of it as a still:

swift run OllinLive MySketches/Specimen.swift --export specimen.png --frame 100

Before moving on, make it yours:

  • Warp the headline before you bead it, using the signedNoise push from Text as geometry. It goes inside the same mapPoints closure as the drift. The beads follow the outline wherever it goes, because they are computed from it.
  • Set Drift to 0 and the sheet holds still, the way a printed specimen does. Set it to 24 and the letters float apart, which is the point where it stops reading as a specimen.
  • Set beadSpacing to 26 and beadSize to 5.5. The beads stop reading as a texture and start reading as a dotted rule.
  • Swap the headline for a word in a script you do not read. Everything here works the same way, and the scripts family below says why.
  • Drop the fill and keep only the beads, on a dark ground. The word stays readable from its outline alone.

Letters one at a time: the per-glyph form

The specimen moved its headline letters as shapes, one glyph at a time, and set its three lines as one string each. There is also a form of drawText that hands you a string one glyph at a time, with no outlines in sight. So each letter can carry its own color, position, and motion. The specimen has no use for it, because it already had the geometry. But it is the move under kinetic typography, where letters move on their own, and the scripts family after it reads through the same closure.

Letters that move: drawText with a closure

Add a closure to drawText and it runs once per letter (once per piece, as the scripts family shows). The state you set inside it applies to that letter alone. It is for a word that moves as a word: a wave, a shuffle, a color per letter. The picture below is the phase trick from Chapter 3 applied to five letters:

The word ollin in five colors, each letter displaced vertically along a wave, with soft fading echoes trailing each letter's motion
let palette = Palette([
    Color(hex: 0x5E60CE), Color(hex: 0x64DFDF),
    Color(hex: 0xFFB703), Color(hex: 0xE56B6F), Color(hex: 0x8AC926),
])

textSize(230)
textAlign(.center, .middle)
drawText("ollin", width / 2, height / 2) { g in
    fill(palette[g.index])
    withState {
        translate(0, sin(time * 2.6 + Double(g.index) * 0.9) * 90)
        g.draw()
    }
}

The closure's g knows which glyph it is (g.index), where it belongs, and how to stamp itself (g.draw()). So you set whatever state you like around it. The code above sets a color per letter and a vertical swing. The + Double(g.index) * 0.9 gives each letter the same swing, a little ahead of the letter before it. The offset makes the word roll like a wave instead of jumping as a block.

Two relatives place the glyphs for you. The form drawText(_:along:) lays a string along a curve you hand it, and the glyphs rotate to follow it. The other, drawText(_:in:), is the box form you set the passage with. The per-glyph reference has the closure's other properties, its bounds and its progress along the string. Text/GlyphWave runs the wave live, and Text/TextOnPath sends a line around a curve.

Every script: shaping, direction, columns, and fallback

One glyph per letter is an English idea. With an outline font, every call in this chapter draws any script with no setup. The system's layout engine shapes the text and finds a font that has the letters. What changes is what a glyph is, which way a line runs, how a paragraph breaks, and where the columns go. The specimen sets Latin, so none of this touched it. Swap its headline for Arabic or Devanagari and all of it does.

Shaping and direction: text in any script

A shaper is the part of a layout engine that turns characters into glyphs. It joins letters, stacks marks, reorders pieces, and picks the forms a script needs at each position. Ollin hands every outline-font string to the system's shaper, and the rules it applies come from Unicode and from the font. So paste in Arabic, Japanese, Devanagari, or Thai and it draws. Four things you could take for granted in English then change, and the figure shows each:

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

A glyph can be smaller than a letter, or larger. The top-left panel is one Devanagari syllable. It is written with four characters and drawn with three glyphs, and one glyph sits to the left of the letter it follows. So the per-glyph closure hands you a piece: one thing a reader would point at. g.text is therefore a String, holding all four characters here. g.character still returns a single Character for the common case.

Direction belongs to the line. The top-right panel numbers an Arabic word as the closure sees it. The pieces come out left to right on the canvas, so g.index 0 is the letter read last. Use that order when sweeping across the drawing, and count backwards to follow the reading. A line can hold both directions at once, and then the neutral characters (spaces, brackets, digits) land wherever the base direction says. Usually the text answers that for itself. A line opening with a bracket cannot, and textDirection(.leftToRight) or .rightToLeft says which you meant.

Some of what you type is a picture. The bottom-left panel draws O 👋 twice, with drawText and then through textToShapes. Only the O comes back as geometry. The emoji font stores each one as a bitmap, so there are no contours. drawText still puts it on the canvas as a picture with its own colors, which is why fill does not touch it. In a closure, g.isPicture marks those, and g.draw() works for them.

Japanese and Thai break lines without spaces. The bottom-right panel is a Japanese paragraph in a box. Japanese may break between almost any two characters, and Thai only between words, and no space tells you either. So box layout asks the system where a line may break. The system's break set carries the Japanese rules with it. Among them, a full stop may not open a line, and an opening bracket may not close one. Every script in the reference has each rule in full. The example Text/Scripts sets five scripts on one sheet with the base direction as a live parameter.

Down the page: vertical text

Japanese is also written the other way, in columns that run down the page and fill right to left. It is the setting for a poem, a title, or a passage that should read as it would in print. The font carries the turned forms this setting needs. One call asks for it:

textDirection(.topToBottom)
textAlign(.right, .top)
drawText("「春」は、あけぼの(をかし)", 820, 80)
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

Every turned character comes from the font. The middle of the figure shows three of them. The bracket lies down, and the comma moves from the bottom left of its square to the top right. Those shapes are the writing system's own answer, kept in the face, and asking for vertical setting is what picks them.

Turning the writing turns the settings with it. A \n starts the next column, to the left. A box wraps against its height. The two halves of textAlign swap jobs, so a column usually starts from .right, .top, the corner the writing opens at. The bottom of the figure sets one passage twice: justified on the left, where every full column reaches the rule, and ragged on the right. Writing in columns has the table of what each setting does once the writing turns. The example Text/Columns sets a passage down the page, justified into its box.

Traditional Mongolian runs down the page too, with columns that fill left to right. Its letters join into one stroke, so the framework shapes each column as a line and stands it up. textDirection(.topToBottomLeftToRight) asks for it. Columns that fill the other way says which face to hand it and why, and Text/MongolianColumns sets a phrase both ways.

What the font could not do: fontsUsed and textMissingCharacters

An outline font's fontsUsed(for:) returns the names of the faces a line borrowed. The call textMissingCharacters returns the characters the current font cannot draw. Reach for the first when a script comes out in the wrong face. Its answer shows when a Latin font has handed your Japanese to another one. Reach for the second with a bitmap or stroke font, which holds only its own glyphs and draws nothing for the rest. For an outline font the second answer is almost always empty. textMissingCharacters in the reference has both.

Where this comes from

Type on a computer screen owes its shape to a long argument about what a letter is. The outline font is a letter as a set of filled curves that scale to any size. PostScript, from 1984, put outline fonts into printers and settled the argument for print. The bitmap font is a letter as a small grid of pixels drawn for one exact size. It is the older answer, and it is still in use. The bundled Cozette is a modern face in that tradition. The stroke font is older still, and comes from machines that held a pen. Allen Hershey digitized his vector faces in the late 1960s so a plotter could write them. They reached the world through the U.S. National Bureau of Standards, and the bundled Hershey Sans descends from that work.

Justification, the business of stretching spaces so both edges line up, was hand craft for centuries. Donald Knuth and Michael Plass gave it an algorithm in 1981 that breaks a whole paragraph at once rather than greedily line by line. The scripts of the world reached computers later and less evenly than Latin did. Unicode's bidirectional algorithm settles a line that runs both ways. The vertical writing this chapter shows is still a setting a layout engine has to be asked for. Full credits are in the project's attribution notes.

Go deeper

  • Text: the full reference, including text on a path, box wrapping, metrics (textWidth, textBounds), variable-font axes, every script with textDirection and textMissingCharacters, columns down the page and the Mongolian columns that fill the other way, hanging punctuation, and loading bitmap, outline, and stroke faces of your own.
  • Appendix B draws the geometry behind the glyphs: Shapes as regions, and Shaping a value for the warp.
  • Worked examples, in Examples/Text/: GlyphWave (per-glyph motion), TextOnPath, TextBox, VariableFont, TypeAsGeometry (the warp live, plus the seeded per-frame jitter), StrokeText and PlaydateFont (the other two font kinds in action), TextVolume (the atlas mode at paragraph scale, which Chapter 9 teaches), GlyphContours (respaced outlines as curve, polygon, and shimmering dots), Scripts (five scripts on one sheet, with the base direction as a live parameter), Columns and MongolianColumns (the two vertical writings), and HangingStops (one passage with and without its stops hung).
  • The Cordeiro homage Derivadas: characters used as marks of ink rather than as words. Each one is set a little smaller than its cell, so the marks stand apart as a line printer's did. A darker mark is two or three characters drawn on the same place, the way the printer struck a line again without moving the paper.
  • The Sato homage StraightLines: a font where every filled cell of a letter is three short horizontals. The type is set around an eye made only of straight lines. The letters are a table of strings, one per row. Type any word into word and it is set wherever it clears the eye.
  • Type as material elsewhere: Chapter 15 cuts, hatches, and stipples text outlines. Chapter 26 extrudes a word into a solid, and Chapter 41 plots any outline, a word included.
  • Next door: Chapter 9 brings in the other kind of outside material, pictures and numbers. Its finished sketch paints a picture with the type you just learned to set.

Contents · Previous: Chapter 7, Tiles that cover the plane · Next: Chapter 9, Pictures and data

↑↓ move ↩ open