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>