File size: 7,632 Bytes
e85709c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e38621f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e85709c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e38621f
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
/* 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; });
}

/* ---- Bet 9 additions: eraser + backdrop + band gains (pure, testable) ---- */

/* Frequency band of an atom. f is cycles/unit; the frame spans 2 units, so
   cycles-per-image = 2f. Bands follow the original splatstack dials:
   coarse < 4 c/img, mid 4–10, fine > 10.  Returns 0|1|2. */
const BAND_EDGES = [2.0, 5.0];
function bandOf(f) { return f < BAND_EDGES[0] ? 0 : f < BAND_EDGES[1] ? 1 : 2; }

/* Erase mask: true for atoms whose center falls inside the brush circle.
   skip(i) can veto (e.g. atoms of hidden layers stay untouched). */
function eraseMask(obs, wx, wy, r, skip) {
  const r2 = r * r;
  return obs.map((a, i) => {
    if (skip && skip(i)) return false;
    const dx = a[0] - wx, dy = a[1] - wy;
    return dx * dx + dy * dy < r2;
  });
}

/* Composite an already-sigmoided field over a backdrop.
   img: Uint8ClampedArray RGBA from sigmoidField/compositePainter (mutated).
   alpha: Float32Array(H*H) from alphaFromPre of the (band-gained) pre field.
   opacity: global gain on alpha, clamped to [0,1] per pixel.
   bg: either [r,g,b] flat color or a Uint8ClampedArray(4*H*H) RGBA image.
   Identity check: with alpha==1 everywhere or bg==the field itself the
   output is unchanged; with opacity such that a==1 the field wins. */
function blendOverBackdrop(img, alpha, opacity, bg, H) {
  const n = H * H;
  const flat = !(bg && bg.length === 4 * n);
  for (let i = 0; i < n; i++) {
    const a = Math.min(1, alpha[i] * opacity);
    const br = flat ? bg[0] : bg[4 * i],
          bgr = flat ? bg[1] : bg[4 * i + 1],
          bb = flat ? bg[2] : bg[4 * i + 2];
    img[4 * i]     = br  + a * (img[4 * i]     - br);
    img[4 * i + 1] = bgr + a * (img[4 * i + 1] - bgr);
    img[4 * i + 2] = bb  + a * (img[4 * i + 2] - bb);
  }
  return img;
}

/* 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,
    BAND_EDGES, bandOf, eraseMask, blendOverBackdrop,
  };
}