Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Slapstack Studio — spawn objects, move them, in Gabor space</title> | |
| <style> | |
| :root { | |
| --chassis:#1d222e; --panel:#262c3b; --panel-edge:#333b4f; --well:#10131b; | |
| --ink:#e3e7f0; --muted:#8d96ab; --phosphor:#ffb454; --outlier:#78808f; | |
| --entropy:#c86bff; --go:#7fd98a; --bad:#ff7a7a; | |
| --mono: ui-monospace,"SF Mono","Cascadia Mono",Menlo,Consolas,monospace; | |
| --cond:"Bahnschrift","Arial Narrow","Roboto Condensed",sans-serif; | |
| } | |
| *{box-sizing:border-box;margin:0} | |
| html,body{height:100%} | |
| body{background:var(--chassis);color:var(--ink);font-family:var(--mono); | |
| font-size:13px;display:flex;flex-direction:column;overflow:hidden} | |
| @media (prefers-reduced-motion:reduce){*{animation:none;transition:none}} | |
| header{display:flex;align-items:baseline;gap:14px;padding:10px 16px 9px; | |
| border-bottom:1px solid var(--panel-edge);flex-wrap:wrap} | |
| header h1{font-family:var(--cond);font-size:20px;letter-spacing:2.5px;font-weight:700} | |
| header h1 em{color:var(--phosphor);font-style:normal} | |
| header .claim{color:var(--muted);font-size:11.5px} | |
| #state-lamp{margin-left:auto;font-size:11px;letter-spacing:1.5px;padding:3px 10px; | |
| border-radius:3px;border:1px solid var(--panel-edge);color:var(--muted)} | |
| #state-lamp.soup{color:var(--outlier);border-color:var(--outlier)} | |
| #state-lamp.binding{color:var(--entropy);border-color:var(--entropy)} | |
| #state-lamp.bound{color:var(--go);border-color:var(--go)} | |
| main{flex:1;display:flex;gap:12px;padding:12px 16px;min-height:0} | |
| #stage-wrap{flex:1;display:flex;align-items:center;justify-content:center;min-width:0} | |
| #stage{position:relative;background:var(--well);border:1px solid var(--panel-edge); | |
| border-radius:6px;box-shadow:inset 0 0 60px rgba(0,0,0,.55)} | |
| #stage canvas{position:absolute;inset:0;width:100%;height:100%;border-radius:6px} | |
| #overlay-canvas{cursor:grab} | |
| #overlay-canvas.dragging{cursor:grabbing} | |
| #stage .hint{position:absolute;bottom:7px;left:0;right:0;text-align:center; | |
| color:var(--muted);font-size:10.5px;pointer-events:none;text-shadow:0 1px 3px #000} | |
| aside{width:330px;flex-shrink:0;display:flex;flex-direction:column;gap:9px; | |
| overflow-y:auto;padding-right:2px} | |
| .card{background:var(--panel);border:1px solid var(--panel-edge);border-radius:6px; | |
| padding:10px 11px} | |
| .card h2{font-family:var(--cond);font-size:12px;letter-spacing:2px;font-weight:700; | |
| color:var(--muted);margin-bottom:8px} | |
| button{font-family:var(--mono);font-size:12px;background:var(--well);color:var(--ink); | |
| border:1px solid var(--panel-edge);border-radius:4px;padding:6px 11px;cursor:pointer} | |
| button:hover{border-color:var(--phosphor);color:var(--phosphor)} | |
| button:focus-visible{outline:2px solid var(--phosphor);outline-offset:1px} | |
| button:disabled{opacity:.35;cursor:default} | |
| button:disabled:hover{border-color:var(--panel-edge);color:var(--ink)} | |
| button.primary{border-color:var(--phosphor);color:var(--phosphor)} | |
| .row{display:flex;gap:7px;align-items:center} | |
| .row+.row{margin-top:7px} | |
| .readout{color:var(--phosphor)} .dim{color:var(--muted)} .spacer{flex:1} | |
| #bind-meter{height:4px;background:var(--well);border-radius:2px;overflow:hidden;margin-top:8px} | |
| #bind-meter>div{height:100%;width:0%;background:var(--entropy)} | |
| #metrics{display:grid;grid-template-columns:1fr 1fr;gap:3px 10px;margin-top:8px;font-size:11.5px} | |
| .layer{border-top:1px solid var(--panel-edge);padding:7px 0;border-radius:3px} | |
| .layer:first-of-type{border-top:0} | |
| .layer.sel{outline:1px solid var(--phosphor);outline-offset:2px} | |
| .layer .name{display:flex;align-items:center;gap:7px;cursor:pointer} | |
| .swatch{width:10px;height:10px;border-radius:2px;flex-shrink:0} | |
| .layer .pose{color:var(--muted);font-size:10.5px;margin:4px 0 5px;min-height:13px} | |
| .belief-bar{flex:1;height:5px;background:var(--well);border-radius:3px;overflow:hidden;max-width:80px} | |
| .belief-bar>div{height:100%;background:var(--go)} | |
| .layer .ctl{display:flex;gap:4px;flex-wrap:wrap;margin-top:5px} | |
| .layer .ctl button{padding:3px 7px;font-size:10.5px} | |
| #library{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px} | |
| .chip{display:flex;align-items:center;gap:5px;background:var(--well); | |
| border:1px solid var(--panel-edge);border-radius:12px;padding:3px 9px;font-size:11px} | |
| .chip button{border:0;padding:0 2px;font-size:12px;background:none} | |
| label.tog{display:flex;gap:8px;align-items:center;cursor:pointer;padding:2px 0;font-size:12px} | |
| label.tog input{accent-color:var(--phosphor)} | |
| input[type=range]{flex:1;accent-color:var(--phosphor)} | |
| .gain-row{display:grid;grid-template-columns:54px 1fr 40px;gap:6px;align-items:center;margin-top:3px} | |
| .gain-row input[type=range]{width:100%;margin:0} | |
| .gain-row .val{text-align:right} | |
| input[type=color]{width:36px;height:22px;padding:1px;border:1px solid var(--panel-edge);background:var(--well);border-radius:4px;cursor:pointer} | |
| #stage.erasing #overlay-canvas{cursor:crosshair} | |
| input[type=text],textarea{font-family:var(--mono);font-size:12px;background:var(--well); | |
| color:var(--ink);border:1px solid var(--panel-edge);border-radius:4px;padding:6px 8px;width:100%} | |
| input[type=text]:focus-visible,textarea:focus-visible{outline:2px solid var(--phosphor);border-color:transparent} | |
| #spawn-status,#prompt-log{color:var(--muted);font-size:10.5px;margin-top:6px; | |
| min-height:13px;white-space:pre-wrap} | |
| #ledger p{font-size:10.5px;line-height:1.5;color:var(--muted);margin-bottom:6px} | |
| #ledger b{color:var(--go);font-weight:400} | |
| #ledger .no{color:var(--entropy)} | |
| details summary{cursor:pointer;font-family:var(--cond);font-size:12px;letter-spacing:2px;color:var(--muted)} | |
| details[open] summary{margin-bottom:7px} | |
| input[type=file]{font-size:10.5px;color:var(--muted);max-width:170px} | |
| @media (max-width:900px){main{flex-direction:column;overflow-y:auto}body{overflow:auto}aside{width:100%}} | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <h1>SLAPSTACK <em>STUDIO</em></h1> | |
| <span class="claim">spawn layers with AI · scramble · bind · grab · stack · every entity is a posterior</span> | |
| <span id="state-lamp" class="soup">ATOM SOUP</span> | |
| </header> | |
| <main> | |
| <div id="stage-wrap"> | |
| <div id="stage"> | |
| <canvas id="scene-canvas"></canvas> | |
| <canvas id="overlay-canvas"></canvas> | |
| <div class="hint">drag = pose clamp · wheel rotate · shift+wheel scale · shift+click = hint · erase tool: brush deletes atoms</div> | |
| </div> | |
| </div> | |
| <aside> | |
| <div class="card"> | |
| <h2>SCENE</h2> | |
| <div class="row"> | |
| <button id="btn-scramble">Scramble</button> | |
| <button id="btn-bind" class="primary">Bind</button> | |
| <span class="spacer"></span> | |
| <span class="dim">noise</span> | |
| <input type="range" id="noise" min="0" max="10" value="1" style="width:60px" aria-label="observation noise"> | |
| </div> | |
| <div class="row"> | |
| <span class="dim">composite</span> | |
| <label class="tog"><input type="radio" name="comp" value="field" checked> field</label> | |
| <label class="tog"><input type="radio" name="comp" value="painter"> painter (depth)</label> | |
| <span class="spacer"></span> | |
| <label class="tog"><input type="checkbox" id="auto-occ" checked> depth occlusion</label> | |
| </div> | |
| <div id="bind-meter"><div></div></div> | |
| <div id="metrics"> | |
| <span class="dim">binding vs truth</span><span class="readout" id="m-acc">—</span> | |
| <span class="dim">mean entropy</span><span class="readout" id="m-ent">—</span> | |
| <span class="dim">BP iterations</span><span class="readout" id="m-it">—</span> | |
| <span class="dim">atoms in scene</span><span class="readout" id="m-n">—</span> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h2>EDIT & LOOK</h2> | |
| <div class="row"> | |
| <span class="dim">tool</span> | |
| <label class="tog"><input type="radio" name="tool" value="pose" checked> pose</label> | |
| <label class="tog"><input type="radio" name="tool" value="erase"> erase</label> | |
| <span class="spacer"></span> | |
| <span class="dim">brush</span> | |
| <label class="tog"><input type="radio" name="brush" value="0.05"> S</label> | |
| <label class="tog"><input type="radio" name="brush" value="0.12" checked> M</label> | |
| <label class="tog"><input type="radio" name="brush" value="0.25"> L</label> | |
| </div> | |
| <div class="row"> | |
| <button id="btn-undo-erase" disabled>Undo erase</button> | |
| <button id="btn-restore-erase" disabled>Restore all</button> | |
| <label class="tog"><input type="checkbox" id="erase-burn"> burn into template</label> | |
| </div> | |
| <div class="row"> | |
| <span class="dim">backdrop</span> | |
| <input type="color" id="bg-color" value="#808080" title="backdrop color"> | |
| <input type="file" id="bg-file" accept="image/*" style="max-width:46%"> | |
| <button id="btn-bg-clear">clear img</button> | |
| </div> | |
| <div class="gain-row"><span class="dim">coarse</span><input type="range" id="g-coarse" min="0" max="200" value="100"><span class="val readout" id="g-coarse-v">1.00</span></div> | |
| <div class="gain-row"><span class="dim">mid</span><input type="range" id="g-mid" min="0" max="200" value="100"><span class="val readout" id="g-mid-v">1.00</span></div> | |
| <div class="gain-row"><span class="dim">fine</span><input type="range" id="g-fine" min="0" max="200" value="100"><span class="val readout" id="g-fine-v">1.00</span></div> | |
| <div class="gain-row"><span class="dim">opacity</span><input type="range" id="g-op" min="0" max="200" value="100"><span class="val readout" id="g-op-v">1.00</span></div> | |
| <div class="dim" style="font-size:10.5px;margin-top:6px">Erase deletes atoms under the brush (render only, until <b>burn</b> writes it into the layer template; Scramble/Bind rebuild the scene from templates). Undo works until the next Bind. Backdrop is presentation and survives everything. Band gains (coarse<4, mid 4–10, fine>10 c/img) scale atom colors in field mode; opacity blends the whole field over the backdrop.</div> | |
| </div> | |
| <div class="card"> | |
| <h2>LAYERS <span class="dim" style="letter-spacing:0">(front is highest)</span></h2> | |
| <div id="layer-list"></div> | |
| <div class="dim" style="font-size:10.5px;margin-top:6px">library — place to add to the scene:</div> | |
| <div id="library"></div> | |
| </div> | |
| <div class="card"> | |
| <h2>SPAWN A LAYER</h2> | |
| <div class="row"> | |
| <input type="text" id="spawn-prompt" placeholder='prompt, e.g. "a red rocket, flat colors"'> | |
| </div> | |
| <div class="row"> | |
| <input type="text" id="spawn-token" placeholder="matte token (e.g. rocket)" style="width:55%"> | |
| <button id="btn-spawn-text">Distill (GPU)</button> | |
| </div> | |
| <div class="row"> | |
| <input type="file" id="spawn-file" accept="image/*"> | |
| <button id="btn-spawn-img">Fit (CPU)</button> | |
| </div> | |
| <div class="row"> | |
| <input type="text" id="spawn-json" placeholder="or paste layer JSON"> | |
| <button id="btn-spawn-json">Add</button> | |
| </div> | |
| <div id="spawn-status">Distill runs Stable Diffusion score distillation with a cross-attention matte (GPU Space only, untested — first run is a smoke test). Fit trains atoms to your image on CPU (~1 min; objects on plain mid-gray come out compact).</div> | |
| </div> | |
| <div class="card"> | |
| <h2>PROMPT AS INTERVENTION</h2> | |
| <div class="row"> | |
| <input type="text" id="prompt" placeholder='e.g. "move the boat left, put the star behind the tractor"'> | |
| <button id="btn-prompt">Do</button> | |
| </div> | |
| <div id="prompt-log">Commands become pose clamps and depth moves; BP re-settles. Free-form uses Claude when this runs inside claude.ai; a local grammar otherwise.</div> | |
| </div> | |
| <div class="card"> | |
| <h2>OVERLAYS</h2> | |
| <label class="tog"><input type="checkbox" id="ov-atoms" checked> atoms, colored by belief</label> | |
| <label class="tog"><input type="checkbox" id="ov-own"> ownership tint</label> | |
| <label class="tog"><input type="checkbox" id="ov-ent"> entropy border glow</label> | |
| <label class="tog"><input type="checkbox" id="ov-pose" checked> pose belief (crosshair + covariance)</label> | |
| <label class="tog"><input type="checkbox" id="ov-hints" checked> semantic hints</label> | |
| </div> | |
| <div class="card" id="ledger"> | |
| <details> | |
| <summary>HONEST LEDGER</summary> | |
| <p><b>Verified in this build:</b> the BP core matches the SlapstackBet6 Python (transform 2e-16, render MSE 8.6e-8, identical binding); drag = pose clamp + re-settle; identity persists through occlusion; depth occlusion removes evidence honestly (marginals revert toward the prior, coverage releases when moved away); the semantic likelihood factor resolves the twins case (two identical stars: 0.05 acc on geometry alone, 0.99 with location priors) and a mildly wrong prior is overridden by evidence; image→layer fitting runs end to end on CPU. <b>Bet 9:</b> the eraser filters per-atom state (order-free, so undo is re-append), burn edits the template so erases survive rebuilds; the backdrop is pure presentation and persists across Bind/Scramble; band gains exploit render linearity (gain·color ≡ gain·contribution), verified in tests_edit.js.</p> | |
| <p><span class="no">Not verified:</span> text→layer (SDS) and cross-attention matte are line-for-line adaptations of the verified Bet-5 GPU loop but have NOT been executed on a GPU — first run is a smoke test; SD attention maps flicker across seeds, which is why they enter BP as a prior with a weight, never as truth. Real-photo scene→atoms remains open. Painter compositing and the alpha matte are presentation choices, not the additive field model.</p> | |
| </details> | |
| </div> | |
| </aside> | |
| </main> | |
| <script> | |
| /* Slapstack Playroom core — faithful JS port of SlapstackBet6 math. | |
| FIELDS: [x, y, theta, su, sv, f, phase, r, g, b] | |
| Verified against bet6_open.py / bet6_bp_binding.py / bet6_multimodal.py | |
| by tests_node.js before being embedded in the playroom. */ | |
| ; | |
| const TAU = Math.PI * 2; | |
| function wrapPi(d) { | |
| return ((d + Math.PI) % TAU + TAU) % TAU - Math.PI; | |
| } | |
| function rotApply(rho, x, y) { | |
| const c = Math.cos(rho), s = Math.sin(rho); | |
| return [c * x - s * y, s * x + c * y]; | |
| } | |
| /* Exact Sim(2) action on atom parameters (the Bet 5 algebra): | |
| xy -> s R xy + t, theta -> theta + rho, sigma -> s sigma, f -> f/s. | |
| Envelope-relative phase and color are INVARIANT. */ | |
| function transformAtoms(atoms, xi) { | |
| const [tx, ty, rho, lam] = xi; | |
| const s = Math.exp(lam); | |
| const out = new Array(atoms.length); | |
| for (let i = 0; i < atoms.length; i++) { | |
| const a = atoms[i]; | |
| const [rx, ry] = rotApply(rho, a[0], a[1]); | |
| out[i] = [ | |
| s * rx + tx, s * ry + ty, | |
| a[2] + rho, | |
| a[3] * s, a[4] * s, | |
| a[5] / s, | |
| a[6], | |
| a[7], a[8], a[9], | |
| ]; | |
| } | |
| return out; | |
| } | |
| /* Sim(2)-invariant intrinsic signature: identity lives here. */ | |
| function signature(atoms) { | |
| return atoms.map(a => [ | |
| Math.log(a[3] * a[5]), | |
| Math.log(a[3] / a[4]), | |
| Math.cos(a[6]), Math.sin(a[6]), | |
| a[7], a[8], a[9], | |
| ]); | |
| } | |
| /* Two pose-vote hypotheses per correspondence (pi-ambiguity fix): | |
| H0: rho = d_theta, phi_obs == phi_tmpl | |
| H1: rho = d_theta + pi, phi_obs == -phi_tmpl */ | |
| function poseVotes2pi(obs, tmpl, sigPhase = 0.35) { | |
| const s = Math.pow((obs[3] / tmpl[3]) * (obs[4] / tmpl[4]) * (tmpl[5] / obs[5]), 1 / 3); | |
| const dTheta = obs[2] - tmpl[2]; | |
| const out = []; | |
| for (let H = 0; H < 2; H++) { | |
| const rho = wrapPi(dTheta + H * Math.PI); | |
| const phiExp = H === 0 ? tmpl[6] : -tmpl[6]; | |
| const dphi = wrapPi(obs[6] - phiExp); | |
| const pc = -0.5 * dphi * dphi / (sigPhase * sigPhase); | |
| const [rx, ry] = rotApply(rho, tmpl[0], tmpl[1]); | |
| out.push([[obs[0] - s * rx, obs[1] - s * ry, rho, Math.log(s)], pc]); | |
| } | |
| return out; | |
| } | |
| /* ------- small dense linear algebra on 4x4 (row-major flat arrays) ------- */ | |
| function mat4Inv(m) { | |
| // Gauss-Jordan, fine for well-conditioned SPD 4x4s here. | |
| const a = m.map(r => r.slice()); | |
| const inv = [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]; | |
| for (let col = 0; col < 4; col++) { | |
| let piv = col; | |
| for (let r = col + 1; r < 4; r++) | |
| if (Math.abs(a[r][col]) > Math.abs(a[piv][col])) piv = r; | |
| [a[col], a[piv]] = [a[piv], a[col]]; | |
| [inv[col], inv[piv]] = [inv[piv], inv[col]]; | |
| const d = a[col][col]; | |
| for (let j = 0; j < 4; j++) { a[col][j] /= d; inv[col][j] /= d; } | |
| for (let r = 0; r < 4; r++) { | |
| if (r === col) continue; | |
| const f = a[r][col]; | |
| for (let j = 0; j < 4; j++) { a[r][j] -= f * a[col][j]; inv[r][j] -= f * inv[col][j]; } | |
| } | |
| } | |
| return inv; | |
| } | |
| function mat4Det(m) { | |
| const a = m.map(r => r.slice()); | |
| let det = 1; | |
| for (let col = 0; col < 4; col++) { | |
| let piv = col; | |
| for (let r = col + 1; r < 4; r++) | |
| if (Math.abs(a[r][col]) > Math.abs(a[piv][col])) piv = r; | |
| if (piv !== col) { [a[col], a[piv]] = [a[piv], a[col]]; det = -det; } | |
| det *= a[col][col]; | |
| if (a[col][col] === 0) return 0; | |
| for (let r = col + 1; r < 4; r++) { | |
| const f = a[r][col] / a[col][col]; | |
| for (let j = col; j < 4; j++) a[r][j] -= f * a[col][j]; | |
| } | |
| } | |
| return det; | |
| } | |
| function mat4Vec(m, v) { | |
| return [0,1,2,3].map(i => m[i][0]*v[0]+m[i][1]*v[1]+m[i][2]*v[2]+m[i][3]*v[3]); | |
| } | |
| function matAdd(A, B, wB = 1) { | |
| return A.map((row, i) => row.map((x, j) => x + wB * B[i][j])); | |
| } | |
| /* Greedy mode-seeking init (angle-aware), port of _density_peaks. */ | |
| function densityPeaks(votes, weights, M, radius = 0.45) { | |
| const scale = [0.15, 0.15, 0.30, 0.20]; | |
| const n = votes.length; | |
| const dens = new Float64Array(n); | |
| for (let i = 0; i < n; i++) { | |
| let acc = 0; | |
| for (let k = 0; k < n; k++) { | |
| const d0 = (votes[k][0] - votes[i][0]) / scale[0]; | |
| const d1 = (votes[k][1] - votes[i][1]) / scale[1]; | |
| const d2 = wrapPi(votes[k][2] - votes[i][2]) / scale[2]; | |
| const d3 = (votes[k][3] - votes[i][3]) / scale[3]; | |
| acc += weights[k] * Math.exp(-0.5 * (d0*d0 + d1*d1 + d2*d2 + d3*d3)); | |
| } | |
| dens[i] = acc; | |
| } | |
| const peaks = []; | |
| const alive = new Uint8Array(n).fill(1); | |
| for (let m = 0; m < M; m++) { | |
| let best = -1, bestD = -Infinity; | |
| for (let i = 0; i < n; i++) | |
| if (alive[i] && dens[i] > bestD) { bestD = dens[i]; best = i; } | |
| if (best < 0) break; | |
| peaks.push(votes[best].slice()); | |
| for (let i = 0; i < n; i++) { | |
| const dxy = Math.hypot(votes[i][0] - votes[best][0], votes[i][1] - votes[best][1]); | |
| const dr = Math.abs(wrapPi(votes[i][2] - votes[best][2])); | |
| if (dxy + dr <= radius) alive[i] = 0; | |
| } | |
| } | |
| return peaks; | |
| } | |
| /* Loopy BP binding of obs atoms to K templates at unknown poses. | |
| Port of bet6_open.bp_bind: candidates carry both pi-hypotheses, | |
| cavity messages, damping, branch-aligned rotation fusion. | |
| Options: clampPose — array of length K; if clampPose[k] is a pose xi, | |
| object k's pose is held fixed (conditioning-as-intervention) and only | |
| the assignment beliefs re-equilibrate around it. */ | |
| function bpBind(templates, obs, opts = {}) { | |
| const iters = opts.iters ?? 40; | |
| const damping = opts.damping ?? 0.5; | |
| const cavity = opts.cavity ?? true; | |
| const sigVar = opts.sigVar ?? 0.08; | |
| const outLL = opts.outLL ?? -14.0; | |
| const clampPose = opts.clampPose ?? null; | |
| const hiddenMask = opts.hiddenMask ?? null; // per-atom: true = no evidence | |
| const onIter = opts.onIter ?? null; | |
| const K = templates.length; | |
| const sigT = templates.map(signature); | |
| const sigO = signature(obs); | |
| const N = obs.length; | |
| const Vdiag = [0.03 * 0.03, 0.03 * 0.03, 0.05 * 0.05, 0.05 * 0.05]; | |
| const Vinv = [[1/Vdiag[0],0,0,0],[0,1/Vdiag[1],0,0],[0,0,1/Vdiag[2],0],[0,0,0,1/Vdiag[3]]]; | |
| const Vmat = [[Vdiag[0],0,0,0],[0,Vdiag[1],0,0],[0,0,Vdiag[2],0],[0,0,0,Vdiag[3]]]; | |
| const P0inv = [[1e-2,0,0,0],[0,1e-2,0,0],[0,0,1e-2,0],[0,0,0,1e-2]]; | |
| // ---- candidate generation: 3 signature-nearest per template, 2 hypotheses | |
| const cands = [], votes = [], base = []; | |
| for (let i = 0; i < N; i++) { | |
| const c = [], v = [], b = []; | |
| if (!(hiddenMask && hiddenMask[i])) { | |
| for (let k = 0; k < K; k++) { | |
| const d2 = sigT[k].map(st => { | |
| let acc = 0; | |
| for (let q = 0; q < 7; q++) { const d = st[q] - sigO[i][q]; acc += d * d; } | |
| return acc; | |
| }); | |
| const order = d2.map((d, j) => [d, j]).sort((p, q) => p[0] - q[0]).slice(0, 3); | |
| for (const [dj, j] of order) { | |
| for (const [xi, pc] of poseVotes2pi(obs[i], templates[k][j])) { | |
| c.push([k, j]); v.push(xi); b.push(-0.5 * dj / sigVar + pc); | |
| } | |
| } | |
| } | |
| } | |
| cands.push(c); votes.push(v); base.push(b); | |
| } | |
| // ---- beliefs seeded from the identity+phase channel | |
| let B = []; | |
| for (let i = 0; i < N; i++) { | |
| const ll = base[i].concat([outLL]); | |
| const mx = Math.max(...ll); | |
| let e = ll.map(x => Math.exp(x - mx)); | |
| const s = e.reduce((a, x) => a + x, 0); | |
| B.push(e.map(x => x / s)); | |
| } | |
| // ---- pose init: density peak of each object's votes (or the clamp) | |
| let mu = []; | |
| for (let k = 0; k < K; k++) { | |
| if (clampPose && clampPose[k]) { mu.push(clampPose[k].slice()); continue; } | |
| const vk = [], wk = []; | |
| for (let i = 0; i < N; i++) | |
| for (let ci = 0; ci < cands[i].length; ci++) | |
| if (cands[i][ci][0] === k) { vk.push(votes[i][ci]); wk.push(B[i][ci]); } | |
| mu.push(vk.length ? densityPeaks(vk, wk, 1)[0] : [0, 0, 0, 0]); | |
| } | |
| let Sig = mu.map(() => [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]); | |
| for (let it = 0; it < iters; it++) { | |
| // pose fusion | |
| const Lam = [], eta = []; | |
| for (let k = 0; k < K; k++) { Lam.push(P0inv.map(r => r.slice())); eta.push([0,0,0,0]); } | |
| for (let i = 0; i < N; i++) { | |
| for (let ci = 0; ci < cands[i].length; ci++) { | |
| const k = cands[i][ci][0]; | |
| const v = votes[i][ci].slice(); | |
| v[2] = mu[k][2] + wrapPi(v[2] - mu[k][2]); | |
| const w = B[i][ci]; | |
| Lam[k] = matAdd(Lam[k], Vinv, w); | |
| const Vv = mat4Vec(Vinv, v); | |
| for (let q = 0; q < 4; q++) eta[k][q] += w * Vv[q]; | |
| } | |
| } | |
| Sig = Lam.map(mat4Inv); | |
| for (let k = 0; k < K; k++) { | |
| if (clampPose && clampPose[k]) { | |
| mu[k] = clampPose[k].slice(); | |
| Sig[k] = [[1e-6,0,0,0],[0,1e-6,0,0],[0,0,1e-6,0],[0,0,0,1e-6]]; | |
| } else { | |
| mu[k] = mat4Vec(Sig[k], eta[k]); | |
| mu[k][2] = wrapPi(mu[k][2]); | |
| } | |
| } | |
| // assignment update with cavity | |
| const newB = []; | |
| for (let i = 0; i < N; i++) { | |
| const nc = cands[i].length; | |
| const ll = new Array(nc + 1); | |
| for (let ci = 0; ci < nc; ci++) { | |
| const k = cands[i][ci][0]; | |
| const v = votes[i][ci].slice(); | |
| v[2] = mu[k][2] + wrapPi(v[2] - mu[k][2]); | |
| let mC, Sk; | |
| if (cavity && !(clampPose && clampPose[k])) { | |
| const Lc = matAdd(Lam[k], Vinv, -B[i][ci]); | |
| const Vv = mat4Vec(Vinv, v); | |
| const ecav = [0,1,2,3].map(q => eta[k][q] - B[i][ci] * Vv[q]); | |
| const Sc = mat4Inv(Lc); | |
| mC = mat4Vec(Sc, ecav); | |
| Sk = Sc; | |
| } else { | |
| mC = mu[k]; Sk = Sig[k]; | |
| } | |
| const r = [v[0] - mC[0], v[1] - mC[1], wrapPi(v[2] - mC[2]), v[3] - mC[3]]; | |
| const Cov = matAdd(Sk, Vmat, 1); | |
| const Ci = mat4Inv(Cov); | |
| const Cr = mat4Vec(Ci, r); | |
| const quad = r[0]*Cr[0] + r[1]*Cr[1] + r[2]*Cr[2] + r[3]*Cr[3]; | |
| ll[ci] = base[i][ci] - 0.5 * quad - 0.5 * Math.log(mat4Det(Cov)); | |
| } | |
| ll[nc] = outLL; | |
| const mx = Math.max(...ll); | |
| let e = ll.map(x => Math.exp(x - mx)); | |
| const s = e.reduce((a, x) => a + x, 0); | |
| e = e.map(x => x / s); | |
| newB.push(e.map((x, q) => damping * x + (1 - damping) * B[i][q])); | |
| } | |
| B = newB; | |
| if (onIter) onIter(it, marginals(), mu, Sig); | |
| } | |
| function marginals() { | |
| const marg = []; | |
| for (let i = 0; i < N; i++) { | |
| if (cands[i].length === 0) { | |
| // no evidence: assignment belief reverts to the prior (uniform), | |
| // not to a confident "outlier" — this is what permanence means. | |
| marg.push(new Array(K + 1).fill(1 / (K + 1))); | |
| continue; | |
| } | |
| const m = new Array(K + 1).fill(0); | |
| for (let ci = 0; ci < cands[i].length; ci++) m[cands[i][ci][0]] += B[i][ci]; | |
| m[K] = B[i][cands[i].length]; | |
| marg.push(m); | |
| } | |
| return marg; | |
| } | |
| return { marg: marginals(), mu, Sig, cands, votes, B }; | |
| } | |
| /* Numpy-matching reference render (verification + full-res compositor). | |
| pre[c] += color_c * env * carrier ; out = sigmoid(2 * pre). | |
| Returns {pre: Float32Array(3*H*H)} pre-sigmoid field. */ | |
| function renderPre(atoms, H, pre) { | |
| pre = pre || new Float32Array(3 * H * H); | |
| const lim = 3.2; // envelope support cut, in sigmas | |
| for (const a of atoms) { | |
| const [ax, ay, th, su, sv, f, ph, r, g, b] = a; | |
| const ct = Math.cos(th), st = Math.sin(th); | |
| const rad = lim * Math.max(su, sv); | |
| // pixel bbox: x in [-1,1] maps to col (H-1)*(x+1)/2 | |
| const x0 = Math.max(0, Math.floor((ax - rad + 1) / 2 * (H - 1))); | |
| const x1 = Math.min(H - 1, Math.ceil((ax + rad + 1) / 2 * (H - 1))); | |
| const y0 = Math.max(0, Math.floor((ay - rad + 1) / 2 * (H - 1))); | |
| const y1 = Math.min(H - 1, Math.ceil((ay + rad + 1) / 2 * (H - 1))); | |
| for (let py = y0; py <= y1; py++) { | |
| const Y = -1 + 2 * py / (H - 1); | |
| const dy = Y - ay; | |
| for (let px = x0; px <= x1; px++) { | |
| const X = -1 + 2 * px / (H - 1); | |
| const dx = X - ax; | |
| const u = ct * dx + st * dy; | |
| const v = -st * dx + ct * dy; | |
| const eArg = 0.5 * ((u / su) * (u / su) + (v / sv) * (v / sv)); | |
| if (eArg > lim * lim / 2 * 1.6) continue; | |
| const env = Math.exp(-eArg); | |
| const car = Math.cos(TAU * f * u + ph); | |
| const ec = env * car; | |
| const idx = py * H + px; | |
| pre[idx] += r * ec; | |
| pre[H * H + idx] += g * ec; | |
| pre[2 * H * H + idx] += b * ec; | |
| } | |
| } | |
| } | |
| return pre; | |
| } | |
| function sigmoidField(pre, H, out) { | |
| out = out || new Uint8ClampedArray(4 * H * H); | |
| const n = H * H; | |
| for (let i = 0; i < n; i++) { | |
| out[4 * i] = 255 / (1 + Math.exp(-2 * pre[i])); | |
| out[4 * i + 1] = 255 / (1 + Math.exp(-2 * pre[n + i])); | |
| out[4 * i + 2] = 255 / (1 + Math.exp(-2 * pre[2 * n + i])); | |
| out[4 * i + 3] = 255; | |
| } | |
| return out; | |
| } | |
| /* Ownership field: P(k|pixel) through the atoms' actual envelopes, | |
| energy-weighted. Port of bet6_open.ownership_field. */ | |
| function ownershipField(obs, marg, K, H) { | |
| const O = []; | |
| for (let k = 0; k <= K; k++) O.push(new Float32Array(H * H)); | |
| const lim = 3.2; | |
| for (let i = 0; i < obs.length; i++) { | |
| const a = obs[i]; | |
| const energy = Math.hypot(a[7], a[8], a[9]); | |
| const ct = Math.cos(a[2]), st = Math.sin(a[2]); | |
| const rad = lim * Math.max(a[3], a[4]); | |
| const x0 = Math.max(0, Math.floor((a[0] - rad + 1) / 2 * (H - 1))); | |
| const x1 = Math.min(H - 1, Math.ceil((a[0] + rad + 1) / 2 * (H - 1))); | |
| const y0 = Math.max(0, Math.floor((a[1] - rad + 1) / 2 * (H - 1))); | |
| const y1 = Math.min(H - 1, Math.ceil((a[1] + rad + 1) / 2 * (H - 1))); | |
| for (let py = y0; py <= y1; py++) { | |
| const Y = -1 + 2 * py / (H - 1); | |
| const dy = Y - a[1]; | |
| for (let px = x0; px <= x1; px++) { | |
| const X = -1 + 2 * px / (H - 1); | |
| const dx = X - a[0]; | |
| const u = ct * dx + st * dy; | |
| const v = -st * dx + ct * dy; | |
| const env = Math.exp(-0.5 * ((u / a[3]) ** 2 + (v / a[4]) ** 2)); | |
| const idx = py * H + px; | |
| for (let k = 0; k <= K; k++) O[k][idx] += marg[i][k] * energy * env; | |
| } | |
| } | |
| } | |
| const P = O.map(() => new Float32Array(H * H)); | |
| const ent = new Float32Array(H * H); | |
| const support = new Uint8Array(H * H); | |
| for (let idx = 0; idx < H * H; idx++) { | |
| let tot = 0; | |
| for (let k = 0; k <= K; k++) tot += O[k][idx]; | |
| support[idx] = tot > 0.05 ? 1 : 0; | |
| let e = 0; | |
| for (let k = 0; k <= K; k++) { | |
| const p = O[k][idx] / (tot + 1e-6); | |
| P[k][idx] = p; | |
| e -= p * Math.log2(p + 1e-12); | |
| } | |
| ent[idx] = e; | |
| } | |
| return { P, ent, support }; | |
| } | |
| /* Assignment entropy per atom, in bits. */ | |
| function atomEntropy(marg) { | |
| return marg.map(m => { | |
| let e = 0; | |
| for (const p of m) e -= p * Math.log2(p + 1e-12); | |
| return e; | |
| }); | |
| } | |
| </script> | |
| <script> | |
| /* 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]; | |
| } | |
| </script> | |
| <script> | |
| const BUILTINS = {"tractor": [[-0.6824, -0.45011, 2.66975, 0.70848, 0.92606, 0.44334, 5.19345, -0.09237, 0.04058, -0.01663], [-0.08584, -0.04802, 0.61626, 0.49888, 0.51436, 0.28444, 2.98078, -0.10011, 0.19875, 0.39398], [0.74864, 0.1407, 2.20893, 0.54502, 0.36724, 0.58724, 1.0072, 0.08821, 0.17557, 0.02929], [0.16677, -0.78973, 0.94754, 0.31915, 0.39057, 0.59383, 1.7488, 0.04293, 0.29863, 0.19046], [0.13943, -0.4621, 0.34775, 0.43809, 0.50801, 0.30553, 2.8646, -0.17673, 0.21056, 0.20304], [-0.05499, 0.43338, 1.6514, 0.61284, 0.45687, 0.32725, 1.11694, 0.16899, -0.3567, -0.20526], [-0.34716, 0.78101, 1.2311, 0.74696, 0.43588, 0.47387, 4.34096, 0.2865, 0.32634, 0.40835], [0.38634, 0.15072, 1.59185, 0.38671, 0.80153, 0.2006, 3.93334, -0.038, 0.2346, 0.21658], [-0.24576, -0.6867, 1.53196, 0.713, 0.38647, 0.52652, 3.95442, 0.02936, -0.05187, 0.00121], [0.35205, 0.08337, 0.53545, 0.47834, 0.43423, 0.76246, 4.15911, -0.20329, -0.08114, 0.10186], [0.35364, -0.86473, 2.06911, 1.01878, 0.6701, 0.60244, 6.00887, -0.05083, 0.02103, 0.00718], [-0.46331, 0.69726, 1.8988, 0.44293, 0.3441, 0.55859, 4.08877, 0.28312, 0.04183, 0.30179], [0.73797, -0.72063, 2.42647, 0.56377, 0.93892, 0.57773, 0.71269, -0.00084, 0.10102, 0.07746], [-0.15339, 0.31724, 0.95726, 0.50801, 0.54256, 0.30858, 0.53938, 0.02523, -0.10313, -0.26048], [0.70309, 0.45386, -0.24644, 0.30297, 0.45741, 0.70925, 4.42917, -0.02503, 0.12105, 0.27342], [0.23943, -0.29863, 0.8422, 0.33233, 0.34051, 0.22202, 3.33993, -0.1503, 0.45788, 0.30974], [-0.56384, 0.25397, 2.42246, 0.96112, 1.21737, 0.23618, 4.36676, 0.05613, 0.13365, 0.11434], [-0.36512, -0.00686, 1.45209, 0.49457, 0.31707, 0.8374, 0.28444, 0.49811, 0.06634, -0.11866], [-0.03486, 0.01865, 1.8686, 0.4669, 0.33716, 0.43868, 0.29331, 0.29541, -0.03762, -0.31555], [0.62497, 0.27048, 2.18782, 0.3361, 0.40184, 0.5221, 5.30031, -0.00354, -0.13067, -0.12973], [-0.41317, 0.11105, 0.67898, 0.48606, 0.59036, 0.28682, 5.49711, -0.10229, -0.22111, -0.29212], [-0.27571, -0.1504, 1.77764, 0.69312, 0.48048, 0.49554, 0.54236, 0.18581, 0.06243, -0.04552], [0.4641, 0.77883, 2.26806, 0.4379, 1.14871, 0.39349, 4.6492, -0.06707, -0.03793, 0.11233], [-0.56653, -0.8675, 1.28471, 0.68397, 0.3365, 0.29354, 1.07811, 0.03291, 0.04284, 0.04632], [-0.08734, 0.59009, 1.70998, 0.5352, 0.32565, 0.59399, 5.31167, -0.02259, 0.03768, 0.00686], [-0.00891, -0.13649, 0.47363, 0.39481, 0.42164, 0.37599, 6.05673, 0.05368, -0.38657, -0.22384], [0.36102, -0.14155, 1.21607, 0.16981, 0.19346, 0.64559, 0.12328, 0.14206, -0.0136, -0.00415], [-0.45032, 0.49812, 2.35641, 0.38137, 0.27461, 0.56179, 6.26456, -0.56531, -0.34132, -0.1892], [0.66365, 0.91126, 2.85526, 0.78185, 0.62224, 0.62045, 4.89805, 0.05002, -0.02027, -0.00306], [-0.78133, -0.49309, 2.58298, 0.49602, 0.94653, 0.35357, 5.84621, -0.0226, -0.05471, 0.01001], [-0.23984, 0.22701, 2.64843, 0.37651, 0.42961, 0.25471, 5.58736, -0.05288, -0.16166, -0.17766], [0.55904, 0.56313, 0.8993, 0.30673, 0.17036, 0.47454, 0.48998, -0.20619, -0.03915, -0.40454], [-0.51254, 0.78367, -0.03254, 0.35668, 0.5844, 0.4059, 5.63151, -0.02093, -0.4402, -0.38576], [-0.49183, 0.16821, -0.10355, 0.13637, 0.01697, 1.68471, 6.20816, -2.19706, -0.06284, -0.29033], [-0.75865, -0.37915, 3.63614, 0.10401, 0.17838, 2.60281, 3.56605, 0.09073, -0.11981, -0.17238], [0.17649, -0.18151, 3.10036, 0.11139, 0.32721, 0.96393, 0.67937, 0.28827, -0.27353, -0.49037], [-0.48705, 0.35371, 1.2931, 0.24513, 0.16342, 2.2225, 1.28356, -1.2217, -0.46139, -0.37179], [-0.01754, 0.55558, 2.53858, 0.27124, 0.13969, 1.24224, 3.99958, 0.01855, -0.10741, -0.03076], [-0.25695, 0.4068, 2.89412, 0.23868, 0.29239, 1.08662, 1.92178, 0.45239, 0.34315, 0.5327], [-0.23956, 0.25592, 2.23383, 0.04539, 0.10912, 3.7646, 2.57708, 5.02682, 0.98909, 0.47807], [0.628, -0.22848, 3.15552, 0.00944, 0.50113, 4.66656, 0.93257, -0.90353, 1.54592, 1.79618], [0.49409, 0.54229, 2.89216, 0.21768, 0.21806, 0.8736, 6.24187, -0.72302, -0.646, -0.51336], [-0.47663, 0.8961, 1.59818, 0.08723, 0.12112, 3.82644, 3.48794, -0.86773, -1.11731, -1.16287], [-0.71023, 0.2454, 1.27084, 0.22689, 0.22869, 1.32049, 1.42892, 0.24752, 0.09838, 0.2392], [-0.6411, -0.32881, 2.22783, 0.10306, 0.05474, 2.24301, 1.94946, -0.72905, 0.94648, 1.07426], [-0.10236, 0.0571, 2.16689, 0.11368, 0.21031, 1.10909, 2.59632, -0.30966, 0.21218, 0.25668], [0.40293, 0.14539, 3.149, 0.16438, 0.29952, 0.80921, 0.18805, 0.0656, -0.28125, -0.43057], [-0.33196, 0.22949, 2.05313, 0.13608, 0.28771, 1.73023, 4.73087, -0.82913, 0.45633, 0.43345], [0.22227, 0.09197, 1.3324, 0.09975, 0.09312, 1.08971, 3.3524, -0.11906, 0.54156, 0.12403], [0.11793, 0.39258, 1.5849, 0.06849, 0.14492, 4.87662, 2.85658, -0.79292, 1.02084, 1.32427], [0.52823, -0.67727, 0.21753, 0.11492, 0.15154, 1.38408, 3.847, -0.26577, 0.63177, 0.75418], [0.59787, -0.90371, 2.38042, 0.07061, 0.07692, 3.31604, 3.69185, 0.2139, -0.53046, -0.54509], [-0.10296, -0.98696, 0.9158, 0.28364, 0.09523, 3.94613, 1.23748, 0.07247, -0.17598, -0.20063], [-0.42743, 0.12942, 1.74352, 0.02628, 0.13178, 2.05194, 3.66078, -1.69632, 0.02142, 0.37604], [0.43108, 0.31065, 1.26171, 0.04935, 0.11457, 3.65908, 0.71281, 1.81829, -0.01638, -0.1652], [-0.64551, 0.15029, 2.79823, 0.10818, 0.0286, 1.30001, 0.2492, 1.64158, -0.61879, -0.63809], [-0.2631, -0.22718, 1.08716, 0.14952, 0.32458, 1.25624, 6.07547, 0.25972, -0.30185, -0.5652], [0.64946, -0.76278, 2.69333, 0.15504, 0.25941, 1.33319, 3.44865, 0.01556, -0.14194, -0.15755], [-0.07011, 0.47643, 0.05323, 0.05179, 0.10503, 2.85549, 4.88342, 0.95629, 1.01843, 1.2136], [0.51219, 0.76466, 1.61491, 0.06949, 0.12168, 2.3771, 0.83677, -1.79585, -1.71946, -1.62417], [-0.65308, -0.0253, 0.05561, 0.12108, 0.24282, 1.96344, 2.99588, -0.63173, 0.8612, 0.96971], [-0.75393, 0.6394, -0.58857, 0.07367, 0.13364, 1.96551, 5.57205, -1.79448, -1.70385, -1.79319], [0.67947, 0.7066, 0.76501, 0.05174, 0.08365, 3.63395, 3.78731, 2.9312, 2.8337, 2.84961], [0.78374, -0.85168, 2.90191, 0.08335, 0.05401, 1.72051, 3.89837, 0.067, -0.14074, -0.13737], [-0.65644, 0.48538, 2.2971, 0.07061, 0.13933, 1.6288, 5.87677, -0.28017, -0.3744, -0.77021], [-0.07299, -0.56282, 2.95749, 0.18481, 0.15234, 1.14593, 4.68782, -0.06461, 0.37184, 0.25643], [0.11921, -0.59593, 2.97536, 0.28076, 0.2065, 0.65973, 0.82948, 0.25906, -0.57613, -0.79168], [-0.15672, 0.40291, -0.49942, 0.09191, 0.12886, 2.13207, 3.86166, 2.41648, 0.83947, 0.87012], [-0.41411, 0.13279, 0.71077, 0.13767, 0.22816, 1.17284, 4.23561, -0.01769, 0.48793, 0.43909], [0.47887, 0.44707, 1.43521, 0.23495, 0.19113, 1.11754, 5.76585, -0.91451, -0.41865, -0.21804], [0.2231, 0.39557, 0.52648, 0.34275, 0.29421, 1.20222, 0.59739, 0.44874, 0.14287, 0.14493], [-0.07979, -0.47271, 2.52165, 0.37694, 0.05142, 2.13301, 4.86104, -0.25241, 0.37106, 0.44839], [-0.06135, -0.67232, 0.00337, 0.06471, 0.16861, 2.13669, 2.27478, -0.64686, 1.07496, 1.29683], [-0.85238, 0.43497, 0.10646, 0.19737, 0.14188, 2.26408, 0.61625, 0.72284, 0.51988, 0.7092], [-0.51825, 0.43221, -0.10887, 0.13855, 0.23206, 1.18669, 3.31322, 1.01161, 0.7205, 0.53151], [-0.12745, 0.61162, 0.59207, 0.07377, 0.20372, 3.15871, 1.16311, -0.81843, -0.967, -1.00712], [0.56756, -0.5616, 3.11854, 0.05498, 0.22625, 2.42556, 3.04, -0.24507, 0.9606, 0.9633], [0.42554, -0.29332, 3.22275, 0.14231, 0.29628, 1.05207, 0.46887, 0.32789, -0.28524, -0.58433], [0.6174, -0.22144, 3.13034, 0.0889, 0.28451, 1.9402, 1.46343, -0.21528, 0.40253, 0.44874], [0.38453, 0.49359, 3.2826, 0.29519, 0.09099, 3.12073, 2.76174, 0.91117, 0.85602, 0.87243], [0.2619, 0.34869, 2.75335, 0.10021, 0.12359, 1.57645, 2.52743, -0.44928, 0.49646, 0.50057], [0.16446, 0.27981, 1.25228, 0.11339, 0.22752, 2.15115, 5.9815, 0.7878, -0.54335, -0.68222], [0.13585, -0.71683, 2.32833, 0.05899, 0.23033, 1.45565, 3.1888, -0.16916, 0.51429, 0.6052], [0.16468, -0.60197, 3.15185, 0.07581, 0.16841, 1.76863, 6.14408, 0.08836, -0.70628, -0.68243], [-0.35743, -0.28283, 1.54047, 0.03937, 0.19287, 1.5841, 3.32628, -0.36208, 0.43891, 0.36518], [-0.62251, -0.38234, 2.61567, 0.15456, 0.32413, 1.04598, 2.40596, -0.06698, 0.0187, 0.04889], [0.79814, 0.37616, 2.43606, 0.18443, 0.12374, 2.91468, 5.89122, 0.37607, 0.34732, 0.36404], [0.51853, 0.51633, 1.9986, 0.1856, 0.16773, 0.71136, 2.96926, 1.181, 0.3687, 0.52399], [-0.85179, -0.45927, 1.21504, 0.16331, 0.12762, 2.08546, 4.38702, -0.06148, 0.04644, 0.05241], [0.82346, -0.30704, 3.00411, 0.11575, 0.11242, 0.90949, 2.33484, 0.08476, -0.0924, -0.16091], [0.27188, -0.88797, 1.57825, 0.03127, 0.2239, 2.98966, 2.26823, -0.58261, 1.38576, 1.67028], [-0.46132, -0.65263, 1.91629, 0.13799, 0.14213, 0.84698, 2.25948, -0.08674, -0.07736, -0.01761], [-0.0886, -0.07273, 0.12768, 0.10024, 0.26852, 1.98655, 3.07155, -0.14939, 0.18659, 0.28131], [0.27019, 0.29867, 0.82501, 0.1431, 0.0816, 3.81301, 1.75651, -0.80205, -0.04131, 0.0189], [0.00722, 0.14136, 0.27264, 0.09149, 0.26244, 1.54872, 3.31034, -0.3603, 0.23216, 0.43633], [-0.13459, 0.52946, 3.39189, 0.04356, 0.13784, 1.61563, 6.20546, -0.968, -0.99753, -0.75933], [0.04028, -0.67528, 3.17229, 0.15631, 0.14623, 2.75131, 2.57373, -0.08824, 0.54121, 0.48411], [0.39384, -0.52184, 3.20355, 0.12168, 0.17843, 0.94272, 0.41448, -0.02293, -0.64596, -0.4227], [0.62453, 0.47184, 2.91956, 0.16326, 0.21354, 1.26663, 2.48642, 0.30674, 0.3324, 0.37472], [0.55621, 0.38204, 1.82273, 0.09645, 0.21059, 1.67487, 2.48445, 0.84619, 0.66986, 0.67577], [-0.81937, 0.493, -0.0036, 0.05319, 0.0932, 2.92422, 2.67774, 2.59517, 2.63921, 2.22331], [-0.09406, -0.6307, 3.13683, 0.01255, 0.21603, 2.82704, 3.40586, 0.63554, -1.39215, -1.53166], [0.64661, 0.49075, 2.50093, 0.15143, 0.13703, 0.80325, 5.39367, -0.41876, -1.24179, -0.74772], [0.52089, 0.00283, 3.20765, 0.15302, 0.20652, 1.36938, 2.85748, -0.45495, 0.55844, 0.44882], [-0.74526, -0.0199, 3.14128, 0.01984, 0.22471, 4.63492, 4.49001, -2.32443, 3.06353, 3.78369], [-0.32624, 0.77647, 1.11274, 0.05544, 0.12966, 2.98644, 0.48049, -2.01201, -1.79998, -2.19936], [0.35253, 0.66839, 2.45969, 0.0704, 0.10646, 2.89173, 3.52159, 2.50863, 2.51213, 2.52348], [-0.35928, 0.52445, 0.30938, 0.27926, 0.20368, 0.67397, 0.64618, -0.68931, -0.71052, -0.44067], [0.26325, -0.73415, 1.56602, 0.14298, 0.22818, 1.3569, 3.30544, -0.26757, 0.49021, 0.37976], [0.586, 0.26585, 2.14199, 0.03439, 0.14258, 1.46884, 5.34673, 0.96545, -0.18662, 0.04527], [-0.43325, 0.67077, 1.4374, 0.34834, 0.21183, 0.64381, 0.78005, -1.10733, -1.12475, -0.93446], [0.60188, -0.86897, 0.58349, 0.06483, 0.08777, 3.37717, 3.87752, -0.28056, 0.65673, 0.6977], [-0.82983, -0.80561, 1.16311, 0.09632, 0.14492, 1.60901, 2.93708, -0.03843, 0.02064, 0.03901], [0.27384, -0.9034, 1.56931, 0.01144, 0.25121, 2.51038, 5.78922, 0.83105, -1.51501, -1.32257], [-0.17077, 0.69291, 1.04495, 0.08344, 0.08726, 2.63368, 1.62487, -0.56447, -0.58933, -0.59421], [0.61545, -0.21363, 0.00674, 0.02712, 0.44163, 3.80444, 4.43907, -1.05569, 1.55318, 1.85023], [-0.45204, -0.24833, 1.67065, 0.15145, 0.20898, 1.1456, 5.80945, 0.25417, -0.58287, -0.44938], [0.91085, 0.30058, 2.55159, 0.32394, 0.11989, 4.42473, 3.81485, -0.19905, -0.20616, -0.19025], [-0.01544, -0.44709, 2.87273, 0.09374, 0.05014, 2.55513, 3.60033, -0.121, 0.72357, 1.00287], [-0.51983, -0.20343, -0.26748, 0.06577, 0.16795, 1.95382, 2.6593, -0.33308, 0.46895, 0.3713], [0.26357, -0.76429, 1.54218, 0.15758, 0.22336, 1.04113, 2.74606, -0.2448, 0.28127, 0.71722], [-0.3753, -0.36271, 1.57935, 0.02425, 0.2412, 3.96846, 1.94527, -1.9547, 2.67872, 3.41142], [-0.54932, -0.16918, 0.31415, 0.12651, 0.14755, 0.72287, 0.30894, 0.4183, -0.36801, -0.68576], [-0.23035, -0.49408, 1.66203, 0.23955, 0.24268, 1.84685, 2.22684, 0.09834, -0.12612, -0.15135], [-0.30076, -1.01231, 0.68756, 0.22471, 0.22201, 1.60389, 0.8549, 0.01592, -0.08469, -0.10949], [-0.62797, 0.69828, 2.19777, 0.09535, 0.15567, 2.33143, 6.10749, -1.68191, -2.18704, -2.31563], [0.54776, 0.16296, 3.13115, 0.07107, 0.14779, 3.33182, 0.19249, 0.53257, -0.48905, -0.70101], [0.28274, -0.85851, 1.56778, 0.07122, 0.22749, 3.08328, 5.4518, 0.32783, -1.029, -0.99974], [-0.75271, 0.29227, 0.74579, 0.03556, 0.08729, 4.60293, 2.32435, 4.08807, 3.24784, 3.005], [0.38325, 0.54477, 0.94102, 0.40818, 0.28921, 1.7405, 1.45806, -0.04071, 0.23743, 0.32516]], "star": [[-0.51053, -0.15273, 0.68805, 0.35961, 0.16414, 0.36307, 5.26169, 0.35404, 0.26297, -0.02025], [-0.07018, -0.79386, 1.46149, 0.27812, 0.34827, 0.66017, 0.5998, 0.02515, -0.03127, -0.01383], [-0.04242, -0.06369, 0.41342, 0.52432, 0.492, 0.32373, 2.82758, -0.0946, -0.04359, 0.19702], [-0.5847, -0.79101, 0.74844, 0.71803, 0.3309, 0.57434, 3.00178, 0.00359, -0.00168, -0.00322], [-0.08832, 0.12294, 1.53853, 0.459, 0.39519, 0.38586, 0.2186, 0.20673, 0.06584, -0.12705], [-0.60689, -0.33177, 1.17408, 0.96799, 0.62049, 0.69716, 5.23717, -0.01384, 0.00645, -0.01124], [0.11267, 0.28884, 1.11652, 0.39831, 0.30015, 0.46066, 4.32536, -0.14439, 0.02783, 0.16473], [0.32993, 0.06371, 0.41569, 0.16036, 0.30224, 0.78318, 6.03392, 0.31667, 0.33124, -0.08115], [0.46703, -0.0168, -0.08327, 0.40889, 0.45086, 0.59604, 4.14405, -0.04784, -0.04327, -0.01626], [0.61214, -0.16662, 2.05353, 0.39932, 0.47518, 0.62537, 6.12889, 0.00102, 0.02287, 0.0025], [-0.86329, -0.74587, 2.69009, 0.63739, 0.86173, 0.3242, 4.08681, -0.04421, -0.04775, 0.0649], [-0.01049, -0.1416, 1.38894, 0.47137, 0.34564, 0.30828, 2.61817, -0.42365, -0.03028, 0.21405], [-0.34958, 0.02443, 1.04418, 0.35261, 0.13573, 0.36934, 2.2475, -0.06532, -0.19908, 0.0011], [0.76035, -0.07882, 0.09598, 0.48621, 0.25489, 0.319, 4.09159, -0.1085, -0.11318, 0.15415], [0.28869, -0.26514, 0.83971, 0.49766, 0.42968, 0.42586, 4.07843, -0.00878, -0.0057, -0.00669], [0.09774, 0.02822, 2.87191, 0.62577, 0.52997, 0.44792, 0.1387, 0.03507, 0.06726, -0.03023], [-0.0697, 0.00158, 0.11391, 0.33412, 0.28107, 0.70131, 5.68176, 0.08611, -0.12642, -0.14202], [0.0488, 0.10928, 1.1197, 0.42092, 0.33285, 0.27918, 3.51575, -0.38059, -0.08911, 0.27614], [-0.0164, 0.04503, 2.89844, 0.54267, 0.40763, 0.33239, 0.07615, 0.26878, 0.18469, -0.06475], [0.62477, -0.25381, 2.3837, 0.37851, 0.37412, 0.23939, 4.97219, 0.17725, 0.07981, -0.00017], [-0.34377, 0.556, 1.43592, 0.30259, 0.52825, 0.61379, 4.12942, -0.10279, -0.05566, 0.04159], [0.03347, -0.16549, 1.55273, 0.26411, 0.54085, 0.52546, 5.74224, 0.38154, 0.11136, -0.06104], [0.57593, 0.35328, 1.28074, 0.52591, 0.59368, 0.28347, 3.16451, -0.0194, 0.00598, -0.00804], [0.56303, 0.91735, 1.15253, 0.38446, 0.53731, 0.41035, 0.65649, -0.01641, -0.00829, -0.03907], [-0.0675, 0.58166, 2.06245, 0.19321, 0.19147, 0.60727, 3.05455, 0.02441, -0.07817, -0.06201], [0.09489, -0.20717, 0.78309, 0.61948, 0.61941, 0.54474, 5.3332, 0.13564, 0.05384, -0.05197], [0.76998, -0.59085, 0.72889, 0.2626, 0.36886, 0.78807, 1.89753, -0.03333, -0.01984, -0.0037], [-0.92937, -0.2769, 1.48164, 0.16458, 0.51391, 0.88393, 4.68962, 0.11147, 0.02193, -0.12731], [-0.04073, -0.3637, 0.41262, 0.24729, 0.51547, 0.74959, 3.67937, -0.13205, -0.08886, 0.04567], [-0.1472, -0.41196, 1.58488, 0.58691, 0.55867, 0.38693, 2.37401, -0.03823, -0.02307, 0.02535], [-0.00614, 0.03833, 2.69916, 0.48144, 0.53005, 0.49706, 3.20669, -0.20773, 0.04657, 0.06422], [-0.20447, 0.05738, 2.97441, 0.15979, 0.22661, 2.44411, 1.39823, -0.30817, -0.09173, 0.12606], [-0.76942, -0.19783, 0.21829, 0.13352, 0.04064, 1.49619, 5.39481, 1.10947, 0.52703, -0.51469], [-0.46293, -0.23911, 1.61092, 0.04634, 0.33172, 3.03375, 5.41794, 1.26103, 0.78286, -0.49522], [0.06065, -0.83499, 1.1654, 0.14828, 0.005, 2.62333, 4.3315, -1.33619, -0.7626, 0.64493], [-0.31575, 0.31703, 0.19491, 0.13551, 0.27143, 1.33853, 6.08375, 0.8196, 0.30475, -0.20821], [-0.56296, -0.08599, 2.18115, 0.11669, 0.28658, 1.93389, 0.64648, 0.58107, 0.24135, -0.26829], [-0.112, 0.09234, 2.63498, 0.09472, 0.08023, 3.33312, 3.20024, 0.55321, 0.85241, 0.62275], [-0.42245, -0.08477, 0.01726, 0.22589, 0.12844, 1.69061, 0.16619, 0.23566, -0.00869, -0.03135], [0.06776, 0.6268, 2.58094, 0.06912, 0.05267, 2.03968, 1.49948, 0.11016, 0.04239, -0.04381], [0.28122, 0.59345, 2.17899, 0.01946, 0.24747, 3.82596, 4.42208, -3.84417, -2.18766, 2.01159], [0.31386, -0.36299, 1.20103, 0.13599, 0.07654, 1.6408, 0.16886, -0.29135, -0.0623, 0.17822], [0.55473, -0.31699, 1.59136, 0.11557, 0.16764, 2.06004, 1.14207, -0.62327, -0.31521, 0.27353], [-0.44106, 0.45828, 0.26594, 0.03281, 0.22193, 3.7483, 2.17469, -2.02698, -1.09346, 0.96396], [0.65717, 0.59846, -0.27109, 0.14513, 0.11559, 3.48087, 5.39501, 0.12239, 0.03585, -0.03452], [-0.14798, -0.45393, 0.32433, 0.01861, 0.79582, 4.4202, 4.92375, 3.28216, 1.43719, -1.25199], [-0.60536, -0.04913, 2.21412, 0.02314, 0.24465, 4.18919, 4.46277, -4.16041, -1.95161, 1.7603], [-0.62697, -0.5098, 1.45758, 0.08486, 0.15698, 1.52263, 1.89466, 0.12829, 0.10561, -0.03876], [0.51449, -0.28711, 1.60491, 0.06129, 0.2162, 3.89484, 3.87665, 0.95616, 0.46459, -0.42435], [0.32116, 0.38261, 2.63277, 0.18795, 0.14359, 1.17108, 0.03199, 0.37471, 0.26449, -0.44471], [0.29948, 0.06897, 2.81495, 0.04949, 0.39792, 2.97239, 5.91684, 1.89518, 0.56408, -0.23856], [-0.5663, 0.85365, 2.05972, 0.08528, 0.06723, 0.8634, 4.01119, 0.17046, 0.05918, -0.06136], [-0.03235, -0.1062, 1.30497, 0.0568, 0.10161, 1.98305, 2.61473, 0.27054, 0.64028, 0.97997], [0.22074, -0.61255, -0.17283, 0.21073, 0.19802, 1.33841, 5.61818, -0.11967, -0.07294, 0.12897], [0.11109, -0.54965, 2.90996, 0.05438, 0.19995, 2.67146, 5.49666, 0.89583, 0.30495, -0.30528], [0.4764, -0.2783, 1.57209, 0.01317, 0.20611, 3.7017, 3.85687, 1.86438, 0.81372, -0.68967], [-0.79551, -0.69126, 1.68869, 0.18938, 0.18063, 1.48869, 2.0417, -0.09177, -0.04576, 0.05076], [-0.69563, -0.39051, 1.65237, 0.17716, 0.12341, 3.1259, 4.8354, -0.17853, -0.09295, 0.08889], [0.4658, 0.20264, 1.3172, 0.11945, 0.05827, 0.84899, 2.47738, 0.35942, 0.24163, -0.24982], [0.12673, -0.61229, -0.30152, 0.02136, 0.30796, 3.50656, 4.4067, -2.95706, -1.8063, 1.69187], [0.01654, -0.88378, 1.64948, 0.13909, 0.02471, 2.18708, 4.81224, 1.36907, 0.71828, -0.64168], [0.52927, 0.0586, 0.94322, 0.05803, 0.13738, 3.00829, 1.65677, 1.20588, 0.70135, -0.64307], [0.11272, 0.53699, 1.59168, 0.27775, 0.20228, 1.15561, 4.67559, -0.23347, -0.19164, 0.09554], [0.19108, 0.06434, 0.15906, 0.05952, 0.08047, 2.55291, 0.86874, -0.65442, -1.18447, -1.1727], [-0.28411, -0.42135, 3.38232, 0.09539, 0.08902, 0.98512, 2.76018, 0.0392, 0.02729, -0.10188], [-0.42219, -0.22683, 2.16676, 0.17983, 0.29019, 0.84596, 2.02503, -0.16512, -0.1051, 0.16705], [0.24219, 0.4444, 2.16647, 0.13174, 0.20905, 1.64917, 0.1576, 0.92216, 0.24084, -0.18353], [0.14429, 0.51728, 1.87757, 0.09842, 0.12765, 2.3035, 0.3321, -0.21602, -0.00533, -0.04041], [0.00122, -0.42501, 3.22421, 0.14635, 0.15283, 1.00079, 3.56606, -0.95341, -0.46443, 0.19811], [0.01914, 0.02327, 1.68727, 0.21134, 0.13233, 1.49743, 6.22365, -0.51815, -0.83356, -0.67064], [0.28187, 0.36377, -0.38082, 0.10839, 0.14596, 0.8208, 5.92113, 1.09395, 0.3801, -0.17753], [0.22536, 0.49825, 2.20265, 0.05758, 0.25291, 2.76728, 0.46329, 1.42985, 0.51988, -0.41777], [-0.45783, -0.0778, 1.84375, 0.14555, 0.14505, 1.25657, 0.34373, 1.67452, 0.522, -0.42744], [0.13987, -0.2105, 2.01916, 0.13301, 0.19415, 1.81491, 0.56274, 1.00172, 0.68436, -0.27158], [0.50098, -0.05012, 0.76651, 0.19449, 0.2237, 1.15756, 3.65245, -0.66405, -0.35603, 0.28384], [0.45732, 0.61373, 1.1175, 0.15351, 0.04725, 0.83834, 0.31755, 1.27973, 0.78829, -0.76025], [-0.31064, 0.32627, 3.03941, 0.17271, 0.11314, 0.89206, 3.90119, -1.08711, -0.48405, 0.42314], [-0.05244, 0.46698, 1.28082, 0.41268, 0.19876, 1.82854, 1.96378, 0.14533, 0.08843, -0.04229], [0.65368, -0.04039, 0.91601, 0.01431, 0.2486, 2.64038, 0.37863, -1.53102, -0.6903, 0.61226], [0.11256, -0.49758, 2.90797, 0.08858, 0.20068, 1.66331, 1.9988, -0.95035, -0.3115, 0.39809], [-0.55115, -0.2651, 1.56625, 0.01558, 0.26992, 4.61937, 1.74074, -4.7308, -2.21314, 2.21696], [-0.57442, 0.29028, 2.07148, 0.07815, 0.11578, 1.77736, 3.97166, 0.24663, 0.10249, -0.12441], [-0.57351, -0.03527, 2.31236, 0.08946, 0.13378, 1.57572, 4.21907, -0.42142, -0.45787, 0.33859], [0.44745, -0.04054, 1.57304, 0.1578, 0.12902, 1.05371, 0.84855, 1.15497, 0.12029, -0.08756], [0.47798, 0.46267, -0.28408, 0.01747, 0.25291, 4.78872, 1.38827, 3.57491, 2.16954, -2.14804], [-0.39951, 0.56329, 3.60983, 0.06129, 0.12474, 2.31554, 0.20968, 1.7986, 0.71715, -0.67198], [0.2029, 0.94365, 1.51817, 0.12817, 0.15872, 0.90829, 5.2715, 0.09825, 0.00224, -0.02612], [-0.2578, 0.40853, 0.87754, 0.17928, 0.23013, 1.80534, 6.23272, 0.42828, 0.23362, -0.20952], [0.62698, -0.08746, 0.60216, 0.13076, 0.15134, 1.24408, 4.03669, -0.78655, -0.2753, 0.33121], [-0.11036, -0.36616, 0.44709, 0.11328, 0.21919, 1.53497, 2.63921, -1.00777, -0.44399, 0.4229], [0.57028, 0.65051, -0.01295, 0.18803, 0.20359, 1.5268, 2.61712, 0.20971, 0.08408, -0.06806], [0.91737, -0.25154, 3.13936, 0.36808, 0.01164, 0.55714, 4.98839, 3.41089, 1.68578, -1.47782], [0.00464, -0.67149, 0.51839, 0.07402, 0.08209, 1.77517, 6.14924, 1.13503, 0.69195, -0.43628], [0.45897, 0.66312, 1.85728, 0.07313, 0.11845, 1.14973, 3.4493, -0.06586, -0.09912, 0.17318], [0.03662, 0.17978, 2.97047, 0.11176, 0.035, 1.47455, 6.27172, -0.71513, -1.26976, -0.78411], [0.61477, -0.81121, 1.1419, 0.13361, 0.13146, 0.78051, 3.17935, -0.0222, 0.00013, -0.0463], [-0.47039, 0.16684, 2.52823, 0.13584, 0.09956, 2.82399, 0.4835, -0.37612, -0.15246, 0.13208], [0.0925, 0.27645, 0.13734, 0.26465, 0.1313, 1.96463, 3.63057, 0.45913, 0.11807, -0.09602], [0.74131, -0.17449, 2.55596, 0.14172, 0.03612, 0.77917, 3.09129, -2.06065, -1.08434, 0.96287], [-0.49518, 0.1883, 0.21456, 0.10268, 0.09144, 2.59442, 3.75846, -0.11141, -0.00155, -0.05859], [-0.08749, -0.04503, 0.36005, 0.16451, 0.14805, 1.75564, 1.96861, 0.37147, 0.61096, 0.57908], [-0.07382, -0.4671, 0.31571, 0.07188, 0.24398, 1.9183, 0.03077, 0.76225, 0.24352, -0.29235], [-0.41184, 0.73294, 4.04825, 0.01519, 0.12271, 2.8311, 3.22725, 0.54958, 0.21104, -0.20166], [0.05866, -0.48599, 1.87293, 0.06712, 0.14864, 1.88058, 2.60092, -0.29807, -0.2459, 0.00046], [-0.22666, 0.54185, 4.08157, 0.04808, 0.18162, 2.6353, 2.15601, -1.70536, -0.5344, 0.645], [0.11034, 0.555, 1.8767, 0.10493, 0.15919, 2.8434, 3.33574, 0.11173, 0.08681, -0.12987], [-0.00432, -0.59956, 0.68151, 0.21, 0.35642, 1.18027, 1.97778, -0.13986, -0.03979, 0.08981], [-0.22001, -0.41801, 1.9694, 0.2259, 0.02907, 0.6921, 0.39981, -0.62639, -0.23372, 0.2716], [-0.69313, 0.19448, 1.77782, 0.09053, 0.15994, 2.16316, 3.30231, 0.03186, 0.04356, -0.026], [-0.07556, 0.43344, 0.94755, 0.04052, 0.33977, 1.96234, 0.87423, 1.98507, 0.77476, -0.48832], [-0.25397, 0.58587, 0.97557, 0.01057, 0.2231, 2.65974, 3.26704, -2.58728, -1.3095, 1.18591], [0.51088, -0.39587, 1.97375, 0.26423, 0.20145, 1.12756, 1.03371, -0.17067, -0.13254, 0.15733], [-0.51523, 0.7042, 1.79156, 0.18224, 0.13081, 1.36296, 5.3965, -0.16144, -0.0463, -0.00371], [-0.32852, -0.24906, 1.52681, 0.08517, 0.13409, 2.13653, 1.6727, -0.87072, -0.15162, 0.25915], [0.00806, 0.84109, 0.15468, 0.10667, 0.14355, 0.93857, 6.05272, -0.02536, -0.02323, -0.01905]], "boat": [[-0.35803, -0.04712, 0.33676, 0.46106, 0.39276, 0.50475, 4.13232, 0.00283, 0.01229, -0.00976], [-0.41203, 0.10897, 2.52432, 0.40229, 0.57243, 0.23464, 5.08722, -0.00107, 0.00666, -0.07483], [0.12509, -0.17784, 2.45227, 0.25832, 0.32034, 0.71608, 3.56406, -0.55495, -0.81191, -0.24526], [-0.14167, -0.02618, 0.79131, 0.39298, 0.77199, 0.53086, 5.38417, 0.08289, -0.01826, -0.03796], [-0.01467, -0.16899, 0.41817, 0.36342, 0.33713, 0.73269, 5.66457, 0.22791, 0.20826, 0.12245], [0.11882, 0.17061, 1.5263, 0.42596, 0.48433, 0.64705, 4.39323, -0.0514, -0.11341, -0.10323], [0.38566, 0.39142, 0.93088, 0.41204, 0.58494, 0.39645, 0.64687, 0.12816, -0.03829, -0.08142], [0.14899, 0.39127, 1.63262, 0.57184, 0.2499, 0.65196, 5.76974, -0.0592, -0.16024, -0.1084], [-0.19028, -0.94504, 1.12366, 0.76464, 0.29497, 0.38367, 4.45984, 0.18743, 0.14662, 0.10917], [0.23686, 0.09142, 1.30661, 0.29763, 0.25272, 0.73147, 4.36912, -0.39693, -0.46958, -0.27099], [0.18348, 0.89217, 1.06554, 0.55058, 0.48248, 0.40182, 1.53431, 0.07829, 0.00204, -0.18879], [0.70699, -0.80219, 0.73109, 0.56942, 0.85824, 0.65426, 3.40796, -0.00738, -0.0733, -0.06328], [0.08463, 0.35836, 1.62099, 0.36608, 0.36466, 0.74301, 2.07875, -0.09204, 0.31805, 0.30015], [0.76143, 0.12624, 2.48314, 0.52285, 0.57768, 0.2519, 4.92292, 0.148, 0.04802, -0.05123], [-0.05321, -0.44144, 2.33538, 0.36346, 0.49199, 0.60418, 3.36667, -0.18666, -0.15261, -0.08766], [-0.49942, 0.39841, 2.1084, 0.46562, 0.37552, 0.79064, 4.52097, 0.01144, -0.00033, 0.00854], [0.12141, -0.21516, 0.92039, 0.34648, 0.19827, 0.3196, 6.14339, 0.22338, 0.23482, 0.45767], [0.11031, -0.58101, 1.14666, 0.51466, 0.84179, 0.23172, 2.30048, -0.1002, 0.06144, 0.03765], [0.03505, 0.09251, 1.79332, 0.55181, 0.44314, 0.35617, 1.28299, 0.10084, 0.28937, 0.17874], [0.19853, 0.07481, 3.03637, 0.23448, 0.52977, 0.37181, 5.88989, 0.19077, 0.22606, -0.15106], [0.05457, -0.27555, 0.96562, 0.33537, 0.23879, 0.41219, 2.59711, -0.4955, -0.37183, -0.08309], [0.24537, -0.88183, -0.01118, 0.39644, 0.76785, 0.60463, 0.83211, 0.08243, 0.07792, 0.08496], [0.35768, -0.15754, 0.02243, 0.40371, 0.34036, 0.4328, 0.9687, 0.27805, 0.11875, 0.05968], [0.45203, -0.6876, 2.51104, 0.64175, 0.47358, 0.9028, 2.91444, 0.03378, 0.01227, 0.00709], [-0.84711, 0.43104, 1.14221, 0.48062, 0.811, 0.58063, 1.42528, -0.01733, 0.03998, 0.02921], [-0.65691, -0.00547, 1.40793, 0.59829, 0.31731, 0.56019, 3.49149, -0.00514, 0.01508, -0.01819], [0.31537, 0.54621, 2.14771, 0.42914, 0.33822, 0.52531, 2.59495, -0.12871, 0.04671, 0.30871], [0.18389, -0.0192, 1.72127, 0.40317, 0.25492, 0.65319, 3.35443, -0.23199, -0.1824, -0.46588], [0.24018, -0.26153, -0.18801, 0.30371, 0.29881, 0.47301, 3.88628, -0.36114, -0.17004, -0.31887], [-0.51405, 0.71201, 1.76857, 0.34024, 0.31019, 0.61892, 3.98611, -0.1618, 0.17146, 0.45924], [-0.56883, 0.49773, 1.86237, 0.20481, 0.23447, 0.84602, 0.11957, 0.15594, -0.29436, -0.53234], [0.23848, 0.06412, 1.51014, 0.11924, 0.22533, 1.53357, 0.82589, 2.20238, 1.65043, 0.62457], [-0.21027, -0.54098, 2.46659, 0.22927, 0.1359, 1.27134, 5.19799, -0.11871, -0.0959, -0.13234], [-0.10351, -0.26834, 0.01181, 0.07839, 0.31304, 2.06626, 2.96916, -1.71056, -1.0688, -0.53551], [-0.0248, -0.55869, 2.72252, 0.17045, 0.16361, 1.86115, 0.88871, 0.67705, 0.66146, 0.1311], [0.72597, 0.40145, 3.83772, 0.0798, 0.09103, 2.35273, 4.93156, 0.25208, -0.42274, -0.58041], [0.34289, 0.39094, 1.38718, 0.16128, 0.19588, 1.19757, 5.685, 0.16747, -0.20192, -0.7312], [0.06644, -0.60577, -0.63073, 0.08736, 0.19748, 1.87261, 0.1431, 1.77121, 0.94503, 0.64713], [-0.21724, -0.33086, 0.47813, 0.08997, 0.08946, 0.89736, 6.21413, -0.23098, -0.24363, -0.16915], [-0.22979, -0.72439, 1.33041, 0.1924, 0.02362, 2.09594, 5.21617, -0.2779, -0.3288, -0.2743], [-0.28104, -0.15587, 1.23101, 0.13788, 0.09774, 0.8023, 3.10253, 0.0241, -0.01377, 0.13467], [0.52665, 0.56464, 0.60309, 0.11355, 0.19963, 1.72993, 0.41176, 0.25423, -0.35009, -1.00354], [-0.09769, 0.05324, 2.59468, 0.10418, 0.0784, 1.84332, 5.30456, -0.43615, -0.27265, -0.47339], [0.14923, 0.43003, 1.26489, 0.25233, 0.08449, 2.11129, 2.12432, -0.07108, -0.0937, -0.14313], [-0.79014, 0.61709, 1.90558, 0.37037, 0.11136, 1.64229, 5.18906, -0.13822, 0.16428, 0.3693], [-0.12659, 0.30183, 1.57897, 0.03235, 0.65149, 3.35957, 4.83163, 0.62631, -1.09254, -2.34957], [0.74631, -0.27895, 2.57092, 0.32236, 0.22899, 1.70782, 4.40745, -0.10511, -0.11866, -0.0974], [-0.4706, -1.01786, 1.562, 0.11467, 0.13857, 1.21133, 5.87333, 0.02135, -0.01299, 0.02737], [-0.05836, 0.04774, 0.62651, 0.22665, 0.15889, 1.20993, 2.69819, -0.14953, -0.32859, -0.33054], [0.48753, 0.18505, 1.40111, 0.09815, 0.21416, 1.35927, 0.28737, -0.22569, -0.15438, -0.10252], [0.19376, 0.78175, 0.86577, 0.232, 0.101, 1.39569, 2.35936, 0.06397, 0.01969, -0.15017], [-0.32573, 0.28337, 1.44029, 0.10177, 0.09071, 2.11344, 1.90905, -0.0877, 0.22625, 0.5373], [-0.10059, 0.20726, -0.15807, 0.04677, 0.10242, 1.83554, 3.26912, -2.11536, -1.82547, -1.57415], [0.84624, -0.47251, 1.47539, 0.20866, 0.22456, 1.01765, 2.08865, 0.01409, 0.01689, 0.01502], [0.50083, -0.29505, 1.39077, 0.08953, 0.07203, 1.88329, 0.12959, -0.05621, -0.08363, -0.1078], [-0.1445, -0.95257, 2.90764, 0.07775, 0.01977, 2.03443, 3.97165, 0.97008, 0.86927, 0.57117], [-0.47625, -0.61653, 2.43245, 0.11514, 0.11414, 1.41168, 3.49398, 0.02675, 0.04917, 0.00409], [0.15157, 0.15012, 1.47682, 0.03508, 0.16766, 3.63825, 3.18341, 0.92059, 0.83145, 0.42329], [0.14193, 0.25131, 2.49569, 0.17593, 0.13607, 2.08532, 0.75981, -0.11116, 0.00501, 0.01148], [-0.66478, 0.65038, 1.19491, 0.35746, 0.06058, 2.73093, 5.53523, 0.11769, -0.16289, -0.3357], [0.61342, 0.56992, 3.7336, 0.02526, 0.21197, 4.26954, 1.86086, -0.6535, 0.93151, 2.24838], [0.6787, -0.92989, 0.41269, 0.25026, 0.38995, 1.63202, 5.8353, -0.00703, -0.02194, -0.01415], [0.04638, -0.55831, 2.42571, 0.12093, 0.16263, 0.73856, 6.14543, 0.36226, 0.49601, 0.35012], [-0.21198, -0.14439, 1.58812, 0.15066, 0.05129, 0.70131, 6.16365, -0.33509, -0.32551, -0.12919], [-0.2144, 0.03032, 2.91624, 0.05753, 0.07852, 1.87215, 0.1741, -0.39673, -0.36386, -0.22987], [-0.13844, -0.71483, 0.07072, 0.07635, 0.33219, 2.26688, 4.82888, 0.2467, 0.18457, 0.16929], [0.15999, -0.29622, 2.69916, 0.20213, 0.24233, 1.002, 5.81437, 1.45309, 1.05946, 0.17348], [-0.07523, 0.37701, 0.46661, 0.06141, 0.03343, 1.56853, 0.70886, -1.05117, -1.23321, -0.96933], [-0.16682, -0.99045, 1.12432, 0.05948, 0.2007, 1.9382, 3.04478, 0.1796, 0.16536, 0.09781], [0.80351, -0.49286, 1.98354, 0.17694, 0.25954, 1.03301, 1.10691, 0.00779, 0.03798, 0.06703], [0.82117, 0.6799, 1.03574, 0.09622, 0.19606, 0.89922, 4.38744, 0.08018, -0.11594, -0.18364], [-0.13149, -0.81441, 0.15398, 0.07111, 0.12125, 1.76403, 2.42052, -0.97914, -1.07737, -0.75186], [-0.61828, -0.31465, 0.80308, 0.12261, 0.14744, 0.48786, 2.49841, -0.08821, -0.04903, 0.05376], [0.39459, -0.10493, 2.5056, 0.09057, 0.16075, 1.27382, 3.16278, -2.35689, -1.60934, -0.60862], [-0.09579, 0.81222, 1.57241, 0.01623, 0.37915, 5.3683, 1.47924, 0.88204, -1.30361, -2.71795], [-0.28004, 0.49948, 1.65494, 0.17664, 0.2661, 0.75148, 0.10037, 0.22502, -0.18689, -0.88598], [0.24841, -0.53404, 2.51898, 0.01783, 0.41616, 3.90649, 2.67916, 1.43219, 1.213, 0.7207], [0.11963, -0.67874, 0.9541, 0.60554, 0.00669, 0.856, 0.23229, -1.54654, -1.29281, -0.86237], [-0.52329, 0.77623, 1.58572, 0.03163, 0.13249, 1.86478, 3.20299, -0.22208, 0.35925, 0.72493], [0.0299, 0.77708, 1.54304, 0.06955, 0.29003, 2.61709, 3.92385, -0.28405, 0.4157, 1.04792], [-0.17194, 0.06405, 2.53053, 0.20492, 0.08149, 1.43345, 5.17384, -0.2267, 0.02543, 0.00314], [0.55317, 0.66661, 1.90108, 0.42554, 0.21636, 1.83571, 6.12928, -0.07583, 0.08871, 0.17245], [-0.44615, 0.12555, 3.12824, 0.23051, 0.18191, 1.53885, 4.09845, 0.04691, 0.01757, 0.05995], [-0.24918, -0.17663, 0.32413, 0.05639, 0.06614, 0.86147, 1.95925, 0.19675, 0.15027, 0.25504], [0.29911, 0.60437, 0.50524, 0.14357, 0.11689, 0.9624, 2.34493, -0.14827, 0.15435, 0.16659], [-0.41892, 0.7034, 1.53665, 0.09705, 0.16946, 1.9209, 3.17691, -0.1839, 0.26102, 0.75296], [-0.79271, 0.58647, 2.62031, 0.05162, 0.19227, 2.43745, 1.26147, 0.4843, -0.74477, -1.61327], [-0.60484, 0.03067, 1.28536, 0.17084, 0.14292, 1.52045, 3.6335, -0.04817, 0.02099, 0.0977], [-0.34671, 0.04527, 3.40945, 0.07248, 0.08656, 1.07657, 2.47549, 0.04936, 0.10153, 0.02661], [-0.22547, 0.15271, 0.20253, 0.16391, 0.13752, 1.71726, 4.2191, -0.02961, 0.15912, 0.14774], [-0.00687, -0.73161, 2.5028, 0.07599, 0.1783, 3.06414, 0.4712, 1.32528, 1.34062, 0.86031], [-0.15703, -0.95468, 3.25628, 0.1045, 0.01257, 2.11908, 1.13725, -0.45612, -0.42957, -0.29404], [-0.05372, 0.24665, 0.22127, 0.08319, 0.07884, 2.87674, 4.38322, -0.70704, -0.72359, -0.67824], [0.24342, 0.81571, 1.44644, 0.29137, 0.25659, 0.76395, 1.4982, 0.29314, -0.19535, -0.53801], [-0.68747, 0.54977, 0.22252, 0.25197, 0.1895, 0.88591, 2.94688, -0.12023, 0.23612, 0.56521], [0.47923, 0.30185, 1.57186, 0.00961, 0.2431, 1.65118, 0.45513, 0.60631, -0.81851, -1.97919], [0.04753, -0.49167, 2.745, 0.17775, 0.21465, 1.05867, 0.09611, 0.74664, 0.67871, 0.72987], [-0.91372, 0.37551, 2.54913, 0.04007, 0.06297, 3.36503, 3.85676, -0.40663, 0.58702, 1.40486], [0.409, 0.69172, 1.80022, 0.23277, 0.04852, 2.95077, 6.09774, 0.09222, -0.18616, -0.43305], [-0.96688, 0.90454, 1.94801, 0.08507, 0.17094, 1.69168, 6.06099, -0.06892, 0.06249, 0.09266], [-0.44291, -0.08531, -0.15556, 0.15896, 0.14925, 2.22345, 4.15054, -0.02685, -0.03191, -0.01925], [-0.22339, 0.07173, 0.92299, 0.05924, 0.04536, 1.11415, 5.24568, -0.18604, -0.34506, -0.07539], [0.56561, -0.04271, -0.6544, 0.02191, 0.16757, 3.69071, 4.20103, -4.09617, -4.05649, -2.88399], [0.35068, -0.35148, 2.51338, 0.05881, 0.30758, 2.72514, 1.79961, -1.26945, -1.30986, -1.07607], [-0.18379, -0.3334, 0.0015, 0.01045, 0.44674, 3.65024, 2.58839, 1.95094, 1.62632, 1.11477], [0.55417, 0.01449, 2.11492, 0.08191, 0.07838, 2.38535, 2.59567, -1.26342, -1.06981, -0.79691], [0.32377, 0.11651, 1.57898, 0.0271, 0.21155, 4.63407, 3.87996, -6.17162, -5.12337, -3.60569]]}; | |
| </script> | |
| <script> | |
| ; | |
| /* ============================ studio app ============================ */ | |
| const PALETTE = [ | |
| ["#ff6b5e",[255,107,94]], ["#5ec8ff",[94,200,255]], ["#ffd35e",[255,211,94]], | |
| ["#7fd98a",[127,217,138]], ["#c86bff",[200,107,255]], ["#5effd0",[94,255,208]], | |
| ["#ff8fc7",[255,143,199]], ["#b0b8ff",[176,184,255]], | |
| ]; | |
| const OUTLIER_RGB = [120,128,143]; | |
| const RES = 192, OWN_RES = 128; | |
| const NOISE_SCALE = [1,1,1,.3,.3,20,3,.5,.5,.5]; | |
| const HINT_W = 6.0, HINT_SIG = 0.4; | |
| /* ------------- state ------------- */ | |
| const S = { | |
| phase: "soup", | |
| layers: [], // {id,name,color,rgb,template,placed,pose,hidden,depth,hints:[]} | |
| selected: -1, // index into layers | |
| obs: [], gt: [], // gt = placed-order index, -1 clutter | |
| placed: [], // layer indices in placed order (BP object order) | |
| marg: null, mu: null, Sig: null, assign: null, | |
| evid: [], covFrac: [], | |
| groupPre: [], groupAlpha: [], // per placed group + clutter last | |
| ownField: null, coastT: [], | |
| }; | |
| let nextDepth = 1, nextId = 1; | |
| /* ---- Bet 9: eraser / backdrop / band-gain state ---- */ | |
| const GAINS = { band: [1, 1, 1], opacity: 1 }; | |
| const backdrop = { custom: false, rgb: [128, 128, 128], img: null }; // img: RGBA RES*RES | |
| let bandPre = null, bandDirty = true; | |
| const fieldAlpha = new Float32Array(RES * RES); | |
| let eraseStack = [], lastCursor = null; | |
| function toolMode(){ return document.querySelector('input[name=tool]:checked').value; } | |
| function brushR(){ return +document.querySelector('input[name=brush]:checked').value; } | |
| function updateEraseButtons(){ | |
| document.getElementById("btn-undo-erase").disabled = !eraseStack.length; | |
| document.getElementById("btn-restore-erase").disabled = !eraseStack.length; | |
| } | |
| function addLayer(name, template, autoPlace) { | |
| const [color, rgb] = PALETTE[(S.layers.length) % PALETTE.length]; | |
| const L = { id: nextId++, name, color, rgb, template, placed: false, | |
| pose: [0,0,0,Math.log(0.5)], hidden: false, depth: 0, hints: [] }; | |
| S.layers.push(L); | |
| if (autoPlace) placeLayer(S.layers.length - 1); | |
| else { renderLibrary(); } | |
| return L; | |
| } | |
| function placeLayer(li) { | |
| const L = S.layers[li]; | |
| L.placed = true; L.hidden = false; L.depth = nextDepth++; | |
| L.pose = [ -0.3 + 0.6*rand(), -0.3 + 0.6*rand(), -Math.PI + TAU*rand(), | |
| Math.log(0.45 + 0.2*rand()) ]; | |
| rebuildScene(); | |
| } | |
| function removeLayer(li) { | |
| S.layers[li].placed = false; | |
| rebuildScene(); | |
| } | |
| /* ------------- scene construction ------------- */ | |
| let rngState = 22222; | |
| function rand(){ rngState=(rngState*1103515245+12345)&0x7fffffff; return rngState/0x7fffffff; } | |
| function rebuildScene() { | |
| S.placed = S.layers.map((L,i)=>L.placed?i:-1).filter(i=>i>=0); | |
| const noiseAmt = 0.006 * Math.pow(3, +document.getElementById("noise").value / 3); | |
| S.obs = []; S.gt = []; S.src = []; | |
| S.placed.forEach((li,k)=>{ | |
| const tmpl = S.layers[li].template; | |
| const tr = transformAtoms(tmpl, S.layers[li].pose); | |
| for (let j = 0; j < tr.length; j++) { | |
| S.obs.push(tr[j]); S.gt.push(k); S.src.push({ li, ref: tmpl[j] }); | |
| } | |
| }); | |
| for (let i=0;i<12;i++){ | |
| const su=0.04+0.08*rand(); | |
| S.obs.push([-0.95+1.9*rand(),-0.95+1.9*rand(),-Math.PI/2+Math.PI*rand(), | |
| su,su*(0.4+0.5*rand()),4+10*rand(),TAU*rand(),rand(),rand(),rand()]); | |
| S.gt.push(-1); S.src.push(null); | |
| } | |
| for (const a of S.obs) for(let q=0;q<10;q++){ | |
| const g=Math.sqrt(-2*Math.log(rand()+1e-9))*Math.cos(TAU*rand()); | |
| a[q]+=g*noiseAmt*NOISE_SCALE[q]; | |
| } | |
| for (const a of S.obs){a[3]=Math.max(a[3],.012);a[4]=Math.max(a[4],.008);a[5]=Math.max(a[5],.5);} | |
| S.marg=null; S.mu=null; S.Sig=null; S.assign=null; | |
| S.evid=S.obs.map(()=>true); | |
| S.covFrac=S.placed.map(()=>0); | |
| S.coastT=S.placed.map(()=>0); | |
| S.ownField=null; | |
| eraseStack=[]; updateEraseButtons(); | |
| setPhase("soup"); | |
| rebuildGroupBuffers(); | |
| renderScene(); drawOverlay(); renderLayerList(); renderLibrary(); updatePanels(); | |
| } | |
| function scramble(){ | |
| for (const li of S.placed){ | |
| S.layers[li].pose=[-0.35+0.7*rand(),-0.35+0.7*rand(),-Math.PI+TAU*rand(), | |
| Math.log(0.4+0.25*rand())]; | |
| } | |
| rebuildScene(); | |
| } | |
| /* ------------- believed grouping (persistent through occlusion) ------------- */ | |
| function believedGroup(i){ return S.assign ? S.assign[i] : -2; } | |
| function updateAssign(){ | |
| if(!S.marg){S.assign=null;return;} | |
| const K=S.placed.length; | |
| if(!S.assign) S.assign=new Array(S.obs.length).fill(-1); | |
| for(let i=0;i<S.obs.length;i++){ | |
| if(S.assign[i]>=0 && !S.evid[i]) continue; // no evidence: identity persists | |
| const m=S.marg[i]; let b=0; | |
| for(let k=1;k<=K;k++) if(m[k]>m[b]) b=k; | |
| S.assign[i]= b===K ? -1 : b; | |
| } | |
| } | |
| function groupAtomIdx(k){ | |
| const out=[]; | |
| for(let i=0;i<S.obs.length;i++) if(believedGroup(i)===k) out.push(i); | |
| return out; | |
| } | |
| /* ------------- rendering ------------- */ | |
| const stage=document.getElementById("stage"); | |
| const sceneC=document.getElementById("scene-canvas"); | |
| const overC=document.getElementById("overlay-canvas"); | |
| sceneC.width=sceneC.height=RES; | |
| let CSS=600; | |
| function fitStage(){ | |
| const wrap=document.getElementById("stage-wrap"); | |
| CSS=Math.max(280,Math.floor(Math.min(wrap.clientWidth,wrap.clientHeight)-8)); | |
| stage.style.width=stage.style.height=CSS+"px"; | |
| overC.width=overC.height=CSS*(window.devicePixelRatio||1); | |
| drawOverlay(); | |
| } | |
| window.addEventListener("resize",fitStage); | |
| function rebuildGroupBuffers(){ | |
| const K=S.placed.length; | |
| S.groupPre=[]; S.groupAlpha=[]; | |
| for(let g=0;g<=K;g++) S.groupPre.push(new Float32Array(3*RES*RES)); | |
| const groupOf=i=>{ const g=S.marg?believedGroup(i):S.gt[i]; return g>=0?g:K; }; | |
| for(let i=0;i<S.obs.length;i++){ | |
| const g=groupOf(i); | |
| if(g<K && S.layers[S.placed[g]].hidden) continue; | |
| renderPre([S.obs[i]],RES,S.groupPre[g]); | |
| } | |
| for(let g=0;g<=K;g++) S.groupAlpha.push(alphaFromPre(S.groupPre[g],RES)); | |
| bandDirty=true; | |
| } | |
| function rebuildOneGroup(k){ | |
| S.groupPre[k]=new Float32Array(3*RES*RES); | |
| if(!S.layers[S.placed[k]].hidden) | |
| renderPre(groupAtomIdx(k).map(i=>S.obs[i]),RES,S.groupPre[k]); | |
| S.groupAlpha[k]=alphaFromPre(S.groupPre[k],RES); | |
| bandDirty=true; | |
| } | |
| /* frequency-band buffers, built lazily only when a band gain != 1. | |
| Same visibility rule as the group buffers; render is linear in atom color, | |
| so gain*color == gain*contribution and Σ_b gain_b·bandPre_b is exact. */ | |
| function rebuildBandBuffers(){ | |
| const K=S.placed.length; | |
| bandPre=[new Float32Array(3*RES*RES),new Float32Array(3*RES*RES),new Float32Array(3*RES*RES)]; | |
| const groupOf=i=>{ const g=S.marg?believedGroup(i):S.gt[i]; return g>=0?g:K; }; | |
| for(let i=0;i<S.obs.length;i++){ | |
| const g=groupOf(i); | |
| if(g<K && S.layers[S.placed[g]].hidden) continue; | |
| renderPre([S.obs[i]],RES,bandPre[bandOf(S.obs[i][5])]); | |
| } | |
| bandDirty=false; | |
| } | |
| const sceneCtx=sceneC.getContext("2d"); | |
| const compositePre=new Float32Array(3*RES*RES); | |
| const sceneImg=new ImageData(RES,RES); | |
| function compMode(){ return document.querySelector('input[name=comp]:checked').value; } | |
| function renderScene(){ | |
| const K=S.placed.length; | |
| const gb=GAINS.band, gainsOn=gb[0]!==1||gb[1]!==1||gb[2]!==1; | |
| const bgOn=backdrop.custom||backdrop.img||GAINS.opacity!==1; | |
| if(compMode()==="field"){ | |
| compositePre.fill(0); | |
| if(gainsOn){ | |
| if(bandDirty) rebuildBandBuffers(); | |
| for(let b=0;b<3;b++){ | |
| const g=gb[b]; if(!g) continue; | |
| const buf=bandPre[b]; | |
| for(let i=0;i<compositePre.length;i++) compositePre[i]+=g*buf[i]; | |
| } | |
| } else { | |
| for(const buf of S.groupPre) | |
| for(let i=0;i<compositePre.length;i++) compositePre[i]+=buf[i]; | |
| } | |
| sigmoidField(compositePre,RES,sceneImg.data); | |
| if(bgOn){ | |
| alphaFromPre(compositePre,RES,fieldAlpha); | |
| blendOverBackdrop(sceneImg.data,fieldAlpha,GAINS.opacity,backdrop.img||backdrop.rgb,RES); | |
| } | |
| } else { | |
| // painter: clutter at the back, then placed layers by depth | |
| const order=[K, ...S.placed.map((li,k)=>k) | |
| .sort((a,b)=>S.layers[S.placed[a]].depth-S.layers[S.placed[b]].depth)]; | |
| compositePainter(order,S.groupPre,S.groupAlpha,RES,sceneImg.data); | |
| if(bgOn){ | |
| // union alpha of everything rendered (band gains are field-mode only) | |
| for(let i=0;i<RES*RES;i++){ | |
| let keep=1; | |
| for(let q=0;q<=K;q++) keep*=1-S.groupAlpha[q][i]; | |
| fieldAlpha[i]=1-keep; | |
| } | |
| blendOverBackdrop(sceneImg.data,fieldAlpha,GAINS.opacity,backdrop.img||backdrop.rgb,RES); | |
| } | |
| } | |
| sceneCtx.putImageData(sceneImg,0,0); | |
| } | |
| function w2p(x){return (x+1)/2*overC.width;} | |
| function p2w(px){return px/overC.width*2-1;} | |
| const ovAtoms=document.getElementById("ov-atoms"),ovOwn=document.getElementById("ov-own"), | |
| ovEnt=document.getElementById("ov-ent"),ovPose=document.getElementById("ov-pose"), | |
| ovHints=document.getElementById("ov-hints"); | |
| for(const el of [ovAtoms,ovOwn,ovEnt,ovPose,ovHints]) el.addEventListener("change",drawOverlay); | |
| document.querySelectorAll('input[name=comp]').forEach(el=> | |
| el.addEventListener("change",()=>{renderScene();})); | |
| function drawOverlay(){ | |
| const ctx=overC.getContext("2d"); | |
| ctx.clearRect(0,0,overC.width,overC.height); | |
| const px1=overC.width/CSS; | |
| const K=S.placed.length; | |
| const colOf=k=>S.layers[S.placed[k]]; | |
| if(S.ownField && (ovOwn.checked||ovEnt.checked)){ | |
| const {P,ent,support}=S.ownField; | |
| const im=ctx.createImageData(OWN_RES,OWN_RES); | |
| for(let i=0;i<OWN_RES*OWN_RES;i++){ | |
| if(!support[i]) continue; | |
| let r=0,g=0,b=0,a=0; | |
| if(ovOwn.checked){ | |
| let best=0; | |
| for(let k=1;k<=K;k++) if(P[k][i]>P[best][i]) best=k; | |
| const rgb=best===K?OUTLIER_RGB:colOf(best).rgb; | |
| r=rgb[0];g=rgb[1];b=rgb[2];a=90*P[best][i]; | |
| } | |
| if(ovEnt.checked){ | |
| const maxE=Math.log2(K+1); | |
| const e=Math.min(1,ent[i]/maxE), ea=160*e*e; | |
| if(ea>a){r=200;g=107;b=255;a=ea;} | |
| } | |
| im.data[4*i]=r;im.data[4*i+1]=g;im.data[4*i+2]=b;im.data[4*i+3]=a; | |
| } | |
| const tmp=document.createElement("canvas"); | |
| tmp.width=tmp.height=OWN_RES; | |
| tmp.getContext("2d").putImageData(im,0,0); | |
| ctx.imageSmoothingEnabled=true; | |
| ctx.drawImage(tmp,0,0,overC.width,overC.height); | |
| } | |
| if(ovAtoms.checked){ | |
| for(let i=0;i<S.obs.length;i++){ | |
| const a=S.obs[i], g=believedGroup(i); | |
| if(g>=0 && S.layers[S.placed[g]].hidden) continue; | |
| let rgb=OUTLIER_RGB, alpha=0.5; | |
| if(g===-2){rgb=[150,155,168];alpha=0.55;} | |
| else if(g>=0){rgb=colOf(g).rgb;alpha=S.evid[i]?0.25+0.75*S.marg[i][g]:0.35;} | |
| else if(S.marg){alpha=0.25+0.55*S.marg[i][K];} | |
| const len=Math.max(3*px1,a[3]*overC.width*0.5); | |
| const cx=w2p(a[0]),cy=w2p(a[1]); | |
| const dx=Math.cos(a[2])*len/2,dy=Math.sin(a[2])*len/2; | |
| ctx.strokeStyle=`rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha})`; | |
| ctx.lineWidth=1.6*px1; | |
| ctx.setLineDash(g>=0&&!S.evid[i]?[3*px1,3*px1]:[]); | |
| ctx.beginPath();ctx.moveTo(cx-dx,cy-dy);ctx.lineTo(cx+dx,cy+dy);ctx.stroke(); | |
| } | |
| ctx.setLineDash([]); | |
| } | |
| if(ovHints.checked){ | |
| for(let k=0;k<K;k++){ | |
| const L=colOf(k); | |
| for(const h of L.hints){ | |
| ctx.strokeStyle=L.color; ctx.lineWidth=1.2*px1; ctx.setLineDash([2*px1,3*px1]); | |
| ctx.beginPath();ctx.arc(w2p(h[0]),w2p(h[1]),HINT_SIG*overC.width/2,0,TAU);ctx.stroke(); | |
| ctx.setLineDash([]); | |
| ctx.beginPath();ctx.arc(w2p(h[0]),w2p(h[1]),3*px1,0,TAU); | |
| ctx.fillStyle=L.color;ctx.fill(); | |
| } | |
| } | |
| } | |
| if(ovPose.checked && S.mu){ | |
| for(let k=0;k<K;k++){ | |
| const mu=S.mu[k],Sg=S.Sig[k],L=colOf(k); | |
| const cx=w2p(mu[0]),cy=w2p(mu[1]); | |
| ctx.strokeStyle=L.color;ctx.lineWidth=1.2*px1; | |
| ctx.beginPath(); | |
| ctx.moveTo(cx-7*px1,cy);ctx.lineTo(cx+7*px1,cy); | |
| ctx.moveTo(cx,cy-7*px1);ctx.lineTo(cx,cy+7*px1);ctx.stroke(); | |
| const Ln=26*px1*Math.exp(mu[3]); | |
| ctx.beginPath();ctx.moveTo(cx,cy); | |
| ctx.lineTo(cx+Math.cos(mu[2])*Ln,cy+Math.sin(mu[2])*Ln);ctx.stroke(); | |
| const coast=S.coastT[k], q=coast*coast*0.0012; | |
| const a11=Sg[0][0]+q,a12=Sg[0][1],a22=Sg[1][1]+q; | |
| const tr=a11+a22,det=a11*a22-a12*a12; | |
| const disc=Math.sqrt(Math.max(0,tr*tr/4-det)); | |
| const l1=tr/2+disc,l2=Math.max(1e-12,tr/2-disc); | |
| const ang=Math.atan2(l1-a11,a12||1e-12); | |
| const ghost=L.hidden||S.covFrac[k]>0.7; | |
| ctx.save();ctx.translate(cx,cy);ctx.rotate(-ang+Math.PI/2); | |
| ctx.setLineDash(ghost?[5*px1,4*px1]:[]); | |
| ctx.beginPath(); | |
| ctx.ellipse(0,0,2*Math.sqrt(l1)*overC.width/2,2*Math.sqrt(l2)*overC.width/2,0,0,TAU); | |
| ctx.stroke();ctx.restore();ctx.setLineDash([]); | |
| if(ghost){ | |
| ctx.fillStyle=L.color; | |
| ctx.font=`${11*px1}px ${getComputedStyle(document.body).fontFamily}`; | |
| ctx.fillText("believed, unseen",cx+10*px1,cy-10*px1); | |
| } | |
| } | |
| } | |
| if(toolMode()==="erase"&&lastCursor){ // eraser brush cursor | |
| ctx.strokeStyle="rgba(255,120,110,0.9)"; | |
| ctx.lineWidth=1.4*px1;ctx.setLineDash([4*px1,3*px1]); | |
| ctx.beginPath(); | |
| ctx.arc(w2p(lastCursor[0]),w2p(lastCursor[1]),brushR()*overC.width/2,0,TAU); | |
| ctx.stroke();ctx.setLineDash([]); | |
| } | |
| } | |
| /* ------------- semantic factor from hints ------------- */ | |
| function makeSemLL(){ | |
| const K=S.placed.length; | |
| let any=false; | |
| for(const li of S.placed) if(S.layers[li].hints.length) any=true; | |
| if(!any) return null; | |
| return (i,k)=>{ | |
| const hs=S.layers[S.placed[k]].hints; | |
| if(!hs.length) return 0; | |
| let best=0; | |
| for(const h of hs){ | |
| const d2=(S.obs[i][0]-h[0])**2+(S.obs[i][1]-h[1])**2; | |
| best=Math.max(best,Math.exp(-0.5*d2/(HINT_SIG*HINT_SIG))); | |
| } | |
| return HINT_W*(best-0.5); | |
| }; | |
| } | |
| /* ------------- BP wiring + depth occlusion ------------- */ | |
| function currentTemplates(){ return S.placed.map(li=>S.layers[li].template); } | |
| function computeEvidence(){ | |
| const K=S.placed.length; | |
| if(!document.getElementById("auto-occ").checked || !S.assign){ | |
| S.evid=S.obs.map((_,i)=>{ | |
| const g=believedGroup(i); | |
| return !(g>=0 && S.layers[S.placed[g]].hidden); | |
| }); | |
| S.covFrac=S.placed.map((li,k)=>S.layers[li].hidden?1:0); | |
| return; | |
| } | |
| const layersMeta=S.placed.map(li=>({depth:S.layers[li].depth,hidden:S.layers[li].hidden})); | |
| const {mask,covFrac}=autoHidden(S.obs,S.assign,layersMeta,S.groupAlpha,RES); | |
| S.evid=mask.map(m=>!m); | |
| S.covFrac=covFrac; | |
| } | |
| function runBind(opts={}){ | |
| const hiddenMask=S.evid.map(e=>!e); | |
| const frames=[]; | |
| const res=bpBind(currentTemplates(),S.obs,{ | |
| iters:opts.iters??40, | |
| clampPose:opts.clampPose??null, | |
| hiddenMask, | |
| semLL:makeSemLL(), | |
| onIter:(it,marg,mu,Sig)=>{ if(opts.animate) frames.push({ | |
| marg,mu:mu.map(m=>m.slice()),Sig:Sig.map(M=>M.map(r=>r.slice()))}); }, | |
| }); | |
| return {res,frames}; | |
| } | |
| function bind(animate=true){ | |
| if(S.phase==="binding"||!S.placed.length) return; | |
| S.assign=null; | |
| eraseStack=[]; updateEraseButtons(); // erase history is stale once BP re-runs | |
| S.evid=S.obs.map(()=>true); // full re-bind: everything is evidence again | |
| for(const li of S.placed) if(S.layers[li].hidden){ | |
| // user-hidden layers stay evidence-free | |
| // (their atoms are excluded per-atom below via gt at soup time) | |
| } | |
| setPhase("binding"); | |
| setTimeout(()=>{ | |
| const {res,frames}=runBind({iters:40,animate}); | |
| if(!animate){finishBind(res);return;} | |
| let f=0; | |
| const meter=document.querySelector("#bind-meter > div"); | |
| const step=()=>{ | |
| if(f<frames.length){ | |
| S.marg=frames[f].marg;S.mu=frames[f].mu;S.Sig=frames[f].Sig; | |
| updateAssign(); | |
| meter.style.width=(100*(f+1)/frames.length)+"%"; | |
| document.getElementById("m-it").textContent=(f+1); | |
| drawOverlay();updatePanels(); | |
| f++;requestAnimationFrame(step); | |
| } else finishBind(res); | |
| }; | |
| step(); | |
| },30); | |
| } | |
| function finishBind(res){ | |
| S.marg=res.marg;S.mu=res.mu;S.Sig=res.Sig; | |
| updateAssign(); | |
| rebuildGroupBuffers(); | |
| occlusionRefresh(true); | |
| renderScene();refreshOwnership(); | |
| setPhase("bound"); | |
| drawOverlay();updatePanels();renderLayerList(); | |
| } | |
| /* recompute evidence from the depth stack; if it changed, refresh marginals | |
| with all poses clamped (occlusion changes evidence, not where things are) */ | |
| function occlusionRefresh(silent){ | |
| const before=S.evid.join(""); | |
| computeEvidence(); | |
| if(S.evid.join("")===before) return; | |
| const clamp=S.mu.map(m=>m.slice()); | |
| const {res}=runBind({iters:8,clampPose:clamp}); | |
| S.marg=res.marg; | |
| updateAssign(); | |
| // coasting for heavily covered layers | |
| for(let k=0;k<S.placed.length;k++) | |
| S.coastT[k]=(S.covFrac[k]>0.7||S.layers[S.placed[k]].hidden)?S.coastT[k]+1:0; | |
| if(!silent){drawOverlay();updatePanels();renderLayerList();} | |
| } | |
| function refreshOwnership(){ | |
| if(!S.marg){S.ownField=null;return;} | |
| const K=S.placed.length; | |
| const vis=[],visMarg=[]; | |
| for(let i=0;i<S.obs.length;i++){ | |
| const g=believedGroup(i); | |
| if(g>=0&&S.layers[S.placed[g]].hidden) continue; | |
| vis.push(S.obs[i]);visMarg.push(S.marg[i]); | |
| } | |
| S.ownField=ownershipField(vis,visMarg,K,OWN_RES); | |
| } | |
| function resettle(k,clampXi){ | |
| const clamp=S.placed.map(()=>null);clamp[k]=clampXi; | |
| const {res}=runBind({iters:10,clampPose:clamp}); | |
| S.marg=res.marg;S.mu=res.mu;S.Sig=res.Sig; | |
| S.mu[k]=clampXi.slice(); | |
| updateAssign(); | |
| rebuildGroupBuffers(); | |
| occlusionRefresh(true); | |
| renderScene();refreshOwnership(); | |
| drawOverlay();updatePanels();renderLayerList(); | |
| document.getElementById("m-it").textContent="10 (re-settle)"; | |
| } | |
| /* ------------- interventions ------------- */ | |
| function applyGesture(k,g){ | |
| for(const i of groupAtomIdx(k)) S.obs[i]=transformAtoms([S.obs[i]],g)[0]; | |
| S.mu[k]=composePose(g,S.mu[k]); | |
| rebuildOneGroup(k); | |
| renderScene();drawOverlay();updatePanels(); | |
| } | |
| /* ------------- eraser (Bet 9) ------------- | |
| Deleting an atom = filtering every per-atom parallel array by the brush | |
| mask. Order of atoms carries no meaning anywhere (additive render, per-atom | |
| marginals), so undo can simply re-append the removed records. | |
| Undo is valid until the next Bind/Scramble/place/remove (those rebuild or | |
| re-infer the per-atom state). "Burn" additionally removes the source atom | |
| from the layer TEMPLATE, so the erase survives scene rebuilds. */ | |
| function eraseAt(wx,wy,stroke){ | |
| const skip=i=>{ const g=believedGroup(i); return g>=0&&S.layers[S.placed[g]].hidden; }; | |
| const mask=eraseMask(S.obs,wx,wy,brushR(),skip); | |
| let any=false; for(const m of mask) if(m){any=true;break;} | |
| if(!any) return; | |
| const kObs=[],kGt=[],kSrc=[],kEvid=[], | |
| kAssign=S.assign?[]:null, kMarg=S.marg?[]:null; | |
| for(let i=0;i<S.obs.length;i++){ | |
| if(mask[i]){ | |
| stroke.atoms.push({a:S.obs[i],gt:S.gt[i],src:S.src[i],evid:S.evid[i], | |
| assign:S.assign?S.assign[i]:null, marg:S.marg?S.marg[i]:null}); | |
| } else { | |
| kObs.push(S.obs[i]);kGt.push(S.gt[i]);kSrc.push(S.src[i]);kEvid.push(S.evid[i]); | |
| if(kAssign)kAssign.push(S.assign[i]); | |
| if(kMarg)kMarg.push(S.marg[i]); | |
| } | |
| } | |
| S.obs=kObs;S.gt=kGt;S.src=kSrc;S.evid=kEvid; | |
| if(kAssign)S.assign=kAssign; | |
| if(kMarg)S.marg=kMarg; | |
| rebuildGroupBuffers();renderScene();drawOverlay();updatePanels(); | |
| } | |
| function commitStroke(stroke){ | |
| if(!stroke.atoms.length) return; | |
| if(document.getElementById("erase-burn").checked){ | |
| for(const rec of stroke.atoms){ | |
| if(!rec.src) continue; // clutter has no template | |
| const L=S.layers[rec.src.li]; | |
| const idx=L.template.indexOf(rec.src.ref); | |
| if(idx>=0){L.template.splice(idx,1);stroke.tmpl.push(rec.src);} | |
| } | |
| } | |
| eraseStack.push(stroke); | |
| updateEraseButtons(); | |
| refreshOwnership();drawOverlay();renderLayerList();renderLibrary(); | |
| } | |
| function undoErase(redraw=true){ | |
| const stroke=eraseStack.pop(); | |
| if(!stroke) return; | |
| for(const t of stroke.tmpl) S.layers[t.li].template.push(t.ref); | |
| for(const rec of stroke.atoms){ | |
| S.obs.push(rec.a);S.gt.push(rec.gt);S.src.push(rec.src);S.evid.push(rec.evid); | |
| if(S.assign)S.assign.push(rec.assign??-1); | |
| if(S.marg)S.marg.push(rec.marg); | |
| } | |
| updateEraseButtons(); | |
| if(redraw){ | |
| rebuildGroupBuffers();renderScene();refreshOwnership(); | |
| drawOverlay();updatePanels();renderLayerList();renderLibrary(); | |
| } | |
| } | |
| function restoreAllErases(){ | |
| while(eraseStack.length) undoErase(false); | |
| rebuildGroupBuffers();renderScene();refreshOwnership(); | |
| drawOverlay();updatePanels();renderLayerList();renderLibrary(); | |
| } | |
| /* ------------- pointer interaction ------------- */ | |
| let drag=null; | |
| overC.addEventListener("pointerdown",e=>{ | |
| const rect=overC.getBoundingClientRect(); | |
| const wx=p2w((e.clientX-rect.left)*overC.width/rect.width); | |
| const wy=p2w((e.clientY-rect.top)*overC.width/rect.width); | |
| if(e.shiftKey){ // semantic hint for the selected layer | |
| const li=S.selected; | |
| if(li>=0&&S.layers[li].placed){ | |
| S.layers[li].hints.push([wx,wy]); | |
| drawOverlay();renderLayerList(); | |
| document.getElementById("prompt-log").textContent= | |
| `hint added for ${S.layers[li].name} — Bind applies it as a location prior.`; | |
| } | |
| return; | |
| } | |
| if(toolMode()==="erase"){ // erase works in soup AND bound | |
| drag={erase:true,stroke:{atoms:[],tmpl:[]}}; | |
| overC.setPointerCapture(e.pointerId); | |
| lastCursor=[wx,wy]; | |
| eraseAt(wx,wy,drag.stroke); | |
| return; | |
| } | |
| if(S.phase!=="bound") return; | |
| let best=-1,bd=0.12*0.12; | |
| for(let i=0;i<S.obs.length;i++){ | |
| const g=believedGroup(i); | |
| if(g<0) continue; | |
| if(S.layers[S.placed[g]].hidden) continue; | |
| const d=(S.obs[i][0]-wx)**2+(S.obs[i][1]-wy)**2; | |
| if(d<bd){bd=d;best=i;} | |
| } | |
| let k=best>=0?believedGroup(best):-1; | |
| if(k<0) for(let kk=0;kk<S.placed.length;kk++) | |
| if((S.layers[S.placed[kk]].hidden||S.covFrac[kk]>0.7)&& | |
| (S.mu[kk][0]-wx)**2+(S.mu[kk][1]-wy)**2<0.05*0.05) k=kk; | |
| if(k<0) return; | |
| drag={k,lx:wx,ly:wy,moved:false}; | |
| overC.classList.add("dragging"); | |
| overC.setPointerCapture(e.pointerId); | |
| }); | |
| overC.addEventListener("pointermove",e=>{ | |
| const rect=overC.getBoundingClientRect(); | |
| const wx=p2w((e.clientX-rect.left)*overC.width/rect.width); | |
| const wy=p2w((e.clientY-rect.top)*overC.width/rect.width); | |
| if(toolMode()==="erase") lastCursor=[wx,wy]; | |
| if(!drag){ if(toolMode()==="erase") drawOverlay(); return; } | |
| if(drag.erase){ eraseAt(wx,wy,drag.stroke); return; } | |
| const dx=wx-drag.lx,dy=wy-drag.ly; | |
| if(dx||dy){applyGesture(drag.k,gestureTranslate(dx,dy));drag.lx=wx;drag.ly=wy;drag.moved=true;} | |
| }); | |
| overC.addEventListener("pointerup",e=>{ | |
| if(!drag) return; | |
| if(drag.erase){ commitStroke(drag.stroke); drag=null; return; } | |
| overC.classList.remove("dragging"); | |
| if(drag.moved) resettle(drag.k,S.mu[drag.k]); | |
| drag=null; | |
| }); | |
| overC.addEventListener("pointerleave",()=>{ | |
| if(lastCursor){ lastCursor=null; drawOverlay(); } | |
| }); | |
| overC.addEventListener("wheel",e=>{ | |
| if(S.phase!=="bound") return; | |
| const rect=overC.getBoundingClientRect(); | |
| const wx=p2w((e.clientX-rect.left)*overC.width/rect.width); | |
| const wy=p2w((e.clientY-rect.top)*overC.width/rect.width); | |
| let k=-1,bd=0.16*0.16; | |
| for(let kk=0;kk<S.placed.length;kk++){ | |
| const d=(S.mu[kk][0]-wx)**2+(S.mu[kk][1]-wy)**2; | |
| if(d<bd){bd=d;k=kk;} | |
| } | |
| if(k<0) return; | |
| e.preventDefault(); | |
| const c=[S.mu[k][0],S.mu[k][1]]; | |
| const g=e.shiftKey?gestureScaleAbout(c,e.deltaY>0?-0.05:0.05) | |
| :gestureRotateAbout(c,e.deltaY>0?0.08:-0.08); | |
| applyGesture(k,g); | |
| clearTimeout(overC._wt); | |
| overC._wt=setTimeout(()=>resettle(k,S.mu[k]),350); | |
| },{passive:false}); | |
| /* ------------- panels ------------- */ | |
| function setPhase(p){ | |
| S.phase=p; | |
| const lamp=document.getElementById("state-lamp"); | |
| lamp.className=p==="soup"?"soup":p==="binding"?"binding":"bound"; | |
| lamp.textContent=p==="soup"?"ATOM SOUP":p==="binding"?"BELIEFS SETTLING":"BOUND — GRABBABLE"; | |
| document.getElementById("btn-bind").disabled=p==="binding"||!S.placed.length; | |
| } | |
| function renderLayerList(){ | |
| const el=document.getElementById("layer-list"); | |
| el.innerHTML=""; | |
| const byDepth=[...S.placed].map((li,k)=>({li,k})) | |
| .sort((a,b)=>S.layers[b.li].depth-S.layers[a.li].depth); | |
| for(const {li,k} of byDepth){ | |
| const L=S.layers[li]; | |
| const idx=S.assign?groupAtomIdx(k):[]; | |
| const conf=idx.length?idx.reduce((a,i)=>a+(S.evid[i]?S.marg[i][k]:0),0)/Math.max(1,idx.filter(i=>S.evid[i]).length):0; | |
| const cov=S.covFrac[k]||0; | |
| const d=document.createElement("div"); | |
| d.className="layer"+(S.selected===li?" sel":""); | |
| d.innerHTML=` | |
| <div class="name" data-li="${li}"> | |
| <span class="swatch" style="background:${L.color}"></span> | |
| <span>${L.name}</span> | |
| <span class="dim">${idx.length||L.template.length} atoms${L.hints.length?` · ${L.hints.length} hint${L.hints.length>1?"s":""}`:""}</span> | |
| <span class="spacer"></span> | |
| <div class="belief-bar"><div style="width:${100*conf}%"></div></div> | |
| </div> | |
| <div class="pose">${S.mu?`t=(${S.mu[k][0].toFixed(2)}, ${S.mu[k][1].toFixed(2)}) \u03c1=${(S.mu[k][2]*180/Math.PI).toFixed(0)}\u00b0 s=${Math.exp(S.mu[k][3]).toFixed(2)}`:"pose: unbound"}${cov>0.05?` · <span style="color:var(--entropy)">${(100*cov).toFixed(0)}% occluded${S.coastT[k]?`, coasting ${S.coastT[k]}`:""}</span>`:""}</div> | |
| <div class="ctl"> | |
| <button data-li="${li}" data-op="rotl">↺</button> | |
| <button data-li="${li}" data-op="rotr">↻</button> | |
| <button data-li="${li}" data-op="grow">+</button> | |
| <button data-li="${li}" data-op="shrink">−</button> | |
| <button data-li="${li}" data-op="front">front</button> | |
| <button data-li="${li}" data-op="back">back</button> | |
| <button data-li="${li}" data-op="hide">${L.hidden?"reveal":"hide"}</button> | |
| ${L.hints.length?`<button data-li="${li}" data-op="clearhints">hints ✕</button>`:""} | |
| <button data-li="${li}" data-op="remove">✕</button> | |
| </div>`; | |
| el.appendChild(d); | |
| } | |
| if(!byDepth.length) | |
| el.innerHTML='<div class="dim" style="font-size:11px">no layers placed — place from the library or spawn one</div>'; | |
| } | |
| document.getElementById("layer-list").addEventListener("click",e=>{ | |
| const nm=e.target.closest(".name"); | |
| if(nm&&!e.target.closest("button")){ | |
| S.selected=+nm.dataset.li===S.selected?-1:+nm.dataset.li; | |
| renderLayerList();return; | |
| } | |
| const b=e.target.closest("button");if(!b)return; | |
| const li=+b.dataset.li,op=b.dataset.op; | |
| const k=S.placed.indexOf(li); | |
| if(op==="remove"){removeLayer(li);return;} | |
| if(op==="clearhints"){S.layers[li].hints=[];drawOverlay();renderLayerList();return;} | |
| if(op==="front"||op==="back"){ | |
| S.layers[li].depth=op==="front"?nextDepth++:(Math.min(...S.placed.map(x=>S.layers[x].depth))-1); | |
| if(S.phase==="bound"){rebuildGroupBuffers();occlusionRefresh(false);renderScene();refreshOwnership();} | |
| renderLayerList();return; | |
| } | |
| if(S.phase!=="bound")return; | |
| if(op==="hide"){ | |
| S.layers[li].hidden=!S.layers[li].hidden; | |
| rebuildGroupBuffers();occlusionRefresh(false);renderScene();refreshOwnership();drawOverlay();renderLayerList(); | |
| return; | |
| } | |
| const c=[S.mu[k][0],S.mu[k][1]]; | |
| if(op==="rotl"){applyGesture(k,gestureRotateAbout(c,-Math.PI/12));resettle(k,S.mu[k]);} | |
| if(op==="rotr"){applyGesture(k,gestureRotateAbout(c,Math.PI/12));resettle(k,S.mu[k]);} | |
| if(op==="grow"){applyGesture(k,gestureScaleAbout(c,0.12));resettle(k,S.mu[k]);} | |
| if(op==="shrink"){applyGesture(k,gestureScaleAbout(c,-0.12));resettle(k,S.mu[k]);} | |
| }); | |
| function renderLibrary(){ | |
| const el=document.getElementById("library"); | |
| el.innerHTML=""; | |
| S.layers.forEach((L,li)=>{ | |
| if(L.placed) return; | |
| const c=document.createElement("span"); | |
| c.className="chip"; | |
| c.innerHTML=`<span class="swatch" style="background:${L.color}"></span>${L.name} | |
| <span class="dim">${L.template.length}</span> | |
| <button data-li="${li}" data-op="place" title="place">+</button>`; | |
| el.appendChild(c); | |
| }); | |
| } | |
| document.getElementById("library").addEventListener("click",e=>{ | |
| const b=e.target.closest("button");if(!b)return; | |
| placeLayer(+b.dataset.li); | |
| }); | |
| function updatePanels(){ | |
| document.getElementById("m-n").textContent=S.obs.length; | |
| const K=S.placed.length; | |
| if(!S.marg){ | |
| document.getElementById("m-acc").textContent="—"; | |
| document.getElementById("m-ent").textContent="—"; | |
| return; | |
| } | |
| let correct=0; | |
| for(let i=0;i<S.gt.length;i++) if(believedGroup(i)===S.gt[i]) correct++; | |
| document.getElementById("m-acc").textContent=(100*correct/S.gt.length).toFixed(1)+"%"; | |
| const ent=atomEntropy(S.marg); | |
| document.getElementById("m-ent").textContent= | |
| (ent.reduce((a,x)=>a+x,0)/ent.length).toFixed(2)+" bits"; | |
| } | |
| /* ------------- spawn: server endpoints ------------- */ | |
| const spawnStatus=document.getElementById("spawn-status"); | |
| async function gradioCall(name,data){ | |
| const base=location.pathname.includes("/studio/")?"..":""; | |
| const r=await fetch(`${base}/gradio_api/call/${name}`,{ | |
| method:"POST",headers:{"Content-Type":"application/json"}, | |
| body:JSON.stringify({data})}); | |
| const {event_id}=await r.json(); | |
| const resp=await fetch(`${base}/gradio_api/call/${name}/${event_id}`); | |
| const text=await resp.text(); | |
| const lines=text.split("\n").filter(l=>l.startsWith("data:")); | |
| if(!lines.length) throw new Error("no data from server"); | |
| const payload=JSON.parse(lines[lines.length-1].slice(5)); | |
| if(payload==null||payload.error) throw new Error(payload?.error||"server error"); | |
| return JSON.parse(payload[0]); | |
| } | |
| function addGenerated(layer,fallbackName){ | |
| const name=(layer.ledger&&layer.ledger.prompt) | |
| ? (layer.ledger.attention&&layer.ledger.attention.token!=="(all content tokens)" | |
| ? layer.ledger.attention.token : layer.ledger.prompt.split(/[ ,]/)[1]||fallbackName) | |
| : fallbackName; | |
| addLayer(name,layer.atoms,true); | |
| spawnStatus.textContent=`layer "${name}": ${layer.atoms.length} atoms · ${layer.ledger.status}`+ | |
| (layer.ledger.attention?` · matte kept ${layer.ledger.attention.atoms_after_matte}/${layer.ledger.attention.atoms_before_matte}`:""); | |
| } | |
| document.getElementById("btn-spawn-text").addEventListener("click",async()=>{ | |
| const prompt=document.getElementById("spawn-prompt").value.trim(); | |
| const token=document.getElementById("spawn-token").value.trim(); | |
| if(!prompt){spawnStatus.textContent="write a prompt first";return;} | |
| spawnStatus.textContent="distilling on the server (GPU, minutes)\u2026"; | |
| try{ | |
| const layer=await gradioCall("layer_from_text",[prompt,"",192,900,30,token,0.35]); | |
| addGenerated(layer,"generated"); | |
| }catch(err){spawnStatus.textContent="distill failed: "+err.message+ | |
| " (needs a GPU Space; on CPU use Fit or paste JSON)";} | |
| }); | |
| document.getElementById("btn-spawn-img").addEventListener("click",async()=>{ | |
| const f=document.getElementById("spawn-file").files[0]; | |
| if(!f){spawnStatus.textContent="choose an image first";return;} | |
| spawnStatus.textContent="fitting atoms to the image (CPU, ~1 min)\u2026"; | |
| try{ | |
| const b64=await new Promise((res,rej)=>{ | |
| const img=new Image(); | |
| img.onload=()=>{ | |
| const c=document.createElement("canvas"); | |
| const s=Math.min(1,160/Math.max(img.width,img.height)); | |
| c.width=Math.round(img.width*s);c.height=Math.round(img.height*s); | |
| c.getContext("2d").drawImage(img,0,0,c.width,c.height); | |
| res(c.toDataURL("image/png").split(",")[1]); | |
| }; | |
| img.onerror=rej; | |
| img.src=URL.createObjectURL(f); | |
| }); | |
| const layer=await gradioCall("layer_from_image_b64",[b64,140,400]); | |
| addGenerated(layer,f.name.replace(/\.[^.]+$/,"")); | |
| }catch(err){spawnStatus.textContent="fit failed: "+err.message+ | |
| " (is this page running from the Space? standalone files can only paste JSON)";} | |
| }); | |
| document.getElementById("btn-spawn-json").addEventListener("click",()=>{ | |
| try{ | |
| const raw=JSON.parse(document.getElementById("spawn-json").value); | |
| const atoms=raw.atoms||raw; | |
| if(!Array.isArray(atoms)||atoms[0].length!==10) throw new Error("expect [[10 fields]...]"); | |
| addLayer("pasted-"+nextId,atoms,true); | |
| spawnStatus.textContent=`pasted layer: ${atoms.length} atoms`; | |
| document.getElementById("spawn-json").value=""; | |
| }catch(err){spawnStatus.textContent="bad JSON: "+err.message;} | |
| }); | |
| /* ------------- prompt as intervention ------------- */ | |
| const promptLog=document.getElementById("prompt-log"); | |
| function layerByName(part){ | |
| for(let k=0;k<S.placed.length;k++) | |
| if(part.includes(S.layers[S.placed[k]].name.toLowerCase())) return k; | |
| return -1; | |
| } | |
| function localParse(text){ | |
| const t=text.toLowerCase(),ops=[]; | |
| for(const part of t.split(/,|;| and /)){ | |
| const k=layerByName(part); | |
| if(k<0) continue; | |
| if(/hide|occlude/.test(part)){ops.push({op:"hide",k});continue;} | |
| if(/show|reveal|unhide/.test(part)){ops.push({op:"show",k});continue;} | |
| if(/front|top|forward/.test(part)&&/bring|put|move|send/.test(part)){ops.push({op:"front",k});continue;} | |
| if(/back|behind|bottom/.test(part)&&/send|put|move|push/.test(part)){ops.push({op:"back",k});continue;} | |
| const deg=part.match(/(-?\d+)\s*(deg|\u00b0)?/); | |
| if(/rotate|turn|spin/.test(part)) | |
| ops.push({op:"rotate",k,deg:deg?+deg[1]:30*(/left|counter|ccw/.test(part)?-1:1)}); | |
| if(/scale|grow|bigger|larger|shrink|smaller/.test(part)) | |
| ops.push({op:"scale",k,ds:/shrink|smaller/.test(part)?-0.15:0.15}); | |
| if(/move|push|slide|put|drag/.test(part)){ | |
| let dx=0,dy=0;const step=0.22; | |
| if(/left/.test(part))dx-=step; | |
| if(/right/.test(part))dx+=step; | |
| if(/up|top/.test(part))dy-=step; | |
| if(/down|bottom/.test(part))dy+=step; | |
| if(/center|middle/.test(part))ops.push({op:"moveto",k,x:0,y:0}); | |
| else if(dx||dy)ops.push({op:"move",k,dx,dy}); | |
| } | |
| } | |
| return ops; | |
| } | |
| async function claudeParse(text){ | |
| const names=S.placed.map(li=>S.layers[li].name); | |
| const sceneDesc=S.placed.map((li,k)=> | |
| `${S.layers[li].name}: t=(${S.mu[k][0].toFixed(2)},${S.mu[k][1].toFixed(2)}) rho=${S.mu[k][2].toFixed(2)} scale=${Math.exp(S.mu[k][3]).toFixed(2)} depth=${S.layers[li].depth} hidden=${S.layers[li].hidden}`).join("; "); | |
| const resp=await fetch("https://api.anthropic.com/v1/messages",{ | |
| method:"POST",headers:{"Content-Type":"application/json"}, | |
| body:JSON.stringify({model:"claude-sonnet-4-6",max_tokens:1000, | |
| messages:[{role:"user",content: | |
| `You control a probabilistic layered scene editor. Coordinates x,y in [-1,1], y grows DOWNWARD. Higher depth = in front. Layers: ${sceneDesc}. | |
| User command: "${text}" | |
| Respond ONLY with a JSON array (no prose, no fences) of ops: | |
| {"op":"move","object":<name>,"dx":n,"dy":n} {"op":"moveto","object":<name>,"x":n,"y":n} | |
| {"op":"rotate","object":<name>,"deg":n} {"op":"scale","object":<name>,"factor":n} | |
| {"op":"front","object":<name>} {"op":"back","object":<name>} | |
| {"op":"hide","object":<name>} {"op":"show","object":<name>} | |
| Valid names: ${JSON.stringify(names)}`}]})}); | |
| const data=await resp.json(); | |
| const txt=data.content.filter(c=>c.type==="text").map(c=>c.text).join(""); | |
| const arr=JSON.parse(txt.replace(/```json|```/g,"").trim()); | |
| return arr.map(o=>{ | |
| const k=S.placed.findIndex(li=>S.layers[li].name===o.object); | |
| if(k<0)return null; | |
| if(o.op==="move")return{op:"move",k,dx:o.dx,dy:o.dy}; | |
| if(o.op==="moveto")return{op:"moveto",k,x:o.x,y:o.y}; | |
| if(o.op==="rotate")return{op:"rotate",k,deg:o.deg}; | |
| if(o.op==="scale")return{op:"scale",k,ds:Math.log(o.factor)}; | |
| return{op:o.op,k}; | |
| }).filter(Boolean); | |
| } | |
| function execOps(ops){ | |
| if(!ops.length){promptLog.textContent="Parsed no intervention. Layers: "+ | |
| S.placed.map(li=>S.layers[li].name).join(", ")+". Verbs: move/rotate/scale/hide/show/front/back.";return;} | |
| const touched=new Set(); | |
| for(const o of ops){ | |
| const li=S.placed[o.k]; | |
| if(o.op==="hide"&&!S.layers[li].hidden){S.layers[li].hidden=true;continue;} | |
| if(o.op==="show"&&S.layers[li].hidden){S.layers[li].hidden=false;continue;} | |
| if(o.op==="front"){S.layers[li].depth=nextDepth++;continue;} | |
| if(o.op==="back"){S.layers[li].depth=Math.min(...S.placed.map(x=>S.layers[x].depth))-1;continue;} | |
| const c=[S.mu[o.k][0],S.mu[o.k][1]]; | |
| if(o.op==="move"){applyGesture(o.k,gestureTranslate(o.dx,o.dy));touched.add(o.k);} | |
| if(o.op==="moveto"){applyGesture(o.k,gestureTranslate(o.x-c[0],o.y-c[1]));touched.add(o.k);} | |
| if(o.op==="rotate"){applyGesture(o.k,gestureRotateAbout(c,o.deg*Math.PI/180));touched.add(o.k);} | |
| if(o.op==="scale"){applyGesture(o.k,gestureScaleAbout(c,o.ds));touched.add(o.k);} | |
| } | |
| for(const k of touched) resettle(k,S.mu[k]); | |
| rebuildGroupBuffers();occlusionRefresh(true);renderScene();refreshOwnership(); | |
| drawOverlay();updatePanels();renderLayerList(); | |
| promptLog.textContent="Clamped: "+ops.map(o=>`${o.op} ${S.layers[S.placed[o.k]].name}`).join(", ")+ | |
| " — beliefs re-settled around the intervention."; | |
| } | |
| async function doPrompt(){ | |
| if(S.phase!=="bound"){promptLog.textContent="Bind first — there is nothing to address in a soup.";return;} | |
| const text=document.getElementById("prompt").value.trim(); | |
| if(!text)return; | |
| let ops=localParse(text); | |
| if(!ops.length){ | |
| promptLog.textContent="Local grammar found nothing; asking Claude\u2026"; | |
| try{ops=await claudeParse(text);} | |
| catch{promptLog.textContent="Claude unavailable outside claude.ai — try: move/rotate/scale/hide/front/back + a layer name.";return;} | |
| } | |
| execOps(ops); | |
| } | |
| document.getElementById("btn-prompt").addEventListener("click",doPrompt); | |
| document.getElementById("prompt").addEventListener("keydown",e=>{if(e.key==="Enter")doPrompt();}); | |
| document.getElementById("auto-occ").addEventListener("change",()=>{ | |
| if(S.phase==="bound"){occlusionRefresh(false);renderScene();} | |
| }); | |
| /* ------------- edit & look wiring (Bet 9) ------------- */ | |
| document.getElementById("btn-undo-erase").addEventListener("click",()=>undoErase()); | |
| document.getElementById("btn-restore-erase").addEventListener("click",restoreAllErases); | |
| document.querySelectorAll('input[name=tool]').forEach(el=>el.addEventListener("change",()=>{ | |
| stage.classList.toggle("erasing",toolMode()==="erase"); | |
| if(toolMode()!=="erase") lastCursor=null; | |
| drawOverlay(); | |
| })); | |
| document.querySelectorAll('input[name=brush]').forEach(el=>el.addEventListener("change",drawOverlay)); | |
| document.getElementById("bg-color").addEventListener("input",e=>{ | |
| const h=e.target.value; | |
| backdrop.rgb=[parseInt(h.slice(1,3),16),parseInt(h.slice(3,5),16),parseInt(h.slice(5,7),16)]; | |
| backdrop.custom=true;renderScene(); | |
| }); | |
| document.getElementById("bg-file").addEventListener("change",e=>{ | |
| const f=e.target.files[0];if(!f)return; | |
| const img=new Image(); | |
| img.onload=()=>{ | |
| const c=document.createElement("canvas");c.width=c.height=RES; | |
| const sc=Math.max(RES/img.width,RES/img.height); // cover-crop | |
| c.getContext("2d").drawImage(img,(RES-img.width*sc)/2,(RES-img.height*sc)/2,img.width*sc,img.height*sc); | |
| backdrop.img=c.getContext("2d").getImageData(0,0,RES,RES).data; | |
| backdrop.custom=true;renderScene(); | |
| URL.revokeObjectURL(img.src); | |
| }; | |
| img.src=URL.createObjectURL(f); | |
| }); | |
| document.getElementById("btn-bg-clear").addEventListener("click",()=>{ | |
| backdrop.img=null;document.getElementById("bg-file").value=""; | |
| renderScene(); | |
| }); | |
| for(const [id,set] of [ | |
| ["g-coarse",v=>GAINS.band[0]=v],["g-mid",v=>GAINS.band[1]=v], | |
| ["g-fine",v=>GAINS.band[2]=v],["g-op",v=>GAINS.opacity=v]]){ | |
| const el=document.getElementById(id); | |
| el.addEventListener("input",()=>{ | |
| const v=el.value/100; set(v); | |
| document.getElementById(id+"-v").textContent=v.toFixed(2); | |
| renderScene(); | |
| }); | |
| } | |
| /* ------------- boot ------------- */ | |
| document.getElementById("btn-scramble").addEventListener("click",scramble); | |
| document.getElementById("btn-bind").addEventListener("click",()=>bind(true)); | |
| addLayer("tractor",BUILTINS.tractor,false); | |
| addLayer("star",BUILTINS.star,false); | |
| addLayer("boat",BUILTINS.boat,false); | |
| S.layers[0].placed=true;S.layers[0].depth=nextDepth++;S.layers[0].pose=[0.3,-0.25,0.3,Math.log(0.55)]; | |
| S.layers[1].placed=true;S.layers[1].depth=nextDepth++;S.layers[1].pose=[-0.4,0.35,-0.9,Math.log(0.5)]; | |
| fitStage(); | |
| rebuildScene(); | |
| </script> | |
| </body> | |
| </html> | |