Skip to main content

max / quasi-type

13.7 KB · 361 lines History Blame Raw
1 # The house glyph set: the durable artifact this pipeline exists to carry.
2 #
3 # Every entry is a mark rather than a letter. Letters, digits and punctuation are
4 # the base face's job and are meant to differ per slot; marks carry meaning, and
5 # the meaning does not change between a terminal, a webview and an egui panel, so
6 # neither should the drawing.
7 #
8 # Sizes are never absolute units, so a glyph is refitted to the next base rather
9 # than redrawn for it. Every dimension is
10 #
11 # band extent x band + base stroke x weight
12 #
13 # and both terms are measured off the base itself (see `quasi-type params`):
14 #
15 # band the symbol band, read off `+`: the extent the base fits its own
16 # symbols into, horizontally and vertically.
17 # stroke `-`'s bbox height, the base's horizontal stroke weight.
18 # stem `|`'s bbox width, the base's vertical stroke weight.
19 # advance the cell width. Marks centre on advance/2, not on the band.
20 #
21 # A dimension with no weight term is written as a bare number.
22 #
23 # WHY THE WEIGHT TERM EXISTS, measured 2026-08-16 across Plex Mono's two weights.
24 # The base redraws its symbols heavier in Bold rather than only thickening them
25 # in place: `•` runs 244 to 308 units wide, `×` 424 to 460, `↑` 414 to 478, and
26 # `✓`, `❌` and `◊` all grow too. What holds still is `█` and `░`, and those are
27 # cell-fill primitives rather than symbols. `+` is the other pattern: identical
28 # bbox in both weights, heavier bars inside it.
29 #
30 # So the set follows the base in both, and which one a mark follows is a property
31 # of the mark rather than a setting:
32 #
33 # solid marks (the triangles) have no stroke to thicken, so they grow.
34 # stroked marks (the X, the box, the return arrow) grow a little and thicken,
35 # which is the pattern `x` and `+` follow in the base.
36 #
37 # Sizing off the band alone would leave a mark at Regular's size inside a Bold
38 # face, reading light among its neighbours. That was this file's first rule and
39 # it was wrong: it generalised from the block elements, which are the one thing
40 # in the base that does not answer weight at all.
41 #
42 # ON A VARIABLE BASE, the weight term stops being sampled at two points and
43 # becomes the axis itself. A cut from a variable base keeps that axis, so every
44 # mark is drawn once per master — the axis default and each end — and the
45 # differences ship as `gvar` deltas. Nothing here changes: the recipe is already
46 # parametric in the base's measurements, and a master is the same recipe read at
47 # another location.
48 #
49 # What it puts on every recipe, including the generators the box-drawing set will
50 # need, is one rule: **a mark's point count and contour order cannot depend on
51 # the measurements**. Deltas are per point, so two masters of one glyph have to
52 # be the same polygon at different sizes. A recipe that grew a contour past some
53 # threshold would interpolate into a different shape halfway along the axis. The
54 # pipeline refuses that case rather than shipping it, and a generator that emits
55 # a fixed topology never meets it.
56 #
57 # Adding a glyph is an edit here plus a re-run against every face that consumes
58 # the set. Nothing downstream needs touching.
59
60 [set]
61 # Bumped to 2 on 2026-08-17: `←` and four more triangles. A face's identity is
62 # (base, set version), so a face carrying a set that grew is a different face
63 # and says so in its own name table.
64 version = 2
65 name = "quasi house glyph set"
66
67 # ---------------------------------------------------------------------------
68 # Sort carets. Decision 591fca15 unified the three renderers on the triangles;
69 # until this set shipped, a webview drawing one fell back per glyph to whatever
70 # the OS ordered first, so the "unified" caret was three platforms' opinions.
71 # ---------------------------------------------------------------------------
72
73 [[glyph]]
74 codepoint = 0x25B2
75 name = "uni25B2"
76 role = "sort caret, ascending"
77 source = "decision 591fca15"
78 shape = "triangle"
79 direction = "up"
80 # Across the base edge, off the band's width.
81 span = { band = 0.75, weight = 0.62 }
82 # Apex distance as a ratio of span. 0.866 is equilateral, at every weight.
83 depth = 0.866
84 anchor = "band-center"
85
86 [[glyph]]
87 codepoint = 0x25BC
88 name = "uni25BC"
89 role = "sort caret, descending"
90 source = "decision 591fca15"
91 shape = "triangle"
92 direction = "down"
93 span = { band = 0.75, weight = 0.62 }
94 depth = 0.866
95 anchor = "band-center"
96
97 # The rest of the triangle run, on the same footing as `←` below: nothing in
98 # Alloy emits them, and ratatui's scroll and selection markers reach for them,
99 # so a face drawing two of the run and leaving four to a fallback is a row of
100 # markers at two weights. Same recipe as the carets above and the same
101 # proportions, rotated; `▾` takes the small separators'.
102 #
103 # `▶` and `▸` differ in size and `▶` and `►` do not differ at all here. Unicode
104 # separates TRIANGLE from POINTER by intent rather than by shape, and a house set
105 # that drew them differently would be inventing a distinction no consumer makes.
106 # ---------------------------------------------------------------------------
107
108 [[glyph]]
109 codepoint = 0x25B6
110 name = "uni25B6"
111 role = "scroll marker, right"
112 source = "alloy widget ceiling, measured 2026-08-17"
113 purpose = "coverage"
114 shape = "triangle"
115 direction = "right"
116 span = { band = 0.75, weight = 0.62 }
117 depth = 0.866
118 anchor = "band-center"
119
120 [[glyph]]
121 codepoint = 0x25C4
122 name = "uni25C4"
123 role = "scroll marker, left"
124 source = "alloy widget ceiling, measured 2026-08-17"
125 purpose = "coverage"
126 shape = "triangle"
127 direction = "left"
128 span = { band = 0.75, weight = 0.62 }
129 depth = 0.866
130 anchor = "band-center"
131
132 [[glyph]]
133 codepoint = 0x25BA
134 name = "uni25BA"
135 role = "scroll marker, right pointer"
136 source = "alloy widget ceiling, measured 2026-08-17"
137 purpose = "coverage"
138 shape = "triangle"
139 direction = "right"
140 span = { band = 0.75, weight = 0.62 }
141 depth = 0.866
142 anchor = "band-center"
143
144 [[glyph]]
145 codepoint = 0x25BE
146 name = "uni25BE"
147 role = "scroll marker, down small"
148 source = "alloy widget ceiling, measured 2026-08-17"
149 purpose = "coverage"
150 shape = "triangle"
151 direction = "down"
152 span = { band = 0.50, weight = 0.50 }
153 depth = 0.75
154 anchor = "band-center"
155
156 # ---------------------------------------------------------------------------
157 # Starship segment separators and error state.
158 # ---------------------------------------------------------------------------
159
160 [[glyph]]
161 codepoint = 0x25B8
162 name = "uni25B8"
163 role = "starship, segment separator"
164 shape = "triangle"
165 direction = "right"
166 # Unicode calls these SMALL, and they sit inline in a prompt. Span is the
167 # vertical extent for a horizontal mark, so it comes off the band's height.
168 span = { band = 0.50, weight = 0.50 }
169 depth = 0.75
170 anchor = "band-center"
171
172 [[glyph]]
173 codepoint = 0x25C2
174 name = "uni25C2"
175 role = "starship, segment separator"
176 shape = "triangle"
177 direction = "left"
178 span = { band = 0.50, weight = 0.50 }
179 depth = 0.75
180 anchor = "band-center"
181
182 [[glyph]]
183 codepoint = 0x2718
184 name = "uni2718"
185 role = "starship, error state"
186 shape = "cross"
187 width = { band = 0.70, weight = 0.50 }
188 height = { band = 0.70, weight = 0.50 }
189 # Calibrated against the base's own cross rather than by eye: HEAVY BALLOT X is
190 # heavier than `×` and not far heavier, so the number is read off the base each
191 # time rather than carried over. A flat 1.5x read as a blob.
192 #
193 # RECALIBRATED 2026-08-17, adopting Atkinson, and this is the coefficient that
194 # does not survive a base change. It was 0.80 against Plex Mono, whose `×`
195 # (U+00D7) fills 38.2% of its own box in Regular and 53.4% in Bold, and ours
196 # filled 40.7% and 55.5% — the margin the rule asks for. Atkinson's bar is
197 # thinner against its own cross, so 0.80 came out *under* the base at every
198 # weight (29.0% against 30.0%, 53.6% against 56.9%), which is a ballot X lighter
199 # than the multiplication sign beside it. At 0.95 of the base's bar it fills
200 # 33.7% and 59.1% against the base's 30.0% and 56.9%.
201 stroke = 0.95
202
203 # ---------------------------------------------------------------------------
204 # Helix whitespace renders. Both sit low in the cell, next to lowercase.
205 # ---------------------------------------------------------------------------
206
207 [[glyph]]
208 codepoint = 0x2423
209 name = "uni2423"
210 role = "helix, space render"
211 shape = "open-box"
212 # RECALIBRATED 2026-08-17, adopting Atkinson. The weight terms were 0.30 and the
213 # box closed up: Atkinson's stem runs 54 units to 158 across `wght` 200-800
214 # against Plex's 1.8x from Regular to Bold, so at the heavy end two risers of
215 # the base's own stroke took 278 of a 413-unit box and left a 135-unit counter.
216 # Measured 0.906 fill of its own bounding box, which rasterises as a blob rather
217 # than as a box, and the proof sheet is where it was seen (`quasi-type proof`).
218 #
219 # The rule the numbers now hold, and the one to keep when the next base arrives:
220 # AN OPEN BOX HAS TO STAY OPEN. Its counter is what makes it read as a box, so
221 # the box grows with the base's stroke rather than the stroke eating the box.
222 # The aperture cannot be held exactly constant — the band term does not scale
223 # with the stroke — so it is fitted at the heavy end, where it is scarce: the
224 # counter is 50% of the box's width at `wght` 800 and 74% at 200.
225 width = { band = 0.72, weight = 1.35 }
226 height = { band = 0.38, weight = 0.60 }
227 # Above the baseline, as a ratio of the band's height.
228 bottom = 0.04
229 # A stroked mark: its bars are the base's own, unmultiplied.
230 stroke = 1.0
231
232 [[glyph]]
233 codepoint = 0x23CE
234 name = "uni23CE"
235 role = "helix, newline render"
236 shape = "return-arrow"
237 # Height of the riser's flat cap, as a ratio of the band's height.
238 top = 0.92
239 # Centre of the shaft, as a ratio of the band's height above its floor.
240 shaft = 0.26
241 # The head is solid, so it takes a weight term. Its coefficient is larger than
242 # the triangles' because the head is measured against the shaft it sits on
243 # rather than against the band: at { 0.36, 0.30 } the Bold head cleared its own
244 # stroke by 38 units and read as a nub rather than an arrow.
245 head_span = { band = 0.20, weight = 1.35 }
246 head_depth = { band = 0.26, weight = 0.20 }
247 stroke = 1.0
248
249 # ---------------------------------------------------------------------------
250 # The arrows. Alloy's own surfaces emit four of these (docs/FONTS.md, measured
251 # against the built image), and a base can ship a full text face without one:
252 # Plex Mono draws all four, Atkinson Hyperlegible Mono draws none.
253 #
254 # One recipe, five entries. The head is the same drawing as the sort caret two
255 # sections up, which is the point of a house set: a triangle in a status line
256 # and a triangle on an arrow should not be two people's triangles.
257 #
258 # `←` is here on a different footing from the other four, and it is worth saying
259 # which: nothing in Alloy emits it, but ratatui draws scrollbar ends with the
260 # whole U+2190-U+2193 run, so a face carrying three of the four leaves the
261 # fourth to a fallback — one arrow in a row of four at another face's weight and
262 # another face's baseline. Measured 2026-08-17 against Alloy's widget ceiling
263 # while adopting Atkinson; the old base drew all four and hid it.
264 # ---------------------------------------------------------------------------
265
266 [[glyph]]
267 codepoint = 0x2190
268 name = "uni2190"
269 role = "leftwards arrow"
270 source = "alloy widget ceiling, measured 2026-08-17"
271 purpose = "coverage"
272 shape = "arrow"
273 direction = "left"
274 length = 0.92
275 head_span = 0.42
276 head_depth = 0.9
277 stroke = 1.0
278
279 [[glyph]]
280 codepoint = 0x2191
281 name = "uni2191"
282 role = "upwards arrow"
283 source = "alloy docs/FONTS.md, the measured surface"
284 purpose = "coverage"
285 shape = "arrow"
286 direction = "up"
287 length = 0.92
288 head_span = 0.42
289 head_depth = 0.9
290 stroke = 1.0
291
292 [[glyph]]
293 codepoint = 0x2193
294 name = "uni2193"
295 role = "downwards arrow"
296 source = "alloy docs/FONTS.md, the measured surface"
297 purpose = "coverage"
298 shape = "arrow"
299 direction = "down"
300 length = 0.92
301 head_span = 0.42
302 head_depth = 0.9
303 stroke = 1.0
304
305 [[glyph]]
306 codepoint = 0x2192
307 name = "uni2192"
308 role = "rightwards arrow"
309 source = "alloy docs/FONTS.md, the measured surface"
310 purpose = "coverage"
311 shape = "arrow"
312 direction = "right"
313 length = 0.92
314 head_span = 0.42
315 head_depth = 0.9
316 stroke = 1.0
317
318 # Two heads on one shaft. The only member of the set that is not a single
319 # gesture, and the reason `both_ends` exists rather than a second shape.
320 #
321 # Its head is smaller than the single-ended arrows above, and that is the
322 # double end paying for itself rather than a taste call: at their proportions
323 # two heads take 82% of the length and meet in the middle, so the mark reads as
324 # a bowtie and not as an arrow with a shaft. Measured off the drawing, not
325 # guessed — see `an_arrow_with_two_heads_still_has_a_shaft`.
326 [[glyph]]
327 codepoint = 0x2195
328 name = "uni2195"
329 role = "up-down arrow"
330 source = "alloy docs/FONTS.md, the measured surface"
331 purpose = "coverage"
332 shape = "arrow"
333 direction = "up"
334 both_ends = true
335 length = 0.98
336 head_span = 0.34
337 head_depth = 0.75
338 stroke = 1.0
339
340 # ---------------------------------------------------------------------------
341 # The cell tier. Box drawing and block elements, generated rather than authored:
342 # their 160 recipes are already written down in Unicode's own character names,
343 # and they must be cell-exact to tile, which is arithmetic rather than drawing.
344 # See `src/cells` for the geometry and `scripts/derive-cell-table.py` for how
345 # the table is derived. Asked for by block so this file stays a record of what
346 # somebody chose rather than 160 stanzas of transcription.
347 #
348 # They are here because a base can be chosen for the quality of its text and
349 # ship neither block: Atkinson Hyperlegible Mono has 0/128 box drawing and 0/32
350 # block elements, measured. Fallback cannot cover the gap, because a terminal
351 # takes its baseline from the primary face alone, so borders arriving from a
352 # fallback face with unrelated cell metrics draw off the line — broken borders
353 # rather than tofu.
354 # ---------------------------------------------------------------------------
355
356 [[generated]]
357 block = "box-drawing"
358
359 [[generated]]
360 block = "block-elements"
361