code wiki / (root) / nx_glyph_stroke.nx

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}