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