nx_font.nx source
↩ module page · 1392 lines · 72383 B
1// nx_font.nx -- RUNG 4 of the sovereign font BUILDER: the FONT ENGINE. A char -> its stroke-composition (in
2// 0..1000 em) + advance width; `font_text` lays out a string (pen advances per glyph). This is the table-driven
3// layer the browser calls. Glyphs are stroke-compositions (rung 1-3); growing the set = DATA. Subset here
4// (the letters for a demo phrase); the alphabet + kanji fill in the same way. ORIGINAL.
5import "nx_glyph_stroke.nx"
6import "nx_aa_raster.nx"
7
8// ---- ★FONT SPEC (the GENERATOR knobs; Metafont-style parameters -- one glyph program, many fonts) ----
9// 0 = pioneer default. Set via font_spec() BEFORE rendering; every glyph re-derives from these:
10// fs_w pen weight em (default 132 = Regular; ~95 Light, ~175 Bold -> OpenType wght)
11// fs_over overshoot em (default 12: how far round ink kisses past the metric lines)
12// fs_passes Chaikin passes (default 1 = crisp grotesque; 2 = softer/rounder)
13// fs_caps terminal style (default 0 = butt/flat = professional; 1 = round = soft/marker style)
14// The metric lines are FUNCTIONS of these (XT/XB/SH/BB/DB/CT in font_emit), so alignment survives any knob
15// change BY CONSTRUCTION -- the property nx_font_family_gate proves mechanically.
16static fs_w: i64
17static fs_over: i64
18static fs_passes: i64
19static fs_caps: i64
20static fs_mod: i64 // stroke MODULATION percent: horizontal pen = vertical pen * mod/100 (0 -> default 88 = grotesque contrast; 100 = uniform/monoline)
21static fs_swell: i64 // WALL SWELL percent: interior CURVE walls = pen * swell/100 (0 -> default 126; 100 = no swell). Grotesques swell round walls over stems (Arial ~130).
22static fs_slant: i64 // OBLIQUE slant percent (the OpenType slnt axis): x += (baseline - y) * slant/100, sheared
23 // ABOUT THE BASELINE so alignment survives BY CONSTRUCTION. 0 = upright; ~18 = 10deg oblique.
24static fs_geo: i64 // GEOMETRIC construction: 1 = machine-regular letterforms from exact shared primitives.
25static fs_wdth: i64 // WIDTH axis (the OpenType wdth): scales centreline x-spacing (stems keep their width ->
26 // TRUE condense/extend, not squish). 100 = normal; ~80 = condensed; ~125 = extended.
27func font_spec(w: i64, over: i64, passes: i64, caps: i64) -> i64 { fs_w=w; fs_over=over; fs_passes=passes; fs_caps=caps; return 0 }
28func font_spec_mod(m: i64) -> i64 { fs_mod=m; return 0 }
29func font_spec_swell(s: i64) -> i64 { fs_swell=s; return 0 }
30func font_spec_slant(s: i64) -> i64 { fs_slant=s; return 0 }
31func font_spec_reset() -> i64 { fs_w=0; fs_over=0; fs_passes=0; fs_caps=0; fs_mod=0; fs_swell=0; fs_slant=0; fs_geo=0; fs_wdth=0; return 0 }
32func font_mod() -> i64 { if fs_mod > 0 { return fs_mod } return 88 }
33func font_swell() -> i64 { if fs_swell > 0 { return fs_swell } return 126 }
34func font_slant() -> i64 { return fs_slant }
35func font_spec_geo(g: i64) -> i64 { fs_geo=g; return 0 }
36func font_spec_wdth(w: i64) -> i64 { fs_wdth=w; return 0 }
37func font_wdth() -> i64 { if fs_wdth <= 0 { return 100 } return fs_wdth }
38// effective pen weight (em). fs_w overrides. GEOMETRIC defaults LIGHTER (85 vs 123) -- measured vs the pros:
39// our old 123 gave a 7px stem at 64px where Century Gothic=4, Segoe/Arial=5 -> ours read chunky/"meh". 85
40// lands a ~5px stem = the refined professional geometric weight. Applies to geo letters AND the i/j/punct
41// that fall through to the organic path, so the whole geometric face is one consistent colour.
42func font_weight() -> i64 { if fs_w > 0 { return fs_w } if fs_geo == 1 { return 91 } return 123 }
43func font_geo() -> i64 { return fs_geo }
44
45// ★GEOMETRIC ARC primitive: a polyline sampling the 16-gon ring from index k0 to k1 (wraps if k1<k0), scaled
46// rx/ry about (cx,cy). EXACT circles/arcs -> bowls and arches are identical by construction (machine regular).
47// Downstream Chaikin + variable-width outline smooth it. Needs the trig table (cs/sn) filled.
48func geo_arc(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64,
49 cx: i64, cy: i64, r: i64, krange: i64, lw: i64, ox: i64, oy: i64, S: i64) -> i64 {
50 let k0: i64 = krange / 100
51 let k1: i64 = krange % 100
52 let ax: *i64 = sys_mmap(8*40) as *i64
53 let ay: *i64 = sys_mmap(8*40) as *i64
54 var kend: i64 = k1
55 if kend < k0 { kend = k1 + 16 }
56 var m: i64 = 0
57 var kk: i64 = k0
58 while kk <= kend {
59 let idx: i64 = kk % 16
60 ax[m] = cx + (r * cs[idx]) / 1000
61 ay[m] = cy + (r * sn[idx]) / 1000
62 m = m + 1
63 kk = kk + 1
64 }
65 fe_stroke(xs, ys, cstart, clen, np, nc, cs, sn, ax, ay, m, lw, ox, oy, S)
66 return 0
67}
68
69// ★ELLIPTICAL arc: like geo_arc but independent rx/ry (for m's narrow-but-full-height humps, g's tail).
70// klw packs krange + lw*100000 (16-arg cap): lw = klw/100000, krange = klw%100000, k0=krange/100, k1=krange%100.
71func geo_earc(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64,
72 cx: i64, cy: i64, rx: i64, ry: i64, klw: i64, ox: i64, oy: i64, S: i64) -> i64 {
73 let lw: i64 = klw / 100000
74 let krange: i64 = klw % 100000
75 let k0: i64 = krange / 100
76 let k1: i64 = krange % 100
77 let ax: *i64 = sys_mmap(8*40) as *i64
78 let ay: *i64 = sys_mmap(8*40) as *i64
79 var kend: i64 = k1
80 if kend < k0 { kend = k1 + 16 }
81 var m: i64 = 0
82 var kk: i64 = k0
83 while kk <= kend {
84 let idx: i64 = kk % 16
85 ax[m] = cx + (rx * cs[idx]) / 1000
86 ay[m] = cy + (ry * sn[idx]) / 1000
87 m = m + 1
88 kk = kk + 1
89 }
90 fe_stroke(xs, ys, cstart, clen, np, nc, cs, sn, ax, ay, m, lw, ox, oy, S)
91 return 0
92}
93// ★WIDTH COMPENSATION, percent (the wght-width interaction every real family has): heavier pens need WIDER
94// letterforms or the counters CLOG (the Bold 'e' filled in -- caught by the family-sheet eyeball). Pure
95// x-scale about 0 applied to glyph geometry AND advances together, so set-width and ink stay proportional:
96// Bold(175) -> +8%, Regular(132) -> 100 (pioneer byte-identical), Light(95) -> -7% (sets a touch tighter).
97func font_ws() -> i64 {
98 var w: i64 = 123 // anchored at the Regular pen (isolated-stem-tuned)
99 if fs_w > 0 { w = fs_w }
100 return 100 + (w - 123) / 5
101}
102
103// stroke an em-space polyline (lx/ly in 0..1000) at pen (ox,oy) grid, scale S (= px*ss), width lw(em).
104// ★CHAIKIN CORNER-CUTTING (fs_passes iterations) smooths faceted polyline curves into true smooth curves
105// before stroking -- kills the "hand-drawn kink" tell on bowls (e/o/a/R/s/g...). Open-polyline variant KEEPS
106// the endpoints (stroke starts/ends stay put); 2-point stems (no interior corner) pass through straight.
107func fe_chaikin(sx: *i64, sy: *i64, n: i64, dx: *i64, dy: *i64) -> i64 {
108 if n < 3 { var k: i64=0; while k<n { dx[k]=sx[k]; dy[k]=sy[k]; k=k+1 } return n }
109 dx[0]=sx[0]; dy[0]=sy[0]
110 var m: i64 = 1
111 var i: i64 = 0
112 while i < n-1 {
113 dx[m] = (3*sx[i] + sx[i+1])/4; dy[m] = (3*sy[i] + sy[i+1])/4; m = m + 1
114 dx[m] = (sx[i] + 3*sx[i+1])/4; dy[m] = (sy[i] + 3*sy[i+1])/4; m = m + 1
115 i = i + 1
116 }
117 dx[m]=sx[n-1]; dy[m]=sy[n-1]; m = m + 1
118 return m
119}
120// ★CORNER-PRESERVING Chaikin with per-output-segment SOURCE tags. Chaikin smooths CURVES; a sharp turn
121// (>~60deg: V/W/A apexes, z corners, k joints) is a CORNER and must stay a corner -- plain corner-cutting
122// FLATTENED the V apex at 25% of its arm length (the truncated-V bug). Interior vertices whose turn has
123// cos(theta) < 500 (x1000) are re-emitted EXACTLY between their two cut points. stag_in = per-input-segment
124// swell tags; stag_out = per-OUTPUT-segment tags (junction segments take the max of the two sources).
125func fe_chaikin_cp(sx: *i64, sy: *i64, stag: *i64, n: i64, dx: *i64, dy: *i64, dtag: *i64) -> i64 {
126 if n < 3 {
127 var k: i64=0
128 while k<n { dx[k]=sx[k]; dy[k]=sy[k]; k=k+1 }
129 if n >= 2 { dtag[0]=stag[0] }
130 return n
131 }
132 dx[0]=sx[0]; dy[0]=sy[0]
133 var m: i64 = 1
134 var i: i64 = 0
135 while i < n-1 {
136 // cut pair inside input segment i; the out-segment ENTERING each new point carries a source tag
137 dx[m] = (3*sx[i] + sx[i+1])/4; dy[m] = (3*sy[i] + sy[i+1])/4
138 var tprev: i64 = stag[i]
139 if i > 0 { if stag[i-1] > tprev { tprev = stag[i-1] } } // junction from the previous segment
140 dtag[m-1] = tprev
141 m = m + 1
142 dx[m] = (sx[i] + 3*sx[i+1])/4; dy[m] = (sy[i] + 3*sy[i+1])/4
143 dtag[m-1] = stag[i]
144 m = m + 1
145 // corner preservation: if vertex i+1 turns sharply, re-emit it exactly
146 if i < n-2 {
147 let d1x: i64 = sx[i+1]-sx[i]
148 let d1y: i64 = sy[i+1]-sy[i]
149 let d2x: i64 = sx[i+2]-sx[i+1]
150 let d2y: i64 = sy[i+2]-sy[i+1]
151 let l1: i64 = gs_isqrt(d1x*d1x+d1y*d1y)
152 let l2: i64 = gs_isqrt(d2x*d2x+d2y*d2y)
153 if l1 > 0 { if l2 > 0 {
154 let cosn: i64 = (d1x*d2x + d1y*d2y) * 1000 / (l1*l2)
155 if cosn < 500 { // turn > 60deg = CORNER
156 dx[m] = sx[i+1]; dy[m] = sy[i+1]
157 var tj: i64 = stag[i]
158 if stag[i+1] > tj { tj = stag[i+1] }
159 dtag[m-1] = tj
160 m = m + 1
161 }
162 } }
163 }
164 i = i + 1
165 }
166 dx[m]=sx[n-1]; dy[m]=sy[n-1]
167 dtag[m-1] = stag[n-2]
168 m = m + 1
169 return m
170}
171func fe_stroke(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64,
172 lx: *i64, ly: *i64, n: i64, lw: i64, ox: i64, oy: i64, S: i64) -> i64 {
173 let ax: *i64 = sys_mmap(8*512) as *i64
174 let ay: *i64 = sys_mmap(8*512) as *i64
175 let bx: *i64 = sys_mmap(8*512) as *i64
176 let by: *i64 = sys_mmap(8*512) as *i64
177 let st: *i64 = sys_mmap(8*512) as *i64 // per-SEGMENT swell tags (1 = interior curve wall)
178 let st2: *i64 = sys_mmap(8*512) as *i64
179 var k: i64 = 0
180 while k < n { ax[k]=lx[k]; ay[k]=ly[k]; k=k+1 }
181 // tag the ORIGINAL segments: interior segments of a curve polyline swell; a 2-point stroke is a STEM
182 // (never swells); the FIRST/LAST segments of open strokes are tails/junction legs (never swell) --
183 // this is what keeps n's right leg the same weight as its left stem. Closed loops swell everywhere.
184 var closed0: i64 = 0
185 if lx[0] == lx[n-1] { if ly[0] == ly[n-1] { closed0 = 1 } }
186 k = 0
187 while k < n - 1 {
188 var t: i64 = 0
189 if closed0 == 1 { t = 1 }
190 else { if k >= 1 { if k <= n-3 { t = 1 } } }
191 st[k] = t
192 k = k + 1
193 }
194 var passes: i64 = 1
195 if fs_passes > 0 { passes = fs_passes } // generator knob (1 = crisp, 2 = soft)
196 if passes > 3 { passes = 3 }
197 var m: i64 = n
198 var p: i64 = 0
199 while p < passes {
200 let m2: i64 = fe_chaikin_cp(ax, ay, st, m, bx, by, st2) // corner-preserving + carries the tags
201 var j: i64 = 0
202 while j < m2 - 1 { st[j] = st2[j]; j = j + 1 }
203 m = m2
204 var k2: i64 = 0
205 while k2 < m { ax[k2]=bx[k2]; ay[k2]=by[k2]; k2=k2+1 }
206 p = p + 1
207 }
208 // generator knob: ROUND terminals (soft style). The cap disc extends w_end/2 past the endpoint, so INSET
209 // each open end by w_end/2 along its direction first -- the cap's ink then lands exactly where butt ink
210 // did (keeps the metric alignment: stems still end ON the baseline). Standard round-cap geometry.
211 let wh2: i64 = (lw * font_mod()) / 100 // horizontal pen (stroke contrast)
212 var w0: i64 = lw
213 var w1: i64 = lw
214 if m >= 2 {
215 var edx: i64 = ax[1]-ax[0]
216 var edy: i64 = ay[1]-ay[0]
217 if edy < 0 { edy = 0 - edy }
218 var el: i64 = gs_isqrt(edx*edx+edy*edy)
219 if el > 0 { w0 = wh2 + ((lw - wh2) * edy) / el }
220 edx = ax[m-1]-ax[m-2]
221 edy = ay[m-1]-ay[m-2]
222 if edy < 0 { edy = 0 - edy }
223 el = gs_isqrt(edx*edx+edy*edy)
224 if el > 0 { w1 = wh2 + ((lw - wh2) * edy) / el }
225 }
226 var capped: i64 = 0
227 if fs_caps == 1 {
228 var closed: i64 = 0
229 if ax[0] == ax[m-1] { if ay[0] == ay[m-1] { closed = 1 } }
230 if closed == 0 { if m >= 2 {
231 capped = 1
232 var h: i64 = w0/2
233 var ddx: i64 = ax[1]-ax[0]
234 var ddy: i64 = ay[1]-ay[0]
235 var dl: i64 = gs_isqrt(ddx*ddx+ddy*ddy)
236 if dl > h { ax[0] = ax[0] + (ddx*h)/dl; ay[0] = ay[0] + (ddy*h)/dl }
237 h = w1/2
238 ddx = ax[m-2]-ax[m-1]
239 ddy = ay[m-2]-ay[m-1]
240 dl = gs_isqrt(ddx*ddx+ddy*ddy)
241 if dl > h { ax[m-1] = ax[m-1] + (ddx*h)/dl; ay[m-1] = ay[m-1] + (ddy*h)/dl }
242 } }
243 }
244 let gx: *i64 = sys_mmap(8*512) as *i64
245 let gy: *i64 = sys_mmap(8*512) as *i64
246 let ws: i64 = (font_ws() * font_wdth()) / 100 // family width-comp x WIDTH axis (x-only; y untouched -> metrics hold)
247 let sl: i64 = font_slant() // oblique shear about the BASELINE (y=750): x += (750-y)*sl/100
248 var i: i64 = 0
249 while i < m {
250 gx[i] = ox + (((ax[i] * ws) / 100 + ((750 - ay[i]) * sl) / 100) * S) / 1000
251 gy[i] = oy + (ay[i] * S) / 1000
252 i = i + 1
253 }
254 // per-SEGMENT pen widths (grid units): role-aware modulated nib -- vertical curve walls swell toward
255 // wsw, stems/tails stay wv, horizontals thin to wh; blended by |dy|/len. Fed to the VARIABLE-WIDTH
256 // OUTLINE stroke (one smooth polygon, width interpolates continuously -> no join-disc lumps).
257 let wvg: i64 = (lw * S) / 1000
258 let whg: i64 = (wh2 * S) / 1000
259 let wswg: i64 = ((lw * font_swell()) / 100 * S) / 1000
260 let segw: *i64 = sys_mmap(8*512) as *i64
261 var closed2: i64 = 0
262 if gx[0] == gx[m-1] { if gy[0] == gy[m-1] { closed2 = 1 } }
263 i = 0
264 while i < m - 1 {
265 let sdx: i64 = gx[i+1]-gx[i]
266 var sdy: i64 = gy[i+1]-gy[i]
267 if sdy < 0 { sdy = 0 - sdy }
268 let sl: i64 = gs_isqrt(sdx*sdx + sdy*sdy)
269 var wtop: i64 = wvg
270 if st[i] == 1 { wtop = wswg }
271 var w2: i64 = wtop
272 if sl > 0 { w2 = whg + ((wtop - whg) * sdy) / sl }
273 segw[i] = w2
274 i = i + 1
275 }
276 gs_stroke_var(xs, ys, cstart, clen, np, nc, gx, gy, m, segw, closed2)
277 if capped == 1 {
278 gs_disc(xs, ys, cstart, clen, np, nc, cs, sn, gx[0], gy[0], (w0*S)/2000, 0)
279 gs_disc(xs, ys, cstart, clen, np, nc, cs, sn, gx[m-1], gy[m-1], (w1*S)/2000, 0)
280 }
281 return 0
282}
283
284// ---- GENERATED-BY nx_font_space_tool (optical sidebearings, Tracy/autowidth method: per-glyph ink
285// measured through the REAL engine in the x-height optical band; every pair gap reads ~2x SB_T=100em ->
286// EVEN RHYTHM, the fix for "ransom-note spacing"). Rerun the tool after any glyph-geometry change. ----
287// font_sp_shift: x-offset applied at layout so each glyph's optical left edge sits at SB_T.
288func font_sp_shift(ch: i64) -> i64 {
289 if ch == 97 { return 35 }
290 if ch == 98 { return -20 }
291 if ch == 99 { return 25 }
292 if ch == 100 { return 35 }
293 if ch == 101 { return 30 }
294 if ch == 102 { return -35 }
295 if ch == 103 { return 35 }
296 if ch == 104 { return -20 }
297 if ch == 105 { return -20 }
298 if ch == 106 { return -20 }
299 if ch == 107 { return -20 }
300 if ch == 108 { return -70 }
301 if ch == 110 { return -20 }
302 if ch == 111 { return 40 }
303 if ch == 112 { return -20 }
304 if ch == 113 { return 35 }
305 if ch == 114 { return -50 }
306 if ch == 115 { return -20 }
307 if ch == 116 { return -20 }
308 if ch == 117 { return -5 }
309 if ch == 118 { return -25 }
310 if ch == 120 { return -40 }
311 if ch == 121 { return -25 }
312 if ch == 122 { return -45 }
313 if ch == 65 { return -30 }
314 if ch == 66 { return -20 }
315 if ch == 67 { return 5 }
316 if ch == 68 { return -20 }
317 if ch == 69 { return -5 }
318 if ch == 70 { return -20 }
319 if ch == 71 { return 5 }
320 if ch == 72 { return -20 }
321 if ch == 73 { return -140 }
322 if ch == 74 { return -25 }
323 if ch == 75 { return -20 }
324 if ch == 76 { return -30 }
325 if ch == 78 { return -20 }
326 if ch == 79 { return 15 }
327 if ch == 80 { return -20 }
328 if ch == 81 { return 15 }
329 if ch == 82 { return -20 }
330 if ch == 83 { return -15 }
331 if ch == 84 { return -85 }
332 if ch == 85 { return -5 }
333 if ch == 86 { return -40 }
334 if ch == 87 { return -10 }
335 if ch == 88 { return -55 }
336 if ch == 89 { return -50 }
337 if ch == 90 { return -85 }
338 if ch == 49 { return -140 }
339 if ch == 50 { return -85 }
340 if ch == 51 { return -60 }
341 if ch == 52 { return -95 }
342 if ch == 53 { return -45 }
343 if ch == 54 { return -10 }
344 if ch == 55 { return -115 }
345 if ch == 56 { return -5 }
346 if ch == 57 { return -20 }
347 if ch == 46 { return -125 }
348 if ch == 44 { return -110 }
349 if ch == 45 { return -80 }
350 if ch == 58 { return -95 }
351 if ch == 33 { return -90 }
352 if ch == 63 { return -85 }
353 if ch == 40 { return -95 }
354 if ch == 41 { return -95 }
355 if ch == 39 { return -105 }
356 if ch == 47 { return -95 }
357 return 0
358}
359func font_adv_em_base(ch: i64) -> i64 {
360 if ch == 32 { return 300 }
361 if ch == 97 { return 490 }
362 if ch == 98 { return 485 }
363 if ch == 99 { return 490 }
364 if ch == 100 { return 490 }
365 if ch == 101 { return 530 }
366 if ch == 102 { return 380 }
367 if ch == 103 { return 510 }
368 if ch == 104 { return 490 }
369 if ch == 105 { return 215 }
370 if ch == 106 { return 350 }
371 if ch == 107 { return 410 }
372 if ch == 108 { return 215 }
373 if ch == 109 { return 710 }
374 if ch == 110 { return 490 }
375 if ch == 111 { return 590 }
376 if ch == 112 { return 485 }
377 if ch == 113 { return 490 }
378 if ch == 114 { return 365 }
379 if ch == 115 { return 470 }
380 if ch == 116 { return 345 }
381 if ch == 117 { return 490 }
382 if ch == 118 { return 450 }
383 if ch == 119 { return 600 }
384 if ch == 120 { return 415 }
385 if ch == 121 { return 450 }
386 if ch == 122 { return 400 }
387 if ch == 65 { return 540 }
388 if ch == 66 { return 515 }
389 if ch == 67 { return 555 }
390 if ch == 68 { return 555 }
391 if ch == 69 { return 500 }
392 if ch == 70 { return 440 }
393 if ch == 71 { return 600 }
394 if ch == 72 { return 555 }
395 if ch == 73 { return 295 }
396 if ch == 74 { return 525 }
397 if ch == 75 { return 505 }
398 if ch == 76 { return 465 }
399 if ch == 77 { return 615 }
400 if ch == 78 { return 555 }
401 if ch == 79 { return 625 }
402 if ch == 80 { return 505 }
403 if ch == 81 { return 625 }
404 if ch == 82 { return 505 }
405 if ch == 83 { return 560 }
406 if ch == 84 { return 425 }
407 if ch == 85 { return 585 }
408 if ch == 86 { return 515 }
409 if ch == 87 { return 585 }
410 if ch == 88 { return 485 }
411 if ch == 89 { return 495 }
412 if ch == 90 { return 440 }
413 if ch == 48 { return 595 }
414 if ch == 49 { return 335 }
415 if ch == 50 { return 475 }
416 if ch == 51 { return 500 }
417 if ch == 52 { return 425 }
418 if ch == 53 { return 525 }
419 if ch == 54 { return 570 }
420 if ch == 55 { return 400 }
421 if ch == 56 { return 585 }
422 if ch == 57 { return 555 }
423 if ch == 46 { return 175 } // hand-clamped: the period dot sits below the optical band (tool sees a sliver)
424 if ch == 44 { return 185 } // hand-clamped, same reason
425 if ch == 45 { return 365 }
426 if ch == 58 { return 205 }
427 if ch == 33 { return 215 }
428 if ch == 63 { return 460 }
429 if ch == 40 { return 290 }
430 if ch == 41 { return 300 }
431 if ch == 39 { return 180 }
432 if ch == 47 { return 365 }
433 if ch == 91 { return 330 } // [
434 if ch == 93 { return 330 } // ]
435 if ch == 37 { return 720 } // %
436 if ch == 59 { return 205 } // ;
437 if ch == 34 { return 430 } // "
438 if ch == 61 { return 600 } // =
439 if ch == 43 { return 600 } // +
440 return 520
441}
442// ---- END GENERATED ----
443// ---- GENERATED-BY nx_font_space_tool (GEOMETRIC mode) ----
444func font_sp_shift_geo(ch: i64) -> i64 {
445 if ch == 97 { return -40 }
446 if ch == 98 { return -35 }
447 if ch == 99 { return -40 }
448 if ch == 100 { return -40 }
449 if ch == 101 { return -40 }
450 if ch == 102 { return -70 }
451 if ch == 103 { return -40 }
452 if ch == 104 { return -35 }
453 if ch == 105 { return -35 }
454 if ch == 106 { return -30 }
455 if ch == 107 { return -35 }
456 if ch == 108 { return -85 }
457 if ch == 109 { return -35 }
458 if ch == 110 { return -35 }
459 if ch == 111 { return -40 }
460 if ch == 112 { return -35 }
461 if ch == 113 { return -40 }
462 if ch == 114 { return -35 }
463 if ch == 115 { return -50 }
464 if ch == 116 { return -55 }
465 if ch == 117 { return -35 }
466 if ch == 118 { return -35 }
467 if ch == 119 { return -20 }
468 if ch == 120 { return -50 }
469 if ch == 121 { return -35 }
470 if ch == 122 { return -45 }
471 if ch == 65 { return -45 }
472 if ch == 66 { return -35 }
473 if ch == 67 { return -30 }
474 if ch == 68 { return -35 }
475 if ch == 69 { return -35 }
476 if ch == 70 { return -35 }
477 if ch == 71 { return -30 }
478 if ch == 72 { return -35 }
479 if ch == 73 { return -175 }
480 if ch == 74 { return -45 }
481 if ch == 75 { return -35 }
482 if ch == 76 { return -45 }
483 if ch == 77 { return -15 }
484 if ch == 78 { return -35 }
485 if ch == 79 { return -20 }
486 if ch == 80 { return -35 }
487 if ch == 81 { return -20 }
488 if ch == 82 { return -35 }
489 if ch == 83 { return -30 }
490 if ch == 84 { return -85 }
491 if ch == 85 { return -40 }
492 if ch == 86 { return -50 }
493 if ch == 87 { return -30 }
494 if ch == 88 { return -65 }
495 if ch == 89 { return -60 }
496 if ch == 90 { return -65 }
497 if ch == 48 { return -35 }
498 if ch == 49 { return -155 }
499 if ch == 50 { return -80 }
500 if ch == 51 { return -60 }
501 if ch == 52 { return -45 }
502 if ch == 53 { return -50 }
503 if ch == 54 { return -45 }
504 if ch == 55 { return -115 }
505 if ch == 56 { return -40 }
506 if ch == 57 { return -55 }
507 if ch == 46 { return -110 }
508 if ch == 44 { return -85 }
509 if ch == 45 { return -80 }
510 if ch == 58 { return -110 }
511 if ch == 33 { return -105 }
512 if ch == 63 { return -100 }
513 if ch == 40 { return -130 }
514 if ch == 41 { return -120 }
515 if ch == 39 { return -120 }
516 if ch == 47 { return -105 }
517 return 0
518}
519func font_adv_em_base_geo(ch: i64) -> i64 {
520 if ch == 32 { return 300 }
521 if ch == 97 { return 610 }
522 if ch == 98 { return 610 }
523 if ch == 99 { return 505 }
524 if ch == 100 { return 610 }
525 if ch == 101 { return 605 }
526 if ch == 102 { return 330 }
527 if ch == 103 { return 610 }
528 if ch == 104 { return 615 }
529 if ch == 105 { return 185 }
530 if ch == 106 { return 310 }
531 if ch == 107 { return 385 }
532 if ch == 108 { return 185 }
533 if ch == 109 { return 705 }
534 if ch == 110 { return 615 }
535 if ch == 111 { return 605 }
536 if ch == 112 { return 610 }
537 if ch == 113 { return 610 }
538 if ch == 114 { return 330 }
539 if ch == 115 { return 390 }
540 if ch == 116 { return 285 }
541 if ch == 117 { return 615 }
542 if ch == 118 { return 430 }
543 if ch == 119 { return 560 }
544 if ch == 120 { return 395 }
545 if ch == 121 { return 430 }
546 if ch == 122 { return 400 }
547 if ch == 65 { return 510 }
548 if ch == 66 { return 500 }
549 if ch == 67 { return 505 }
550 if ch == 68 { return 505 }
551 if ch == 69 { return 435 }
552 if ch == 70 { return 425 }
553 if ch == 71 { return 540 }
554 if ch == 72 { return 525 }
555 if ch == 73 { return 225 }
556 if ch == 74 { return 475 }
557 if ch == 75 { return 480 }
558 if ch == 76 { return 415 }
559 if ch == 77 { return 580 }
560 if ch == 78 { return 525 }
561 if ch == 79 { return 555 }
562 if ch == 80 { return 460 }
563 if ch == 81 { return 555 }
564 if ch == 82 { return 480 }
565 if ch == 83 { return 510 }
566 if ch == 84 { return 425 }
567 if ch == 85 { return 520 }
568 if ch == 86 { return 495 }
569 if ch == 87 { return 545 }
570 if ch == 88 { return 465 }
571 if ch == 89 { return 475 }
572 if ch == 90 { return 460 }
573 if ch == 48 { return 525 }
574 if ch == 49 { return 285 }
575 if ch == 50 { return 445 }
576 if ch == 51 { return 470 }
577 if ch == 52 { return 475 }
578 if ch == 53 { return 485 }
579 if ch == 54 { return 500 }
580 if ch == 55 { return 405 }
581 if ch == 56 { return 515 }
582 if ch == 57 { return 485 }
583 if ch == 46 { return 175 }
584 if ch == 44 { return 185 }
585 if ch == 45 { return 365 }
586 if ch == 58 { return 175 }
587 if ch == 33 { return 185 }
588 if ch == 63 { return 405 }
589 if ch == 40 { return 245 }
590 if ch == 41 { return 240 }
591 if ch == 39 { return 150 }
592 if ch == 47 { return 345 }
593 return 520
594}
595// ---- END GEO GENERATED ----
596func font_adv_em(ch: i64) -> i64 {
597 var b: i64 = font_adv_em_base(ch)
598 if fs_geo == 1 { b = font_adv_em_base_geo(ch) }
599 return (((b * font_ws()) / 100) * font_wdth()) / 100 // advance tracks the WIDTH axis so spacing stays proportional
600}
601func font_sp_shift_disp(ch: i64) -> i64 { if fs_geo == 1 { return font_sp_shift_geo(ch) } return font_sp_shift(ch) }
602func font_adv(ch: i64, S: i64) -> i64 { return (font_adv_em(ch) * S) / 1000 }
603
604// ★GEOMETRIC (machine-regular) lowercase: bowls = ONE exact circle reused; arches = ONE exact semicircle
605// reused; stems = exact verticals on a fixed grid. Returns 1 if it drew the glyph, 0 = fall through to the
606// organic font_emit. Monoline is expected (set mod=100, swell=100 for the "Geometric" style).
607func font_emit_geo(ch: i64, xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64,
608 ox: i64, oy: i64, S: i64) -> i64 {
609 var W: i64 = font_weight()
610 var OV: i64 = 12
611 if fs_over > 0 { OV = fs_over }
612 let WHp: i64 = (W * font_mod()) / 100
613 let V2H: i64 = (W + WHp) / 4
614 let XT: i64 = 250 - OV + V2H // round top / bottom extrema (ink kisses x-height/baseline +- overshoot)
615 let XB: i64 = 750 + OV - V2H
616 let CY: i64 = (XT + XB) / 2 // x-height circle centre y
617 let R: i64 = (XB - XT) / 2 // x-height circle radius (the MODULE -- every bowl/arch uses it)
618 let SPR: i64 = CY // arch spring line (arch = top half of the module circle)
619 let ax: *i64 = sys_mmap(8*16) as *i64
620 let ay: *i64 = sys_mmap(8*16) as *i64
621 // stem grid: left stem x=130, the module circle sits tangent to it (centre 130+R)
622 let LX: i64 = 130
623 let BC: i64 = LX + R // bowl centre x for stem+bowl letters (b d p q)
624 if ch == 111 { // o -- the module circle
625 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 130+R, CY, R, R, W, ox,oy,S)
626 return 1
627 }
628 if ch == 99 { // c -- module circle open on the right (idx 2..14)
629 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, 130+R, CY, R, 214, W, ox,oy,S)
630 return 1
631 }
632 if ch == 101 { // e -- c-arc (open lower-right) + horizontal bar at mid
633 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, 130+R, CY, R, 216, W, ox,oy,S) // idx 2..16: bottom->left->top->right (closes top-right, opens lower-right)
634 ax[0]=130; ay[0]=CY; ax[1]=130+2*R; ay[1]=CY
635 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
636 return 1
637 }
638 if ch == 110 { // n -- left stem + top-half arch + right stem
639 ax[0]=LX;ay[0]=250; ax[1]=LX;ay[1]=750
640 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
641 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, LX+R, SPR, R, 816, W, ox,oy,S) // left->top->right semicircle
642 ax[0]=LX+2*R;ay[0]=SPR-40; ax[1]=LX+2*R;ay[1]=750 // stem overlaps into the arch (fills the junction seam)
643 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
644 return 1
645 }
646 if ch == 104 { // h -- ascender stem + arch + right stem
647 ax[0]=LX;ay[0]=110; ax[1]=LX;ay[1]=750
648 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
649 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, LX+R, SPR, R, 816, W, ox,oy,S)
650 ax[0]=LX+2*R;ay[0]=SPR-40; ax[1]=LX+2*R;ay[1]=750 // h: overlap stem into arch
651 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
652 return 1
653 }
654 if ch == 117 { // u -- left stem + bottom-half arch + right stem
655 ax[0]=LX;ay[0]=250; ax[1]=LX;ay[1]=SPR
656 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
657 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, LX+R, SPR, R, 8, W, ox,oy,S) // right->bottom->left semicircle
658 ax[0]=LX+2*R;ay[0]=250; ax[1]=LX+2*R;ay[1]=SPR
659 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
660 return 1
661 }
662 if ch == 114 { // r -- stem + top-left quarter arch
663 ax[0]=LX;ay[0]=250; ax[1]=LX;ay[1]=750
664 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
665 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, LX+R, SPR, R, 812, W, ox,oy,S) // left->top quarter
666 return 1
667 }
668 if ch == 109 { // m -- 3 stems + 2 ELLIPTICAL arches (narrow rx, FULL ry -> humps reach x-height top like n)
669 let mr: i64 = (R*3)/5 // narrow hump half-width (m is a double-arch)
670 let mk: i64 = 816 + W*100000 // packed krange(top-half 8..16) + weight
671 ax[0]=LX;ay[0]=250; ax[1]=LX;ay[1]=750
672 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
673 geo_earc(xs,ys,cstart,clen,np,nc,cs,sn, LX+mr, CY, mr, R, mk, ox,oy,S)
674 ax[0]=LX+2*mr;ay[0]=CY-40; ax[1]=LX+2*mr;ay[1]=750 // overlap into arch (seam fill)
675 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
676 geo_earc(xs,ys,cstart,clen,np,nc,cs,sn, LX+3*mr, CY, mr, R, mk, ox,oy,S)
677 ax[0]=LX+4*mr;ay[0]=CY-40; ax[1]=LX+4*mr;ay[1]=750 // overlap into arch (seam fill)
678 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
679 return 1
680 }
681 if ch == 98 { // b -- ascender stem + module circle (tangent)
682 ax[0]=LX;ay[0]=110; ax[1]=LX;ay[1]=750
683 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
684 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, BC, CY, R, R, W, ox,oy,S)
685 return 1
686 }
687 if ch == 100 { // d -- right ascender stem + module circle
688 ax[0]=LX+2*R;ay[0]=110; ax[1]=LX+2*R;ay[1]=750
689 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
690 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, BC, CY, R, R, W, ox,oy,S)
691 return 1
692 }
693 if ch == 112 { // p -- descender stem + module circle
694 ax[0]=LX;ay[0]=250; ax[1]=LX;ay[1]=900
695 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
696 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, BC, CY, R, R, W, ox,oy,S)
697 return 1
698 }
699 if ch == 113 { // q -- module circle + right descender stem
700 ax[0]=LX+2*R;ay[0]=250; ax[1]=LX+2*R;ay[1]=900
701 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
702 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, BC, CY, R, R, W, ox,oy,S)
703 return 1
704 }
705 if ch == 97 { // a -- module circle + right stem (single-storey, geometric)
706 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, BC, CY, R, R, W, ox,oy,S)
707 ax[0]=LX+2*R;ay[0]=CY-R+40; ax[1]=LX+2*R;ay[1]=750
708 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
709 return 1
710 }
711 if ch == 115 { // s -- two stacked circles sharing the CENTRE point (top circle's bottom == bottom circle's top == (sx,CY))
712 let rs: i64 = (XB - XT) / 4
713 let sx: i64 = 130 + rs + 12
714 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, sx, CY-rs, rs, 414, W, ox,oy,S) // top bowl: centre(4)->left(8)->top(12)->upper-right(14)
715 geo_arc(xs,ys,cstart,clen,np,nc,cs,sn, sx, CY+rs, rs, 1206, W, ox,oy,S) // bottom bowl: centre(12)->right(0)->bottom(4)->lower-left(6)
716 return 1
717 }
718 if ch == 103 { // g -- single-storey, smooth gentle-J tail (Futura model): closed circle bowl + a tail that descends with
719 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, BC, CY, R, R, W, ox,oy,S) // MINIMAL rightward bulge (so it is not a 9) and hooks decisively LEFT (so it is not a q); 6 pts, Chaikin-smoothed
720 ax[0]=BC+(R*52)/100; ay[0]=712 // on the bowl, lower-right
721 ax[1]=BC+(R*58)/100; ay[1]=802 // descend nearly straight (small bulge)
722 ax[2]=BC+(R*46)/100; ay[2]=880 // begin the curve
723 ax[3]=BC+(R*4)/100; ay[3]=928 // DEEP bottom of the descender (registers at small px)
724 ax[4]=BC-(R*50)/100; ay[4]=910 // strong LEFT hook
725 ax[5]=BC-(R*88)/100; ay[5]=846 // open terminal, up-left
726 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
727 return 1
728 }
729 return 0 // straights (l i t k v w x y z s f j m) -> organic path (already geometric enough)
730}
731
732// emit one glyph's stroke-composition at pen (ox,oy) grid, scale S. Em: baseline 750, x-height-top 250,
733// ascender-top 100, descender 900, stem width ~70. Strokes -> curves come free.
734func font_emit(ch: i64, xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64,
735 ox: i64, oy: i64, S: i64) -> i64 {
736 if fs_geo == 1 { if font_emit_geo(ch, xs, ys, cstart, clen, np, nc, cs, sn, ox, oy, S) == 1 { return 0 } }
737 let ax: *i64 = sys_mmap(8*16) as *i64
738 let ay: *i64 = sys_mmap(8*16) as *i64
739 var W: i64 = font_weight() // pen weight (em): organic Regular=123 (tuned on isolated-stem vs Arial-600);
740 // fs_w overrides (Light ~95, Bold ~160); geo defaults 85 (lighter=pro)
741 var OV: i64 = 12 // overshoot (em): how far round ink kisses past the metric lines
742 if fs_over > 0 { OV = fs_over } // generator knob
743 // ★PARAMETRIC METRIC LINES (METAFONT-style: coordinates as functions of the pen). The stroke's INK
744 // extends V2=W/2 past the centerline, so CURVE EXTREMA paths sit V2 INSIDE the intended ink line, while
745 // stem BUTT ends sit exactly ON it. This kills the systematic stem-vs-bowl misalignment (baseline bounce,
746 // uneven x-height) BY CONSTRUCTION -- change W and the whole font re-aligns. The generator core.
747 let V2: i64 = W/2
748 let WH2: i64 = (W * font_mod()) / 100 // horizontal pen (stroke contrast/modulation)
749 let V2H: i64 = (W + WH2) / 4 // curve-extremum half-pen: the segments AROUND an extremum are
750 // part-diagonal, so their width blends W..WH2 -- use the mean
751 // half-pen so extremum ink still kisses the overshoot line (at
752 // mod=100 this reduces to W/2 exactly)
753 let XT: i64 = 250 - OV + V2H // round TOP extremum: ink kisses x-height - overshoot
754 let XB: i64 = 750 + OV - V2H // round BOTTOM extremum: ink kisses baseline + overshoot
755 let SH: i64 = 250 + V2H // arch/shoulder top + x-height BAR: ink exactly AT x-height 250
756 let BB: i64 = 750 - V2H // baseline BAR: ink exactly AT baseline 750
757 let DB: i64 = 900 + OV - V2H // descender round bottom: ink kisses descender + overshoot
758 let CT: i64 = 130 - OV + V2H // cap round TOP extremum: ink kisses cap height - overshoot
759 let CB: i64 = 130 + V2H // cap corner/bar top path: ink AT cap height 130
760 let WSC: i64 = (font_ws() * font_wdth()) / 100 // family width-comp x WIDTH axis (applies to disc x-positions here;
761 // stroke geometry gets it inside fe_stroke)
762 let SL: i64 = font_slant() // oblique shear for disc x-positions (strokes get it in fe_stroke)
763 let DR: i64 = (45 * W) / 123 // i/j dot radius: scales with the pen weight
764 let PR: i64 = (57 * W) / 123 // punctuation dot radius
765 if ch == 110 { // n -- wide counter (condensed fix): stems 130/390
766 ax[0]=130;ay[0]=750; ax[1]=130;ay[1]=250
767 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
768 ax[0]=130;ay[0]=430; ax[1]=180;ay[1]=SH+28; ax[2]=260;ay[2]=SH; ax[3]=340;ay[3]=SH+28; ax[4]=390;ay[4]=430; ax[5]=390;ay[5]=750
769 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
770 return 0
771 }
772 if ch == 104 { // h -- ascender stem + wide arch
773 ax[0]=130;ay[0]=750; ax[1]=130;ay[1]=110
774 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
775 ax[0]=130;ay[0]=430; ax[1]=180;ay[1]=SH+28; ax[2]=260;ay[2]=SH; ax[3]=340;ay[3]=SH+28; ax[4]=390;ay[4]=430; ax[5]=390;ay[5]=750
776 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
777 return 0
778 }
779 if ch == 105 { // i
780 ax[0]=130;ay[0]=750; ax[1]=130;ay[1]=250
781 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
782 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((130*WSC)/100 + ((750-160)*SL)/100)*S)/1000, oy+(160*S)/1000, (DR*S)/1000, 0)
783 return 0
784 }
785 if ch == 115 { // s -- double curve (extrema on the parametric lines)
786 ax[0]=372;ay[0]=XT+44; ax[1]=292;ay[1]=XT+4; ax[2]=190;ay[2]=XT; ax[3]=140;ay[3]=350; ax[4]=178;ay[4]=436
787 ax[5]=275;ay[5]=478; ax[6]=350;ay[6]=528; ax[7]=380;ay[7]=616; ax[8]=330;ay[8]=XB+4; ax[9]=222;ay[9]=XB; ax[10]=118;ay[10]=XB-42
788 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,11, W, ox,oy,S)
789 return 0
790 }
791 if ch == 116 { // t -- stem + foot hook (ink lands ~750) + crossbar ON x-height
792 ax[0]=180;ay[0]=130; ax[1]=180;ay[1]=614; ax[2]=222;ay[2]=684; ax[3]=298;ay[3]=670
793 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
794 ax[0]=70;ay[0]=SH; ax[1]=316;ay[1]=SH
795 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
796 return 0
797 }
798 if ch == 101 { // e -- bar + open bowl on the parametric lines
799 ax[0]=95;ay[0]=505; ax[1]=395;ay[1]=505
800 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
801 ax[0]=395;ay[0]=505; ax[1]=385;ay[1]=372; ax[2]=255;ay[2]=XT; ax[3]=128;ay[3]=XT+36; ax[4]=88;ay[4]=500
802 ax[5]=126;ay[5]=XB-44; ax[6]=250;ay[6]=XB; ax[7]=378;ay[7]=XB-36
803 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,8, W, ox,oy,S)
804 return 0
805 }
806 if ch == 111 { // o -- ring on the parametric lines
807 ax[0]=432;ay[0]=500; ax[1]=388;ay[1]=XT+62; ax[2]=255;ay[2]=XT; ax[3]=122;ay[3]=XT+62; ax[4]=78;ay[4]=500
808 ax[5]=122;ay[5]=XB-62; ax[6]=255;ay[6]=XB; ax[7]=388;ay[7]=XB-62; ax[8]=432;ay[8]=500
809 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,9, W, ox,oy,S)
810 return 0
811 }
812 if ch == 114 { // r -- stem + shoulder arm at x-height
813 ax[0]=160;ay[0]=750; ax[1]=160;ay[1]=250
814 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
815 ax[0]=160;ay[0]=430; ax[1]=205;ay[1]=SH+28; ax[2]=290;ay[2]=SH; ax[3]=356;ay[3]=SH+16
816 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
817 return 0
818 }
819 if ch == 108 { // l -- ascender vertical
820 ax[0]=180;ay[0]=110; ax[1]=180;ay[1]=750
821 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
822 return 0
823 }
824 if ch == 99 { // c -- open arc on the parametric lines
825 ax[0]=398;ay[0]=352; ax[1]=270;ay[1]=XT+4; ax[2]=140;ay[2]=XT+28; ax[3]=90;ay[3]=500; ax[4]=140;ay[4]=XB-28; ax[5]=270;ay[5]=XB+4; ax[6]=398;ay[6]=648
826 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
827 return 0
828 }
829 if ch == 100 { // d -- bowl + right ascender stem
830 ax[0]=350;ay[0]=352; ax[1]=270;ay[1]=XT+4; ax[2]=170;ay[2]=XT; ax[3]=110;ay[3]=XT+34; ax[4]=85;ay[4]=500; ax[5]=110;ay[5]=XB-34; ax[6]=195;ay[6]=XB; ax[7]=290;ay[7]=XB+6; ax[8]=350;ay[8]=648
831 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,9, W, ox,oy,S)
832 ax[0]=350;ay[0]=110; ax[1]=350;ay[1]=750
833 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
834 return 0
835 }
836 if ch == 97 { // a -- bowl + right x-height stem
837 ax[0]=350;ay[0]=352; ax[1]=270;ay[1]=XT+4; ax[2]=170;ay[2]=XT; ax[3]=108;ay[3]=XT+34; ax[4]=85;ay[4]=500; ax[5]=108;ay[5]=XB-34; ax[6]=185;ay[6]=XB; ax[7]=290;ay[7]=XB+6; ax[8]=350;ay[8]=648
838 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,9, W, ox,oy,S)
839 ax[0]=350;ay[0]=280; ax[1]=350;ay[1]=750
840 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
841 return 0
842 }
843 if ch == 117 { // u -- wide counter + bottom curve on the parametric line
844 ax[0]=130;ay[0]=250; ax[1]=130;ay[1]=580; ax[2]=180;ay[2]=XB-28; ax[3]=255;ay[3]=XB; ax[4]=330;ay[4]=XB-28; ax[5]=390;ay[5]=580
845 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
846 ax[0]=390;ay[0]=250; ax[1]=390;ay[1]=750
847 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
848 return 0
849 }
850 if ch == 109 { // m -- wide double counter: stems 110/350/590
851 ax[0]=110;ay[0]=750; ax[1]=110;ay[1]=250
852 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
853 ax[0]=110;ay[0]=430; ax[1]=155;ay[1]=SH+28; ax[2]=230;ay[2]=SH; ax[3]=305;ay[3]=SH+28; ax[4]=350;ay[4]=430; ax[5]=350;ay[5]=750
854 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
855 ax[0]=350;ay[0]=430; ax[1]=395;ay[1]=SH+28; ax[2]=470;ay[2]=SH; ax[3]=545;ay[3]=SH+28; ax[4]=590;ay[4]=430; ax[5]=590;ay[5]=750
856 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
857 return 0
858 }
859 if ch == 119 { // w -- zigzag (apex ink kisses 762)
860 ax[0]=85;ay[0]=250; ax[1]=185;ay[1]=XB; ax[2]=300;ay[2]=400; ax[3]=415;ay[3]=XB; ax[4]=515;ay[4]=250
861 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
862 return 0
863 }
864 if ch == 112 { // p -- descender stem + bowl (bowl bottom kisses baseline)
865 ax[0]=130;ay[0]=250; ax[1]=130;ay[1]=900
866 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
867 ax[0]=130;ay[0]=344; ax[1]=215;ay[1]=SH+2; ax[2]=320;ay[2]=SH+22; ax[3]=384;ay[3]=430; ax[4]=386;ay[4]=550; ax[5]=340;ay[5]=652; ax[6]=228;ay[6]=XB; ax[7]=130;ay[7]=644
868 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,8, W, ox,oy,S)
869 return 0
870 }
871 if ch == 98 { // b -- left ascender stem + right bowl
872 ax[0]=130;ay[0]=110; ax[1]=130;ay[1]=750
873 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
874 ax[0]=130;ay[0]=344; ax[1]=215;ay[1]=SH+2; ax[2]=320;ay[2]=SH+22; ax[3]=384;ay[3]=430; ax[4]=386;ay[4]=550; ax[5]=340;ay[5]=652; ax[6]=228;ay[6]=XB; ax[7]=130;ay[7]=644
875 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,8, W, ox,oy,S)
876 return 0
877 }
878 if ch == 102 { // f -- hooked ascender (hook ink kisses ~108) + bar ON x-height
879 ax[0]=362;ay[0]=240; ax[1]=290;ay[1]=100+V2H; ax[2]=214;ay[2]=122+V2H; ax[3]=190;ay[3]=300; ax[4]=190;ay[4]=750
880 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
881 ax[0]=85;ay[0]=SH; ax[1]=330;ay[1]=SH
882 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
883 return 0
884 }
885 if ch == 103 { // g -- bowl (kisses baseline) + descender loop (kisses 912)
886 ax[0]=355;ay[0]=352; ax[1]=270;ay[1]=XT+4; ax[2]=170;ay[2]=XT; ax[3]=110;ay[3]=XT+34; ax[4]=88;ay[4]=495; ax[5]=112;ay[5]=XB-36; ax[6]=190;ay[6]=XB; ax[7]=282;ay[7]=XB-8; ax[8]=355;ay[8]=628
887 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,9, W, ox,oy,S)
888 ax[0]=355;ay[0]=270; ax[1]=355;ay[1]=778; ax[2]=316;ay[2]=DB; ax[3]=212;ay[3]=DB+8; ax[4]=126;ay[4]=DB-32
889 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
890 return 0
891 }
892 if ch == 106 { // j -- descender hook (kisses 912) + dot
893 ax[0]=250;ay[0]=250; ax[1]=250;ay[1]=780; ax[2]=214;ay[2]=DB; ax[3]=134;ay[3]=DB+6; ax[4]=76;ay[4]=DB-38
894 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
895 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((250*WSC)/100 + ((750-160)*SL)/100)*S)/1000, oy+(160*S)/1000, (DR*S)/1000, 0)
896 return 0
897 }
898 if ch == 107 { // k -- ascender stem + two diagonals
899 ax[0]=130;ay[0]=110; ax[1]=130;ay[1]=750
900 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
901 ax[0]=360;ay[0]=300; ax[1]=130;ay[1]=530
902 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
903 ax[0]=210;ay[0]=455; ax[1]=375;ay[1]=750
904 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
905 return 0
906 }
907 if ch == 113 { // q -- bowl + right descender stem
908 ax[0]=350;ay[0]=352; ax[1]=270;ay[1]=XT+4; ax[2]=170;ay[2]=XT; ax[3]=110;ay[3]=XT+34; ax[4]=85;ay[4]=500; ax[5]=110;ay[5]=XB-34; ax[6]=185;ay[6]=XB; ax[7]=290;ay[7]=XB+6; ax[8]=350;ay[8]=648
909 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,9, W, ox,oy,S)
910 ax[0]=350;ay[0]=280; ax[1]=350;ay[1]=900
911 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
912 return 0
913 }
914 if ch == 118 { // v -- two diagonals (apex ink kisses 762)
915 ax[0]=90;ay[0]=250; ax[1]=250;ay[1]=XB; ax[2]=410;ay[2]=250
916 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
917 return 0
918 }
919 if ch == 120 { // x -- crossing diagonals
920 ax[0]=100;ay[0]=250; ax[1]=400;ay[1]=750
921 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
922 ax[0]=400;ay[0]=250; ax[1]=100;ay[1]=750
923 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
924 return 0
925 }
926 if ch == 121 { // y -- v + descender tail
927 ax[0]=90;ay[0]=250; ax[1]=250;ay[1]=670
928 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
929 ax[0]=410;ay[0]=250; ax[1]=250;ay[1]=670; ax[2]=196;ay[2]=828; ax[3]=122;ay[3]=872
930 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
931 return 0
932 }
933 if ch == 122 { // z -- bars ON the metric lines + diagonal
934 ax[0]=95;ay[0]=SH; ax[1]=390;ay[1]=SH; ax[2]=105;ay[2]=BB; ax[3]=400;ay[3]=BB
935 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
936 return 0
937 }
938 // ---- UPPERCASE A-Z (cap band: top 130 .. baseline 750) ----
939 if ch == 65 { // A -- apex ink at cap height
940 ax[0]=100;ay[0]=750; ax[1]=300;ay[1]=CB; ax[2]=500;ay[2]=750
941 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
942 ax[0]=185;ay[0]=520; ax[1]=415;ay[1]=520
943 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
944 return 0
945 }
946 if ch == 66 { // B -- bars on the cap metric lines
947 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=750
948 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
949 ax[0]=130;ay[0]=CB; ax[1]=320;ay[1]=CB; ax[2]=405;ay[2]=280; ax[3]=320;ay[3]=430; ax[4]=130;ay[4]=434
950 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
951 ax[0]=130;ay[0]=434; ax[1]=340;ay[1]=438; ax[2]=440;ay[2]=560; ax[3]=340;ay[3]=BB; ax[4]=130;ay[4]=BB
952 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
953 return 0
954 }
955 if ch == 67 { // C -- open arc on the cap lines
956 ax[0]=475;ay[0]=262; ax[1]=380;ay[1]=196; ax[2]=245;ay[2]=CT; ax[3]=140;ay[3]=262; ax[4]=115;ay[4]=440
957 ax[5]=140;ay[5]=618; ax[6]=245;ay[6]=XB; ax[7]=380;ay[7]=684; ax[8]=475;ay[8]=618
958 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,9, W, ox,oy,S)
959 return 0
960 }
961 if ch == 68 { // D
962 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=750
963 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
964 ax[0]=130;ay[0]=CB; ax[1]=300;ay[1]=CB; ax[2]=430;ay[2]=300; ax[3]=465;ay[3]=440; ax[4]=430;ay[4]=580; ax[5]=300;ay[5]=BB; ax[6]=130;ay[6]=BB
965 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
966 return 0
967 }
968 if ch == 69 { // E
969 ax[0]=450;ay[0]=CB; ax[1]=130;ay[1]=CB; ax[2]=130;ay[2]=BB; ax[3]=460;ay[3]=BB
970 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
971 ax[0]=130;ay[0]=436; ax[1]=400;ay[1]=436
972 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
973 return 0
974 }
975 if ch == 70 { // F
976 ax[0]=450;ay[0]=CB; ax[1]=130;ay[1]=CB; ax[2]=130;ay[2]=750
977 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
978 ax[0]=130;ay[0]=436; ax[1]=390;ay[1]=436
979 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
980 return 0
981 }
982 if ch == 71 { // G -- C + inner bar
983 ax[0]=475;ay[0]=262; ax[1]=380;ay[1]=196; ax[2]=245;ay[2]=CT; ax[3]=140;ay[3]=262; ax[4]=115;ay[4]=440
984 ax[5]=140;ay[5]=618; ax[6]=245;ay[6]=XB; ax[7]=390;ay[7]=684; ax[8]=480;ay[8]=608; ax[9]=480;ay[9]=486
985 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,10, W, ox,oy,S)
986 ax[0]=480;ay[0]=486; ax[1]=340;ay[1]=486
987 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
988 return 0
989 }
990 if ch == 72 { // H
991 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=750
992 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
993 ax[0]=470;ay[0]=130; ax[1]=470;ay[1]=750
994 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
995 ax[0]=130;ay[0]=436; ax[1]=470;ay[1]=436
996 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
997 return 0
998 }
999 if ch == 73 { // I -- stem + top/bottom serifs (vs l/1)
1000 ax[0]=190;ay[0]=CB; ax[1]=390;ay[1]=CB
1001 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1002 ax[0]=290;ay[0]=CB; ax[1]=290;ay[1]=BB
1003 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1004 ax[0]=190;ay[0]=BB; ax[1]=390;ay[1]=BB
1005 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1006 return 0
1007 }
1008 if ch == 74 { // J
1009 ax[0]=430;ay[0]=130; ax[1]=430;ay[1]=590; ax[2]=390;ay[2]=XB-24; ax[3]=280;ay[3]=XB; ax[4]=172;ay[4]=XB-24; ax[5]=135;ay[5]=600
1010 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
1011 return 0
1012 }
1013 if ch == 75 { // K
1014 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=750
1015 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1016 ax[0]=450;ay[0]=130; ax[1]=135;ay[1]=470
1017 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1018 ax[0]=250;ay[0]=395; ax[1]=470;ay[1]=750
1019 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1020 return 0
1021 }
1022 if ch == 76 { // L
1023 ax[0]=140;ay[0]=130; ax[1]=140;ay[1]=BB; ax[2]=450;ay[2]=BB
1024 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1025 return 0
1026 }
1027 if ch == 77 { // M -- corner joins ink at cap height
1028 ax[0]=110;ay[0]=750; ax[1]=112;ay[1]=CB; ax[2]=310;ay[2]=540; ax[3]=508;ay[3]=CB; ax[4]=510;ay[4]=750
1029 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1030 return 0
1031 }
1032 if ch == 78 { // N -- corner joins on the cap lines
1033 ax[0]=130;ay[0]=750; ax[1]=130;ay[1]=CB; ax[2]=470;ay[2]=BB; ax[3]=470;ay[3]=130
1034 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
1035 return 0
1036 }
1037 if ch == 79 { // O -- closed ring (ry from the parametric lines)
1038 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 300,440, 190,(XB-CT)/2, W, ox,oy,S)
1039 return 0
1040 }
1041 if ch == 80 { // P
1042 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=750
1043 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1044 ax[0]=130;ay[0]=CB; ax[1]=330;ay[1]=CB; ax[2]=430;ay[2]=300; ax[3]=330;ay[3]=474; ax[4]=130;ay[4]=474
1045 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1046 return 0
1047 }
1048 if ch == 81 { // Q -- O + tail
1049 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 300,440, 190,(XB-CT)/2, W, ox,oy,S)
1050 ax[0]=370;ay[0]=610; ax[1]=505;ay[1]=780
1051 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1052 return 0
1053 }
1054 if ch == 82 { // R -- P + leg
1055 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=750
1056 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1057 ax[0]=130;ay[0]=CB; ax[1]=330;ay[1]=CB; ax[2]=430;ay[2]=300; ax[3]=330;ay[3]=474; ax[4]=130;ay[4]=474
1058 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1059 ax[0]=280;ay[0]=474; ax[1]=470;ay[1]=750
1060 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1061 return 0
1062 }
1063 if ch == 83 { // S -- cap double curve on the lines
1064 ax[0]=455;ay[0]=266; ax[1]=350;ay[1]=CT+12; ax[2]=205;ay[2]=CT+14; ax[3]=125;ay[3]=286; ax[4]=165;ay[4]=400
1065 ax[5]=300;ay[5]=448; ax[6]=430;ay[6]=506; ax[7]=468;ay[7]=612; ax[8]=382;ay[8]=XB-4; ax[9]=222;ay[9]=XB; ax[10]=112;ay[10]=614
1066 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,11, W, ox,oy,S)
1067 return 0
1068 }
1069 if ch == 84 { // T
1070 ax[0]=100;ay[0]=CB; ax[1]=500;ay[1]=CB
1071 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1072 ax[0]=300;ay[0]=CB; ax[1]=300;ay[1]=750
1073 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1074 return 0
1075 }
1076 if ch == 85 { // U
1077 ax[0]=130;ay[0]=130; ax[1]=130;ay[1]=560; ax[2]=180;ay[2]=XB-34; ax[3]=300;ay[3]=XB; ax[4]=420;ay[4]=XB-34; ax[5]=470;ay[5]=560; ax[6]=470;ay[6]=130
1078 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
1079 return 0
1080 }
1081 if ch == 86 { // V -- apex ink kisses 762
1082 ax[0]=105;ay[0]=130; ax[1]=300;ay[1]=XB; ax[2]=495;ay[2]=130
1083 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1084 return 0
1085 }
1086 if ch == 87 { // W
1087 ax[0]=85;ay[0]=130; ax[1]=185;ay[1]=XB; ax[2]=305;ay[2]=360; ax[3]=425;ay[3]=XB; ax[4]=525;ay[4]=130
1088 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1089 return 0
1090 }
1091 if ch == 88 { // X
1092 ax[0]=115;ay[0]=130; ax[1]=485;ay[1]=750
1093 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1094 ax[0]=485;ay[0]=130; ax[1]=115;ay[1]=750
1095 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1096 return 0
1097 }
1098 if ch == 89 { // Y
1099 ax[0]=110;ay[0]=130; ax[1]=300;ay[1]=440
1100 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1101 ax[0]=490;ay[0]=130; ax[1]=300;ay[1]=440; ax[2]=300;ay[2]=750
1102 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1103 return 0
1104 }
1105 if ch == 90 { // Z
1106 ax[0]=120;ay[0]=CB; ax[1]=470;ay[1]=CB; ax[2]=125;ay[2]=BB; ax[3]=480;ay[3]=BB
1107 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
1108 return 0
1109 }
1110 // ---- DIGITS 0-9 (cap band, on the parametric lines) ----
1111 if ch == 48 { // 0
1112 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 300,440, 175,(XB-CT)/2, W, ox,oy,S)
1113 return 0
1114 }
1115 if ch == 49 { // 1
1116 ax[0]=200;ay[0]=268; ax[1]=310;ay[1]=CB; ax[2]=310;ay[2]=BB
1117 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1118 ax[0]=190;ay[0]=BB; ax[1]=430;ay[1]=BB
1119 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1120 return 0
1121 }
1122 if ch == 50 { // 2
1123 ax[0]=145;ay[0]=272; ax[1]=225;ay[1]=CB+6; ax[2]=370;ay[2]=CB; ax[3]=450;ay[3]=292; ax[4]=425;ay[4]=420; ax[5]=140;ay[5]=BB; ax[6]=470;ay[6]=BB
1124 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
1125 return 0
1126 }
1127 if ch == 51 { // 3
1128 ax[0]=150;ay[0]=246; ax[1]=250;ay[1]=CT+12; ax[2]=380;ay[2]=CT+20; ax[3]=430;ay[3]=292; ax[4]=380;ay[4]=418; ax[5]=285;ay[5]=446
1129 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
1130 ax[0]=285;ay[0]=446; ax[1]=400;ay[1]=482; ax[2]=455;ay[2]=588; ax[3]=392;ay[3]=XB-26; ax[4]=242;ay[4]=XB; ax[5]=140;ay[5]=624
1131 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,6, W, ox,oy,S)
1132 return 0
1133 }
1134 if ch == 52 { // 4
1135 ax[0]=400;ay[0]=130; ax[1]=140;ay[1]=515; ax[2]=475;ay[2]=515
1136 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1137 ax[0]=400;ay[0]=130; ax[1]=400;ay[1]=750
1138 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1139 return 0
1140 }
1141 if ch == 53 { // 5
1142 ax[0]=440;ay[0]=CB; ax[1]=165;ay[1]=CB; ax[2]=155;ay[2]=405
1143 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1144 ax[0]=155;ay[0]=405; ax[1]=300;ay[1]=368; ax[2]=425;ay[2]=428; ax[3]=462;ay[3]=556; ax[4]=392;ay[4]=XB-26; ax[5]=235;ay[5]=XB; ax[6]=130;ay[6]=618
1145 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
1146 return 0
1147 }
1148 if ch == 54 { // 6
1149 ax[0]=420;ay[0]=222; ax[1]=310;ay[1]=CT+6; ax[2]=200;ay[2]=246; ax[3]=145;ay[3]=380; ax[4]=135;ay[4]=540; ax[5]=170;ay[5]=636; ax[6]=240;ay[6]=678
1150 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
1151 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 305,565, 158,(XB-434)/2, W, ox,oy,S)
1152 return 0
1153 }
1154 if ch == 55 { // 7
1155 ax[0]=130;ay[0]=CB; ax[1]=470;ay[1]=CB; ax[2]=260;ay[2]=750
1156 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,3, W, ox,oy,S)
1157 return 0
1158 }
1159 if ch == 56 { // 8 -- two rings on the lines
1160 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 300,304, 148,304-CT, W, ox,oy,S)
1161 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 300,580, 170,XB-580, W, ox,oy,S)
1162 return 0
1163 }
1164 if ch == 57 { // 9 -- mirrored 6
1165 ax[0]=180;ay[0]=658; ax[1]=290;ay[1]=XB-4; ax[2]=400;ay[2]=636; ax[3]=452;ay[3]=505; ax[4]=462;ay[4]=345; ax[5]=430;ay[5]=264; ax[6]=360;ay[6]=222
1166 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
1167 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 295,308, 158,308-CT, W, ox,oy,S)
1168 return 0
1169 }
1170 // ---- HEADING PUNCTUATION ----
1171 if ch == 46 { // .
1172 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-695)*SL)/100)*S)/1000, oy+(695*S)/1000, (PR*S)/1000, 0)
1173 return 0
1174 }
1175 if ch == 44 { // ,
1176 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-695)*SL)/100)*S)/1000, oy+(695*S)/1000, (PR*S)/1000, 0)
1177 ax[0]=210;ay[0]=740; ax[1]=155;ay[1]=855
1178 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, (80*W)/123, ox,oy,S)
1179 return 0
1180 }
1181 if ch == 45 { // -
1182 ax[0]=130;ay[0]=440; ax[1]=400;ay[1]=440
1183 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1184 return 0
1185 }
1186 if ch == 58 { // :
1187 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-360)*SL)/100)*S)/1000, oy+(360*S)/1000, (PR*S)/1000, 0)
1188 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-695)*SL)/100)*S)/1000, oy+(695*S)/1000, (PR*S)/1000, 0)
1189 return 0
1190 }
1191 if ch == 33 { // !
1192 ax[0]=200;ay[0]=135; ax[1]=200;ay[1]=545
1193 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1194 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-700)*SL)/100)*S)/1000, oy+(700*S)/1000, (PR*S)/1000, 0)
1195 return 0
1196 }
1197 if ch == 63 { // ?
1198 ax[0]=140;ay[0]=268; ax[1]=225;ay[1]=CB+6; ax[2]=365;ay[2]=CB; ax[3]=435;ay[3]=296; ax[4]=400;ay[4]=410; ax[5]=290;ay[5]=466; ax[6]=290;ay[6]=560
1199 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,7, W, ox,oy,S)
1200 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((290*WSC)/100 + ((750-700)*SL)/100)*S)/1000, oy+(700*S)/1000, (PR*S)/1000, 0)
1201 return 0
1202 }
1203 if ch == 40 { // (
1204 ax[0]=330;ay[0]=110; ax[1]=245;ay[1]=270; ax[2]=215;ay[2]=440; ax[3]=245;ay[3]=610; ax[4]=330;ay[4]=780
1205 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1206 return 0
1207 }
1208 if ch == 41 { // )
1209 ax[0]=170;ay[0]=110; ax[1]=255;ay[1]=270; ax[2]=285;ay[2]=440; ax[3]=255;ay[3]=610; ax[4]=170;ay[4]=780
1210 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,5, W, ox,oy,S)
1211 return 0
1212 }
1213 if ch == 39 { // '
1214 ax[0]=200;ay[0]=130; ax[1]=190;ay[1]=270
1215 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, (85*W)/123, ox,oy,S)
1216 return 0
1217 }
1218 if ch == 47 { // /
1219 ax[0]=400;ay[0]=110; ax[1]=160;ay[1]=790
1220 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1221 return 0
1222 }
1223 if ch == 91 { // [ -- bracket
1224 ax[0]=320;ay[0]=140; ax[1]=200;ay[1]=140; ax[2]=200;ay[2]=760; ax[3]=320;ay[3]=760
1225 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
1226 return 0
1227 }
1228 if ch == 93 { // ]
1229 ax[0]=180;ay[0]=140; ax[1]=300;ay[1]=140; ax[2]=300;ay[2]=760; ax[3]=180;ay[3]=760
1230 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,4, W, ox,oy,S)
1231 return 0
1232 }
1233 if ch == 37 { // % -- diagonal + two small rings
1234 ax[0]=500;ay[0]=150; ax[1]=160;ay[1]=750
1235 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1236 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 250,270, 100,120, (80*W)/123, ox,oy,S)
1237 fe_ring(xs,ys,cstart,clen,np,nc,cs,sn, 470,630, 100,120, (80*W)/123, ox,oy,S)
1238 return 0
1239 }
1240 if ch == 59 { // ; -- top dot + comma tail
1241 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-360)*SL)/100)*S)/1000, oy+(360*S)/1000, (PR*S)/1000, 0)
1242 gs_disc(xs,ys,cstart,clen,np,nc,cs,sn, ox+(((200*WSC)/100 + ((750-695)*SL)/100)*S)/1000, oy+(695*S)/1000, (PR*S)/1000, 0)
1243 ax[0]=210;ay[0]=740; ax[1]=155;ay[1]=855
1244 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, (80*W)/123, ox,oy,S)
1245 return 0
1246 }
1247 if ch == 34 { // " -- double quote
1248 ax[0]=180;ay[0]=140; ax[1]=170;ay[1]=290
1249 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, (85*W)/123, ox,oy,S)
1250 ax[0]=320;ay[0]=140; ax[1]=310;ay[1]=290
1251 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, (85*W)/123, ox,oy,S)
1252 return 0
1253 }
1254 if ch == 61 { // = -- two bars
1255 ax[0]=140;ay[0]=390; ax[1]=460;ay[1]=390
1256 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1257 ax[0]=140;ay[0]=530; ax[1]=460;ay[1]=530
1258 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1259 return 0
1260 }
1261 if ch == 43 { // + -- cross
1262 ax[0]=300;ay[0]=300; ax[1]=300;ay[1]=600
1263 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1264 ax[0]=150;ay[0]=450; ax[1]=450;ay[1]=450
1265 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, ax,ay,2, W, ox,oy,S)
1266 return 0
1267 }
1268 return 0 // space / unknown -> just advance
1269}
1270
1271// closed 16-point ellipse ring stroked as a polyline (O, 0, Q, 8-bowls). cx,cy center; rx,ry radii (em).
1272// 16 points (matches the 16-gon join discs -> one consistent trig table -> smooth, no wobble).
1273func fe_ring(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64,
1274 cx: i64, cy: i64, rx: i64, ry: i64, lw: i64, ox: i64, oy: i64, S: i64) -> i64 {
1275 let rx2: *i64 = sys_mmap(8*20) as *i64
1276 let ry2: *i64 = sys_mmap(8*20) as *i64
1277 var k: i64 = 0
1278 while k < 16 {
1279 rx2[k] = cx + (rx * cs[k]) / 1000
1280 ry2[k] = cy + (ry * sn[k]) / 1000
1281 k = k + 1
1282 }
1283 rx2[16] = rx2[0]
1284 ry2[16] = ry2[0]
1285 fe_stroke(xs,ys,cstart,clen,np,nc,cs,sn, rx2,ry2,17, lw, ox,oy,S)
1286 return 0
1287}
1288
1289// which chars have a vector glyph (headings switch to the vector font only when a whole run qualifies)
1290func font_has(ch: i64) -> i64 {
1291 if ch == 32 { return 1 }
1292 if ch >= 97 { if ch <= 122 { return 1 } } // a-z
1293 if ch >= 65 { if ch <= 90 { return 1 } } // A-Z
1294 if ch >= 48 { if ch <= 57 { return 1 } } // 0-9
1295 if ch == 46 { return 1 }
1296 if ch == 44 { return 1 }
1297 if ch == 45 { return 1 }
1298 if ch == 58 { return 1 }
1299 if ch == 33 { return 1 }
1300 if ch == 63 { return 1 }
1301 if ch == 40 { return 1 }
1302 if ch == 41 { return 1 }
1303 if ch == 39 { return 1 }
1304 if ch == 47 { return 1 }
1305 return 0
1306}
1307func font_run_has(text: *u8, len: i64) -> i64 {
1308 var i: i64 = 0
1309 while i < len { if font_has(text[i] & 0xff) == 0 { return 0 } i = i + 1 }
1310 return 1
1311}
1312// EXTENDED coverage (the glyphs added for the browser: % [ ] ; " = +). Used ONLY by the GUI/atlas fast
1313// path so the EXACT gate path (font_run_has above) is byte-unchanged -> no NishiOS/OCR/polish regression.
1314func font_has_ext(ch: i64) -> i64 {
1315 if font_has(ch) == 1 { return 1 }
1316 if ch == 37 { return 1 }
1317 if ch == 91 { return 1 }
1318 if ch == 93 { return 1 }
1319 if ch == 59 { return 1 }
1320 if ch == 34 { return 1 }
1321 if ch == 61 { return 1 }
1322 if ch == 43 { return 1 }
1323 return 0
1324}
1325func font_run_has_ext(text: *u8, len: i64) -> i64 {
1326 var i: i64 = 0
1327 while i < len { if font_has_ext(text[i] & 0xff) == 0 { return 0 } i = i + 1 }
1328 return 1
1329}
1330// fill the 16-step trig table (22.5deg steps) that BOTH gs_disc (round joins/caps on every glyph) AND
1331// fe_ring use -- one consistent table. ★BUG FIXED: the old 12-entry init left cs[12..15] uninitialized,
1332// so the 16-gon join discs read garbage -> malformed joins = the heading stroke WOBBLE. Now 16 clean.
1333func font_trig_init(cs: *i64, sn: *i64) -> i64 {
1334 cs[0]=1000; sn[0]=0
1335 cs[1]=924; sn[1]=383
1336 cs[2]=707; sn[2]=707
1337 cs[3]=383; sn[3]=924
1338 cs[4]=0; sn[4]=1000
1339 cs[5]=0-383; sn[5]=924
1340 cs[6]=0-707; sn[6]=707
1341 cs[7]=0-924; sn[7]=383
1342 cs[8]=0-1000;sn[8]=0
1343 cs[9]=0-924; sn[9]=0-383
1344 cs[10]=0-707;sn[10]=0-707
1345 cs[11]=0-383;sn[11]=0-924
1346 cs[12]=0; sn[12]=0-1000
1347 cs[13]=383; sn[13]=0-924
1348 cs[14]=707; sn[14]=0-707
1349 cs[15]=924; sn[15]=0-383
1350 return 0
1351}
1352// ---- VECTOR METRICS for the measured layout (heading runs, scale sc): em size 15*sc px ----
1353func font_vec_adv_px(ch: i64, sc: i64) -> i64 { return (font_adv_em(ch) * 15 * sc) / 1000 }
1354func font_vec_text_w(text: *u8, start: i64, end: i64, sc: i64) -> i64 {
1355 var w: i64 = 0
1356 var i: i64 = start
1357 while i < end { w = w + font_vec_adv_px(text[i] & 0xff, sc); i = i + 1 }
1358 return w
1359}
1360func font_vec_next_break(text: *u8, len: i64, avail_px: i64, start: i64, sc: i64) -> i64 {
1361 if start >= len { return len }
1362 var w: i64 = 0
1363 var i: i64 = start
1364 var brk: i64 = 0 - 1
1365 while i < len {
1366 w = w + font_vec_adv_px(text[i] & 0xff, sc)
1367 if w > avail_px {
1368 if brk > start { return brk + 1 }
1369 if i > start { return i }
1370 return start + 1
1371 }
1372 if (text[i] & 0xff) == 32 { brk = i }
1373 i = i + 1
1374 }
1375 return len
1376}
1377
1378// lay out + emit a whole string at (ox,oy) grid, scale S. Returns the pen x after the string.
1379func font_text(text: *u8, len: i64, xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64,
1380 cs: *i64, sn: *i64, ox: i64, oy: i64, S: i64) -> i64 {
1381 var pen: i64 = ox
1382 var i: i64 = 0
1383 while i < len {
1384 let ch: i64 = text[i] & 0xff
1385 // optical spacing: shift each glyph so its measured optical left edge sits at the target sidebearing
1386 let sh: i64 = (((((font_sp_shift_disp(ch) * font_ws()) / 100) * font_wdth()) / 100) * S) / 1000
1387 font_emit(ch, xs, ys, cstart, clen, np, nc, cs, sn, pen + sh, oy, S)
1388 pen = pen + font_adv(ch, S)
1389 i = i + 1
1390 }
1391 return pen
1392}