Skip to main content

max / quasi-type

The house font pipeline: a pinned base face plus the house glyph set in, a Quasi <Slot> face out.

git clone https://makenot.work/git/max/quasi-type.git git clone git@ssh.makenot.work:max/quasi-type.git
Name Size
/ bases/
/ glyphs/
/ scripts/
/ src/
/ tests/
· .gitignore 105 B
· Cargo.lock 14.1 KB
· Cargo.toml 1.9 KB
· LICENSE 1.0 KB
· README.md 10.1 KB
· rust-toolchain.toml 86 B

README

quasi-type

The house font pipeline. A pinned base face plus the house glyph set goes in, a Quasi <Slot> face comes out, and the run is repeatable when the upstream moves or when a slot changes its base.

cargo run -- build quasi-mono
Quasi Mono from Atkinson Hyperlegible Mono 2.001 (171 marks)
  Quasi Mono ExtraLight  ttf   69.7K  woff2   30.7K  +171 marks, cmap covers all 176
                         wght 200-800, and the marks vary with it. At rest this face
                         is wght 200 (ExtraLight), so a consumer names the weight it
                         wants: `font-weight: 200 800`.
  OFL.txt                the base's licence, as OFL requires

Why it exists

The faces worth setting text in ship minimal mark inventories on principle. Atkinson Hyperlegible Mono draws 359 codepoints and not one of them is box drawing, a block element or an arrow. That is a property of a good text face rather than a defect, so the answer is to patch rather than to shop.

Split the vocabulary in two. Letters, digits and punctuation are the base face’s job and are meant to differ per slot. Marks (carets, separators, status symbols, whitespace renders) carry meaning, and the meaning is the same in a terminal, a webview and an egui panel, so the drawing should be too. Without a cut face, a mark like falls back per glyph to whatever the OS orders first, and unifying the sort caret means unifying on three platforms’ opinions.

What is in the box

  • glyphs/manifest.toml — the house glyph set. Eleven authored marks, plus the two Unicode blocks of cell furniture it asks for by name. This is the durable artifact; the pipeline is what consumes it.
  • src/cells/ — box drawing and block elements, generated rather than drawn. Their 160 recipes are already written down in Unicode’s own character names, and they have to be cell-exact to tile, which is arithmetic. The table is derived from those names by scripts/derive-cell-table.py and committed.
  • bases/pins.toml — the bases, pinned by version and sha256, and the slots cut from them. Two today: quasi-mono from Atkinson Hyperlegible Mono and quasi-body from Atkinson Hyperlegible Next, the two halves of one superfamily.
  • out/ — built faces. Not committed: they rebuild from the checkout, and a committed binary is a second source of truth.

Using it from another crate

A consumer cuts the face rather than vendoring it, for the same reason out/ is not committed. quasi_type::cut is the whole pipeline in one call:

// build.rs
let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
let cut = quasi_type::cut("quasi-mono", &out.join("bases"), false).unwrap();
std::fs::write(out.join("QuasiMono.ttf"), &cut.faces[0].ttf).unwrap();

Take it as a git dependency, and give it a cache directory: bases are downloaded and checksummed there, so a build with a warm cache needs no network.

Do not reassemble the steps build walks. A second caller writing its own version string or skipping the coverage assertion is how two builds of the same slot stop being the same face.

quasi-type build <slot>     cut every face of a slot, verify, write to out/
quasi-type verify <slot>    cut without writing; assert coverage only
quasi-type params <slot>    print what each base face measures
quasi-type params --base <id>   measure a base no slot names yet
quasi-type list             print the house glyph set and the pinned slots
quasi-type proof <slot>     rasterise the cut face to out/, so it can be seen

How a mark is drawn

Parametrically, off the base’s own measurements, so the set refits to the next base rather than being redrawn for it. Every dimension is

band extent x band  +  base stroke x weight

with both terms read out of the face (quasi-type params): the symbol band off +, the horizontal stroke off -, the vertical stroke off |, the cell off hmtx.

Consistent means same design, not byte-identical. in Quasi Mono and in a future Quasi Body will not share an outline; they read as the same mark, each tuned to sit among its own neighbours.

The weight term is measured rather than chosen. A base redraws its symbols heavier rather than only thickening them in place, while its block elements hold still, being cell-fill primitives. So solid marks grow and stroked marks thicken, and the X is calibrated against the base’s own × rather than by eye.

A coefficient is a relationship to the base, so it is refitted when the base moves. Check each one against the base at both ends of the axis rather than against a number somebody wrote down. The two that move most are the ballot X’s, which reads light if the base’s bar is thin against its own ×, and the space render’s, which closes into a blob if the base’s stem grows steeply across wght.

