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}