code wiki / (root) / nx_arousal_render.nx

nx_arousal_render.nx source

↩ module page · 358 lines · 15855 B

1// nx_arousal_render.nx -- the LOOK organ. Emits a PNG of arousal SHIFTING OVER TIME. 2// 3// x = TIME (0..900s, spanning the published 664-743s time-to-peak). y = stacked site bands in real 4// Beer-Lambert colour, then a morphology strip, then the drive track itself. 5// 6// ★TIME-VARYING DRIVE: with a track file the drive is NOT a step input. Real arousal rises, collapses 7// on interruption, and recovers -- that is the whole clinical picture in the dual-control model 8// (SIS1 performance-failure / SIS2 consequence threat suppressing SES excitation). An inhibition 9// event visibly extinguishes the flush and reshapes the morphology, which is the view a sex-therapy 10// user would actually be shown. 11// 12// ★CORRECTNESS: this integrates ONCE forward with a per-step drive lookup. Calling a_perf(t,...) per 13// column re-integrates from zero assuming CONSTANT drive -- correct for a step, WRONG for a track. 14// 15// usage: nx_arousal_render <out.png> <sex0m1f> <drive_permil> <melanin_permil> [propensity] [track] 16// track file = "t_ms drive_permil" lines, ascending; drive is piecewise-constant. 17// license_tier: ORIGINAL expect_exit: 0 18import "nx_arousal_skin_lib.nx" 19import "nx_doc_layout.nx" 20import "nx_png_write.nx" 21const R_MAGIC_15000: i64 = 15000 22const R_MAGIC_185000: i64 = 185000 23const R_MAGIC_210000: i64 = 210000 24 25const R_W: i64 = 860 26const R_GUT: i64 = 120 27const R_PW: i64 = 740 28const R_HEAD: i64 = 30 29const R_BAND: i64 = 40 30const R_MORPH: i64 = 60 31const R_TRACK: i64 = 40 32const R_GEN: i64 = 40 33const R_AXIS: i64 = 24 34const R_SPAN_MS: i64 = 900000 35const R_TAU_NIP: i64 = 6000 36const R_MAXPT: i64 = 512 37 38func r_px(img: *u8, x: i64, y: i64, r: i64, g: i64, b: i64) -> i64 { 39 if x < 0 { return 0 } 40 if x >= R_W { return 0 } 41 let o: i64 = (y * R_W + x) * 3 42 img[o] = ((r * 255) / 1000) as u8 43 img[o+1] = ((g * 255) / 1000) as u8 44 img[o+2] = ((b * 255) / 1000) as u8 45 return 0 46} 47// 8x8 glyph blit into the RGB buffer. nx_doc_blit targets an 8-bit GRAYSCALE bitmap, so the glyph 48// TABLE INDEXING is reused via nx_doc_glyph and only the pixel write differs. 49func r_char(img: *u8, tbl: *u8, x: i64, y: i64, ch: i64, cr: i64, cg: i64, cb: i64) -> i64 { 50 let gl: *u8 = nx_doc_glyph(tbl, ch) 51 var r: i64 = 0 52 while r < 8 { 53 let bits: i64 = gl[r] as i64 54 var c: i64 = 0 55 while c < 8 { 56 if ((bits >> c) & 1) == 1 { r_px(img, x + c, y + r, cr, cg, cb) } 57 c = c + 1 58 } 59 r = r + 1 60 } 61 return 0 62} 63func r_text(img: *u8, tbl: *u8, x: i64, y: i64, s: *u8, cr: i64, cg: i64, cb: i64) -> i64 { 64 var i: i64 = 0 65 while s[i] != (0 as u8) { 66 r_char(img, tbl, x + i * 8, y, s[i] as i64, cr, cg, cb) 67 i = i + 1 68 } 69 return 0 70} 71// right-aligned decimal, for the time axis 72func r_num(img: *u8, tbl: *u8, x: i64, y: i64, v: i64, cr: i64, cg: i64, cb: i64) -> i64 { 73 var m: i64 = v 74 var nd: i64 = 1 75 while m >= 10 { m = m / 10; nd = nd + 1 } 76 var i: i64 = 0 77 m = v 78 while i < nd { 79 var dv: i64 = m 80 var k: i64 = 0 81 while k < nd - 1 - i { dv = dv / 10; k = k + 1 } 82 r_char(img, tbl, x + i * 8, y, 48 + (dv % 10), cr, cg, cb) 83 i = i + 1 84 } 85 return 0 86} 87 88func main(argc: i64, argv: *i64) -> i64 { 89 if argc < 5 { a_puts("REFUSED reason=render_needs_4_args\n" as *u8); return 2 } 90 let path: *u8 = argv[1] as *u8 91 let sex: i64 = a_atoi(argv[2] as *u8) 92 let drive0: i64 = a_atoi(argv[3] as *u8) 93 let mel: i64 = a_atoi(argv[4] as *u8) 94 var prop: i64 = 1000 95 if argc >= 6 { prop = a_atoi(argv[5] as *u8) } 96 if drive0 <= 0 { a_puts("REFUSED reason=nonpositive_drive\n" as *u8); return 3 } 97 if drive0 > 1000 { a_puts("REFUSED reason=drive_over_permil\n" as *u8); return 3 } 98 if mel < 0 { a_puts("REFUSED reason=melanin_out_of_range\n" as *u8); return 3 } 99 if mel > 1000 { a_puts("REFUSED reason=melanin_out_of_range\n" as *u8); return 3 } 100 101 let cl: *i64 = sys_mmap(16) as *i64 102 cl[0] = 0 103 let cbuf: *u8 = sys_read_file("arousal_params.conf" as *u8, cl) 104 let cn: i64 = cl[0] 105 let ext: *i64 = sys_mmap(SK_EXTN * 8) as *i64 106 ext[0] = a_conf(cbuf, cn, "mel_ext_r" as *u8, 300) 107 ext[1] = a_conf(cbuf, cn, "mel_ext_g" as *u8, 500) 108 ext[2] = a_conf(cbuf, cn, "mel_ext_b" as *u8, 700) 109 ext[3] = a_conf(cbuf, cn, "hb_ext_r" as *u8, 100) 110 ext[4] = a_conf(cbuf, cn, "hb_ext_g" as *u8, 700) 111 ext[5] = a_conf(cbuf, cn, "hb_ext_b" as *u8, 900) 112 ext[6] = a_conf(cbuf, cn, "hb_base" as *u8, 150) 113 ext[7] = a_conf(cbuf, cn, "hb_span" as *u8, 600) 114 ext[8] = a_conf(cbuf, cn, "flush_span" as *u8, 300) 115 let nipmask: i64 = a_conf(cbuf, cn, "nipple_mask" as *u8, 600) 116 let tau_tume: i64 = a_conf(cbuf, cn, "tau_tume_ms" as *u8, R_MAGIC_15000) 117 let rig_thresh: i64 = a_conf(cbuf, cn, "rig_thresh" as *u8, 550) 118 let tip_factor: i64 = a_conf(cbuf, cn, "tip_factor" as *u8, 780) 119 var tau: i64 = a_conf(cbuf, cn, "tau_slow_m_ms" as *u8, R_MAGIC_185000) 120 if sex == 1 { tau = a_conf(cbuf, cn, "tau_slow_f_ms" as *u8, R_MAGIC_210000) } 121 122 // --- optional drive track ------------------------------------------------------------------ 123 let ta: *i64 = sys_mmap(R_MAXPT * 8) as *i64 124 let da: *i64 = sys_mmap(R_MAXPT * 8) as *i64 125 var nt: i64 = 0 126 if argc >= 7 { 127 let tl: *i64 = sys_mmap(16) as *i64 128 tl[0] = 0 129 let tb: *u8 = sys_read_file(argv[6] as *u8, tl) 130 let tn: i64 = tl[0] 131 if (tb as i64) == 0 { a_puts("REFUSED reason=track_unreadable\n" as *u8); return 4 } 132 let p: *i64 = sys_mmap(16) as *i64 133 p[0] = 0 134 var done: i64 = 0 135 while done == 0 { 136 a_skipnd(tb, tn, p) 137 if p[0] >= tn { done = 1 } else { 138 let tv: i64 = a_pint(tb, tn, p) 139 a_skipnd(tb, tn, p) 140 let dv: i64 = a_pint(tb, tn, p) 141 if tv < 0 { done = 1 } else { 142 if dv < 0 { done = 1 } else { 143 if dv > 1000 { a_puts("REFUSED reason=track_drive_over_permil\n" as *u8); return 3 } 144 if nt > 0 { if tv < ta[nt-1] { a_puts("REFUSED reason=track_not_ascending\n" as *u8); return 3 } } 145 if nt < R_MAXPT { ta[nt] = tv; da[nt] = dv; nt = nt + 1 } else { done = 1 } 146 } 147 } 148 } 149 } 150 if nt == 0 { a_puts("REFUSED reason=track_empty\n" as *u8); return 4 } 151 } 152 153 // --- ONE forward integration, per-step drive lookup ------------------------------------------ 154 let colp: *i64 = sys_mmap(R_PW * 8) as *i64 155 let cold: *i64 = sys_mmap(R_PW * 8) as *i64 156 let coln: *i64 = sys_mmap(R_PW * 8) as *i64 157 let colt: *i64 = sys_mmap(R_PW * 8) as *i64 158 var cur: i64 = 0 159 var curn: i64 = 0 160 var curt: i64 = 0 161 var t: i64 = 0 162 var ti: i64 = 0 163 var x: i64 = 0 164 while x < R_PW { 165 let ttarget: i64 = (x * R_SPAN_MS) / (R_PW - 1) 166 var dr: i64 = drive0 167 var stepping: i64 = 1 168 while stepping == 1 { 169 if t >= ttarget { stepping = 0 } else { 170 dr = drive0 171 if nt > 0 { 172 var adv: i64 = 1 173 while adv == 1 { 174 if ti + 1 < nt { if ta[ti+1] <= t { ti = ti + 1 } else { adv = 0 } } else { adv = 0 } 175 } 176 dr = da[ti] 177 } 178 cur = a_lag(cur, dr * 1000, tau) 179 var ntgt: i64 = dr * 1000 180 if dr < 150 { ntgt = 0 } 181 curn = a_lag(curn, ntgt, R_TAU_NIP) 182 // tumescence: fast volume fill on its OWN clock, not the slow systemic envelope 183 curt = a_lag(curt, dr * 1000, tau_tume) 184 t = t + 50 185 } 186 } 187 if nt > 0 { dr = da[ti] } 188 colp[x] = cur / 1000 189 cold[x] = dr 190 coln[x] = curn / 1000 191 colt[x] = curt / 1000 192 x = x + 1 193 } 194 195 // --- events --------------------------------------------------------------------------------- 196 let out: *i64 = sys_mmap(SK_OUTN * 8) as *i64 197 let onset: *i64 = sys_mmap(SK_NSITES * 8) as *i64 198 var si: i64 = 0 199 while si < SK_NSITES { onset[si] = 0 - 1; si = si + 1 } 200 var peak_x: i64 = 0 201 var peak_v: i64 = 0 - 1 202 var px: i64 = 0 203 while px < R_PW { 204 var ps: i64 = 0 205 while ps < SK_NSITES { 206 sk_eval(ps, colp[px], mel, prop, ext, out) 207 if out[5] > 0 { if onset[ps] < 0 { onset[ps] = px } } 208 ps = ps + 1 209 } 210 let pa: i64 = a_nipple_apparent(coln[px], a_areola_engorge(colp[px]), nipmask) 211 if pa > peak_v { peak_v = pa; peak_x = px } 212 px = px + 1 213 } 214 215 // --- draw ------------------------------------------------------------------------------------ 216 let h: i64 = R_HEAD + SK_NSITES * R_BAND + R_MORPH + R_GEN + R_TRACK + R_AXIS 217 let img: *u8 = sys_mmap(R_W * h * 3) 218 // paint the whole canvas dark first -- the gutter and header are outside the plot loop 219 var by: i64 = 0 220 while by < h { 221 var bx: i64 = 0 222 while bx < R_W { r_px(img, bx, by, 55, 60, 70); bx = bx + 1 } 223 by = by + 1 224 } 225 x = 0 226 while x < R_PW { 227 var s: i64 = 0 228 while s < SK_NSITES { 229 sk_eval(s, colp[x], mel, prop, ext, out) 230 var mark: i64 = 0 231 if onset[s] >= 0 { if x >= onset[s] { if x < onset[s] + 3 { mark = 1 } } } 232 var yy: i64 = 0 233 while yy < R_BAND { 234 let y: i64 = R_HEAD + s * R_BAND + yy 235 if yy < 2 { r_px(img, R_GUT + x, y, 60, 60, 60) } else { 236 if mark == 1 { r_px(img, R_GUT + x, y, 1000, 1000, 1000) } else { r_px(img, R_GUT + x, y, out[2], out[3], out[4]) } 237 } 238 yy = yy + 1 239 } 240 s = s + 1 241 } 242 let app: i64 = a_nipple_apparent(coln[x], a_areola_engorge(colp[x]), nipmask) 243 let top: i64 = R_MORPH - (app * R_MORPH) / 1000 244 var pk: i64 = 0 245 if x >= peak_x { if x < peak_x + 3 { pk = 1 } } 246 var my: i64 = 0 247 while my < R_MORPH { 248 let y2: i64 = R_HEAD + SK_NSITES * R_BAND + my 249 if my < 2 { r_px(img, R_GUT + x, y2, 60, 60, 60) } else { 250 if pk == 1 { r_px(img, R_GUT + x, y2, 1000, 1000, 1000) } else { 251 if my >= top { r_px(img, R_GUT + x, y2, 950, 780, 760) } else { r_px(img, R_GUT + x, y2, 90, 90, 110) } 252 } 253 } 254 my = my + 1 255 } 256 // GENITAL strip. Male: TUMESCENCE filled with RIGIDITY drawn over it -- the gap between the 257 // two IS the physiology (volume fills before pressure develops, so early arousal is all 258 // tumescence and no rigidity). Female: genital vasocongestion, which has no such split. 259 var gfill: i64 = 0 260 var gline: i64 = 0 - 1 261 if sex == 0 { 262 gfill = colt[x] 263 gline = a_rigidity_tip(a_rigidity(colt[x], rig_thresh), tip_factor) 264 } else { 265 sk_eval(5, colp[x], mel, prop, ext, out) 266 gfill = out[0] 267 } 268 let gtop: i64 = R_GEN - (gfill * R_GEN) / 1000 269 var gl: i64 = 0 - 1 270 if gline >= 0 { gl = R_GEN - (gline * R_GEN) / 1000 } 271 var gy: i64 = 0 272 while gy < R_GEN { 273 let y4: i64 = R_HEAD + SK_NSITES * R_BAND + R_MORPH + gy 274 if gy < 2 { r_px(img, R_GUT + x, y4, 60, 60, 60) } else { 275 if gl >= 0 { if gy >= gl { if gy < gl + 2 { r_px(img, R_GUT + x, y4, 1000, 940, 500) } else { if gy >= gtop { r_px(img, R_GUT + x, y4, 700, 480, 470) } else { r_px(img, R_GUT + x, y4, 40, 45, 60) } } } else { if gy >= gtop { r_px(img, R_GUT + x, y4, 700, 480, 470) } else { r_px(img, R_GUT + x, y4, 40, 45, 60) } } } else { 276 if gy >= gtop { r_px(img, R_GUT + x, y4, 700, 480, 470) } else { r_px(img, R_GUT + x, y4, 40, 45, 60) } 277 } 278 } 279 gy = gy + 1 280 } 281 // drive track strip -- the STIMULUS, so an inhibition event is visible as cause next to effect 282 let dtop: i64 = R_TRACK - (cold[x] * R_TRACK) / 1000 283 var dy: i64 = 0 284 while dy < R_TRACK { 285 let y3: i64 = R_HEAD + SK_NSITES * R_BAND + R_MORPH + R_GEN + dy 286 if dy < 2 { r_px(img, R_GUT + x, y3, 60, 60, 60) } else { 287 if dy >= dtop { r_px(img, R_GUT + x, y3, 380, 620, 900) } else { r_px(img, R_GUT + x, y3, 40, 45, 60) } 288 } 289 dy = dy + 1 290 } 291 x = x + 1 292 } 293 294 // ---- LABELS. The render was previously unreadable: six near-identical bands, no axis, no names. 295 // A chart without labels is not a chart, and no numeric tooth can detect that. 296 let tbl: *u8 = font8x8_table() 297 r_text(img, tbl, 8, 11, "AROUSAL TWIN" as *u8, 1000, 1000, 1000) 298 if sex == 0 { r_text(img, tbl, 118, 11, "male" as *u8, 700, 760, 850) } else { r_text(img, tbl, 118, 11, "female" as *u8, 700, 760, 850) } 299 r_text(img, tbl, 200, 11, "skin flush onset by site, over time" as *u8, 620, 670, 750) 300 if nt > 0 { r_text(img, tbl, 600, 11, "with inhibition event" as *u8, 950, 700, 420) } 301 302 // site band names in the gutter 303 var ls: i64 = 0 304 while ls < SK_NSITES { 305 r_text(img, tbl, 8, R_HEAD + ls * R_BAND + 16, sk_name(ls), 880, 900, 930) 306 ls = ls + 1 307 } 308 r_text(img, tbl, 8, R_HEAD + SK_NSITES * R_BAND + 26, "nipple" as *u8, 880, 900, 930) 309 if sex == 0 { 310 r_text(img, tbl, 8, R_HEAD + SK_NSITES * R_BAND + R_MORPH + 10, "tumescence" as *u8, 880, 900, 930) 311 r_text(img, tbl, 8, R_HEAD + SK_NSITES * R_BAND + R_MORPH + 22, "+rigidity" as *u8, 1000, 940, 500) 312 } else { 313 // NOT the same thing as the "genital" skin band above: that is surface flush, this is 314 // vasocongestion. Two labels reading "genital" made the panel ambiguous on sight. 315 r_text(img, tbl, 8, R_HEAD + SK_NSITES * R_BAND + R_MORPH + 10, "genital" as *u8, 880, 900, 930) 316 r_text(img, tbl, 8, R_HEAD + SK_NSITES * R_BAND + R_MORPH + 22, "vasocong." as *u8, 880, 900, 930) 317 } 318 r_text(img, tbl, 8, R_HEAD + SK_NSITES * R_BAND + R_MORPH + R_GEN + 16, "drive" as *u8, 880, 900, 930) 319 320 // time axis: ticks every 150 s 321 let ay: i64 = R_HEAD + SK_NSITES * R_BAND + R_MORPH + R_GEN + R_TRACK 322 var tk: i64 = 0 323 while tk <= 6 { 324 let tsec: i64 = tk * 150 325 let tx: i64 = R_GUT + (tsec * 1000 * (R_PW - 1)) / R_SPAN_MS 326 var ty: i64 = 0 327 while ty < 4 { r_px(img, tx, ay + ty, 500, 540, 600); ty = ty + 1 } 328 // clamp the label inside the canvas -- the final tick sits at the right edge and its text 329 // ran off the end, which is exactly the kind of thing only looking at it catches 330 var lx: i64 = tx - 10 331 var nd2: i64 = 1 332 var mm: i64 = tsec 333 while mm >= 10 { mm = mm / 10; nd2 = nd2 + 1 } 334 if lx + nd2 * 8 > R_W - 2 { lx = R_W - 2 - nd2 * 8 } 335 r_num(img, tbl, lx, ay + 7, tsec, 750, 790, 850) 336 tk = tk + 1 337 } 338 r_text(img, tbl, 8, ay + 7, "seconds" as *u8, 620, 670, 750) 339 340 let rc: i64 = nx_png_write_rgb(path, img, R_W, h) 341 if rc < 0 { a_puts("REFUSED reason=png_write_failed\n" as *u8); return 4 } 342 a_puts("ok wrote=" as *u8); a_puts(path) 343 a_puts(" w=" as *u8); a_putn(R_W) 344 a_puts(" h=" as *u8); a_putn(h) 345 a_puts(" track_pts=" as *u8); a_putn(nt) 346 a_puts(" nip_peak_x=" as *u8); a_putn(peak_x) 347 // onsets reported in MILLISECONDS, not pixels: a pixel index is meaningless outside this render 348 // and silently changes whenever the canvas is resized. 349 var oi: i64 = 0 350 while oi < SK_NSITES { 351 a_puts(" onset_" as *u8); a_putn(oi) 352 a_puts("=" as *u8) 353 if onset[oi] < 0 { a_putn(0 - 1) } else { a_putn((onset[oi] * R_SPAN_MS) / (R_PW - 1)) } 354 oi = oi + 1 355 } 356 a_puts("\n" as *u8) 357 return 0 358}