Spaces:
Running
Running
File size: 9,297 Bytes
a3b5cf9 2699c3a a3b5cf9 2699c3a a3b5cf9 2699c3a a3b5cf9 2699c3a 003797e a3b5cf9 003797e a3b5cf9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | <!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>
|