visual-tree / reveal_widget.html
ShesterG's picture
Color final/leaf patch numbers green in reveal animation
2699c3a
Raw
History Blame Contribute Delete
9.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
html,body{margin:0;height:100%;background:#0f1117;color:#e8eaf0;font-family:'Inter','Segoe UI',sans-serif;overflow:hidden}
#wrap{display:flex;height:100vh;gap:18px;padding:14px;box-sizing:border-box}
/* video LEFT, tree RIGHT */
#left{flex:0 0 auto;display:flex;flex-direction:column;align-items:center;justify-content:center}
#right{flex:1 1 auto;position:relative}
svg{width:100%;height:100%}
.edge{stroke:#3a3f55;stroke-width:1}
.node{transition:fill .15s ease}
#imgbox{position:relative;border-radius:8px;overflow:hidden;box-shadow:0 0 0 1px #2a2d3e}
#imgbox img{display:block}
#hl{position:absolute;left:0;top:0;pointer-events:none}
#cap{font-size:12px;color:#7b8290;margin-top:8px;width:360px;text-align:center;line-height:1.35;
height:4.05em;overflow:hidden;display:flex;align-items:center;justify-content:center}
#ctrl{margin-top:10px;display:flex;gap:8px;align-items:center}
button{background:#1a2240;color:#e8eaf0;border:1px solid #5b9cf6;border-radius:6px;padding:5px 11px;font-size:13px;cursor:pointer}
button:hover{background:#24305a}
.legend{position:absolute;right:10px;top:8px;font-size:12px;color:#c8ccd8;line-height:1.7}
.dot{display:inline-block;width:11px;height:11px;border-radius:50%;margin-right:6px;vertical-align:middle;border:1px solid #0f1117}
</style>
</head>
<body>
<div id="wrap">
<div id="left">
<div id="imgbox"><img id="img"><canvas id="hl"></canvas></div>
<div id="cap"></div>
<div id="ctrl">
<button id="restart">⟲ Restart</button>
<button id="play">⏸ Pause</button>
<button id="back">◂ Back</button>
<button id="step">Forward ▸</button>
</div>
</div>
<div id="right">
<svg id="svg"></svg>
<div class="legend">
<div><span class="dot" style="background:#ffffff"></span>not reached yet</div>
<div><span class="dot" style="background:#f4a261"></span>in the frontier (queued)</div>
<div><span class="dot" style="background:#5b9cf6"></span>now showing in video</div>
<div><span class="dot" style="background:#b00018"></span>already looked at</div>
<div style="margin-top:6px;border-top:1px solid #2a2d3e;padding-top:6px">patch # = depth · <b style="color:#3ee08a">green</b> = leaf reached (final)</div>
</div>
</div>
</div>
<script>
const STEM = new URLSearchParams(location.search).get('stem') || 'food_tree';
const WHITE='#ffffff', QUEUED='#f4a261', PRESENT='#5b9cf6', PAST='#b00018';
let T, G, P, NODES, ORDER, children, N, FMAX;
let f, timer, playing=true;
const img=document.getElementById('img'), cv=document.getElementById('hl'), ctx=cv.getContext('2d');
const svg=document.getElementById('svg'), right=document.getElementById('right'), cap=document.getElementById('cap');
function sizeImage(){
const S=Math.min(window.innerHeight*0.58, window.innerWidth*0.34, 440);
img.style.width=S+'px'; img.style.height=S+'px';
cv.width=S; cv.height=S; cv.style.width=S+'px'; cv.style.height=S+'px';
}
// CUMULATIVE reveal, matching reveal_hierarchy.mp4 exactly:
// pd[p] = depth of the deepest looked-at segment containing patch p
// pclear[p]= true once p's LEAF segment has been looked at (then full bright + white number)
// brightness LUT: coarse=dark -> deep=brighter (BMIN..BCAP, gamma), leaf=1.0
const BMIN=0.18, BCAP=0.72, BGAMMA=1.7;
function lut(d){ return BMIN+(BCAP-BMIN)*Math.pow(T.maxdepth? d/T.maxdepth : 0, BGAMMA); }
function renderImage(f){
const S=cv.width, cell=S/G; ctx.clearRect(0,0,S,S);
const looked=Math.min(f,N);
const pd=new Int16Array(P), pcl=new Uint8Array(P);
for(let i=0;i<looked;i++){ const nd=NODES[ORDER[i]], dep=nd.depth, lf=nd.leaf?1:0;
nd.patches.forEach(p=>{ pd[p]=dep; pcl[p]=lf; }); } // update ONLY this segment's patches
// dim overlay so the <img> beneath shows at each patch's brightness
for(let p=0;p<P;p++){ const b=pcl[p]?1.0:lut(pd[p]), a=1-b;
if(a>0.002){ ctx.fillStyle='rgba(6,7,12,'+a.toFixed(3)+')';
ctx.fillRect((p%G)*cell,(Math.floor(p/G))*cell,cell,cell); } }
// per-patch level number: GREEN once its leaf (final) segment is reached, else pale grey
const LEAFNUM='#3ee08a';
ctx.font=Math.round(cell*0.46)+'px monospace'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.lineWidth=2;
for(let p=0;p<P;p++){ const x=(p%G)*cell+cell/2, y=(Math.floor(p/G))*cell+cell/2;
const fin=pcl[p];
ctx.font=(fin?'bold ':'')+Math.round(cell*0.46)+'px monospace';
ctx.strokeStyle='rgba(0,0,0,0.85)'; ctx.strokeText(pd[p],x,y);
ctx.fillStyle=fin?LEAFNUM:'#cfd4e0'; ctx.fillText(pd[p],x,y); }
// blue outline of the segment currently being looked at (matches the "present" node)
const present=(f>=1&&f<=N)?ORDER[f-1]:null;
if(present!=null){ const ps=NODES[present].patches, set=new Set(ps);
ctx.strokeStyle='rgba(91,156,246,0.95)'; ctx.lineWidth=2;
ps.forEach(p=>{ const r=Math.floor(p/G),c=p%G;
[[r-1,c],[r+1,c],[r,c-1],[r,c+1]].forEach(([rr,cc])=>{
if(rr<0||rr>=G||cc<0||cc>=G||!set.has(rr*G+cc)){
ctx.beginPath();
if(rr<r){ctx.moveTo(c*cell,r*cell);ctx.lineTo((c+1)*cell,r*cell);}
else if(rr>r){ctx.moveTo(c*cell,(r+1)*cell);ctx.lineTo((c+1)*cell,(r+1)*cell);}
else if(cc<c){ctx.moveTo(c*cell,r*cell);ctx.lineTo(c*cell,(r+1)*cell);}
else{ctx.moveTo((c+1)*cell,r*cell);ctx.lineTo((c+1)*cell,(r+1)*cell);}
ctx.stroke();
}});
});
}
}
// priority = MEAN CLS-attention over the segment's patches (per-patch saliency),
// not total mass — favors small, highly-attended parts (eyes/head) over the big torso.
const PRI = n => (n.attm!=null ? n.attm : n.att/Math.max(n.n,1));
// best-first traversal: pop highest mean-attention node, then queue its children
function buildOrder(){
children=NODES.map(()=>[]);
NODES.forEach(n=>{ if(n.parent>=0) children[n.parent].push(n.id); });
const q=[0], steps=[];
while(q.length){
let bi=0; for(let i=1;i<q.length;i++) if(PRI(NODES[q[i]])>PRI(NODES[q[bi]])) bi=i;
const id=q.splice(bi,1)[0];
children[id].forEach(c=>q.push(c));
steps.push(id);
}
return steps;
}
let circles=[];
function drawTree(){
const W=right.clientWidth, H=right.clientHeight, pad=28;
svg.setAttribute('viewBox',`0 0 ${W} ${H}`);
const xs=x=>pad+(x/Math.max(T.nleaves-1,1))*(W-2*pad);
const ys=d=>pad+(d/Math.max(T.maxdepth,1))*(H-2*pad);
let s='';
NODES.forEach(n=>{ if(n.parent>=0){const p=NODES[n.parent];
s+=`<line class="edge" x1="${xs(p.x)}" y1="${ys(p.depth)}" x2="${xs(n.x)}" y2="${ys(n.depth)}"/>`;}});
NODES.forEach(n=>{ const r=Math.min(20,2.5+1.9*Math.sqrt(n.att));
s+=`<circle class="node" data-id="${n.id}" cx="${xs(n.x)}" cy="${ys(n.depth)}" r="${r}"
fill="${WHITE}" stroke="#0f1117" stroke-width="1"/>`;});
svg.innerHTML=s;
circles=[...svg.querySelectorAll('.node')];
}
// Reconstruct the whole state from frame index f (so back/forward are symmetric).
// f = 0 : nothing looked at, frontier = {root}, video blank
// f = 1..N : looking at ORDER[f-1] (blue + on video); ORDER[0..f-2] = red; pushed children = amber
// f = N+1 : done — all red, nothing blue, video blank
function render(){
const st=NODES.map(()=>WHITE);
const looked=Math.min(f,N);
for(let i=0;i<looked;i++) st[ORDER[i]]=PAST;
const pushed=new Set([0]);
for(let i=0;i<looked;i++) children[ORDER[i]].forEach(c=>pushed.add(c));
pushed.forEach(id=>{ if(st[id]===WHITE) st[id]=QUEUED; });
const present=(f>=1 && f<=N)? ORDER[f-1] : null;
if(present!=null) st[present]=PRESENT;
circles.forEach(el=>{ el.setAttribute('fill', st[+el.dataset.id]); });
renderImage(f);
if(f===0) cap.textContent='Frontier = {root}. The highest-attention node gets looked at next.';
else if(f<=N){ const id=ORDER[f-1], nch=children[id].length;
cap.textContent=`Step ${f}/${N}: look at node #${id} (mean ${PRI(NODES[id]).toFixed(2)}% attn/patch, ${NODES[id].n} patches)`+
(nch?` → push ${nch} child segment${nch>1?'s':''} to frontier.`:` → leaf, nothing to push.`); }
else cap.textContent='Done — every segment has been revealed; nothing left in the frontier.';
}
function goTo(nf){ f=Math.max(0,Math.min(FMAX,nf)); render(); }
function stop(){ playing=false; clearInterval(timer); document.getElementById('play').textContent='▶ Play'; }
function start(){ playing=true; document.getElementById('play').textContent='⏸ Pause';
clearInterval(timer); timer=setInterval(()=>{ if(f>=FMAX){ stop(); return; } goTo(f+1); },700); }
document.getElementById('back').onclick=()=>{ stop(); goTo(f-1); };
document.getElementById('step').onclick=()=>{ stop(); goTo(f+1); };
document.getElementById('play').onclick=()=>{ if(f>=FMAX) goTo(0); playing?stop():start(); };
document.getElementById('restart').onclick=()=>{ goTo(0); start(); };
window.addEventListener('resize',()=>{ if(T){ sizeImage(); drawTree(); render(); } });
let dataReady=false, imgReady=false;
function go(){ if(!(dataReady&&imgReady)) return;
sizeImage(); P=G*G; ORDER=buildOrder(); N=ORDER.length; FMAX=N+1; drawTree(); goTo(0); start(); }
img.onload=()=>{ imgReady=true; go(); };
T=window.TREE; G=T.g; NODES=T.nodes; dataReady=true;
img.src=window.IMG_SRC; // injected data URI
</script>
</body>
</html>