| 1 |
# quasi-type |
| 2 |
|
| 3 |
The house font pipeline. A pinned base face plus the house glyph set goes in, a |
| 4 |
`Quasi <Slot>` face comes out, and the run is repeatable when the upstream moves |
| 5 |
or when a slot changes its base. |
| 6 |
|
| 7 |
``` |
| 8 |
cargo run -- build quasi-mono |
| 9 |
``` |
| 10 |
|
| 11 |
``` |
| 12 |
Quasi Mono from Atkinson Hyperlegible Mono 2.001 (171 marks) |
| 13 |
Quasi Mono ExtraLight ttf 69.7K woff2 30.7K +171 marks, cmap covers all 176 |
| 14 |
wght 200-800, and the marks vary with it. At rest this face |
| 15 |
is wght 200 (ExtraLight), so a consumer names the weight it |
| 16 |
wants: `font-weight: 200 800`. |
| 17 |
OFL.txt the base's licence, as OFL requires |
| 18 |
``` |
| 19 |
|
| 20 |
## Why it exists |
| 21 |
|
| 22 |
The faces worth setting text in ship minimal mark inventories on principle. |
| 23 |
Atkinson Hyperlegible Mono draws 359 codepoints and not one of them is box |
| 24 |
drawing, a block element or an arrow. That is a property of a good text face |
| 25 |
rather than a defect, so the answer is to patch rather than to shop. |
| 26 |
|
| 27 |
Split the vocabulary in two. **Letters, digits and punctuation** are the base |
| 28 |
face's job and are meant to differ per slot. **Marks** (carets, separators, |
| 29 |
status symbols, whitespace renders) carry meaning, and the meaning is the same |
| 30 |
in a terminal, a webview and an egui panel, so the drawing should be too. |
| 31 |
Without a cut face, a mark like `▲` falls back per glyph to whatever the OS |
| 32 |
orders first, and unifying the sort caret means unifying on three platforms' |
| 33 |
opinions. |
| 34 |
|
| 35 |
## What is in the box |
| 36 |
|
| 37 |
- `glyphs/manifest.toml` — the house glyph set. Eleven authored marks, plus the |
| 38 |
two Unicode blocks of cell furniture it asks for by name. This is the durable |
| 39 |
artifact; the pipeline is what consumes it. |
| 40 |
- `src/cells/` — box drawing and block elements, generated rather than drawn. |
| 41 |
Their 160 recipes are already written down in Unicode's own character names, |
| 42 |
and they have to be cell-exact to tile, which is arithmetic. The table is |
| 43 |
derived from those names by `scripts/derive-cell-table.py` and committed. |
| 44 |
- `bases/pins.toml` — the bases, pinned by version and sha256, and the slots cut |
| 45 |
from them. Two today: `quasi-mono` from Atkinson Hyperlegible Mono and |
| 46 |
`quasi-body` from Atkinson Hyperlegible Next, the two halves of one |
| 47 |
superfamily. |
| 48 |
- `out/` — built faces. Not committed: they rebuild from the checkout, and a |
| 49 |
committed binary is a second source of truth. |
| 50 |
|
| 51 |
## Using it from another crate |
| 52 |
|
| 53 |
A consumer cuts the face rather than vendoring it, for the same reason `out/` is |
| 54 |
not committed. `quasi_type::cut` is the whole pipeline in one call: |
| 55 |
|
| 56 |
```rust |
| 57 |
// build.rs |
| 58 |
let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()); |
| 59 |
let cut = quasi_type::cut("quasi-mono", &out.join("bases"), false).unwrap(); |
| 60 |
std::fs::write(out.join("QuasiMono.ttf"), &cut.faces[0].ttf).unwrap(); |
| 61 |
``` |
| 62 |
|
| 63 |
Take it as a git dependency, and give it a cache directory: bases are downloaded |
| 64 |
and checksummed there, so a build with a warm cache needs no network. |
| 65 |
|
| 66 |
Do not reassemble the steps `build` walks. A second caller writing its own |
| 67 |
version string or skipping the coverage assertion is how two builds of the same |
| 68 |
slot stop being the same face. |
| 69 |
|
| 70 |
``` |
| 71 |
quasi-type build <slot> cut every face of a slot, verify, write to out/ |
| 72 |
quasi-type verify <slot> cut without writing; assert coverage only |
| 73 |
quasi-type params <slot> print what each base face measures |
| 74 |
quasi-type params --base <id> measure a base no slot names yet |
| 75 |
quasi-type list print the house glyph set and the pinned slots |
| 76 |
quasi-type proof <slot> rasterise the cut face to out/, so it can be seen |
| 77 |
``` |
| 78 |
|
| 79 |
## How a mark is drawn |
| 80 |
|
| 81 |
Parametrically, off the base's own measurements, so the set refits to the next |
| 82 |
base rather than being redrawn for it. Every dimension is |
| 83 |
|
| 84 |
band extent x band + base stroke x weight |
| 85 |
|
| 86 |
with both terms read out of the face (`quasi-type params`): the symbol band off |
| 87 |
`+`, the horizontal stroke off `-`, the vertical stroke off `|`, the cell off |
| 88 |
`hmtx`. |
| 89 |
|
| 90 |
Consistent means same design, not byte-identical. `▲` in Quasi Mono and in a |
| 91 |
future Quasi Body will not share an outline; they read as the same mark, each |
| 92 |
tuned to sit among its own neighbours. |
| 93 |
|
| 94 |
The weight term is measured rather than chosen. A base redraws its symbols |
| 95 |
heavier rather than only thickening them in place, while its block elements hold |
| 96 |
still, being cell-fill primitives. So solid marks grow and stroked marks thicken, |
| 97 |
and the X is calibrated against the base's own `×` rather than by eye. |
| 98 |
|
| 99 |
**A coefficient is a relationship to the base, so it is refitted when the base |
| 100 |
moves.** Check each one against the base at both ends of the axis rather than |
| 101 |
against a number somebody wrote down. The two that move most are the ballot X's, |
| 102 |
which reads light if the base's bar is thin against its own `×`, and the space |
| 103 |
render's, which closes into a blob if the base's stem grows steeply across |
| 104 |
`wght`. |
| 105 |
|
| 106 |
## Adding a glyph |
| 107 |
|
| 108 |
Edit `glyphs/manifest.toml` and re-run. Nothing downstream changes: the set is |
| 109 |
data, and the coverage assertion picks the new codepoint up on its own. |
| 110 |
|
| 111 |
Braille and the sextant sets stay out until a surface wants one. No `Canvas` and |
| 112 |
no `Sparkline` exists anywhere in the tree. |
| 113 |
|
| 114 |
## Variable bases |
| 115 |
|
| 116 |
A variable base is one file covering a range (Atkinson Hyperlegible Mono is |
| 117 |
`wght` 200 to 800) and a cut **keeps that axis** rather than instancing a |
| 118 |
weight out of it. Instancing is the cheaper path and throws away the thing the |
| 119 |
base was chosen for. |
| 120 |
|
| 121 |
So a mark is drawn once per master: at the axis default, and at each end of the |
| 122 |
axis. The recipes are already parametric in the base's own measurements, so a |
| 123 |
master is the same recipe read at another location rather than a second drawing. |
| 124 |
The differences between them ship as `gvar` deltas, and the mark then answers |
| 125 |
the axis the way the base's own glyphs do. Without them a spliced glyph holds |
| 126 |
still across the whole range: right at the default instance, and a light table |
| 127 |
border inside a bold one everywhere else. |
| 128 |
|
| 129 |
Two things this asks of a recipe, both free if it is parametric: |
| 130 |
|
| 131 |
- **Its point count cannot depend on the measurements.** Deltas are per point, |
| 132 |
so the masters have to be the same polygon at different sizes. The pipeline |
| 133 |
refuses a mark that changes topology instead of shipping one that interpolates |
| 134 |
into a different shape halfway along the axis. |
| 135 |
- **A mark that ignores weight gets no variation data**, rather than an empty |
| 136 |
tuple saying so at length. |
| 137 |
|
| 138 |
**Watch the default instance.** Keeping the axis means keeping the base's |
| 139 |
default, and a base does not have to default to Regular: Atkinson Mono defaults |
| 140 |
to `wght` 200 and its own name table reads `ExtraLight`. A face cut from it is |
| 141 |
`ExtraLight` at rest, whatever a consumer hoped. The pin declares the style and |
| 142 |
the build asserts it against the base, so the trap fails the build rather than |
| 143 |
reaching a screen. A consumer names the weight it wants |
| 144 |
(`font-weight: 200 800` in the `@font-face`, and the browser resolves `normal` |
| 145 |
to 400) instead of loading the file and taking what it opens at. |
| 146 |
|
| 147 |
## Looking at it |
| 148 |
|
| 149 |
quasi-type proof quasi-mono --px 32 |
| 150 |
|
| 151 |
Writes a grayscale PNG per master to `out/`: the authored marks on one row, then |
| 152 |
box drawing, arcs, dashes and block elements set adjacently on the cell grid, |
| 153 |
over a faint cell rule and baseline. |
| 154 |
|
| 155 |
Every other check here measures a bounding box or an ink area, and a glyph can |
| 156 |
have a correct bbox while curling the wrong way, showing a seam, or reading as a |
| 157 |
bowtie. Draw the glyphs on a screen before believing a green suite. |
| 158 |
|
| 159 |
## Slots, and what a slot takes |
| 160 |
|
| 161 |
A slot is a house type role, and the output family tracks the slot rather than |
| 162 |
the base, so a slot can change its base without a consumer changing a family |
| 163 |
name. |
| 164 |
|
| 165 |
A slot may take the whole set (`glyphs = "*"`) or the authored marks alone |
| 166 |
(`glyphs = "marks"`). `quasi-body` takes the marks: box drawing and block |
| 167 |
elements are sized against the cell and have to be cell-exact to tile, and a |
| 168 |
proportional face has no cell, so a `┌` in it would draw a corner that joins |
| 169 |
nothing. Its coverage floor is the role's for the same reason: a body face is |
| 170 |
asked for everything a described screen sets in copy and for none of the four |
| 171 |
codepoints that exist to tile. |
| 172 |
|
| 173 |
**A mark takes the reference glyph's advance**, which is `+`'s: what the recipes |
| 174 |
measure their band against and what `quasi-type params` reads the cell off. On a |
| 175 |
monospace base that is the cell. On a proportional one it is the width the base |
| 176 |
sets its own symbols on, and it answers the axis by pointing at that glyph's own |
| 177 |
`HVAR` delta set, so a mark narrows with `+` where `+` narrows. `HVAR`'s advance |
| 178 |
map is indexed by glyph id, so a glyph appended past its end and left to itself |
| 179 |
inherits whatever the last entry happens to say. |
| 180 |
|
| 181 |
## Adding a base |
| 182 |
|
| 183 |
Pin it in `bases/pins.toml` with its sha256, then read its licence. Most OFL |
| 184 |
faces declare a Reserved Font Name, and clause 3 bars it as a prefix and as a |
| 185 |
suffix alike; the `quasi-*` naming clears that by construction, so the only live |
| 186 |
gate is whether anyone reached for the base's own name. The pipeline refuses a |
| 187 |
slot whose family name carries its base's reserved word. |
| 188 |
|
| 189 |
A base also has to be measurable: it needs `|`, `-` and `+`, since those are |
| 190 |
what the recipes refit against. One missing is an error rather than a guess. |
| 191 |
|
| 192 |
A base arrives one of two ways and the pin says which. A project that publishes |
| 193 |
releases gets an archive `url` plus a `path` per face; one that publishes none |
| 194 |
gets a `url` per face instead, pinned at a commit. Do not pin a generated source tarball: GitHub builds those on |
| 195 |
demand and the bytes can change, which would read here as "upstream moved" and |
| 196 |
mean nothing of the sort. |
| 197 |
|
| 198 |
Two bases are refused. One that varies on more than one axis needs a decision |
| 199 |
about the master grid first, since drawing at each end of one axis says nothing |
| 200 |
about the corners of two. And one whose advances vary while its `HVAR` carries |
| 201 |
no advance mapping, since there is then nothing for an appended glyph to point |
| 202 |
at. |
| 203 |
|
| 204 |
## Licence |
| 205 |
|
| 206 |
The pipeline is MIT. A face it cuts is OFL 1.1, inherited from its base, and |
| 207 |
`build` writes the base's licence text beside the files because the OFL requires |
| 208 |
it to travel with a modified build. The base's copyright and the lineage go in |
| 209 |
the face's own `name` table, which is where the licence asks for them and where |
| 210 |
a Reserved Font Name does not reach. |
| 211 |
|
| 212 |
Design record: wiki `typography-standard`. |
| 213 |
|