Show code
<!doctype html>
<meta charset="utf-8">
<title>Knuth-Morris-Pratt string search</title>
<style>
html,body{margin:0;height:100%;background:#070a12;display:grid;place-items:center;font-family:system-ui,sans-serif}
canvas{background:#0b1020;border-radius:16px;box-shadow:0 24px 70px #000a}
</style>
<canvas id=c width=960 height=540></canvas>
<script>
const c=document.getElementById('c'),x=c.getContext('2d'),W=960,H=540;
const R=a=>a[Math.floor(Math.random()*a.length)];let S;
function init(){
const A=Math.random()<.5?['A','B']:['A','B','C'];
const m=3+Math.floor(Math.random()*3),n=25+Math.floor(Math.random()*6);
let p='',t='';
for(let k=0;k<m;k++)p+=R(A);
for(let k=0;k<n;k++)t+=R(A);
for(let k=0;k<2;k++){const a=Math.floor(Math.random()*(n-m+1));t=t.slice(0,a)+p+t.slice(a+m)}
S={p,t,m,n,lps:Array(m).fill(0),ph:0,tk:1,tl:0,tcmp:null,tok:1,i:0,j:0,cmp:null,ok:1,
comps:0,mat:[],off:0,doff:0,tmr:0,fl:0,msg:'Phase 1 - building the prefix table (LPS)',col:'#9fb4d6'};
}
function step(){
if(S.ph==0){
if(S.tk>=S.m){S.ph=1;S.tcmp=null;S.msg='Phase 2 - sliding the pattern along the text';S.col='#9fb4d6';return}
S.tcmp=[S.tk,S.tl];S.comps++;
if(S.p[S.tk]==S.p[S.tl]){S.tok=1;S.tl++;S.lps[S.tk]=S.tl;S.msg='p['+S.tk+'] = p['+S.tl+"-1] -> lps["+S.tk+'] = '+S.tl;S.tk++;S.col='#3ddc84'}
else if(S.tl>0){S.tok=0;S.msg='mismatch -> fall back to lps['+(S.tl-1)+'] = '+S.lps[S.tl-1];S.tl=S.lps[S.tl-1];S.col='#ff6b7a'}
else{S.tok=0;S.lps[S.tk]=0;S.msg='no border -> lps['+S.tk+'] = 0';S.tk++;S.col='#ff6b7a'}
return;
}
if(S.ph==2){init();return}
if(S.i>=S.n){S.ph=2;S.cmp=null;S.msg='Scan complete - '+S.mat.length+' match(es) in '+S.comps+' comparisons. Restarting...';S.col='#f5c542';return}
S.cmp=[S.i,S.j];S.comps++;
if(S.t[S.i]==S.p[S.j]){
S.ok=1;S.i++;S.j++;S.col='#3ddc84';S.msg='t['+S.cmp[0]+'] = p['+S.cmp[1]+"] = '"+S.p[S.cmp[1]]+"' -> advance both";
if(S.j==S.m){S.mat.push(S.i-S.m);S.fl=34;S.msg='MATCH at index '+(S.i-S.m)+' -> continue with lps = '+S.lps[S.m-1];S.j=S.lps[S.m-1];S.col='#f5c542'}
}else{
S.ok=0;S.col='#ff6b7a';
if(S.j>0){const b=S.lps[S.j-1];S.msg="mismatch '"+S.t[S.i]+"' vs '"+S.p[S.j]+"' -> jump "+(S.j-b)+' (lps['+(S.j-1)+'] = '+b+'), no backtrack in text';S.j=b}
else{S.msg="mismatch '"+S.t[S.i]+"' vs '"+S.p[0]+"' -> shift by 1";S.i++}
}
S.off=S.i-S.j;
}
function row(s,x0,y,cw,f,h){
for(let k=0;k<s.length;k++){const cx=x0+k*cw,st=f(k);
x.fillStyle=st[0];x.fillRect(cx+1.5,y,cw-3,h);
x.strokeStyle=st[1];x.lineWidth=2;x.strokeRect(cx+1.5,y,cw-3,h);
x.fillStyle=st[2];x.font='bold 19px ui-monospace,monospace';x.textAlign='center';
x.fillText(s[k],cx+cw/2,y+h/2+1)}
}
function box(bx,by,lab,val,col){
x.fillStyle='#101830';x.fillRect(bx,by,272,60);x.strokeStyle='#22314f';x.lineWidth=1;x.strokeRect(bx+.5,by+.5,271,59);
x.textAlign='left';x.fillStyle='#6d7f9e';x.font='12px system-ui';x.fillText(lab,bx+16,by+19);
x.fillStyle=col;x.font='bold 24px ui-monospace,monospace';x.fillText(val,bx+16,by+41);
}
function draw(){
x.fillStyle='#0b1020';x.fillRect(0,0,W,H);x.textBaseline='middle';x.textAlign='left';
x.fillStyle='#eaf1ff';x.font='bold 25px system-ui';x.fillText('Knuth-Morris-Pratt string search',40,34);
x.fillStyle='#6d7f9e';x.font='13px ui-monospace,monospace';x.fillText('pattern "'+S.p+'" · text of '+S.n+' chars · alphabet-driven random instance',40,60);
const pw=46,px=110,py=100;
x.fillStyle='#8aa2c8';x.font='12px system-ui';x.textAlign='right';x.fillText('PREFIX',100,py+17);x.fillText('TABLE',100,py+50);
row(S.p,px,py,pw,k=>{
if(S.ph==0&&S.tcmp&&k==S.tcmp[0])return[S.tok?'#153a28':'#3b141d',S.tok?'#3ddc84':'#ff6b7a','#fff'];
if(S.ph==0&&S.tcmp&&k==S.tcmp[1])return['#152a4c','#5aa9ff','#dbe9ff'];
return['#141d33','#25324f','#cfe0ff']},34);
for(let k=0;k<S.m;k++){const d=S.ph>0||k<S.tk,cx=px+k*pw;
x.fillStyle='#0e1526';x.fillRect(cx+1.5,py+36,pw-3,28);x.strokeStyle='#25324f';x.lineWidth=1;x.strokeRect(cx+1.5,py+36,pw-3,28);
x.textAlign='center';x.font='bold 16px ui-monospace,monospace';x.fillStyle=d?'#f5c542':'#2c3a58';x.fillText(d?S.lps[k]:'?',cx+pw/2,py+51)}
x.strokeStyle='#1a2440';x.lineWidth=1;x.beginPath();x.moveTo(40,196);x.lineTo(920,196);x.stroke();
const cw=Math.min(30,(W-190)/S.n),tx=110,ty=232,py2=300;
x.textAlign='right';x.fillStyle='#8aa2c8';x.font='12px system-ui';x.fillText('TEXT',100,ty+17);x.fillText('PATTERN',100,py2+17);
row(S.t,tx,ty,cw,k=>{
if(S.ph==1&&S.cmp&&k==S.cmp[0])return[S.ok?'#153a28':'#3b141d',S.ok?'#3ddc84':'#ff6b7a','#fff'];
if(S.mat.some(a=>k>=a&&k<a+S.m))return['#0f2c1d','#2c7a4b','#a9efc6'];
return['#141d33','#25324f','#cfe0ff']},34);
S.mat.forEach(a=>{x.fillStyle=S.fl>0&&a==S.mat[S.mat.length-1]?'#f5c542':'#3ddc84';x.fillRect(tx+a*cw+2,ty+38,S.m*cw-4,4)});
const ox=tx+S.doff*cw;
row(S.p,ox,py2,cw,k=>{
if(S.ph==1&&S.cmp&&k==S.cmp[1])return[S.ok?'#153a28':'#3b141d',S.ok?'#3ddc84':'#ff6b7a','#fff'];
if(S.ph==1&&k<S.j)return['#152a4c','#5aa9ff','#dbe9ff'];
return['#161f38','#2b3a5c','#9fb4d6']},34);
if(S.ph==1&&S.cmp){const a=tx+S.cmp[0]*cw+cw/2,b=ox+S.cmp[1]*cw+cw/2;
x.strokeStyle=S.ok?'#3ddc84':'#ff6b7a';x.setLineDash([4,4]);x.lineWidth=2;x.beginPath();
x.moveTo(a,ty+36);x.lineTo(a,ty+48);x.lineTo(b,py2-12);x.lineTo(b,py2-2);x.stroke();x.setLineDash([])}
x.textAlign='left';x.fillStyle=S.col;x.font='bold 16px ui-monospace,monospace';x.fillText(S.msg,40,378);
box(40,410,'CHARACTER COMPARISONS',''+S.comps,'#eaf1ff');
box(344,410,'MATCHES FOUND',S.mat.length+(S.mat.length?' @ '+S.mat.join(', '):''),'#3ddc84');
box(648,410,'POINTERS','i = '+Math.min(S.i,S.n)+' j = '+S.j,'#5aa9ff');
x.fillStyle='#4a5a78';x.font='12px system-ui';x.fillText('the text pointer i never moves backwards - on a mismatch the pattern jumps forward by j - lps[j-1]',40,502);
}
function loop(){
S.doff+=(S.off-S.doff)*.22;if(Math.abs(S.off-S.doff)<.012)S.doff=S.off;
if(S.fl>0)S.fl--;
if(++S.tmr>=(S.ph==0?20:S.ph==1?27:110)&&S.doff==S.off){S.tmr=0;step()}
draw();requestAnimationFrame(loop);
}
init();loop();
</script>