File size: 10,340 Bytes
20b7fd5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
<!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}
  /* super-res 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}
  #cv{display:block}
  #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;flex-wrap:wrap;justify-content: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}
  button:disabled{opacity:.5;cursor:default}
  #dl{border-color:#3ee08a}
  .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"><canvas id="cv"></canvas><img id="img" style="display:none"></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>
      <button id="dl">⬇ Download</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 sharpening</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">image starts blurry (root) → each region <b style="color:#3ee08a">snaps sharp</b> at its leaf</div>
    </div>
  </div>
</div>
<script>
const WHITE='#ffffff', QUEUED='#f4a261', PRESENT='#5b9cf6', PAST='#b00018';
let T, G, P, NODES, ORDER, children, N, FMAX, maxdepth;
let f, timer, playing=true;
let S=440, R0=26, NB=16, blurLv=[];                       // blur pyramid (level 0 = sharp)

const img=document.getElementById('img'), cv=document.getElementById('cv'), ctx=cv.getContext('2d');
const svg=document.getElementById('svg'), right=document.getElementById('right'), cap=document.getElementById('cap');

function sizeImage(){
  S=Math.round(Math.min(window.innerHeight*0.58, window.innerWidth*0.34, 440));
  cv.width=S; cv.height=S; cv.style.width=S+'px'; cv.style.height=S+'px';
  R0=Math.max(8, Math.round(S*0.06));                     // max blur radius scales with canvas
}

// NB full-image blur levels: draw the sharp image, then the blurred image over it
// (the sharp base keeps borders from darkening where the blur samples off-canvas).
function buildBlur(){
  blurLv=[];
  for(let i=0;i<NB;i++){
    const r=R0*i/(NB-1);
    const c=document.createElement('canvas'); c.width=S; c.height=S;
    const cx=c.getContext('2d');
    cx.drawImage(img,0,0,S,S);
    if(r>=0.3){ cx.filter='blur('+r.toFixed(2)+'px)'; cx.drawImage(img,0,0,S,S); cx.filter='none'; }
    blurLv.push(c);
  }
}

// priority = MEAN CLS-attention over the segment's patches (favors small salient parts).
const PRI = n => (n.attm!=null ? n.attm : n.att/Math.max(n.n,1));
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;
}

// leaf -> 0 (fully sharp); else coarser, linear in depth (root = fully blurred).
function targetRadius(j){ return NODES[j].leaf ? 0 : R0*(maxdepth-NODES[j].depth)/Math.max(maxdepth,1); }

// per-patch blur radius after looking at ORDER[0..looked-1] (deepest looked segment wins).
function curRadius(f){
  const c=new Float32Array(P).fill(R0), looked=Math.min(f,N);
  for(let i=0;i<looked;i++){ const j=ORDER[i], tr=targetRadius(j); NODES[j].patches.forEach(p=>{ c[p]=tr; }); }
  return c;
}

function renderImage(f){
  const cell=S/G, cur=curRadius(f);
  ctx.clearRect(0,0,S,S);
  for(let p=0;p<P;p++){
    const lvf=Math.max(0,Math.min(NB-1, cur[p]/R0*(NB-1)));
    const lo=Math.floor(lvf), hi=Math.min(lo+1,NB-1), fr=lvf-lo;
    const c=(p%G)*cell, r=Math.floor(p/G)*cell;
    ctx.globalAlpha=1;  ctx.drawImage(blurLv[lo], c,r,cell,cell, c,r,cell,cell);
    if(fr>0.002){ ctx.globalAlpha=fr; ctx.drawImage(blurLv[hi], c,r,cell,cell, c,r,cell,cell); }
  }
  ctx.globalAlpha=1;
  // blue outline of the segment currently being sharpened (ties the video to the tree)
  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 rr0=Math.floor(p/G),cc0=p%G;
      [[rr0-1,cc0],[rr0+1,cc0],[rr0,cc0-1],[rr0,cc0+1]].forEach(([rr,cc])=>{
        if(rr<0||rr>=G||cc<0||cc>=G||!set.has(rr*G+cc)){
          ctx.beginPath();
          if(rr<rr0){ctx.moveTo(cc0*cell,rr0*cell);ctx.lineTo((cc0+1)*cell,rr0*cell);}
          else if(rr>rr0){ctx.moveTo(cc0*cell,(rr0+1)*cell);ctx.lineTo((cc0+1)*cell,(rr0+1)*cell);}
          else if(cc<cc0){ctx.moveTo(cc0*cell,rr0*cell);ctx.lineTo(cc0*cell,(rr0+1)*cell);}
          else{ctx.moveTo((cc0+1)*cell,rr0*cell);ctx.lineTo((cc0+1)*cell,(rr0+1)*cell);}
          ctx.stroke();
        }});
    });
  }
  // progress bar (matches the orange bar in the exported mp4)
  ctx.fillStyle='#f4a261'; ctx.fillRect(0,0,S*Math.min(f,N)/Math.max(N,1),5);
}

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')];
}

function render(){
  const st=NODES.map(()=>WHITE), 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='Fully blurred = root segment. The highest-attention segment sharpens next.';
  else if(f<=N){ const id=ORDER[f-1], nch=children[id].length;
    cap.textContent=`Step ${f}/${N}: sharpen node #${id} (mean ${PRI(NODES[id]).toFixed(2)}% attn/patch, ${NODES[id].n} patches)`+
      (nch?` → push ${nch} child segment${nch>1?'s':''}.`:` → leaf: this region snaps to full detail.`); }
  else cap.textContent='Done — every region has snapped to full resolution.';
}

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(); };

// ---- client-side download: record the canvas play-through as a .webm ----
const dlBtn=document.getElementById('dl');
if(typeof MediaRecorder==='undefined' || !cv.captureStream){ dlBtn.style.display='none'; }
dlBtn.onclick=()=>{
  stop(); const stream=cv.captureStream(30);
  let mime='video/webm;codecs=vp9'; if(!MediaRecorder.isTypeSupported(mime)) mime='video/webm';
  const rec=new MediaRecorder(stream,{mimeType:mime, videoBitsPerSecond:6e6}), chunks=[];
  rec.ondataavailable=e=>{ if(e.data.size) chunks.push(e.data); };
  rec.onstop=()=>{ const blob=new Blob(chunks,{type:mime}); const url=URL.createObjectURL(blob);
    const a=document.createElement('a'); a.href=url; a.download='reveal_superres.webm';
    document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
    dlBtn.textContent='⬇ Download'; dlBtn.disabled=false; start(); };
  dlBtn.textContent='● recording…'; dlBtn.disabled=true;
  rec.start(); goTo(0);
  let k=0; const HOLD=4, tick=setInterval(()=>{                 // hold each look a few frames, then advance
    if(k>=(N+2)*HOLD){ clearInterval(tick); rec.stop(); return; }
    goTo(Math.floor(k/HOLD)); k++;
  }, 1000/30);
};

window.addEventListener('resize',()=>{ if(T){ sizeImage(); buildBlur(); drawTree(); render(); } });

let dataReady=false, imgReady=false;
function go(){ if(!(dataReady&&imgReady)) return;
  sizeImage(); P=G*G; buildBlur(); 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; maxdepth=T.maxdepth; dataReady=true;
img.src=window.IMG_SRC;   // injected data URI
</script>
</body>
</html>