Show code
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Huffman Coding — Live Build</title>
<style>
html,body{margin:0;height:100%;background:#04060c;overflow:hidden;display:flex;align-items:center;justify-content:center}
canvas{max-width:100vw;max-height:100vh;display:block}
</style>
</head>
<body>
<canvas id="c" width="1200" height="760"></canvas>
<script>
const cv=document.getElementById('c'),g=cv.getContext('2d'),W=1200,H=760,BY=548,LH=52;
const SENTENCES=["huffman coding builds optimal prefix codes","the quick brown fox jumps over lazy dogs","entropy favours the shortest code words","mississippi banks and sweet bananas","a canvas grows one binary tree per second","abracadabra alakazam hocus pocus magic"];
const F=(s,w)=>(w||400)+" "+s+"px ui-monospace,Menlo,Consolas,monospace";
const ease=u=>u<0?0:u>1?1:(u<.5?4*u*u*u:1-Math.pow(-2*u+2,3)/2);
let S,q,all,pair,ph,t,pos,posA,posB,newId,root,codes,revealed,steps,letters;
function sortQ(){q.sort((a,b)=>a.f-b.f||a.id-b.id)}
function layout(rs){const P={};let i=0,tot=0;rs.forEach(r=>tot+=r.leaves);const L=72,R=W-72,sl=(R-L)/Math.max(tot,1);
const walk=n=>{if(!n.l)return P[n.id]={x:L+sl*(i+++.5),y:BY};const a=walk(n.l),b=walk(n.r);return P[n.id]={x:(a.x+b.x)/2,y:Math.min(a.y,b.y)-LH}};
rs.forEach(walk);return P}
function lerpP(A,B,u){const P={};for(const k in B)P[k]=A[k]?{x:A[k].x+(B[k].x-A[k].x)*u,y:A[k].y+(B[k].y-A[k].y)*u}:B[k];return P}
function nodesOf(){const o=[],w=n=>{if(n.l){w(n.l);w(n.r)}o.push(n)};q.forEach(w);return o}
function buildCodes(){codes=[];const rec=(n,c)=>{if(!n.l){codes.push({ch:n.ch,f:n.f,c:c||"0"});return}rec(n.l,c+"0");rec(n.r,c+"1")};rec(root,"");
codes.sort((a,b)=>a.c.length-b.c.length||(a.ch<b.ch?-1:1))}
function init(){S=SENTENCES[Math.random()*SENTENCES.length|0];const m=new Map();letters=0;
for(const ch of S.replace(/[^a-z]/g,"")){m.set(ch,(m.get(ch)||0)+1);letters++}
let uid=0;q=[...m].map(([ch,f])=>({id:uid++,ch:ch,f:f,leaves:1}));nextId=uid;all=q.length;steps=0;
sortQ();pos=layout(q);pair=[q[0],q[1]];ph="hl";t=0;root=null;codes=[];revealed=0;newId=-1}
let nextId=0,last=0;
function update(dt){t+=dt;
if(ph==="hl"&&t>.5){const[a,b]=pair;posA=pos;q=q.filter(n=>n!==a&&n!==b);
const p={id:nextId++,f:a.f+b.f,l:a,r:b,leaves:a.leaves+b.leaves};q.push(p);sortQ();posB=layout(q);newId=p.id;steps++;ph="mv";t=0}
else if(ph==="mv"&&t>.72){pos=posB;newId=-1;t=0;if(q.length<2){root=q[0];buildCodes();ph="codes"}else{pair=[q[0],q[1]];ph="hl"}}
else if(ph==="codes"){revealed=Math.min(codes.length,Math.floor(t/.26));if(t>codes.length*.26+3.6)init()}}
function pathSet(){const s=new Set();if(ph!=="codes"||!revealed)return s;let n=root;s.add(n.id);
for(const b of codes[revealed-1].c){if(!n.l)break;n=b==="0"?n.l:n.r;s.add(n.id)}return s}
function node(P,n,a,hot){const p=P[n.id];if(!p)return;const leaf=!n.l,r=leaf?19:14;g.globalAlpha=a;
const hue=leaf?(n.ch.charCodeAt(0)*37)%360:200;
if(hot){g.beginPath();g.arc(p.x,p.y,r+7+Math.sin(performance.now()/140)*2.5,0,7);g.fillStyle="hsla("+hue+",90%,60%,.22)";g.fill()}
g.beginPath();g.arc(p.x,p.y,r,0,7);g.fillStyle=leaf?"hsla("+hue+",70%,55%,.9)":"#0d1626";g.fill();
g.lineWidth=hot?2.5:1.4;g.strokeStyle=leaf?"hsla("+hue+",90%,78%,.95)":hot?"#ffd479":"#3d5b86";g.stroke();
g.textAlign="center";g.fillStyle=leaf?"#08111c":"#8fd4f0";g.font=F(leaf?17:12,700);g.fillText(leaf?n.ch.toUpperCase():n.f,p.x,p.y+(leaf?6:4));
if(leaf){g.fillStyle="#7f8fa8";g.font=F(12);g.fillText(n.f,p.x,p.y+r+15)}g.globalAlpha=1}
function render(){const gr=g.createLinearGradient(0,0,0,H);gr.addColorStop(0,"#0a1020");gr.addColorStop(1,"#04060c");g.fillStyle=gr;g.fillRect(0,0,W,H);
g.textAlign="left";g.fillStyle="#7ee8fa";g.font=F(23,700);g.fillText("HUFFMAN CODING",40,46);
g.fillStyle="#54637d";g.font=F(14);g.fillText('"'+S+'" '+letters+" letters, "+all+" symbols",40,70);
g.textAlign="right";g.fillStyle="#9aa7bd";g.font=F(14);
g.fillText(ph==="codes"?"tree complete — walking for code words":"merge "+steps+" / "+(all-1)+" queue: "+q.length+" nodes",W-40,46);
const u=ph==="mv"?ease(t/.72):1,P=ph==="mv"?lerpP(posA,posB,u):pos,hi=pathSet(),ns=nodesOf();
for(const n of ns){if(!n.l)continue;const a=P[n.id];if(!a)continue;const na=n.id===newId?u:1;
for(const[ch,bit]of[[n.l,"0"],[n.r,"1"]]){const b=P[ch.id];if(!b)continue;const on=hi.has(ch.id)&&hi.has(n.id);
g.globalAlpha=na;g.beginPath();g.moveTo(a.x,a.y);g.lineTo(b.x,b.y);g.strokeStyle=on?"#ffd479":"#26456b";g.lineWidth=on?3:1.6;g.stroke();
g.fillStyle=on?"#ffd479":"#3f6a99";g.font=F(11,700);g.textAlign="center";g.fillText(bit,(a.x+b.x)/2+(bit==="0"?-9:9),(a.y+b.y)/2+4);g.globalAlpha=1}}
const hot=ph==="hl"?new Set(pair.map(n=>n.id)):new Set();
for(const n of ns)node(P,n,n.id===newId?u:1,hot.has(n.id)||hi.has(n.id));
if(ph!=="codes"){g.textAlign="left";g.fillStyle="#4d5d75";g.font=F(12,700);g.fillText("PRIORITY QUEUE — smallest two merge next",40,632);
const n=q.length,w=Math.min(64,(W-80)/n-8);
q.forEach((nd,i)=>{const x=40+i*(w+8),y=648,on=ph==="hl"&&(nd===pair[0]||nd===pair[1]);
g.fillStyle=on?"rgba(255,212,121,.18)":"rgba(126,232,250,.07)";g.strokeStyle=on?"#ffd479":"#1e3550";
g.lineWidth=1.2;g.beginPath();g.roundRect(x,y,w,46,7);g.fill();g.stroke();g.textAlign="center";
g.fillStyle=nd.ch?"hsl("+(nd.ch.charCodeAt(0)*37)%360+",70%,68%)":"#6fa8d6";g.font=F(15,700);g.fillText(nd.ch?nd.ch.toUpperCase():"•",x+w/2,y+21);
g.fillStyle="#8b9ab2";g.font=F(12);g.fillText(nd.f,x+w/2,y+38)});
g.textAlign="left";g.fillStyle="#3d4a5e";g.font=F(12);g.fillText("building tree bottom-up: pop 2 smallest → merge → push back",40,724)}
else{g.textAlign="left";g.fillStyle="#4d5d75";g.font=F(12,700);g.fillText("CODE WORDS (prefix-free, read root → leaf)",40,626);
let bits=0;codes.forEach((c,i)=>{if(i>=revealed)return;bits+=c.f*c.c.length;const col=i/5|0,row=i%5,x=40+col*195,y=650+row*20;
g.fillStyle="hsl("+(c.ch.charCodeAt(0)*37)%360+",70%,66%)";g.font=F(14,700);g.fillText(c.ch.toUpperCase(),x,y);
g.fillStyle="#5f6f88";g.font=F(12);g.fillText("×"+c.f,x+18,y);g.fillStyle=i===revealed-1?"#ffd479":"#8fd4f0";g.font=F(13,700);g.fillText(c.c,x+56,y)});
const before=letters*8;g.textAlign="right";g.font=F(14,700);g.fillStyle="#6f8099";
g.fillText("fixed 8-bit: "+before+" bits",W-40,668);g.fillStyle="#7ee8fa";g.fillText("huffman: "+bits+" bits",W-40,690);
g.fillStyle="#8de08d";g.fillText("saved "+(revealed?Math.round((1-bits/before)*100):0)+"% avg "+(revealed?(bits/letters).toFixed(2):"0.00")+" bits/letter",W-40,712);
if(revealed>=codes.length){g.fillStyle="#3d4a5e";g.font=F(12);g.fillText("restarting with a new sentence…",W-40,736)}}}
function frame(ts){const dt=Math.min(.05,(ts-last)/1000||0);last=ts;update(dt);render();requestAnimationFrame(frame)}
init();requestAnimationFrame(frame);
</script>
</body>
</html>