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