feat(learn): engine-driven 3D parameter-landscape sandbox
Interactive /learn page rebuilt on the real NISPS engine (nisps.wasm):
- Stacked 3D landscapes = MLP output channels sampled over the 2D input grid
(the N-dimensional 'sandwich'); orbitable yaw/pitch camera, top-down = flat 2D.
- Three zones L→R: XY control (with +/- feedback markers) → MLP stack → output faders.
- Real training: push-away (feedback_down) + explore-and-place (enter/reroll/nudge/
commit), undo, randomise, nudge; landscapes re-sample and morph after each verdict.
- Params numberbox (1..8); new layers grow downward with the top slab pinned.
- Minimal-by-default with a discrete info switch revealing placeholder copy.
Standalone no-build page; loads ../nisps.{js,wasm}; deployed at /next/learn/.
This commit is contained in:
parent
9ad0b8b9a0
commit
c955e94a7a
1 changed files with 484 additions and 0 deletions
484
assets/media/learn/index.html
Normal file
484
assets/media/learn/index.html
Normal file
|
|
@ -0,0 +1,484 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Manifold — the parameter landscape</title>
|
||||||
|
<style>
|
||||||
|
:root{
|
||||||
|
--bg:#0d0d0d; --bg-1:#141414; --bg-2:#1c1c1c; --bg-3:#242424;
|
||||||
|
--fg:#e8e8e8; --fg-mute:#9a9a9a; --fg-dim:#5a5a5a;
|
||||||
|
--line:#2a2a2a; --line-strong:#3a3a3a;
|
||||||
|
--accent:#ff6a00; --accent-2:#00ccff; --good:#6bc26b; --bad:#5aa6c8;
|
||||||
|
--font-mono:'JetBrains Mono','Fira Code','SF Mono',Menlo,Consolas,monospace;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
html,body{margin:0;height:100%;background:var(--bg);color:var(--fg);
|
||||||
|
font-family:var(--font-mono);font-size:14px;-webkit-font-smoothing:antialiased;overflow:hidden}
|
||||||
|
|
||||||
|
#app{position:fixed;inset:0}
|
||||||
|
#scene{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:none;cursor:grab}
|
||||||
|
#scene:active{cursor:grabbing}
|
||||||
|
|
||||||
|
/* floating HUD blocks */
|
||||||
|
.hud{position:absolute;z-index:5}
|
||||||
|
/* three zones, left → right: input · MLP stack · outputs */
|
||||||
|
.hud-l{left:24px;top:50%;transform:translateY(-50%);display:flex;flex-direction:column;gap:10px;align-items:flex-start}
|
||||||
|
.hud-r{right:24px;top:50%;transform:translateY(-50%)}
|
||||||
|
.hud-bc{left:50%;bottom:22px;transform:translateX(-50%)}
|
||||||
|
.hud-tr{right:18px;top:18px}
|
||||||
|
.zonecap{font-size:9px;letter-spacing:.16em;text-transform:uppercase;color:var(--fg-dim);margin-bottom:2px}
|
||||||
|
|
||||||
|
/* output fader bank (1-D projection of each layer's value at the point) */
|
||||||
|
.outs{display:flex;gap:13px;align-items:flex-start;background:rgba(20,20,20,.55);
|
||||||
|
border:1px solid var(--line);border-radius:12px;padding:12px 14px;backdrop-filter:blur(4px)}
|
||||||
|
.fader{display:flex;flex-direction:column;align-items:center;gap:6px;width:30px}
|
||||||
|
.fader .track{position:relative;width:8px;height:200px;background:var(--bg-3);
|
||||||
|
border:1px solid var(--line);border-radius:999px;overflow:hidden}
|
||||||
|
.fader .fill{position:absolute;left:0;right:0;bottom:0;border-radius:999px;transition:height .05s linear}
|
||||||
|
.fader .handle{position:absolute;left:50%;width:16px;height:3px;border-radius:2px;
|
||||||
|
transform:translate(-50%,1.5px);box-shadow:0 0 8px currentColor}
|
||||||
|
.fader .flab{font-size:9px;letter-spacing:.03em;text-transform:uppercase;max-width:34px;text-align:center;line-height:1.1}
|
||||||
|
.fader .fval{font-size:10px;font-variant-numeric:tabular-nums;color:var(--fg)}
|
||||||
|
|
||||||
|
/* XY pad */
|
||||||
|
#ndpad{display:block;border:1px solid var(--line);border-radius:8px;background:var(--bg-2);
|
||||||
|
cursor:crosshair;touch-action:none}
|
||||||
|
.padcap{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--fg-dim)}
|
||||||
|
|
||||||
|
/* parameter numberbox */
|
||||||
|
.kbox{display:flex;align-items:center;gap:8px;background:var(--bg-1);border:1px solid var(--line);
|
||||||
|
border-radius:8px;padding:5px 8px;width:fit-content}
|
||||||
|
.kbox .lab{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--fg-mute)}
|
||||||
|
.kbox .val{min-width:1.4ch;text-align:center;color:var(--fg);font-size:15px;font-variant-numeric:tabular-nums}
|
||||||
|
.step{width:24px;height:24px;display:grid;place-items:center;padding:0;font-size:15px;line-height:1;
|
||||||
|
background:var(--bg-2);border:1px solid var(--line-strong);border-radius:6px;color:var(--fg);cursor:pointer}
|
||||||
|
.step:hover{background:var(--bg-3)}
|
||||||
|
.step:disabled{opacity:.3;cursor:default}
|
||||||
|
|
||||||
|
/* action bar */
|
||||||
|
.actionbar{display:flex;align-items:center;gap:8px;background:rgba(20,20,20,.82);
|
||||||
|
border:1px solid var(--line);border-radius:12px;padding:8px;backdrop-filter:blur(6px)}
|
||||||
|
.act{display:flex;flex-direction:column;align-items:center;gap:3px;min-width:58px;padding:8px 6px;
|
||||||
|
background:var(--bg-2);border:1px solid var(--line-strong);border-radius:9px;color:var(--fg);
|
||||||
|
cursor:pointer;font-family:inherit;transition:border-color .12s,background .12s}
|
||||||
|
.act:hover{background:var(--bg-3)}
|
||||||
|
.act .ic{font-size:18px;line-height:1}
|
||||||
|
.act .tx{font-size:9px;letter-spacing:.04em;color:var(--fg-mute);text-transform:uppercase}
|
||||||
|
.act.up:hover{border-color:var(--accent);box-shadow:0 0 12px rgba(255,106,0,.25)}
|
||||||
|
.act.down:hover{border-color:var(--accent-2);box-shadow:0 0 12px rgba(0,204,255,.22)}
|
||||||
|
.act:disabled{opacity:.35;cursor:default;box-shadow:none}
|
||||||
|
.sep{width:1px;align-self:stretch;background:var(--line);margin:2px 2px}
|
||||||
|
|
||||||
|
/* mode toggle */
|
||||||
|
.modeseg{display:inline-flex;border:1px solid var(--line-strong);border-radius:9px;overflow:hidden}
|
||||||
|
.modeseg button{border:0;border-radius:0;padding:0 12px;height:100%;min-height:54px;font-family:inherit;
|
||||||
|
font-size:11px;letter-spacing:.04em;background:var(--bg-2);color:var(--fg-mute);cursor:pointer}
|
||||||
|
.modeseg button.on{background:var(--accent);color:#0d0d0d}
|
||||||
|
|
||||||
|
/* info toggle */
|
||||||
|
#infoBtn{width:34px;height:34px;border-radius:50%;border:1px solid var(--line-strong);background:rgba(20,20,20,.82);
|
||||||
|
color:var(--fg-mute);font-size:15px;cursor:pointer;backdrop-filter:blur(6px)}
|
||||||
|
#infoBtn:hover{color:var(--accent);border-color:var(--accent)}
|
||||||
|
|
||||||
|
/* fading rotate hint */
|
||||||
|
#hint{position:absolute;left:50%;top:18px;transform:translateX(-50%);z-index:4;
|
||||||
|
font-size:11px;letter-spacing:.08em;color:var(--fg-dim);pointer-events:none;
|
||||||
|
transition:opacity 1s ease;opacity:.85}
|
||||||
|
#hint.gone{opacity:0}
|
||||||
|
|
||||||
|
/* loading */
|
||||||
|
#loading{position:absolute;inset:0;display:grid;place-items:center;z-index:9;background:var(--bg);
|
||||||
|
color:var(--fg-dim);font-size:13px;letter-spacing:.1em}
|
||||||
|
#loading.gone{display:none}
|
||||||
|
#loading b{color:var(--accent);letter-spacing:-.01em}
|
||||||
|
|
||||||
|
/* prose drawer (revealed by the info switch) */
|
||||||
|
#prose{position:absolute;top:0;right:0;bottom:0;width:min(440px,86vw);z-index:8;overflow:auto;
|
||||||
|
background:rgba(13,13,13,.96);border-left:1px solid var(--line);padding:40px 26px 64px;
|
||||||
|
transform:translateX(102%);transition:transform .28s ease}
|
||||||
|
body:not(.minimal) #prose{transform:none}
|
||||||
|
#prose h1{font-size:24px;margin:0 0 8px}
|
||||||
|
#prose h1 b{color:var(--accent)}
|
||||||
|
#prose p{color:var(--fg-mute);line-height:1.6;margin:0 0 12px}
|
||||||
|
#prose p b{color:var(--fg)}
|
||||||
|
#prose .lbl{font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);margin:26px 0 8px}
|
||||||
|
#prose .ph{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--fg-dim);
|
||||||
|
border:1px dashed var(--line-strong);border-radius:6px;padding:6px 9px;display:inline-block;margin-bottom:14px}
|
||||||
|
#prose .bars{display:flex;flex-direction:column;gap:7px;margin-top:6px}
|
||||||
|
#prose .ro{display:flex;align-items:center;gap:10px}
|
||||||
|
#prose .ro span{width:74px;font-size:10px;text-transform:uppercase;letter-spacing:.05em;color:var(--fg-mute)}
|
||||||
|
#prose .bar{flex:1;height:7px;background:var(--bg-3);border:1px solid var(--line);border-radius:999px;overflow:hidden;position:relative}
|
||||||
|
#prose .bar i{position:absolute;left:0;top:0;bottom:0;border-radius:999px}
|
||||||
|
#prose .num{width:3.2ch;text-align:right;font-variant-numeric:tabular-nums;color:var(--fg);font-size:12px}
|
||||||
|
#prose a{color:var(--accent-2);text-decoration:none}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="minimal">
|
||||||
|
<div id="app">
|
||||||
|
<canvas id="scene"></canvas>
|
||||||
|
|
||||||
|
<div id="hint">drag to rotate · top-down view = the flat 2-D map</div>
|
||||||
|
|
||||||
|
<!-- ZONE 1 · input: the 2-D control + parameter count (left) -->
|
||||||
|
<div class="hud hud-l">
|
||||||
|
<div class="zonecap">input · 2-D control</div>
|
||||||
|
<canvas id="ndpad" width="140" height="140"></canvas>
|
||||||
|
<div class="kbox">
|
||||||
|
<span class="lab">params</span>
|
||||||
|
<button class="step" id="kdown" title="fewer parameters">−</button>
|
||||||
|
<span class="val" id="kval">1</span>
|
||||||
|
<button class="step" id="kup" title="more parameters">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ZONE 3 · outputs: vertical faders, one per layer (right) -->
|
||||||
|
<div class="hud hud-r">
|
||||||
|
<div class="zonecap">outputs</div>
|
||||||
|
<div class="outs" id="outs"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- action bar (bottom-center) -->
|
||||||
|
<div class="hud hud-bc actionbar">
|
||||||
|
<button class="act" id="aRand"><span class="ic">🎲</span><span class="tx">randomise</span></button>
|
||||||
|
<button class="act" id="aNudge"><span class="ic">〜</span><span class="tx">nudge</span></button>
|
||||||
|
<button class="act" id="aUndo"><span class="ic">↶</span><span class="tx">undo</span></button>
|
||||||
|
<div class="sep"></div>
|
||||||
|
<button class="act up" id="aUp"><span class="ic">👍</span><span class="tx">keep</span></button>
|
||||||
|
<button class="act down" id="aDown"><span class="ic">👎</span><span class="tx" id="downTx">push away</span></button>
|
||||||
|
<div class="sep"></div>
|
||||||
|
<div class="modeseg">
|
||||||
|
<button id="mPush" class="on">push‑away</button>
|
||||||
|
<button id="mExp">explore</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="infoBtn" class="hud hud-tr" title="show / hide explanation">ⓘ</button>
|
||||||
|
|
||||||
|
<div id="loading"><span>loading the <b> NISPS </b> engine…</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- placeholder explanatory copy (revealed by the ⓘ switch; to be reworked) -->
|
||||||
|
<div id="prose">
|
||||||
|
<h1>The <b>parameter landscape</b></h1>
|
||||||
|
<p class="ph">placeholder copy — to be reworked</p>
|
||||||
|
<p>Two numbers go in — your <b>X / Y control position</b> — and a whole fistful of parameters comes
|
||||||
|
out. You can't picture a dozen dimensions at once, so picture them <b>stacked</b>: each <b>slab</b> is one
|
||||||
|
parameter, drawn as a 3-D landscape whose height at any spot is that parameter's value. <b>One probe</b>
|
||||||
|
reads every slab at the same control position.</p>
|
||||||
|
<p>Seen straight from above the slabs overlap into a single flat map — it looks 2-D. <b>Tilt it</b> and the
|
||||||
|
reveal lands: it was a stack of 3-D surfaces all along. Add parameters to grow the sandwich.</p>
|
||||||
|
<p>Every surface here is the <b>real NISPS network</b> sampled across the input space. <b>👍 keep</b> teaches
|
||||||
|
the net that this sound belongs at this spot and the landscape bends to honour it; <b>👎</b> either pushes the
|
||||||
|
mapping away from the current sound or (in explore mode) re-rolls a fresh candidate to audition. <b>nudge</b>
|
||||||
|
and <b>randomise</b> perturb the net; <b>undo</b> steps back.</p>
|
||||||
|
<div class="lbl">Values under the probe</div>
|
||||||
|
<div class="bars" id="ndbars"></div>
|
||||||
|
<p style="margin-top:24px"><a href="../">← back to the instrument</a> · <a href="../animations/">animated explainers →</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
const clamp=(v,a=0,b=1)=>v<a?a:v>b?b:v;
|
||||||
|
const cssv=n=>getComputedStyle(document.documentElement).getPropertyValue(n).trim();
|
||||||
|
const MONO=cssv('--font-mono')||'monospace';
|
||||||
|
|
||||||
|
const NAMES=['Cutoff','Resonance','Drive','Tone','Decay','Air','Pitch','Width'];
|
||||||
|
const ABBR =['Cut','Res','Drv','Ton','Dec','Air','Pit','Wid'];
|
||||||
|
const MAXK=NAMES.length;
|
||||||
|
const OUT=126, IN=2, GRID=12, N=GRID, PTS=(N+1)*(N+1);
|
||||||
|
|
||||||
|
let K=1; // parameters (slabs) visualised — from the same net
|
||||||
|
let cx=0.5, cy=0.5; // 2-D control position in [0,1]
|
||||||
|
let mode='pushaway'; // 'pushaway' | 'explore'
|
||||||
|
let exploring=false; // explore-and-place scratchpad session active
|
||||||
|
let ready=false, auto=true, autoT=0.6;
|
||||||
|
|
||||||
|
// camera — START TOP-DOWN (looks 2-D); drag tilts to reveal the 3-D stack
|
||||||
|
let yaw=0, pitch=Math.PI/2, dragRot=false, lastX=0, lastY=0;
|
||||||
|
|
||||||
|
// ===== engine (vanilla wrapper over nisps.wasm; same C ABI the manifold uses) =====
|
||||||
|
const E={
|
||||||
|
mod:null,h:0,bIn:0,bOut:0,bF:0,bL:0,bW:0,bFb:0,wc:0,
|
||||||
|
async init(){
|
||||||
|
// nisps.{js,wasm} sit one level up at /next/ in production; probe a few bases.
|
||||||
|
let base=null,src=null;
|
||||||
|
for(const b of ['../','','./']){
|
||||||
|
try{const r=await fetch(b+'nisps.js');if(r.ok){src=await r.text();base=b;break;}}catch(e){}
|
||||||
|
}
|
||||||
|
if(base===null) throw new Error('nisps.js not found');
|
||||||
|
if(!window.createNispsModule)(0,eval)(src);
|
||||||
|
this.mod=await window.createNispsModule({locateFile:p=>p.endsWith('.wasm')?base+'nisps.wasm':p});
|
||||||
|
const m=this.mod;
|
||||||
|
this.h=m._nisps_ml_create(IN,OUT,0,0,(Date.now()>>>0));
|
||||||
|
this.wc=m._nisps_ml_weight_count(this.h);
|
||||||
|
this.bF=m._malloc(IN*4); this.bL=m._malloc(OUT*4); this.bFb=m._malloc(OUT*4);
|
||||||
|
this.bW=m._malloc(this.wc*4);
|
||||||
|
this.bIn=m._malloc(PTS*IN*4); this.bOut=m._malloc(PTS*OUT*4);
|
||||||
|
},
|
||||||
|
inferXY(x,y){const m=this.mod;m._nisps_ml_set_input(this.h,0,x);m._nisps_ml_set_input(this.h,1,y);
|
||||||
|
m._nisps_ml_process(this.h);const p=m._nisps_ml_outputs(this.h);
|
||||||
|
return new Float32Array(m.HEAPF32.buffer,p,OUT);},
|
||||||
|
inferGrid(){const m=this.mod;const inv=new Float32Array(m.HEAPF32.buffer,this.bIn,PTS*IN);
|
||||||
|
let q=0;for(let j=0;j<=N;j++)for(let i=0;i<=N;i++){inv[q++]=i/N;inv[q++]=j/N;}
|
||||||
|
m._nisps_ml_infer_batch(this.h,this.bIn,PTS,this.bOut);
|
||||||
|
return new Float32Array(new Float32Array(m.HEAPF32.buffer,this.bOut,PTS*OUT));},
|
||||||
|
addExample(f,l){const m=this.mod;const fv=new Float32Array(m.HEAPF32.buffer,this.bF,IN);
|
||||||
|
const lv=new Float32Array(m.HEAPF32.buffer,this.bL,OUT);fv[0]=f[0];fv[1]=f[1];
|
||||||
|
for(let i=0;i<OUT;i++)lv[i]=l[i]||0;m._nisps_ml_add_example(this.h,this.bF,this.bL);},
|
||||||
|
train(lr,it,me){return this.mod._nisps_ml_train(this.h,lr,it,me,0);},
|
||||||
|
draw(s){this.mod._nisps_ml_draw_weights(this.h,s);},
|
||||||
|
move(sp,s){this.mod._nisps_ml_move_weights(this.h,sp,s,0);},
|
||||||
|
setMode(i){this.mod._nisps_ml_feedback_set_mode(this.h,i);},
|
||||||
|
down(out,sp,s){const m=this.mod;const ov=new Float32Array(m.HEAPF32.buffer,this.bFb,OUT);
|
||||||
|
ov.fill(0);for(let i=0;i<OUT;i++)ov[i]=out[i]||0;m._nisps_ml_feedback_down(this.h,this.bFb,sp,s,0);},
|
||||||
|
enterExplore(s){this.mod._nisps_ml_feedback_enter_explore(this.h,s);},
|
||||||
|
reroll(s){this.mod._nisps_ml_feedback_reroll(this.h,s);},
|
||||||
|
coreNudge(a){this.mod._nisps_ml_feedback_nudge(this.h,a);},
|
||||||
|
coreUndo(){this.mod._nisps_ml_feedback_undo(this.h);},
|
||||||
|
exitExplore(){this.mod._nisps_ml_feedback_exit_explore(this.h);},
|
||||||
|
getWeights(){this.mod._nisps_ml_get_weights(this.h,this.bW);
|
||||||
|
return new Float32Array(new Float32Array(this.mod.HEAPF32.buffer,this.bW,this.wc));},
|
||||||
|
setWeights(w){const v=new Float32Array(this.mod.HEAPF32.buffer,this.bW,this.wc);v.set(w);
|
||||||
|
this.mod._nisps_ml_set_weights(this.h,this.bW);},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Low spread = polarised weights = strong spatial relief (measured); high spread
|
||||||
|
// flattens the surface toward 0.5. Keep it low so the landscapes show real terrain.
|
||||||
|
const SPREAD=0.1, NUDGE=0.05, BOOT_SPREAD=0.05;
|
||||||
|
let grid=null, probeOut=new Float32Array(OUT);
|
||||||
|
function H(k,i,j){return grid?grid[(j*(N+1)+i)*OUT+k]:0.5;}
|
||||||
|
function resample(){grid=E.inferGrid();}
|
||||||
|
function updateProbe(){const o=E.inferXY(cx,cy);probeOut=new Float32Array(o);}
|
||||||
|
|
||||||
|
// ---- weight-snapshot undo (push-away mode; explore mode uses the core ring) ----
|
||||||
|
const undoStack=[];
|
||||||
|
function pushUndo(){undoStack.push(E.getWeights());if(undoStack.length>24)undoStack.shift();}
|
||||||
|
function popUndo(){const w=undoStack.pop();if(w){E.setWeights(w);return true;}return false;}
|
||||||
|
|
||||||
|
// ---- feedback examples marked on the 2-D control plane (like the main app) ----
|
||||||
|
// {x,y,sign} — sign +1 kept (👍), -1 pushed away (👎). undo pops the last.
|
||||||
|
const marks=[];
|
||||||
|
|
||||||
|
// ===== actions (all backed by the real engine) =====
|
||||||
|
function act(name){
|
||||||
|
if(!ready)return;
|
||||||
|
if(mode==='pushaway'){
|
||||||
|
if(name==='rand'){pushUndo();E.draw(SPREAD);}
|
||||||
|
else if(name==='nudge'){pushUndo();E.move(0.04,SPREAD);}
|
||||||
|
else if(name==='undo'){popUndo();marks.pop();}
|
||||||
|
else if(name==='up'){pushUndo();const o=new Float32Array(E.inferXY(cx,cy));E.addExample([cx,cy],o);E.train(1.0,200,1e-4);marks.push({x:cx,y:cy,sign:1});}
|
||||||
|
else if(name==='down'){pushUndo();const o=new Float32Array(E.inferXY(cx,cy));E.down(o,0.15,SPREAD);marks.push({x:cx,y:cy,sign:-1});}
|
||||||
|
}else{ // explore-and-place
|
||||||
|
if(name==='rand'||name==='down'){
|
||||||
|
if(!exploring){E.setMode(3);E.enterExplore(SPREAD);exploring=true;}else E.reroll(SPREAD);
|
||||||
|
}else if(name==='nudge'){
|
||||||
|
if(!exploring){E.setMode(3);E.enterExplore(SPREAD);exploring=true;}else E.coreNudge(NUDGE);
|
||||||
|
}else if(name==='undo'){
|
||||||
|
if(exploring)E.coreUndo();else{popUndo();marks.pop();}
|
||||||
|
}else if(name==='up'){
|
||||||
|
const o=new Float32Array(E.inferXY(cx,cy));
|
||||||
|
if(exploring){E.exitExplore();exploring=false;}
|
||||||
|
E.addExample([cx,cy],o);E.train(1.0,200,1e-4);marks.push({x:cx,y:cy,sign:1});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resample();updateProbe();reflectButtons();redraw();
|
||||||
|
}
|
||||||
|
function setMode(m){
|
||||||
|
if(m===mode)return;
|
||||||
|
if(mode==='explore'&&exploring){E.exitExplore();exploring=false;}
|
||||||
|
mode=m;E.setMode(m==='explore'?3:0);
|
||||||
|
document.getElementById('mPush').classList.toggle('on',m==='pushaway');
|
||||||
|
document.getElementById('mExp').classList.toggle('on',m==='explore');
|
||||||
|
document.getElementById('downTx').textContent=m==='explore'?'skip':'push away';
|
||||||
|
if(ready){resample();updateProbe();reflectButtons();redraw();}
|
||||||
|
}
|
||||||
|
function reflectButtons(){
|
||||||
|
const ud=document.getElementById('aUndo');
|
||||||
|
ud.disabled=(mode==='explore'?false:undoStack.length===0)&&!exploring;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 3-D projection: yaw + pitch orbit, bounding-sphere fit, X inverted =====
|
||||||
|
const SX=300,SZ=300,LAYER_GAP=120,ZAMP=92,MARGIN=44;
|
||||||
|
// Anchor the view to the TOP slab (k=0) at a FIXED world Y and a FIXED screen
|
||||||
|
// position, independent of K. So adding parameters grows the stack DOWNWARD and
|
||||||
|
// the top slab always stays the top — it never re-centres onto new layers.
|
||||||
|
const AWY=ZAMP*0.5, ANCHOR_FY=0.42;
|
||||||
|
let CW=800,CH=600,scale=1;
|
||||||
|
function computeView(){
|
||||||
|
let R=1; // bounding-sphere radius about the top anchor
|
||||||
|
for(let k=0;k<K;k++)for(const z of[0,1])for(const gx of[-0.5,0.5])for(const gy of[-0.5,0.5]){
|
||||||
|
const X=gx*SX,Y=(-k*LAYER_GAP+z*ZAMP)-AWY,Z=gy*SZ;const d=Math.sqrt(X*X+Y*Y+Z*Z);if(d>R)R=d;}
|
||||||
|
scale=(Math.min(CW,CH)/2-MARGIN)/R;
|
||||||
|
}
|
||||||
|
function project(gx,gy,zv,k){
|
||||||
|
const X=gx*SX, Y=(-k*LAYER_GAP+zv*ZAMP)-AWY, Z=-gy*SZ; // Z negated: pad-up = map-up
|
||||||
|
const cyA=Math.cos(yaw),syA=Math.sin(yaw);
|
||||||
|
const x1=X*cyA-Z*syA, z1=X*syA+Z*cyA;
|
||||||
|
const cpA=Math.cos(pitch),spA=Math.sin(pitch);
|
||||||
|
const y2=Y*cpA-z1*spA, z2=Y*spA+z1*cpA;
|
||||||
|
return {x:CW/2+x1*scale,y:CH*ANCHOR_FY-y2*scale,depth:z2,wx:X,wy:Y,wz:Z};
|
||||||
|
}
|
||||||
|
|
||||||
|
const DPR=Math.max(1,Math.min(2,window.devicePixelRatio||1));
|
||||||
|
const scene=document.getElementById('scene'),sctx=scene.getContext('2d');
|
||||||
|
const pad=document.getElementById('ndpad'),pctx=pad.getContext('2d');
|
||||||
|
pad.width=140*DPR;pad.height=140*DPR;pctx.scale(DPR,DPR);
|
||||||
|
function resize(){
|
||||||
|
const r=scene.getBoundingClientRect();CW=Math.max(320,Math.round(r.width));CH=Math.max(320,Math.round(r.height));
|
||||||
|
scene.width=CW*DPR;scene.height=CH*DPR;sctx.setTransform(DPR,0,0,DPR,0,0);computeView();
|
||||||
|
}
|
||||||
|
|
||||||
|
const hueOf=k=>K>1?(26+(196-26)*k/(K-1)):26;
|
||||||
|
const colCss=(k,a)=>`hsla(${hueOf(k).toFixed(0)},85%,60%,${a})`;
|
||||||
|
const LIGHT=(()=>{const v=[0.35,0.86,0.36],m=Math.hypot(v[0],v[1],v[2]);return[v[0]/m,v[1]/m,v[2]/m];})();
|
||||||
|
|
||||||
|
function renderScene(){
|
||||||
|
const c=sctx;c.clearRect(0,0,CW,CH);
|
||||||
|
const quads=[],labels=[];
|
||||||
|
for(let k=0;k<K;k++){
|
||||||
|
const hue=hueOf(k),P=[];
|
||||||
|
for(let j=0;j<=N;j++){P[j]=[];for(let i=0;i<=N;i++)P[j][i]=project(i/N-0.5,j/N-0.5,H(k,i,j),k);}
|
||||||
|
for(let j=0;j<N;j++)for(let i=0;i<N;i++){
|
||||||
|
const a=P[j][i],b=P[j][i+1],d=P[j+1][i+1],e=P[j+1][i];
|
||||||
|
const ux=d.wx-a.wx,uy=d.wy-a.wy,uz=d.wz-a.wz,vx=b.wx-e.wx,vy=b.wy-e.wy,vz=b.wz-e.wz;
|
||||||
|
const nx=uy*vz-uz*vy,ny=uz*vx-ux*vz,nz=ux*vy-uy*vx,nm=Math.hypot(nx,ny,nz)||1;
|
||||||
|
const L=clamp(0.32+0.62*Math.abs((nx*LIGHT[0]+ny*LIGHT[1]+nz*LIGHT[2])/nm));
|
||||||
|
quads.push({a,b,d,e,hue,L,depth:(a.depth+b.depth+d.depth+e.depth)/4});
|
||||||
|
}
|
||||||
|
const lp=project(-0.5,0.5,H(k,0,N),k);labels.push({x:lp.x,y:lp.y,hue,name:NAMES[k]});
|
||||||
|
}
|
||||||
|
quads.sort((p,q)=>q.depth-p.depth);
|
||||||
|
for(const Q of quads){
|
||||||
|
c.fillStyle=`hsla(${Q.hue},80%,${(26+Q.L*44).toFixed(0)}%,0.66)`;
|
||||||
|
c.strokeStyle=`hsla(${Q.hue},80%,82%,0.18)`;c.lineWidth=0.6;
|
||||||
|
c.beginPath();c.moveTo(Q.a.x,Q.a.y);c.lineTo(Q.b.x,Q.b.y);c.lineTo(Q.d.x,Q.d.y);c.lineTo(Q.e.x,Q.e.y);c.closePath();
|
||||||
|
c.fill();c.stroke();
|
||||||
|
}
|
||||||
|
c.font='11px '+MONO;c.textAlign='right';c.textBaseline='middle';
|
||||||
|
for(const Lb of labels){c.fillStyle=`hsla(${Lb.hue},85%,74%,0.95)`;c.fillText(Lb.name,Lb.x-6,Lb.y);}
|
||||||
|
// probe column at (cx,cy) through every slab
|
||||||
|
const gx=cx-0.5,gy=cy-0.5;
|
||||||
|
const top=project(gx,gy,1.18,0),bot=project(gx,gy,-0.18,K-1);
|
||||||
|
c.strokeStyle='rgba(255,255,255,0.32)';c.lineWidth=1;c.setLineDash([3,4]);
|
||||||
|
c.beginPath();c.moveTo(top.x,top.y);c.lineTo(bot.x,bot.y);c.stroke();c.setLineDash([]);
|
||||||
|
for(let k=0;k<K;k++){
|
||||||
|
const p=project(gx,gy,clamp(probeOut[k]),k),fl=project(gx,gy,0,k);
|
||||||
|
c.strokeStyle=colCss(k,0.5);c.lineWidth=1;c.beginPath();c.moveTo(p.x,p.y);c.lineTo(fl.x,fl.y);c.stroke();
|
||||||
|
c.fillStyle=colCss(k,1);c.shadowColor=colCss(k,1);c.shadowBlur=12;
|
||||||
|
c.beginPath();c.arc(p.x,p.y,6,0,Math.PI*2);c.fill();c.shadowBlur=0;
|
||||||
|
c.fillStyle='#0d0d0d';c.beginPath();c.arc(p.x,p.y,2.2,0,Math.PI*2);c.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function drawPad(){
|
||||||
|
pctx.clearRect(0,0,140,140);pctx.fillStyle='#1c1c1c';pctx.fillRect(0,0,140,140);
|
||||||
|
pctx.strokeStyle='rgba(255,255,255,0.06)';pctx.lineWidth=1;
|
||||||
|
for(let i=1;i<4;i++){const t=i/4*140;pctx.beginPath();pctx.moveTo(t,0);pctx.lineTo(t,140);pctx.stroke();
|
||||||
|
pctx.beginPath();pctx.moveTo(0,t);pctx.lineTo(140,t);pctx.stroke();}
|
||||||
|
// feedback examples (👍 +, 👎 −) — same convention as the main app
|
||||||
|
const TAU=Math.PI*2;
|
||||||
|
for(const m of marks){const mx=m.x*140,my=(1-m.y)*140,col=m.sign>0?'#ff6a00':'#00ccff';
|
||||||
|
pctx.globalAlpha=0.28;pctx.fillStyle=col;pctx.beginPath();pctx.arc(mx,my,8,0,TAU);pctx.fill();
|
||||||
|
pctx.globalAlpha=1;pctx.strokeStyle=col;pctx.lineWidth=1.5;pctx.beginPath();pctx.arc(mx,my,5,0,TAU);pctx.stroke();
|
||||||
|
pctx.fillStyle=col;pctx.font='9px '+MONO;pctx.textAlign='center';pctx.textBaseline='middle';
|
||||||
|
pctx.fillText(m.sign>0?'+':'–',mx,my+0.5);}
|
||||||
|
const dx=cx*140,dy=(1-cy)*140;
|
||||||
|
pctx.strokeStyle='rgba(255,255,255,0.1)';pctx.beginPath();pctx.moveTo(0,dy);pctx.lineTo(140,dy);
|
||||||
|
pctx.moveTo(dx,0);pctx.lineTo(dx,140);pctx.stroke();
|
||||||
|
pctx.fillStyle='#ff6a00';pctx.shadowColor='#ff6a00';pctx.shadowBlur=12;
|
||||||
|
pctx.beginPath();pctx.arc(dx,dy,6,0,Math.PI*2);pctx.fill();pctx.shadowBlur=0;
|
||||||
|
pctx.fillStyle='#0d0d0d';pctx.beginPath();pctx.arc(dx,dy,2.4,0,Math.PI*2);pctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// readout bars (prose drawer only)
|
||||||
|
const barsEl=document.getElementById('ndbars');let barI=[],barN=[];
|
||||||
|
function buildBars(){barsEl.innerHTML='';barI=[];barN=[];
|
||||||
|
for(let k=0;k<K;k++){const ro=document.createElement('div');ro.className='ro';
|
||||||
|
ro.innerHTML=`<span style="color:${colCss(k,0.95)}">${NAMES[k]}</span>`+
|
||||||
|
`<div class="bar"><i style="background:${colCss(k,1)}"></i></div><div class="num">0.50</div>`;
|
||||||
|
barsEl.appendChild(ro);barI[k]=ro.querySelector('i');barN[k]=ro.querySelector('.num');}}
|
||||||
|
function updateBars(){if(!barI.length)return;for(let k=0;k<K;k++){const v=clamp(probeOut[k]);
|
||||||
|
barI[k].style.width=(v*100)+'%';barN[k].textContent=v.toFixed(2);}}
|
||||||
|
|
||||||
|
// output faders — the 1-D projection of each layer's value at the current point
|
||||||
|
const TRACK_H=200;
|
||||||
|
const outsEl=document.getElementById('outs');let fFill=[],fHan=[],fVal=[];
|
||||||
|
function buildOuts(){outsEl.innerHTML='';fFill=[];fHan=[];fVal=[];
|
||||||
|
for(let k=0;k<K;k++){const col=colCss(k,1);const f=document.createElement('div');f.className='fader';
|
||||||
|
f.innerHTML=`<div class="track"><div class="fill" style="background:${col}"></div>`+
|
||||||
|
`<div class="handle" style="background:${col};color:${col}"></div></div>`+
|
||||||
|
`<div class="flab" style="color:${colCss(k,0.95)}" title="${NAMES[k]}">${ABBR[k]}</div><div class="fval">0.50</div>`;
|
||||||
|
outsEl.appendChild(f);fFill[k]=f.querySelector('.fill');fHan[k]=f.querySelector('.handle');fVal[k]=f.querySelector('.fval');}}
|
||||||
|
function updateOuts(){if(!fFill.length)return;for(let k=0;k<K;k++){const v=clamp(probeOut[k]);
|
||||||
|
fFill[k].style.height=(v*100)+'%';fHan[k].style.bottom=(v*TRACK_H)+'px';fVal[k].textContent=v.toFixed(2);}}
|
||||||
|
|
||||||
|
function redraw(){renderScene();drawPad();updateBars();updateOuts();}
|
||||||
|
|
||||||
|
// ===== interaction =====
|
||||||
|
function setK(n){K=clamp(n,1,MAXK);document.getElementById('kval').textContent=K;
|
||||||
|
document.getElementById('kup').disabled=K>=MAXK;document.getElementById('kdown').disabled=K<=1;
|
||||||
|
computeView();buildBars();buildOuts();updateProbe&&(probeOut=new Float32Array(E.inferXY(cx,cy)));redraw();}
|
||||||
|
|
||||||
|
function stopAuto(){auto=false;document.getElementById('hint').classList.add('gone');}
|
||||||
|
|
||||||
|
// pad → control position
|
||||||
|
function setFromPad(e){const r=pad.getBoundingClientRect();
|
||||||
|
cx=clamp((e.clientX-r.left)/r.width);cy=clamp(1-(e.clientY-r.top)/r.height);}
|
||||||
|
let padDrag=false;
|
||||||
|
pad.addEventListener('pointerdown',e=>{padDrag=true;pad.setPointerCapture(e.pointerId);stopAuto();setFromPad(e);updateProbe();redraw();});
|
||||||
|
pad.addEventListener('pointermove',e=>{if(padDrag){setFromPad(e);updateProbe();redraw();}});
|
||||||
|
pad.addEventListener('pointerup',()=>padDrag=false);
|
||||||
|
|
||||||
|
// scene → orbit (pitch inverted; pitch up to π/2 = exact top-down)
|
||||||
|
scene.addEventListener('pointerdown',e=>{dragRot=true;scene.setPointerCapture(e.pointerId);stopAuto();lastX=e.clientX;lastY=e.clientY;});
|
||||||
|
scene.addEventListener('pointermove',e=>{if(!dragRot)return;
|
||||||
|
yaw+=(e.clientX-lastX)*0.01;
|
||||||
|
pitch=clamp(pitch+(e.clientY-lastY)*0.008,0.06,Math.PI/2); // vertical (Y) drag — inverted
|
||||||
|
lastX=e.clientX;lastY=e.clientY;redraw();});
|
||||||
|
scene.addEventListener('pointerup',()=>dragRot=false);
|
||||||
|
scene.addEventListener('pointercancel',()=>dragRot=false);
|
||||||
|
|
||||||
|
// buttons
|
||||||
|
const B=(id,fn)=>document.getElementById(id).addEventListener('click',()=>{stopAuto();fn();});
|
||||||
|
B('aRand',()=>act('rand'));B('aNudge',()=>act('nudge'));B('aUndo',()=>act('undo'));
|
||||||
|
B('aUp',()=>act('up'));B('aDown',()=>act('down'));
|
||||||
|
B('kup',()=>setK(K+1));B('kdown',()=>setK(K-1));
|
||||||
|
document.getElementById('mPush').addEventListener('click',()=>{stopAuto();setMode('pushaway');});
|
||||||
|
document.getElementById('mExp').addEventListener('click',()=>{stopAuto();setMode('explore');});
|
||||||
|
document.getElementById('infoBtn').addEventListener('click',()=>{
|
||||||
|
document.body.classList.toggle('minimal');
|
||||||
|
document.getElementById('infoBtn').textContent=document.body.classList.contains('minimal')?'ⓘ':'✕';});
|
||||||
|
|
||||||
|
window.addEventListener('resize',()=>{resize();redraw();});
|
||||||
|
|
||||||
|
function loop(){
|
||||||
|
if(auto&&ready&&!padDrag&&!dragRot){
|
||||||
|
autoT+=0.006;cx=0.5+0.42*Math.sin(autoT*0.9);cy=0.5+0.42*Math.sin(autoT*1.27+1.3);
|
||||||
|
updateProbe();redraw();
|
||||||
|
}
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== boot =====
|
||||||
|
(async()=>{
|
||||||
|
try{
|
||||||
|
await E.init();
|
||||||
|
E.draw(BOOT_SPREAD); // a fresh net is near-flat; draw for visible relief
|
||||||
|
window.__learn={E,setK,resample,redraw,
|
||||||
|
range(k){let lo=1,hi=0;for(let j=0;j<=N;j++)for(let i=0;i<=N;i++){const v=H(k,i,j);if(v<lo)lo=v;if(v>hi)hi=v;}return[lo,hi];}};
|
||||||
|
resize();setK(1);resample();updateProbe();reflectButtons();
|
||||||
|
ready=true;document.getElementById('loading').classList.add('gone');
|
||||||
|
redraw();requestAnimationFrame(loop);
|
||||||
|
// fade the hint after a while
|
||||||
|
setTimeout(()=>{if(auto)document.getElementById('hint').classList.add('gone');},6000);
|
||||||
|
}catch(err){
|
||||||
|
console.error('[learn] engine init failed:',err);
|
||||||
|
document.getElementById('loading').innerHTML='<span style="color:#c8553a">engine failed to load — see console</span>';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue