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 byscripts/derive-cell-table.pyand committed.bases/pins.toml— the bases, pinned by version and sha256, and the slots cut from them. Two today:quasi-monofrom Atkinson Hyperlegible Mono andquasi-bodyfrom 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.