Skip to main content

max / quasi-type

10.1 KB · 213 lines History Blame Raw
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