Show code
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Levenshtein DP Matrix</title>
<style>
html,body{margin:0;height:100%;background:#0d1117;color:#c9d1d9;font:14px ui-monospace,SFMono-Regular,Menlo,monospace;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px}
canvas{border-radius:12px;box-shadow:0 10px 40px #0008}
#out{min-height:96px;width:min(92vw,640px);background:#161b22;border:1px solid #30363d;border-radius:10px;padding:12px 16px;line-height:1.6;white-space:pre-wrap}
.k{color:#8b949e}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="out"></div>
<script>
const words=["keyboard","elephant","computer","notebook","umbrella","mountain","sunlight","triangle","chocolate","telephone","butterfly","adventure","backspace","algorithm","developer","raspberry","blueprint","landscape","pineapple","waterfall"];
const cv=document.getElementById('c'),ctx=cv.getContext('2d'),out=document.getElementById('out');
const cs=42,ox=74,oy=100;
let A,B,n,m,d,dir,ts,order,fi,phase,path,ops,ci,cj,acc,wait,hold,prev,CW,CH;
function resize(){const dpr=window.devicePixelRatio||1;CW=ox+(m+1)*cs+26;CH=oy+(n+1)*cs+30;cv.style.width=CW+'px';cv.style.height=CH+'px';cv.width=CW*dpr;cv.height=CH*dpr;ctx.setTransform(dpr,0,0,dpr,0,0);}
function init(){
A=words[Math.random()*words.length|0];do{B=words[Math.random()*words.length|0]}while(B===A);
n=A.length;m=B.length;d=[];dir=[];ts=[];
for(let i=0;i<=n;i++){d.push(new Array(m+1).fill(null));dir.push(new Array(m+1).fill(0));ts.push(new Array(m+1).fill(0));}
order=[];for(let i=0;i<=n;i++)for(let j=0;j<=m;j++)order.push([i,j]);
fi=0;phase=0;path=new Set();ops=[];ci=n;cj=m;acc=0;wait=0;resize();
}
function step(now){
const p=order[fi++],i=p[0],j=p[1];
if(!i&&!j){d[0][0]=0;dir[0][0]=0;}
else if(!i){d[0][j]=j;dir[0][j]=1;}
else if(!j){d[i][0]=i;dir[i][0]=2;}
else{const c=A[i-1]===B[j-1]?0:1,sub=d[i-1][j-1]+c,ins=d[i][j-1]+1,del=d[i-1][j]+1;
let best=sub,k=c?3:4;
if(ins<best){best=ins;k=1}
if(del<best){best=del;k=2}
d[i][j]=best;dir[i][j]=k;}
ts[i][j]=now;
}
function back(){
path.add(ci+','+cj);
if(!ci&&!cj){phase=2;ops.reverse();hold=performance.now();return}
const k=dir[ci][cj];
if(k===1){ops.push('insert "'+B[cj-1]+'" before position '+(ci+1));cj--}
else if(k===2){ops.push('delete "'+A[ci-1]+'" at position '+ci);ci--}
else{if(k===3)ops.push('replace "'+A[ci-1]+'" -> "'+B[cj-1]+'" at position '+ci);ci--;cj--}
}
function arrow(k){return k===1?'←':k===2?'↑':'↖'}
function draw(now){
ctx.fillStyle='#0d1117';ctx.fillRect(0,0,CW,CH);
ctx.textAlign='center';ctx.textBaseline='middle';
ctx.font='bold 22px ui-monospace,monospace';ctx.fillStyle='#e6edf3';
ctx.fillText(A+' → '+B,CW/2,34);
ctx.font='13px ui-monospace,monospace';ctx.fillStyle='#8b949e';
ctx.fillText(phase===0?'filling dynamic programming matrix':phase===1?'walking the backtrace':'edit distance = '+d[n][m],CW/2,60);
ctx.font='bold 19px ui-monospace,monospace';ctx.fillStyle='#7d8590';
for(let j=1;j<=m;j++)ctx.fillText(B[j-1].toUpperCase(),ox+(j+.5)*cs,oy-24);
for(let i=1;i<=n;i++)ctx.fillText(A[i-1].toUpperCase(),ox-26,oy+(i+.5)*cs);
const cur=fi>0?order[fi-1]:null;
for(let i=0;i<=n;i++)for(let j=0;j<=m;j++){
if(d[i][j]===null)continue;
const e=1-Math.pow(1-Math.min(1,(now-ts[i][j])/280),3),s=cs-5;
const on=path.has(i+','+j),live=phase===0&&cur&&cur[0]===i&&cur[1]===j;
ctx.save();ctx.translate(ox+(j+.5)*cs,oy+(i+.5)*cs);ctx.scale(.65+.35*e,.65+.35*e);ctx.globalAlpha=.15+.85*e;
ctx.fillStyle=on?'#f0883e':live?'#1f6feb':'#161b22';
ctx.strokeStyle=on?'#ffd7a8':live?'#79c0ff':'#30363d';ctx.lineWidth=on||live?2:1;
ctx.beginPath();ctx.roundRect(-s/2,-s/2,s,s,7);ctx.fill();ctx.stroke();
ctx.fillStyle=on?'#26160a':'#c9d1d9';ctx.font='bold 16px ui-monospace,monospace';
ctx.fillText(d[i][j],1,2);
const k=dir[i][j];
if(k){ctx.font='11px ui-monospace,monospace';
ctx.fillStyle=on?'#4a2c0d':k===4?'#3fb950':k===1?'#58a6ff':k===2?'#d29922':'#db6d28';
ctx.fillText(arrow(k),-s/2+10,-s/2+10);}
ctx.restore();
}
ctx.textAlign='left';ctx.font='12px ui-monospace,monospace';
const leg=[['↖ match','#3fb950'],['↖ replace','#db6d28'],['← insert','#58a6ff'],['↑ delete','#d29922']];
leg.forEach((L,i)=>{ctx.fillStyle=L[1];ctx.fillText(L[0],18+i*Math.min(120,(CW-40)/4),CH-14)});
}
function report(){
if(phase===0){out.innerHTML='<span class="k">computing '+fi+' / '+order.length+' cells...</span>';return}
const head='<span class="k">distance('+A+', '+B+') = </span><b>'+d[n][m]+'</b>\n';
const body=ops.length?ops.map((o,i)=>(i+1)+'. '+o).join('\n'):'no operations needed';
out.innerHTML=head+(phase===1?'<span class="k">backtracing...</span>\n':'')+body;
}
function loop(now){
if(!prev)prev=now;const dt=Math.min(64,now-prev);prev=now;
if(phase===0){acc+=dt;while(acc>=36&&fi<order.length){acc-=36;step(now)}
if(fi>=order.length){wait+=dt;if(wait>650){phase=1;acc=0}}}
else if(phase===1){acc+=dt;while(acc>=165&&phase===1){acc-=165;back()}}
else if(now-hold>4200){init();}
draw(now);report();requestAnimationFrame(loop);
}
init();requestAnimationFrame(loop);
addEventListener('resize',()=>resize());
</script>
</body>
</html>