<meta charset="utf-8">
<title>Graham Scan — Convex Hull Animation</title>
<style>
html,body{margin:0;height:100%;background:#070a12;display:grid;place-items:center}
canvas{background:radial-gradient(circle at 50% 42%,#111c33,#070a12 72%);border:1px solid #1d2c47;border-radius:12px;box-shadow:0 24px 70px #000a;max-width:96vw;height:auto}
</style>
<canvas id="c" width="900" height="600"></canvas>
<script>
const c=document.getElementById('c'),g=c.getContext('2d'),W=900,H=600;
let P,st,i,sortI,phase,steps,pops,flash,tick,hold,piv;
const ang=a=>Math.atan2(piv.y-a.y,a.x-piv.x);
const dist=(a,b)=>Math.hypot(a.x-b.x,a.y-b.y);
const cross=(o,a,b)=>(a.x-o.x)*(o.y-b.y)-(o.y-a.y)*(b.x-o.x);
function reset(){
P=[];
for(let k=0;k<40;k++)P.push({x:70+Math.random()*(W-140),y:115+Math.random()*(H-190)});
piv=P[0];
for(const q of P)if(q.y>piv.y||(q.y===piv.y&&q.x<piv.x))piv=q;
P.sort((a,b)=>a===piv?-1:b===piv?1:(ang(a)-ang(b))||(dist(piv,a)-dist(piv,b)));
st=[P[0]];i=1;sortI=0;phase='sort';steps=0;pops=0;flash=[];tick=0;hold=0;
}
function step(){
steps++;
if(phase==='sort'){if(++sortI>=P.length)phase='scan';return;}
if(phase!=='scan')return;
if(i>=P.length){phase='done';hold=120;return;}
const n=st.length;
if(n>=2&&cross(st[n-2],st[n-1],P[i])<=0){
const d=st.pop();flash.push({x:d.x,y:d.y,t:1});pops++;
}else st.push(P[i++]);
}
function dot(p,r,col){g.beginPath();g.arc(p.x,p.y,r,0,7);g.fillStyle=col;g.fill();}
function draw(){
g.clearRect(0,0,W,H);
const shown=phase==='sort'?sortI:P.length,fade=phase==='sort'?0.38:0.08;
g.lineWidth=1;g.strokeStyle='rgba(96,150,230,'+fade+')';
for(let k=1;k<shown;k++){g.beginPath();g.moveTo(piv.x,piv.y);g.lineTo(P[k].x,P[k].y);g.stroke();}
if(phase==='sort'&&sortI<P.length){
const p=P[sortI];
g.strokeStyle='#ffd166';g.lineWidth=2;g.beginPath();g.moveTo(piv.x,piv.y);g.lineTo(p.x,p.y);g.stroke();
dot(p,6,'#ffd166');
}
for(let k=0;k<P.length;k++){
const p=P[k],used=phase!=='sort'&&k<i;
dot(p,used?3.4:3,used?'rgba(150,180,220,.85)':'rgba(110,140,180,.55)');
}
if(st.length>1){
g.beginPath();g.moveTo(st[0].x,st[0].y);
for(let k=1;k<st.length;k++)g.lineTo(st[k].x,st[k].y);
if(phase==='done'){g.closePath();g.fillStyle='rgba(80,230,185,.10)';g.fill();}
g.strokeStyle=phase==='done'?'#5ff0c0':'#4fc3ff';
g.lineWidth=2.6;g.shadowColor=g.strokeStyle;g.shadowBlur=14;g.stroke();g.shadowBlur=0;
}
for(const p of st)dot(p,5,phase==='done'?'#5ff0c0':'#4fc3ff');
if(phase==='scan'&&i<P.length){
const top=st[st.length-1],cd=P[i],n=st.length,
bad=n>=2&&cross(st[n-2],st[n-1],cd)<=0,col=bad?'#ff6b7e':'#ffd166';
g.setLineDash([7,7]);g.lineWidth=2.2;g.strokeStyle=col;
g.beginPath();g.moveTo(top.x,top.y);g.lineTo(cd.x,cd.y);g.stroke();g.setLineDash([]);
dot(cd,6.5,col);
g.beginPath();g.arc(cd.x,cd.y,10+3*Math.sin(tick*0.18),0,7);
g.strokeStyle=col;g.lineWidth=1.4;g.stroke();
if(bad){
g.strokeStyle='rgba(255,107,126,.85)';g.lineWidth=2;
g.beginPath();g.moveTo(st[n-2].x,st[n-2].y);g.lineTo(st[n-1].x,st[n-1].y);g.stroke();
}
}
g.font='12px ui-monospace,Menlo,Consolas,monospace';
for(const f of flash){
g.beginPath();g.arc(f.x,f.y,7+(1-f.t)*30,0,7);
g.strokeStyle='rgba(255,90,110,'+f.t.toFixed(2)+')';g.lineWidth=2.2;g.stroke();
g.fillStyle='rgba(255,140,155,'+f.t.toFixed(2)+')';g.fillText('POP',f.x+12,f.y-14-(1-f.t)*16);
}
dot(piv,7,'#ff5ec7');
g.strokeStyle='rgba(255,94,199,.6)';g.lineWidth=1.5;
g.beginPath();g.arc(piv.x,piv.y,12,0,7);g.stroke();
g.fillStyle='#ff9ada';g.fillText('pivot',piv.x-14,piv.y+28);
g.font='15px ui-monospace,Menlo,Consolas,monospace';
g.fillStyle='#cfe3ff';g.fillText('GRAHAM SCAN · CONVEX HULL · n = 40',22,32);
g.font='13px ui-monospace,Menlo,Consolas,monospace';
const label={sort:'sorting by polar angle',scan:'scanning',done:'hull complete'}[phase];
g.fillStyle='#7fa6d8';
g.fillText('phase: '+label,22,56);
g.fillText('step: '+steps+' stack: '+st.length+' pops: '+pops+' scanned: '+Math.min(i,P.length)+'/'+P.length,22,76);
if(phase==='done'){
g.fillStyle='#5ff0c0';
g.fillText('hull vertices: '+st.length+' — restarting with a fresh random set…',22,H-24);
}
}
function loop(){
tick++;
for(const f of flash)f.t-=0.022;
flash=flash.filter(f=>f.t>0);
if(phase==='done'){if(--hold<=0)reset();}
else if(tick%(phase==='sort'?2:7)===0)step();
draw();
requestAnimationFrame(loop);
}
reset();loop();
</script>