visual-tree / superres_widget.html
ShesterG's picture
Add 'Reveal video' tab: super-resolution reveal widget + downloadable MP4
20b7fd5
Raw History Blame Contribute Delete
10.3 kB
<!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>