code wiki / (root) / nx_manga_bubble.nx

nx_manga_bubble.nx source

↩ module page · 350 lines · 15909 B

1// nx_manga_bubble.nx -- R2 of the mangagen lane (comparewatch- contract mangagen_mb_bubble_draw): 2// SPEECH BUBBLES onto a manga page. Composes the incumbents rather than re-implementing any of them: 3// nx_aa_raster aa_render -- the anti-aliased polygon fill (nonzero winding, CW fills) 4// nx_nishi_font_core nf_draw_text_mem / nf_adv_px -- OUR OWN stroke letterforms and their metrics 5// nx_img_to_rgb / nx_png_write -- the page in, the page out 6// THE DONE-RULE, pre-declared on /compare/mangagen before a line of this was written: 7// "Bubble geometry + tails + sovereign TTF raster; overflow REFUSES, never silently clips." 8// So text that cannot fit the bubble's inner box is a REFUSAL naming the measured overflow -- never a 9// silent crop, never a shrink-to-fit that quietly rewrites the author's lettering. 10// usage: nx_manga_bubble <in.png|blank:WxH> <out.png> <cx> <cy> <rx> <ry> <tipx> <tipy> <tdir 0-23> <ch> <text...> 11// license_tier: ORIGINAL expect_exit: 0 12import "nx_str.nx" 13import "nx_syscalls.nx" 14import "nx_aa_raster.nx" 15import "nx_png_write.nx" 16import "nx_img_to_rgb.nx" 17import "nx_nishi_font_core.nx" 18const MB_MAGIC_8192: i64 = 8192 19// ---- POLICY vs MATHEMATICS (rule 11) ------------------------------------------------------------- 20// The estate's rule-11 ruler (nx_magic) only sees literals at or above its threshold, so it reported ONE 21// site here. That is a FILTERED read: the numbers that actually decide how a balloon LOOKS are all small. 22// They are named below and split by kind, because the two kinds have different futures: 23// MATHEMATICS -- fixed by geometry, not by taste. Naming is all they need; they will never be tuned. 24// POLICY -- the lettering house style. Named here as the first step; the honest end state is a 25// style row beside the layout rows, and MB_STROKE already comes in as an argument. 26const MB_N: i64 = 24 27const MB_SS: i64 = 4 28// MATHEMATICS: the largest axis-aligned box inside an ellipse is 1/sqrt(2) of its diameter, x1000. 29const MB_INSCRIBED_PERMIL: i64 = 707 30// POLICY: house lettering style. 31const MB_STROKE_DEFAULT: i64 = 3 32const MB_LEAD_DIV: i64 = 4 33const MB_MARGIN_PX: i64 = 8 34const MB_INK_R: i64 = 16 35const MB_INK_G: i64 = 16 36const MB_INK_B: i64 = 20 37const MB_PAPER: i64 = 255 38const MB_TEXT_INK: i64 = 0 39const MB_MASK: i64 = 1048576 40const MB_PTS: i64 = 512 41const MB_LINES: i64 = 24 42func mb_puts(s: *u8) -> i64 { sys_write(1, s, nx_str_len(s)); return 0 } 43func mb_pi(v: i64) -> i64 { 44 let t: *u8 = sys_mmap(32) 45 let o: *u8 = sys_mmap(32) 46 var m: i64 = v 47 var k: i64 = 0 48 if m < 0 { m = 0 - m; sys_write(1, "-" as *u8, 1) } 49 if m == 0 { t[0] = 48; k = 1 } 50 while m > 0 { t[k] = 48 + (m % 10); m = m / 10; k = k + 1 } 51 var i: i64 = 0 52 while i < k { o[i] = t[k - 1 - i]; i = i + 1 } 53 sys_write(1, o, k) 54 return 0 55} 56// ⚠STOPS at the first non-digit. An atoi that merely SKIPS non-digits reads "900x600" as 900600 -- 57// caught by the gate's T1 on the very first run, because blank:WxH parses both halves with this. 58func mb_atoi(s: *u8) -> i64 { 59 var i: i64 = 0 60 var v: i64 = 0 61 var neg: i64 = 0 62 if s[0] == (45 as u8) { neg = 1; i = 1 } 63 var go: i64 = 1 64 while go == 1 { 65 let c: i64 = s[i] as i64 66 if c < 48 { go = 0 } else { 67 if c > 57 { go = 0 } else { v = v * 10 + (c - 48); i = i + 1 } 68 } 69 } 70 if neg == 1 { return 0 - v } 71 return v 72} 73// 24-gon tables x1000, angle k*15deg, y-down so positive sin is DOWN the page. 74func mb_tables(cs: *i64, sn: *i64) -> i64 { 75 cs[0]=1000; cs[1]=966; cs[2]=866; cs[3]=707; cs[4]=500; cs[5]=259 76 cs[6]=0; cs[7]=0-259; cs[8]=0-500; cs[9]=0-707; cs[10]=0-866; cs[11]=0-966 77 cs[12]=0-1000; cs[13]=0-966; cs[14]=0-866; cs[15]=0-707; cs[16]=0-500; cs[17]=0-259 78 cs[18]=0; cs[19]=259; cs[20]=500; cs[21]=707; cs[22]=866; cs[23]=966 79 sn[0]=0; sn[1]=259; sn[2]=500; sn[3]=707; sn[4]=866; sn[5]=966 80 sn[6]=1000; sn[7]=966; sn[8]=866; sn[9]=707; sn[10]=500; sn[11]=259 81 sn[12]=0; sn[13]=0-259; sn[14]=0-500; sn[15]=0-707; sn[16]=0-866; sn[17]=0-966 82 sn[18]=0-1000; sn[19]=0-966; sn[20]=0-866; sn[21]=0-707; sn[22]=0-500; sn[23]=0-259 83 return 0 84} 85// append the bubble ring as one CW contour, in aa grid units (pixel*MB_SS). 86func mb_ring(xs: *i64, ys: *i64, cst: *i64, cln: *i64, gb: *i64, cs: *i64, sn: *i64, cx: i64, cy: i64, rx: i64, ry: i64) -> i64 { 87 let p: i64 = gb[0] 88 var k: i64 = 0 89 while k < MB_N { 90 xs[p+k] = (cx + (rx * cs[k]) / 1000) * MB_SS 91 ys[p+k] = (cy + (ry * sn[k]) / 1000) * MB_SS 92 k = k + 1 93 } 94 cst[gb[1]] = p 95 cln[gb[1]] = MB_N 96 gb[0] = p + MB_N 97 gb[1] = gb[1] + 1 98 return 0 99} 100// append the tail as one CW triangle: two base vertices on the ring either side of vertex tdir, 101// apex at the tip. Winding matches the ring so nonzero-fill UNIONS them into one silhouette. 102func mb_tail(xs: *i64, ys: *i64, cst: *i64, cln: *i64, gb: *i64, cs: *i64, sn: *i64, spec: *i64, grow: i64) -> i64 { 103 let cx: i64 = spec[0] 104 let cy: i64 = spec[1] 105 let rx: i64 = spec[2] + grow 106 let ry: i64 = spec[3] + grow 107 let td: i64 = spec[6] 108 var a: i64 = td - 1 109 if a < 0 { a = a + MB_N } 110 var b: i64 = td + 1 111 if b >= MB_N { b = b - MB_N } 112 let p: i64 = gb[0] 113 xs[p+0] = (cx + (rx * cs[a]) / 1000) * MB_SS 114 ys[p+0] = (cy + (ry * sn[a]) / 1000) * MB_SS 115 xs[p+1] = spec[4] * MB_SS 116 ys[p+1] = spec[5] * MB_SS 117 xs[p+2] = (cx + (rx * cs[b]) / 1000) * MB_SS 118 ys[p+2] = (cy + (ry * sn[b]) / 1000) * MB_SS 119 cst[gb[1]] = p 120 cln[gb[1]] = 3 121 gb[0] = p + 3 122 gb[1] = gb[1] + 1 123 return 0 124} 125// composite an aa coverage mask into the i64 framebuffer with a flat ink. 126// fb packing is the font core's: r | g<<8 | b<<16 (low byte RED), so the two agree by construction. 127func mb_cov(fb: *i64, W: i64, H: i64, cov: *u8, ir: i64, ig: i64, ib: i64) -> i64 { 128 var i: i64 = 0 129 let n: i64 = W * H 130 while i < n { 131 let a: i64 = cov[i] as i64 132 if a > 0 { 133 let bgv: i64 = fb[i] 134 let br: i64 = bgv & 255 135 let bg2: i64 = (bgv >> 8) & 255 136 let bb: i64 = (bgv >> 16) & 255 137 let nr: i64 = (ir * a + br * (255 - a)) / 255 138 let ng: i64 = (ig * a + bg2 * (255 - a)) / 255 139 let nb: i64 = (ib * a + bb * (255 - a)) / 255 140 fb[i] = (nr & 255) | ((ng & 255) << 8) | ((nb & 255) << 16) 141 } 142 i = i + 1 143 } 144 return 0 145} 146// greedy word wrap into line offsets. Returns line count, or a NEGATIVE refusal code: 147// -1 a single word is wider than the inner box (no wrap can save it) 148// -2 more lines than the caller's line table can hold 149func mb_wrap(txt: *u8, tlen: i64, adv: i64, boxw: i64, ls: *i64, le: *i64) -> i64 { 150 let percols: i64 = boxw / adv 151 if percols < 1 { return 0 - 1 } 152 var nl: i64 = 0 153 var i: i64 = 0 154 while i < tlen { 155 // skip leading spaces 156 var go1: i64 = 1 157 while go1 == 1 { if i >= tlen { go1 = 0 } else { if txt[i] == (32 as u8) { i = i + 1 } else { go1 = 0 } } } 158 if i >= tlen { return nl } 159 let start: i64 = i 160 var last: i64 = 0 - 1 161 var cur: i64 = i 162 var go2: i64 = 1 163 while go2 == 1 { 164 if cur >= tlen { go2 = 0 } else { 165 if cur - start >= percols { go2 = 0 } else { 166 if txt[cur] == (32 as u8) { last = cur } 167 cur = cur + 1 168 } 169 } 170 } 171 var endp: i64 = cur 172 if cur < tlen { 173 if txt[cur] != (32 as u8) { 174 if last > start { endp = last } else { return 0 - 1 } 175 } 176 } 177 if nl >= MB_LINES { return 0 - 2 } 178 ls[nl] = start 179 le[nl] = endp 180 nl = nl + 1 181 i = endp 182 } 183 return nl 184} 185// THE CONTRACT (watched as mangagen_mb_bubble_draw): draw one outlined speech bubble with its tail and 186// its lettering onto the framebuffer, or REFUSE. spec = cx,cy,rx,ry,tipx,tipy,tdir,ch,stroke. 187// Returns 0 drawn, or a named non-zero refusal -- never a silent clip. 188func mb_bubble_draw(fb: *i64, W: i64, H: i64, spec: *i64, txt: *u8, tlen: i64, mask: *u8) -> i64 { 189 let cs: *i64 = sys_mmap(8 * MB_N) as *i64 190 let sn: *i64 = sys_mmap(8 * MB_N) as *i64 191 mb_tables(cs, sn) 192 let xs: *i64 = sys_mmap(8 * MB_PTS) as *i64 193 let ys: *i64 = sys_mmap(8 * MB_PTS) as *i64 194 let cst: *i64 = sys_mmap(8 * 64) as *i64 195 let cln: *i64 = sys_mmap(8 * 64) as *i64 196 let gb: *i64 = sys_mmap(32) as *i64 197 let cov: *u8 = sys_mmap(W * H + 64) 198 let stroke: i64 = spec[8] 199 // FIT FIRST, DRAW SECOND: a refusal must not leave half a bubble on the page. 200 let ch: i64 = spec[7] 201 let adv: i64 = nf_adv_px(ch) 202 if adv < 1 { mb_puts("MANGA-BUBBLE-REFUSE cell height too small to advance a glyph\n" as *u8); return 4 } 203 // the largest axis-aligned box inside an ellipse is 2*r/sqrt(2) ~ 707 per-mille of the diameter, 204 // then a margin so the lettering does not kiss the stroke. 205 let boxw: i64 = (spec[2] * 2 * MB_INSCRIBED_PERMIL) / 1000 - 2 * stroke - MB_MARGIN_PX 206 let boxh: i64 = (spec[3] * 2 * MB_INSCRIBED_PERMIL) / 1000 - 2 * stroke - MB_MARGIN_PX 207 let lh: i64 = ch + ch / MB_LEAD_DIV 208 let ls: *i64 = sys_mmap(8 * MB_LINES) as *i64 209 let le: *i64 = sys_mmap(8 * MB_LINES) as *i64 210 let nl: i64 = mb_wrap(txt, tlen, adv, boxw, ls, le) 211 if nl == 0 - 1 { 212 mb_puts("MANGA-BUBBLE-REFUSE a word is wider than the bubble: inner box " as *u8); mb_pi(boxw) 213 mb_puts("px fits " as *u8); mb_pi(boxw / adv); mb_puts(" chars at cell " as *u8); mb_pi(ch) 214 mb_puts("px. Widen rx, shrink ch, or rewrite the line -- refusing rather than clipping.\n" as *u8) 215 return 5 216 } 217 if nl == 0 - 2 { mb_puts("MANGA-BUBBLE-REFUSE more lines than the bubble can hold\n" as *u8); return 5 } 218 if nl * lh > boxh { 219 mb_puts("MANGA-BUBBLE-REFUSE text overflows the bubble: " as *u8); mb_pi(nl) 220 mb_puts(" lines at " as *u8); mb_pi(lh); mb_puts("px need " as *u8); mb_pi(nl * lh) 221 mb_puts("px, inner box is " as *u8); mb_pi(boxh) 222 mb_puts("px. Grow ry, shrink ch, or split the balloon -- refusing rather than clipping.\n" as *u8) 223 return 5 224 } 225 // ---- outline pass: the silhouette grown by the stroke, inked dark ---- 226 gb[0] = 0 227 gb[1] = 0 228 mb_ring(xs, ys, cst, cln, gb, cs, sn, spec[0], spec[1], spec[2] + stroke, spec[3] + stroke) 229 mb_tail(xs, ys, cst, cln, gb, cs, sn, spec, stroke) 230 aa_render(xs, ys, cst, cln, gb[1], cov, W, H, MB_SS) 231 mb_cov(fb, W, H, cov, MB_INK_R, MB_INK_G, MB_INK_B) 232 // ---- fill pass: the same silhouette at true size, inked paper-white ---- 233 gb[0] = 0 234 gb[1] = 0 235 mb_ring(xs, ys, cst, cln, gb, cs, sn, spec[0], spec[1], spec[2], spec[3]) 236 mb_tail(xs, ys, cst, cln, gb, cs, sn, spec, 0) 237 aa_render(xs, ys, cst, cln, gb[1], cov, W, H, MB_SS) 238 mb_cov(fb, W, H, cov, MB_PAPER, MB_PAPER, MB_PAPER) 239 // ---- lettering: centred, on the font core's own metrics ---- 240 var y: i64 = spec[1] - (nl * lh) / 2 241 var li: i64 = 0 242 while li < nl { 243 let sl: i64 = le[li] - ls[li] 244 let wpx: i64 = sl * adv 245 let x: i64 = spec[0] - wpx / 2 246 let seg: *u8 = (txt as i64 + ls[li]) as *u8 247 nf_draw_text_mem(fb, W, H, mask, x, y, ch, seg, sl, MB_TEXT_INK) 248 y = y + lh 249 li = li + 1 250 } 251 mb_puts(" bubble at " as *u8); mb_pi(spec[0]); mb_puts("," as *u8); mb_pi(spec[1]) 252 mb_puts(" r=" as *u8); mb_pi(spec[2]); mb_puts("x" as *u8); mb_pi(spec[3]) 253 mb_puts(" tail->" as *u8); mb_pi(spec[4]); mb_puts("," as *u8); mb_pi(spec[5]) 254 mb_puts(" lines=" as *u8); mb_pi(nl); mb_puts(" cell=" as *u8); mb_pi(ch) 255 mb_puts("px fits " as *u8); mb_pi(nl * lh); mb_puts("/" as *u8); mb_pi(boxh); mb_puts("px\n" as *u8) 256 return 0 257} 258func main(argc: i64, argv: *i64) -> i64 { 259 if argc < 12 { 260 mb_puts("usage: nx_manga_bubble <in.png|blank:WxH> <out.png> <cx> <cy> <rx> <ry> <tipx> <tipy> <tdir 0-23> <ch> <text...>\n" as *u8) 261 mb_puts(" tdir is the ring vertex the tail leaves from: 6 = straight down, 0 = right, 12 = up, 18 = left.\n" as *u8) 262 mb_puts(" text that cannot fit REFUSES with the measurement -- it is never clipped or shrunk.\n" as *u8) 263 sys_exit(2); return 2 264 } 265 let inp: *u8 = argv[1] as *u8 266 let outp: *u8 = argv[2] as *u8 267 var W: i64 = 0 268 var H: i64 = 0 269 let fbw: *i64 = sys_mmap(32) as *i64 270 var src: *u8 = 0 as *u8 271 if inp[0] == (98 as u8) { 272 // blank:WxH -- an empty page, for the gate and for a bubble sheet with no art under it 273 var q: i64 = 0 274 while inp[q] != (0 as u8) { if inp[q] == (58 as u8) { break } q = q + 1 } 275 let wp: *u8 = (inp as i64 + q + 1) as *u8 276 W = mb_atoi(wp) 277 var r: i64 = 0 278 while wp[r] != (0 as u8) { if wp[r] == (120 as u8) { break } r = r + 1 } 279 let hp: *u8 = (wp as i64 + r + 1) as *u8 280 H = mb_atoi(hp) 281 } else { 282 fbw[0] = 0 283 fbw[1] = 0 284 src = nx_img_to_rgb(inp, fbw) 285 if src == 0 as *u8 { mb_puts("MANGA-BUBBLE-REFUSE cannot decode input page: " as *u8); mb_puts(inp); mb_puts("\n" as *u8); sys_exit(3); return 3 } 286 W = fbw[0] 287 H = fbw[1] 288 } 289 if W < 16 { mb_puts("MANGA-BUBBLE-REFUSE page width too small\n" as *u8); sys_exit(3); return 3 } 290 if H < 16 { mb_puts("MANGA-BUBBLE-REFUSE page height too small\n" as *u8); sys_exit(3); return 3 } 291 let fb: *i64 = sys_mmap(8 * W * H + 64) as *i64 292 var i: i64 = 0 293 let n: i64 = W * H 294 if (src as i64) == 0 { 295 while i < n { fb[i] = 255 | (255 << 8) | (255 << 16); i = i + 1 } 296 } else { 297 while i < n { 298 let o: i64 = i * 3 299 fb[i] = (src[o] as i64) | ((src[o+1] as i64) << 8) | ((src[o+2] as i64) << 16) 300 i = i + 1 301 } 302 } 303 let spec: *i64 = sys_mmap(128) as *i64 304 spec[0] = mb_atoi(argv[3] as *u8) 305 spec[1] = mb_atoi(argv[4] as *u8) 306 spec[2] = mb_atoi(argv[5] as *u8) 307 spec[3] = mb_atoi(argv[6] as *u8) 308 spec[4] = mb_atoi(argv[7] as *u8) 309 spec[5] = mb_atoi(argv[8] as *u8) 310 spec[6] = mb_atoi(argv[9] as *u8) 311 spec[7] = mb_atoi(argv[10] as *u8) 312 // POLICY, NAMED: the stroke is the house default rather than a literal buried in the draw path. 313 // ⚠It is deliberately NOT read from argv[11]: that slot is the first WORD OF DIALOGUE, so a flag 314 // there would silently restyle any balloon whose line happened to start with the flag letter. The 315 // honest home for stroke, ink and leading is a style row beside layouts.conf -- filed, not faked. 316 spec[8] = MB_STROKE_DEFAULT 317 if spec[6] < 0 { spec[6] = 0 } 318 if spec[6] >= MB_N { spec[6] = MB_N - 1 } 319 // join argv[11..] with single spaces into one line of dialogue 320 let txt: *u8 = sys_mmap(MB_MAGIC_8192) 321 var tl: i64 = 0 322 var a: i64 = 11 323 while a < argc { 324 let w: *u8 = argv[a] as *u8 325 var k: i64 = 0 326 while w[k] != (0 as u8) { txt[tl] = w[k]; tl = tl + 1; k = k + 1 } 327 if a + 1 < argc { txt[tl] = 32 as u8; tl = tl + 1 } 328 a = a + 1 329 } 330 txt[tl] = 0 as u8 331 mb_puts("=== nx_manga_bubble page=" as *u8); mb_pi(W); mb_puts("x" as *u8); mb_pi(H); mb_puts("\n" as *u8) 332 let mask: *u8 = sys_mmap(MB_MASK) 333 let rc: i64 = mb_bubble_draw(fb, W, H, spec, txt, tl, mask) 334 if rc != 0 { sys_exit(rc); return rc } 335 let rgb: *u8 = sys_mmap(W * H * 3 + 64) 336 i = 0 337 while i < n { 338 let v: i64 = fb[i] 339 let o: i64 = i * 3 340 rgb[o] = (v & 255) as u8 341 rgb[o+1] = ((v >> 8) & 255) as u8 342 rgb[o+2] = ((v >> 16) & 255) as u8 343 i = i + 1 344 } 345 let wrc: i64 = nx_png_write_rgb(outp, rgb, W, H) 346 if wrc != 0 { mb_puts("MANGA-BUBBLE-REFUSE png write failed\n" as *u8); sys_exit(6); return 6 } 347 mb_puts("MANGA-BUBBLE-OK " as *u8); mb_puts(outp); mb_puts("\n" as *u8) 348 sys_exit(0) 349 return 0 350}