Adding a glyph

Edit glyphs/manifest.toml and re-run. Nothing downstream changes: the set is data, and the coverage assertion picks the new codepoint up on its own.

Braille and the sextant sets stay out until a surface wants one. No Canvas and no Sparkline exists anywhere in the tree.

Variable bases

A variable base is one file covering a range (Atkinson Hyperlegible Mono is wght 200 to 800) and a cut keeps that axis rather than instancing a weight out of it. Instancing is the cheaper path and throws away the thing the base was chosen for.

So a mark is drawn once per master: at the axis default, and at each end of the axis. The recipes are already parametric in the base’s own measurements, so a master is the same recipe read at another location rather than a second drawing. The differences between them ship as gvar deltas, and the mark then answers the axis the way the base’s own glyphs do. Without them a spliced glyph holds still across the whole range: right at the default instance, and a light table border inside a bold one everywhere else.

Two things this asks of a recipe, both free if it is parametric:

  • Its point count cannot depend on the measurements. Deltas are per point, so the masters have to be the same polygon at different sizes. The pipeline refuses a mark that changes topology instead of shipping one that interpolates into a different shape halfway along the axis.
  • A mark that ignores weight gets no variation data, rather than an empty tuple saying so at length.

Watch the default instance. Keeping the axis means keeping the base’s default, and a base does not have to default to Regular: Atkinson Mono defaults to wght 200 and its own name table reads ExtraLight. A face cut from it is ExtraLight at rest, whatever a consumer hoped. The pin declares the style and the build asserts it against the base, so the trap fails the build rather than reaching a screen. A consumer names the weight it wants (font-weight: 200 800 in the @font-face, and the browser resolves normal to 400) instead of loading the file and taking what it opens at.

Looking at it

quasi-type proof quasi-mono --px 32

Writes a grayscale PNG per master to out/: the authored marks on one row, then box drawing, arcs, dashes and block elements set adjacently on the cell grid, over a faint cell rule and baseline.

Every other check here measures a bounding box or an ink area, and a glyph can have a correct bbox while curling the wrong way, showing a seam, or reading as a bowtie. Draw the glyphs on a screen before believing a green suite.

Slots, and what a slot takes

A slot is a house type role, and the output family tracks the slot rather than the base, so a slot can change its base without a consumer changing a family name.

A slot may take the whole set (glyphs = "*") or the authored marks alone (glyphs = "marks"). quasi-body takes the marks: box drawing and block elements are sized against the cell and have to be cell-exact to tile, and a proportional face has no cell, so a in it would draw a corner that joins nothing. Its coverage floor is the role’s for the same reason: a body face is asked for everything a described screen sets in copy and for none of the four codepoints that exist to tile.

A mark takes the reference glyph’s advance, which is +’s: what the recipes measure their band against and what quasi-type params reads the cell off. On a monospace base that is the cell. On a proportional one it is the width the base sets its own symbols on, and it answers the axis by pointing at that glyph’s own HVAR delta set, so a mark narrows with + where + narrows. HVAR’s advance map is indexed by glyph id, so a glyph appended past its end and left to itself inherits whatever the last entry happens to say.

Adding a base

Pin it in bases/pins.toml with its sha256, then read its licence. Most OFL faces declare a Reserved Font Name, and clause 3 bars it as a prefix and as a suffix alike; the quasi-* naming clears that by construction, so the only live gate is whether anyone reached for the base’s own name. The pipeline refuses a slot whose family name carries its base’s reserved word.

A base also has to be measurable: it needs |, - and +, since those are what the recipes refit against. One missing is an error rather than a guess.

A base arrives one of two ways and the pin says which. A project that publishes releases gets an archive url plus a path per face; one that publishes none gets a url per face instead, pinned at a commit. Do not pin a generated source tarball: GitHub builds those on demand and the bytes can change, which would read here as “upstream moved” and mean nothing of the sort.

Two bases are refused. One that varies on more than one axis needs a decision about the master grid first, since drawing at each end of one axis says nothing about the corners of two. And one whose advances vary while its HVAR carries no advance mapping, since there is then nothing for an appended glyph to point at.

Licence

The pipeline is MIT. A face it cuts is OFL 1.1, inherited from its base, and build writes the base’s licence text beside the files because the OFL requires it to travel with a modified build. The base’s copyright and the lineage go in the face’s own name table, which is where the licence asks for them and where a Reserved Font Name does not reach.

Design record: wiki typography-standard.