nx_glyph_stroke.nx source
↩ module page · 288 lines · 13175 B
1// nx_glyph_stroke.nx -- RUNG 1 of the sovereign FONT BUILDER: the STROKE PEN. A glyph stroke is a centerline
2// polyline + a width; this renders it as a filled thick outline (perpendicular-offset segment quads + round
3// joins/caps as discs), all wound the SAME way so the AA rasterizer's nonzero fill UNIONS them into one smooth
4// shape. Curves are just a curved centerline -> the hard letters (s, arches) become easy, and it's the natural
5// unit for CJK strokes/radicals (the path to kanji). No floats: integer isqrt for the perpendicular. ORIGINAL.
6import "nx_syscalls.nx"
7const K_MAGIC_250000: i64 = 250000
8
9// integer square root (Newton).
10func gs_isqrt(n: i64) -> i64 {
11 if n <= 0 { return 0 }
12 var x: i64 = n
13 var y: i64 = (x + 1) / 2
14 while y < x { x = y; y = (x + n / x) / 2 }
15 return x
16}
17
18// emit one thick segment quad p0->p1 of width w, wound CCW (consistent with gs_disc) so nonzero UNIONS.
19func gs_seg(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, x0: i64, y0: i64, x1: i64, y1: i64, w: i64) -> i64 {
20 let dx: i64 = x1 - x0
21 let dy: i64 = y1 - y0
22 let len: i64 = gs_isqrt(dx*dx + dy*dy)
23 if len == 0 { return 0 }
24 let hw: i64 = w / 2
25 let nx: i64 = ((0 - dy) * hw) / len // left-normal * half-width
26 let ny: i64 = (dx * hw) / len
27 let p: i64 = np[0]
28 xs[p+0]=x0+nx; ys[p+0]=y0+ny
29 xs[p+1]=x0-nx; ys[p+1]=y0-ny
30 xs[p+2]=x1-nx; ys[p+2]=y1-ny
31 xs[p+3]=x1+nx; ys[p+3]=y1+ny
32 cstart[nc[0]]=p; clen[nc[0]]=4
33 np[0]=p+4; nc[0]=nc[0]+1
34 return 0
35}
36
37// emit a 16-gon disc (round join/cap) of radius r at (cx,cy), wound CCW. rev=1 -> reverse (a hole).
38func gs_disc(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64, cx: i64, cy: i64, r: i64, rev: i64) -> i64 {
39 let p: i64 = np[0]
40 var k: i64 = 0
41 while k < 16 {
42 var kk: i64 = k
43 if rev == 1 { kk = 15 - k }
44 xs[p+k] = cx + (r * cs[kk]) / 1000
45 ys[p+k] = cy + (r * sn[kk]) / 1000
46 k = k + 1
47 }
48 cstart[nc[0]]=p; clen[nc[0]]=16
49 np[0]=p+16; nc[0]=nc[0]+1
50 return 0
51}
52
53// stroke a polyline (n points) at width w: segment quads + a disc at every INTERIOR vertex (round joins).
54// ★TERMINALS: OPEN strokes get FLAT (butt) caps -- no end disc, the perpendicular segment quad ends the
55// stroke square. That is the professional grotesque/geometric look (Helvetica/Arial), vs round blobs that
56// read as a casual marker font. CLOSED loops (o/e/O/0 bowls, first==last) keep all discs (every point is a
57// join, not a terminal) so the ring stays continuous.
58func gs_stroke(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64, ptsx: *i64, ptsy: *i64, n: i64, w: i64) -> i64 {
59 var closed: i64 = 0
60 if ptsx[0] == ptsx[n-1] { if ptsy[0] == ptsy[n-1] { closed = 1 } }
61 var i: i64 = 0
62 while i < n - 1 {
63 gs_seg(xs, ys, cstart, clen, np, nc, ptsx[i], ptsy[i], ptsx[i+1], ptsy[i+1], w)
64 i = i + 1
65 }
66 i = 0
67 while i < n {
68 var put: i64 = 1
69 if closed == 0 { // open stroke: butt caps -> skip the two endpoints
70 if i == 0 { put = 0 }
71 if i == n-1 { put = 0 }
72 }
73 if put == 1 { gs_disc(xs, ys, cstart, clen, np, nc, cs, sn, ptsx[i], ptsy[i], w / 2, 0) }
74 i = i + 1
75 }
76 return 0
77}
78
79// ★ROLE-AWARE MODULATED stroke (the Metafont nib): per-segment width from stroke DIRECTION -- vertical
80// segments get wv (or wsw when tagged as interior CURVE walls: grotesques swell round walls over stems to
81// compensate the optical thinning of curves), horizontal get wh, linear blend by |dy|/len between. Join
82// discs take the LOCAL max of adjacent segment widths so joints stay covered. Open strokes keep butt ends;
83// closed loops disc every vertex. tags==0 -> no swelling. gs_stroke (uniform) stays for existing callers.
84func gs_stroke_mod2(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64, cs: *i64, sn: *i64,
85 ptsx: *i64, ptsy: *i64, n: i64, wv: i64, wh: i64, wsw: i64, tags: *i64) -> i64 {
86 if n < 2 { return 0 }
87 let segw: *i64 = sys_mmap(8*(n+2)) as *i64
88 var i: i64 = 0
89 while i < n - 1 {
90 let dx: i64 = ptsx[i+1]-ptsx[i]
91 var ady: i64 = ptsy[i+1]-ptsy[i]
92 if ady < 0 { ady = 0 - ady }
93 let len: i64 = gs_isqrt(dx*dx + ady*ady)
94 var wtop: i64 = wv
95 if (tags as i64) != 0 { if tags[i] == 1 { wtop = wsw } }
96 var w: i64 = wtop
97 if len > 0 { w = wh + ((wtop - wh) * ady) / len }
98 segw[i] = w
99 gs_seg(xs, ys, cstart, clen, np, nc, ptsx[i], ptsy[i], ptsx[i+1], ptsy[i+1], w)
100 i = i + 1
101 }
102 var closed: i64 = 0
103 if ptsx[0] == ptsx[n-1] { if ptsy[0] == ptsy[n-1] { closed = 1 } }
104 i = 0
105 while i < n {
106 var put: i64 = 1
107 if closed == 0 { if i == 0 { put = 0 } ; if i == n-1 { put = 0 } } // butt ends on open strokes
108 if put == 1 {
109 var wp: i64 = 0
110 var wn: i64 = 0
111 if i > 0 { wp = segw[i-1] }
112 if i < n-1 { wn = segw[i] }
113 if closed == 1 { if i == 0 { wp = segw[n-2] } ; if i == n-1 { wn = segw[0] } }
114 var r: i64 = wp
115 if wn > r { r = wn }
116 gs_disc(xs, ys, cstart, clen, np, nc, cs, sn, ptsx[i], ptsy[i], r / 2, 0)
117 }
118 i = i + 1
119 }
120 return 0
121}
122
123// signed shoelace area x2 of a polygon (this convention: clockwise-on-screen/y-down = NEGATIVE -- the same
124// orientation gs_disc traces, so "solid" outlines must be NEGATIVE to add with discs under nonzero fill).
125func gs_area2(px: *i64, py: *i64, n: i64) -> i64 {
126 var a: i64 = 0
127 var i: i64 = 0
128 var j: i64 = n - 1
129 while i < n { a = a + (px[j] + px[i]) * (py[j] - py[i]); j = i; i = i + 1 }
130 return a
131}
132// emit polygon with the requested orientation: want_pos=1 -> positive shoelace area, else negative.
133func gs_emit_oriented(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64,
134 px: *i64, py: *i64, n: i64, want_pos: i64) -> i64 {
135 let a: i64 = gs_area2(px, py, n)
136 var fwd: i64 = 1
137 if want_pos == 1 { if a < 0 { fwd = 0 } }
138 if want_pos == 0 { if a > 0 { fwd = 0 } }
139 let p: i64 = np[0]
140 var k: i64 = 0
141 while k < n {
142 var src: i64 = k
143 if fwd == 0 { src = n-1-k }
144 xs[p+k]=px[src]; ys[p+k]=py[src]
145 k = k + 1
146 }
147 cstart[nc[0]]=p; clen[nc[0]]=n
148 np[0]=p+n; nc[0]=nc[0]+1
149 return 0
150}
151
152// ★VARIABLE-WIDTH OUTLINE stroke: the professional construction for a MODULATED pen. Instead of segment
153// quads + join discs (whose radius-mismatch BULGES wherever adjacent segments differ in width -- the ragged
154// edges the swelled nib exposed), emit ONE smooth outline: per-point width = mean of adjacent segment
155// widths, per-point MITER offset along the angle bisector (clamped 2.5x), left side forward + right side
156// back = a single closed polygon (butt ends fall out flat). Closed loops emit outer + reversed inner ring
157// (nonzero winding -> clean annulus). Width now interpolates CONTINUOUSLY along curves -- no lumps.
158func gs_stroke_var(xs: *i64, ys: *i64, cstart: *i64, clen: *i64, np: *i64, nc: *i64,
159 ptsx: *i64, ptsy: *i64, n: i64, segw: *i64, closed: i64) -> i64 {
160 if n < 2 { return 0 }
161 var m: i64 = n
162 if closed == 1 { m = n - 1 } // drop the duplicated closing point
163 if m < 2 { return 0 }
164 let lxo: *i64 = sys_mmap(8*(m+2)) as *i64
165 let lyo: *i64 = sys_mmap(8*(m+2)) as *i64
166 let rxo: *i64 = sys_mmap(8*(m+2)) as *i64
167 let ryo: *i64 = sys_mmap(8*(m+2)) as *i64
168 var i: i64 = 0
169 while i < m {
170 // adjacent segment indices (wrap when closed; clamp when open)
171 var sp: i64 = i - 1
172 var sn2: i64 = i
173 if closed == 1 { if sp < 0 { sp = m - 1 } ; if sn2 > m-1 { sn2 = 0 } }
174 else { if sp < 0 { sp = 0 } ; if sn2 > n-2 { sn2 = n-2 } }
175 // unit normals (x1000) of the two adjacent segments
176 var p0: i64 = i - 1
177 if p0 < 0 { if closed == 1 { p0 = m - 1 } else { p0 = 0 } }
178 var p2: i64 = i + 1
179 if p2 > m-1 { if closed == 1 { p2 = 0 } else { p2 = m-1 } }
180 var d1x: i64 = ptsx[i]-ptsx[p0]
181 var d1y: i64 = ptsy[i]-ptsy[p0]
182 var d2x: i64 = ptsx[p2]-ptsx[i]
183 var d2y: i64 = ptsy[p2]-ptsy[i]
184 if p0 == i { d1x = d2x; d1y = d2y } // open start: use the forward segment
185 if p2 == i { d2x = d1x; d2y = d1y } // open end: use the backward segment
186 let l1: i64 = gs_isqrt(d1x*d1x+d1y*d1y)
187 let l2: i64 = gs_isqrt(d2x*d2x+d2y*d2y)
188 var n1x: i64 = 0
189 var n1y: i64 = 0
190 var n2x: i64 = 0
191 var n2y: i64 = 0
192 if l1 > 0 { n1x = (0-d1y)*1000/l1; n1y = d1x*1000/l1 }
193 if l2 > 0 { n2x = (0-d2y)*1000/l2; n2y = d2x*1000/l2 }
194 var nmx: i64 = n1x + n2x
195 var nmy: i64 = n1y + n2y
196 var LL: i64 = nmx*nmx + nmy*nmy
197 // per-point width = mean of adjacent segment widths
198 let w: i64 = (segw[sp] + segw[sn2]) / 2
199 var vx: i64 = 0
200 var vy: i64 = 0
201 if LL >= K_MAGIC_250000 { // miter: v = nm * w * 1000 / |nm|^2 (exact w/2 when straight)
202 vx = (nmx * w * 1000) / LL
203 vy = (nmy * w * 1000) / LL
204 } else { // near-reversal: clamp 2x half-width along the bisector
205 let ln: i64 = gs_isqrt(LL)
206 if ln > 0 { vx = (nmx * w) / ln; vy = (nmy * w) / ln }
207 }
208 // ★ASYMMETRIC SHARP CORNERS (what real grotesques do): the INNER side keeps the EXACT miter (an
209 // under-length inner miter makes the sides cross = a bowtie that CANCELS the apex -- the truncated-V
210 // bug's second act), while the OUTER side is BEVELLED at 1.15x half-width (Helvetica flat-cuts its
211 // V apex) so pointed apexes don't spear far past the metric lines. Inner = the side toward the
212 // neighbours' chord midpoint (the concave side).
213 var vxL: i64 = vx
214 var vyL: i64 = vy
215 var vxR: i64 = 0 - vx
216 var vyR: i64 = 0 - vy
217 let lex: i64 = gs_isqrt(vx*vx + vy*vy)
218 let cl: i64 = (w * 115) / 200
219 if lex > cl {
220 let cmx: i64 = (ptsx[p0] + ptsx[p2]) / 2 - ptsx[i]
221 let cmy: i64 = (ptsy[p0] + ptsy[p2]) / 2 - ptsy[i]
222 let side: i64 = vx*cmx + vy*cmy // >0: +v points toward the chord = inner
223 if side > 0 { // L(+v) inner-exact; R(-v) outer-bevel
224 vxR = (vxR * cl) / lex
225 vyR = (vyR * cl) / lex
226 } else { // R(-v) inner-exact; L(+v) outer-bevel
227 vxL = (vxL * cl) / lex
228 vyL = (vyL * cl) / lex
229 }
230 }
231 lxo[i] = ptsx[i] + vxL
232 lyo[i] = ptsy[i] + vyL
233 rxo[i] = ptsx[i] + vxR
234 ryo[i] = ptsy[i] + vyR
235 i = i + 1
236 }
237 // ★WINDING NORMALIZATION: every solid outline must wind the SAME way as gs_disc (NEGATIVE shoelace in
238 // this y-down convention) or nonzero-fill CANCELS where strokes/discs overlap (bowl-stem junction
239 // notches; cap discs punching holes -- the eyeballed "candy-cane" bug). A closed ring's INNER contour
240 // is the one exception: it winds OPPOSITE (positive) = the counter.
241 if closed == 0 {
242 // one polygon: left side forward, right side backward (butt ends flat)
243 let tpx: *i64 = sys_mmap(8*(2*m+2)) as *i64
244 let tpy: *i64 = sys_mmap(8*(2*m+2)) as *i64
245 var k: i64 = 0
246 while k < m { tpx[k]=lxo[k]; tpy[k]=lyo[k]; k=k+1 }
247 k = 0
248 while k < m { tpx[m+k]=rxo[m-1-k]; tpy[m+k]=ryo[m-1-k]; k=k+1 }
249 gs_emit_oriented(xs, ys, cstart, clen, np, nc, tpx, tpy, 2*m, 0)
250 } else {
251 // annulus: OUTER ring disc-winding (negative), INNER ring positive (the counter/hole)
252 var aL: i64 = gs_area2(lxo, lyo, m)
253 var aR: i64 = gs_area2(rxo, ryo, m)
254 var absL: i64 = aL
255 if absL < 0 { absL = 0 - absL }
256 var absR: i64 = aR
257 if absR < 0 { absR = 0 - absR }
258 if absL >= absR {
259 gs_emit_oriented(xs, ys, cstart, clen, np, nc, lxo, lyo, m, 0)
260 gs_emit_oriented(xs, ys, cstart, clen, np, nc, rxo, ryo, m, 1)
261 } else {
262 gs_emit_oriented(xs, ys, cstart, clen, np, nc, rxo, ryo, m, 0)
263 gs_emit_oriented(xs, ys, cstart, clen, np, nc, lxo, lyo, m, 1)
264 }
265 }
266 return 0
267}
268
269// fill a 16-entry cos/sin table (x1000) into cs/sn (angles 0,22.5,...,337.5).
270func gs_init_trig(cs: *i64, sn: *i64) -> i64 {
271 cs[0]=1000; sn[0]=0
272 cs[1]=924; sn[1]=383
273 cs[2]=707; sn[2]=707
274 cs[3]=383; sn[3]=924
275 cs[4]=0; sn[4]=1000
276 cs[5]=0-383; sn[5]=924
277 cs[6]=0-707; sn[6]=707
278 cs[7]=0-924; sn[7]=383
279 cs[8]=0-1000;sn[8]=0
280 cs[9]=0-924; sn[9]=0-383
281 cs[10]=0-707;sn[10]=0-707
282 cs[11]=0-383;sn[11]=0-924
283 cs[12]=0; sn[12]=0-1000
284 cs[13]=383; sn[13]=0-924
285 cs[14]=707; sn[14]=0-707
286 cs[15]=924; sn[15]=0-383
287 return 0
288}