Spaces:
Running
Running
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,
};
}
|