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