v2 source, NB2 prompt contract, and the slice editor

This commit is contained in:
2026-07-27 10:59:22 +10:00
commit e45f5729fa
10 changed files with 2200 additions and 0 deletions
+271
View File
@@ -0,0 +1,271 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Exploded Cluster — slice editor</title>
<style>
:root{
--void:#070b14; --panel:#0d1422; --line:#1e2b42;
--cyan:#3fbaf5; --magenta:#e879f9; --ink:#c8d6ee; --dim:#6b7fa3;
}
*{box-sizing:border-box}
body{margin:0;background:var(--void);color:var(--ink);
font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;
display:grid;grid-template-columns:1fr 330px;height:100vh;overflow:hidden}
@media (max-width:860px){body{grid-template-columns:1fr;grid-template-rows:44vh 1fr}}
#stage{position:relative;overflow:auto;display:grid;place-items:center;padding:20px;
background:var(--void)}
#frame{position:relative;line-height:0;box-shadow:0 0 0 1px var(--line);background:var(--void)}
#frame img#base{display:block;max-width:100%;height:auto}
.part{position:absolute;inset:0;background-size:100% 100%;background-repeat:no-repeat;
will-change:transform}
#overlay{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}
#hint{position:absolute;left:10px;top:10px;font-size:11px;color:var(--dim);
background:rgba(7,11,20,.85);padding:4px 8px;border:1px solid var(--line);pointer-events:none}
#panel{background:var(--panel);border-left:1px solid var(--line);
overflow-y:auto;padding:14px;display:flex;flex-direction:column;gap:13px}
h1{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--cyan);margin:0}
h2{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);
margin:0 0 6px;font-weight:400}
.row{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
button{background:#12203a;color:var(--ink);border:1px solid var(--line);
padding:6px 9px;font:inherit;font-size:11px;cursor:pointer;border-radius:3px}
button:hover{border-color:var(--cyan);color:#fff}
button.pri{background:var(--cyan);color:#04121f;border-color:var(--cyan);font-weight:700}
:is(button,input,textarea,select):focus-visible{outline:2px solid var(--cyan);outline-offset:2px}
input[type=file]{font-size:11px;width:100%}
input[type=text]{background:#08101c;border:1px solid var(--line);color:var(--ink);
font:inherit;font-size:11px;padding:4px 6px;border-radius:3px;width:100%}
input[type=range]{accent-color:var(--cyan);width:100%;height:18px}
textarea{width:100%;height:100px;background:#08101c;border:1px solid var(--line);
color:var(--ink);font:inherit;font-size:10px;padding:6px;border-radius:3px}
.note{font-size:10px;color:var(--dim);line-height:1.45;margin:6px 0 0}
.sep{height:1px;background:var(--line)}
.cut{border:1px solid var(--line);border-radius:4px;padding:8px;background:#0a1120}
.cut.on{border-color:var(--cyan)}
.cut h3{margin:0 0 6px;font-size:11px;font-weight:400;color:var(--ink);
display:flex;justify-content:space-between;align-items:center}
.cut h3 b{font-weight:400;font-size:10px;color:var(--dim);font-variant-numeric:tabular-nums}
.sl{display:grid;grid-template-columns:34px 1fr 38px;gap:6px;align-items:center;
font-size:10px;color:var(--dim);margin-top:3px}
.sl output{font-variant-numeric:tabular-nums;text-align:right;color:var(--ink)}
</style>
</head>
<body>
<div id="stage">
<div id="frame">
<img id="base" alt="">
<svg id="overlay" viewBox="0 0 100 100" preserveAspectRatio="none"></svg>
<div id="hint">load a hero →</div>
</div>
</div>
<div id="panel">
<h1>Slice editor</h1>
<div>
<h2>1 · hero image</h2>
<input type="file" id="file" accept="image/*">
<p class="note">Slices apply automatically. Pick the file, hit ▶ play.</p>
</div>
<div class="sep"></div>
<div>
<h2>2 · cut lines</h2>
<div id="geo" style="display:flex;flex-direction:column;gap:7px"></div>
<p class="note">Each cut traces a slab's <em>bottom</em> edge — left corner, down to the
point, up to the right corner. If a slab's glow gets stranded in the slice below it,
drag that corner <em>down</em> until the glow comes back.</p>
<div class="row" style="margin-top:7px">
<button id="guides">guides: on</button>
<button id="showbase">original: hidden</button>
<button id="reset">reset</button>
</div>
</div>
<div class="sep"></div>
<div>
<h2>3 · collapse &amp; scrub</h2>
<div class="sl"><span>spread</span>
<input type="range" id="spread" min="0" max="20" step=".5" value="11">
<output id="spreadv">11</output></div>
<input type="range" id="scrub" min="0" max="1" step="0.001" value="1" style="margin-top:8px">
<div class="row" style="justify-content:space-between">
<span class="note" style="margin:0">collapsed</span>
<button id="play" class="pri">▶ play</button>
<span class="note" style="margin:0">hero</span>
</div>
</div>
<div class="sep"></div>
<div>
<h2>4 · manifest</h2>
<input type="text" id="scene" value="course-I-docker-layers">
<div class="row" style="margin-top:6px">
<button id="export" class="pri">export</button>
<button id="copy">copy</button>
</div>
<textarea id="json" spellcheck="false" placeholder="manifest JSON appears here"></textarea>
</div>
</div>
<script>
const $ = s => document.querySelector(s);
const frame = $('#frame'), base = $('#base'), overlay = $('#overlay');
const SVGNS = 'http://www.w3.org/2000/svg';
/* ── geometry ──────────────────────────────────────────────────────────────
One cut per slab, following its BOTTOM silhouette: left corner → bottom tip →
right corner. A slab's region is bounded ABOVE by the cut of the slab in front
of it (exactly where it's occluded) and BELOW by its own cut. Nothing is
duplicated; nothing loses its body. */
const DEFAULT = {
glass:{ l:27.5, tip:45.5, r:27.0 },
code: { l:42.5, tip:59.5, r:42.0 },
deps: { l:57.0, tip:73.5, r:56.5 }
};
const X = { l:28.3, r:71.4 }; // where the slabs' side corners sit horizontally
let GEO = structuredClone(DEFAULT);
/* Seam fix: regions expand UPWARD only. The overlap always lands under the slab
painted in front of it, and collapsing moves that slab further down still — so
the overlap can never be revealed. Expanding downward would ghost. */
const EPS = 0.7;
const Vof = g => [[X.l, g.l], [50, g.tip], [X.r, g.r]];
const up = (v,e) => v.map(p => [p[0], +(p[1]-e).toFixed(2)]);
/* region between two silhouettes; sides run out to the frame edge (pure void) */
function band(top, bot){
const p = [];
if (top) p.push([0,top[0][1]], ...top, [100,top[2][1]]); else p.push([0,0],[100,0]);
if (bot) p.push([100,bot[2][1]], ...[...bot].reverse(), [0,bot[0][1]]);
else p.push([100,100],[0,100]);
return p;
}
let SPREAD = 11, parts = [], imgSrc = null, showBase = false, showGuides = true;
function build(){
const g = Vof(GEO.glass), c = Vof(GEO.code), d = Vof(GEO.deps);
parts = [
{ name:'base · the foundation', points: band(up(d,EPS), null), dy: 0 },
{ name:'deps · dependencies', points: band(up(c,EPS), d), dy: SPREAD },
{ name:'code · your app', points: band(up(g,EPS), c), dy: SPREAD*2 },
{ name:'writable · the top layer', points: band(null, g), dy: SPREAD*3 }
];
}
/* ── render ─────────────────────────────────────────────────────────────── */
function paint(){
const t = +$('#scrub').value;
frame.querySelectorAll('.part').forEach((el,i) => {
el.style.transform = `translate(0, ${(parts[i].dy*(1-t)).toFixed(3)}%)`;
});
}
function render(){
frame.querySelectorAll('.part').forEach(n => n.remove());
parts.forEach((p,i) => {
const d = document.createElement('div');
d.className = 'part';
if (imgSrc) d.style.backgroundImage = `url(${imgSrc})`;
d.style.clipPath = 'polygon(' + p.points.map(q => q[0]+'% '+q[1]+'%').join(',') + ')';
d.style.zIndex = i + 1;
frame.insertBefore(d, overlay);
});
base.style.opacity = imgSrc ? (showBase ? 1 : 0) : 1;
overlay.innerHTML = '';
if (showGuides) ['glass','code','deps'].forEach(k => {
const pl = document.createElementNS(SVGNS,'polyline');
pl.setAttribute('points', Vof(GEO[k]).map(p=>p.join(',')).join(' '));
pl.setAttribute('fill','none');
pl.setAttribute('stroke', k==='glass' ? '#e879f9' : k==='code' ? '#3fbaf5' : '#7ee787');
pl.setAttribute('stroke-width','.3');
pl.setAttribute('vector-effect','non-scaling-stroke');
pl.setAttribute('opacity','.85');
overlay.appendChild(pl);
});
paint();
}
/* ── controls ───────────────────────────────────────────────────────────── */
const LABEL = { glass:['writable', 'glass slab'], code:['code','magenta slab'], deps:['deps','circuit slab'] };
function geoUI(){
const host = $('#geo'); host.innerHTML = '';
Object.keys(GEO).forEach(k => {
const box = document.createElement('div'); box.className = 'cut';
const h = document.createElement('h3');
h.innerHTML = `<span>${LABEL[k][0]} <b>${LABEL[k][1]}</b></span>`;
box.appendChild(h);
[['l','left'],['tip','point'],['r','right']].forEach(([key,lab]) => {
const row = document.createElement('div'); row.className = 'sl';
const nm = document.createElement('span'); nm.textContent = lab;
const sl = document.createElement('input');
sl.type='range'; sl.min='0'; sl.max='100'; sl.step='.25'; sl.value=GEO[k][key];
sl.setAttribute('aria-label', `${LABEL[k][0]} ${lab} edge`);
const out = document.createElement('output'); out.textContent = GEO[k][key];
sl.oninput = e => { GEO[k][key] = +e.target.value; out.textContent = e.target.value;
box.classList.add('on'); build(); render(); };
row.append(nm, sl, out); box.appendChild(row);
});
host.appendChild(box);
});
}
$('#file').onchange = e => {
const f = e.target.files[0]; if (!f) return;
const r = new FileReader();
r.onload = () => { imgSrc = r.result; base.src = imgSrc;
$('#hint').textContent = 'sliced — hit ▶ play'; build(); render(); };
r.readAsDataURL(f);
};
$('#spread').oninput = e => { SPREAD = +e.target.value; $('#spreadv').textContent = e.target.value;
build(); render(); };
$('#scrub').oninput = paint;
$('#guides').onclick = e => { showGuides = !showGuides;
e.target.textContent = 'guides: ' + (showGuides?'on':'off'); render(); };
$('#showbase').onclick = e => { showBase = !showBase;
e.target.textContent = 'original: ' + (showBase?'shown':'hidden'); render(); };
$('#reset').onclick = () => { GEO = structuredClone(DEFAULT); geoUI(); build(); render(); };
$('#play').onclick = () => {
const s = $('#scrub');
if (matchMedia('(prefers-reduced-motion: reduce)').matches) { s.value = 1; paint(); return; }
const t0 = performance.now(), dur = 1700;
const step = now => {
const t = Math.min(1, (now - t0) / dur);
s.value = 1 - Math.pow(1 - t, 3); // easeOutCubic — settles AT hero, never past it
paint();
if (t < 1) requestAnimationFrame(step);
};
s.value = 0; paint(); requestAnimationFrame(step);
};
const manifest = () => ({
scene: $('#scene').value,
note: 'travel clamps at hero (t=1); regions expand upward by EPS to kill clip-path seams',
eps: EPS, spread: SPREAD, cuts: GEO, xCorners: X,
parts: parts.map(p => ({ name:p.name, points:p.points, rest:{dx:0, dy:p.dy} }))
});
$('#export').onclick = () => { $('#json').value = JSON.stringify(manifest(), null, 2); };
$('#copy').onclick = async () => {
$('#json').value = JSON.stringify(manifest(), null, 2);
try { await navigator.clipboard.writeText($('#json').value); $('#copy').textContent='copied'; }
catch { $('#json').select(); }
setTimeout(() => $('#copy').textContent = 'copy', 1200);
};
geoUI(); build(); render();
</script>
</body>
</html>