brain-lab / brain_lab /static /feature-view.js
Chungulus's picture
Deploy Brain Lab CPU classroom app with verified saved research playback
4155f6b verified
Raw History Blame Contribute Delete
6.84 kB
// Fixed image filters provide an honest illustrative CNN view; API internals
// are unavailable. The output labels/probabilities are actual visual responses.
export const featureHTML = () => `<div class="feature-toolbar"><span class="note" style="margin:0">Drawing → filters → features → guesses</span><button class="secondary" id="expand-features">Expand view ↗</button></div><div id="feature-home"><div class="feature-scroll" id="feature-scroll"><canvas id="feature-network" width="1140" height="530" aria-label="Illustrative image-filter layers connected to actual API guesses"></canvas></div></div><p class="note" id="feature-status">Tiles are fixed filters of your drawing, not Qwen or Decider’s internal activations. Output bars show actual visual candidate probabilities.</p><dialog id="feature-dialog" class="feature-dialog"><div class="toolbar"><h2>Inside the drawing</h2><button class="secondary" id="close-features">Close</button></div><div id="feature-full"></div><p class="note">Illustrative fixed filters · actual API guesses at the output</p></dialog>`;
const kernels = [
[-1,0,1,-2,0,2,-1,0,1], [-1,-2,-1,0,0,0,1,2,1],
[0,-1,0,-1,4,-1,0,-1,0], [1,1,1,1,1,1,1,1,1].map(x=>x/9),
[2,-1,-1,-1,2,-1,-1,-1,2], [-1,-1,2,-1,2,-1,2,-1,-1]
];
const names=['Vertical edges','Horizontal edges','Fine detail','Blur / ink density','Diagonal ↘','Diagonal ↗'];
function conv(a,n,k){const out=new Float32Array(n*n);for(let y=0;y<n;y++)for(let x=0;x<n;x++){let v=0;for(let j=-1;j<=1;j++)for(let i=-1;i<=1;i++)v+=a[Math.max(0,Math.min(n-1,y+j))*n+Math.max(0,Math.min(n-1,x+i))]*k[(j+1)*3+i+1];out[y*n+x]=v}return out}
function pool(a,n){const m=n/2,out=new Float32Array(m*m);for(let y=0;y<m;y++)for(let x=0;x<m;x++)out[y*m+x]=Math.max(a[y*2*n+x*2],a[y*2*n+x*2+1],a[(y*2+1)*n+x*2],a[(y*2+1)*n+x*2+1]);return out}
function tile(a,n,signed=true){const c=document.createElement('canvas');c.width=c.height=n;const ctx=c.getContext('2d'),im=ctx.createImageData(n,n);let max=.001;for(const v of a)max=Math.max(max,Math.abs(v));a.forEach((v,i)=>{const t=Math.max(-1,Math.min(1,v/max)),color=signed?(t<0?[213,86,60]:[47,139,181]):[47,139,181],q=signed?Math.abs(t):Math.max(0,t);for(let j=0;j<3;j++)im.data[i*4+j]=Math.round(248*(1-q)+color[j]*q);im.data[i*4+3]=255});ctx.putImageData(im,0,0);return c}
export function featureView(source){
const canvas=document.querySelector('#feature-network'),ctx=canvas.getContext('2d');
const tiny=document.createElement('canvas');tiny.width=tiny.height=48;
let layers=[],rows=[],busy=false,alive=true,raf,queued=false,hover=null;
const xs=[55,235,385,540,710,960],ys=Array.from({length:6},(_,i)=>100+i*65);
function update(){const c=tiny.getContext('2d');c.drawImage(source,0,0,48,48);const pixels=c.getImageData(0,0,48,48).data,a=new Float32Array(48*48);for(let i=0;i<a.length;i++)a[i]=1-(pixels[i*4]+pixels[i*4+1]+pixels[i*4+2])/765;const filtered=kernels.map(k=>conv(a,48,k)),relu=filtered.map(a=>a.map(v=>Math.max(0,v))),pooled=relu.map(a=>pool(a,48)),deep=pooled.map((a,i)=>conv(a,24,kernels[(i+2)%6]));layers=[[tile(a,48,false)],filtered.map(a=>tile(a,48)),relu.map(a=>tile(a,48,false)),pooled.map(a=>tile(a,24,false)),deep.map(a=>tile(a,24))];queued=false;draw()}
function draw(t=performance.now()){
if(!alive)return;ctx.fillStyle='#fbfcf9';ctx.fillRect(0,0,1140,530);
const headers=[['input','48 × 48 × 1'],['conv_1','48 × 48 × 6'],['relu_1','48 × 48 × 6'],['max_pool','24 × 24 × 6'],['conv_2','24 × 24 × 6'],['output','open vocabulary']];
ctx.textAlign='center';headers.forEach(([h,s],i)=>{ctx.fillStyle='#4c6258';ctx.font='13px Manrope, sans-serif';ctx.fillText(h,xs[i]+27,27);ctx.fillStyle='#97a298';ctx.font='10px sans-serif';ctx.fillText(s,xs[i]+27,45)});
// Dense, softly curved connections echo the supplied feature-map reference.
ctx.lineWidth=.8;
for(let col=0;col<5;col++)for(let a=0;a<(col===0?1:6);a++)for(let b=0;b<6;b++){
const y1=col===0?263:ys[a]+27,y2=ys[b]+27,x1=xs[col]+54,x2=xs[col+1];
ctx.strokeStyle=busy&&Math.floor(t/450)%5===col?'#b7cdc966':'#b8c5ba28';ctx.beginPath();ctx.moveTo(x1,y1);ctx.bezierCurveTo(x1+50,y1,x2-50,y2,x2,y2);ctx.stroke();
}
layers.forEach((tiles,col)=>tiles.forEach((im,row)=>{const y=col===0?236:ys[row];ctx.drawImage(im,xs[col],y,54,54);ctx.strokeStyle='#dbe4db';ctx.strokeRect(xs[col],y,54,54);if(hover?.col===col&&hover.row===row){ctx.strokeStyle='#327764';ctx.lineWidth=2;ctx.strokeRect(xs[col]-2,y-2,58,58);ctx.lineWidth=.8}}));
ctx.textAlign='left';for(let i=0;i<6;i++){const row=rows[i],y=ys[i];ctx.fillStyle=row?'#51685a':'#a5afa4';ctx.font='12px sans-serif';let label=row?.label||(busy?'Searching…':'Awaiting a guess');while(ctx.measureText(label).width>148)label=label.slice(0,-2)+'…';ctx.fillText(label,xs[5],y+19);ctx.fillStyle='#e7ece4';ctx.fillRect(xs[5],y+31,150,5);if(row){ctx.fillStyle=i===0?'#488f77':'#9fbbb0';ctx.fillRect(xs[5],y+31,150*row.probability,5);ctx.fillStyle='#8a998d';ctx.font='9px sans-serif';ctx.fillText(Math.round(row.probability*100)+'%',xs[5],y+50)}}
const g=ctx.createLinearGradient(235,0,764,0);g.addColorStop(0,'#d5563c');g.addColorStop(.5,'#f8f8f4');g.addColorStop(1,'#2f8bb5');ctx.fillStyle=g;ctx.fillRect(235,505,529,5);ctx.fillStyle='#94a196';ctx.font='9px sans-serif';ctx.fillText('negative',235,523);ctx.fillText('0',493,523);ctx.fillText('positive · normalized per tile',635,523);
if(hover){ctx.fillStyle='#476c5e';ctx.font='11px sans-serif';ctx.fillText(hover.col===0?'Drawing pixels':names[hover.row],55,485)}
}
function animate(t){if(busy){draw(t);raf=requestAnimationFrame(animate)}}
const dialog=document.querySelector('#feature-dialog'),scroll=document.querySelector('#feature-scroll');
document.querySelector('#expand-features').onclick=()=>{document.querySelector('#feature-full').append(scroll);dialog.showModal();draw()};
const close=()=>dialog.close();document.querySelector('#close-features').onclick=close;
dialog.onclose=()=>document.querySelector('#feature-home')?.append(scroll);
canvas.onpointermove=e=>{const r=canvas.getBoundingClientRect(),x=(e.clientX-r.left)*1140/r.width,y=(e.clientY-r.top)*530/r.height;hover=null;for(let col=0;col<5;col++)for(let row=0;row<(col===0?1:6);row++){const yy=col===0?236:ys[row];if(x>=xs[col]&&x<=xs[col]+54&&y>=yy&&y<=yy+54)hover={col,row}}if(!busy)draw()};canvas.onpointerleave=()=>{hover=null;if(!busy)draw()};
update();
return {changed(){rows=[];if(!queued){queued=true;requestAnimationFrame(()=>{if(alive)update()})}},result(candidates){rows=candidates.slice(0,6);draw()},processing(value){busy=value;cancelAnimationFrame(raf);if(busy&&!matchMedia('(prefers-reduced-motion: reduce)').matches)raf=requestAnimationFrame(animate);else draw()},destroy(){alive=false;cancelAnimationFrame(raf);if(dialog.open)dialog.close()}};
}