File size: 5,806 Bytes
e85709c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/* Slapstack Studio layer math — NEW mechanics on top of the verified core.
   core.js is untouched; everything here is presentation-adjacent inference
   plumbing, tested by tests_studio.js:
     renderEnv      per-layer envelope-energy buffer (for alpha + coverage)
     coverageOf     how much front-stack mass sits on each atom center
     autoHidden     coverage -> per-atom evidence mask (depth occlusion)
     compositePainter  back-to-front alpha compositing of layer buffers
     composePose    Sim(2) composition in vote coordinates [tx,ty,rho,lam]
*/
"use strict";

const ALPHA_C = 2.0;        // alpha = 1 - exp(-ALPHA_C * bounded envelope energy)
const COVER_THRESH = 0.55;  // atom loses evidence when this covered

/* Opacity of a layer = where it actually PAINTS: per-pixel deviation of
   the pre-sigmoid field from neutral. Cancelling atom pairs (high energy,
   zero sum) are correctly transparent — envelope-based opacity is not used
   because it saturates on exactly those invisible pairs.
   a = 1 - exp(-ALPHA_C * max(0, |preR|+|preG|+|preB| - FLOOR)). */
const ALPHA_FLOOR = 0.08;
function alphaFromPre(pre, H, out) {
  out = out || new Float32Array(H * H);
  const n = H * H;
  for (let i = 0; i < n; i++) {
    const m = Math.abs(pre[i]) + Math.abs(pre[n + i]) + Math.abs(pre[2 * n + i]);
    out[i] = m > ALPHA_FLOOR ? 1 - Math.exp(-ALPHA_C * (m - ALPHA_FLOOR)) : 0;
  }
  return out;
}

/* coverage of each atom center by a front stack of alpha buffers:
   cov_i = 1 - prod_front (1 - a_m(x_i)). */
function coverageOf(atoms, frontAlphaBufs, H) {
  const out = new Float32Array(atoms.length);
  for (let i = 0; i < atoms.length; i++) {
    const px = Math.max(0, Math.min(H - 1, Math.round((atoms[i][0] + 1) / 2 * (H - 1))));
    const py = Math.max(0, Math.min(H - 1, Math.round((atoms[i][1] + 1) / 2 * (H - 1))));
    let keep = 1;
    for (const buf of frontAlphaBufs) keep *= 1 - buf[py * H + px];
    out[i] = 1 - keep;
  }
  return out;
}

/* Per-atom evidence mask from the depth stack.
   alphaBufs aligned with layers. An atom of layer k is evidence-free if the
   strictly-in-front stack covers it beyond COVER_THRESH, or its layer is
   user-hidden. */
function autoHidden(obs, layerOf, layers, alphaBufs, H) {
  const mask = new Array(obs.length).fill(false);
  const covFrac = layers.map(() => 0);
  const counts = layers.map(() => 0);
  for (let i = 0; i < obs.length; i++) {
    const k = layerOf[i];
    if (k < 0) continue;                       // clutter: always evidenced
    counts[k]++;
    if (layers[k].hidden) { mask[i] = true; covFrac[k]++; continue; }
    const front = [];
    for (let m = 0; m < layers.length; m++)
      if (m !== k && !layers[m].hidden && layers[m].depth > layers[k].depth)
        front.push(alphaBufs[m]);
    if (!front.length) continue;
    const cov = coverageOf([obs[i]], front, H)[0];
    if (cov > COVER_THRESH) { mask[i] = true; covFrac[k]++; }
  }
  for (let k = 0; k < layers.length; k++)
    covFrac[k] = counts[k] ? covFrac[k] / counts[k] : 0;
  return { mask, covFrac };
}

/* Painter compositing: layers back-to-front by depth.
   rgbBufs[k]: Float32Array(3*H*H) pre-sigmoid; alphaBufs[k]: from
   alphaFromPre. Base is mid-gray (sigmoid(0)). */
function compositePainter(order, rgbBufs, alphaBufs, H, out) {
  out = out || new Uint8ClampedArray(4 * H * H);
  const n = H * H;
  const acc = new Float32Array(3 * n);
  for (let i = 0; i < n; i++) { acc[i] = 127.5; acc[n + i] = 127.5; acc[2 * n + i] = 127.5; }
  for (const k of order) {
    const pre = rgbBufs[k], al = alphaBufs[k];
    for (let i = 0; i < n; i++) {
      const a = al[i];
      if (a < 1e-3) continue;
      acc[i]         += a * (255 / (1 + Math.exp(-2 * pre[i]))         - acc[i]);
      acc[n + i]     += a * (255 / (1 + Math.exp(-2 * pre[n + i]))     - acc[n + i]);
      acc[2 * n + i] += a * (255 / (1 + Math.exp(-2 * pre[2 * n + i])) - acc[2 * n + i]);
    }
  }
  for (let i = 0; i < n; i++) {
    out[4 * i] = acc[i]; out[4 * i + 1] = acc[n + i];
    out[4 * i + 2] = acc[2 * n + i]; out[4 * i + 3] = 255;
  }
  return out;
}

/* Backdrop trim: whole-image fits carry large-sigma background pads that
   cover the full frame; for OBJECT layers, drop atoms whose envelope is
   wider than sigMax. HEURISTIC, honestly: it also kills any genuinely
   large object parts. The untrimmed layer is kept in the library. */
function trimAtoms(atoms, sigMax = 0.35) {
  const kept = atoms.filter(a => Math.max(a[3], a[4]) <= sigMax);
  if (!kept.length) return atoms.slice();
  // re-center xy so the trimmed set is a canonical template again
  let mx = 0, my = 0;
  for (const a of kept) { mx += a[0]; my += a[1]; }
  mx /= kept.length; my /= kept.length;
  return kept.map(a => { const b = a.slice(); b[0] -= mx; b[1] -= my; return b; });
}

/* Sim(2) composition in vote coordinates: (g2 ∘ g1). */
function composePose(g2, g1) {
  const s2 = Math.exp(g2[3]);
  const c = Math.cos(g2[2]), s = Math.sin(g2[2]);
  const wrap = d => ((d + Math.PI) % (2 * Math.PI) + 2 * Math.PI) % (2 * Math.PI) - Math.PI;
  return [s2 * (c * g1[0] - s * g1[1]) + g2[0],
          s2 * (s * g1[0] + c * g1[1]) + g2[1],
          wrap(g1[2] + g2[2]), g1[3] + g2[3]];
}
function gestureTranslate(dx, dy) { return [dx, dy, 0, 0]; }
function gestureRotateAbout(c, drho) {
  const co = Math.cos(drho), si = Math.sin(drho);
  return [c[0] - (co * c[0] - si * c[1]), c[1] - (si * c[0] + co * c[1]), drho, 0];
}
function gestureScaleAbout(c, ds) {
  const s = Math.exp(ds);
  return [c[0] - s * c[0], c[1] - s * c[1], 0, ds];
}

if (typeof module !== "undefined") {
  module.exports = {
    ALPHA_C, COVER_THRESH, alphaFromPre, coverageOf, autoHidden,
    compositePainter, composePose, gestureTranslate, gestureRotateAbout,
    gestureScaleAbout, trimAtoms,
  };
}