// Fixed image filters provide an honest illustrative CNN view; API internals // are unavailable. The output labels/probabilities are actual visual responses. export const featureHTML = () => `
Drawing → filters → features → guesses

Tiles are fixed filters of your drawing, not Qwen or Decider’s internal activations. Output bars show actual visual candidate probabilities.

Inside the drawing

Illustrative fixed filters · actual API guesses at the output

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