code wiki / (root) / nx_glyph_stroke.nx

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}