Spaces:
Running
Running
| <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> | |