OverviewCode › R61
Code · blind round

R61 · Convex hull built step by step

blind round 3/8 local models answered

Answers below are anonymized and shuffled. Read, judge, pick a favourite — then scroll down and reveal the models.

The reveal also holds a frontier reference on the same task (Claude Opus 5) — kept out of sight here so it cannot steer your judgement.

Show task
Create ONE self-contained HTML file (STRICT LIMIT: max 120 lines total) that computes the convex hull of 40 random points with the Graham scan and animates the construction on a <canvas>. Draw all points, sort them, then walk the scan one step at a time: show the current candidate segment, and visibly POP points again when a right turn is detected. Draw the finished hull as a closed polygon and show a running step counter. Smooth requestAnimationFrame animation, NO user input, starts immediately and keeps running (restart with a fresh random instance when finished). No external libraries, no comments needed. Output ONLY the HTML file content, nothing else.

The answers — blind

Local models (Brain cluster) — identical prompt, anonymized order
MODEL Aqwen3:14bSTATLIMFORMAT
Show code
```html
<!DOCTYPE html>
<html>
<head>
<title>Graham Scan Animation</title>
<style>body {margin:0;overflow:hidden;}#canvas {display:block;background-color:#111;}</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
let W, H;

function resize() {W=window.innerWidth;H=window.innerHeight;canvas.width=W;canvas.height=H;}
resize();window.addEventListener('resize', resize);

let points = [];
let P0 = null;
let sortedPoints = [];
let hullStack = [];
let step = 0;
let running = false;
const N_POINTS = 40;

function generatePoints() {
    points = [];
    for (let i = 0; i < N_POINTS; i++) {
        points.push({x: Math.random() * W, y: Math.random() * H});
    }
}

// Cross product / Orientation check
function orientation(p, q, r) {
    const val = (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
    if (Math.abs(val) < 0.1) return 0; // Collinear
    return (val > 0) ? 1 : 2; // 1: Left turn, 2: Right turn
}

// Distance squared helper for sorting tie-breaker
function distSq(p1, p2) {
    return (p1.x - p2.x)**2 + (p1.y - p2.y)**2;
}

// Comparison function for angle sort around P0
function compareAngles(a, b) {
    const o = orientation(P0, a, b);
    if (o == 2) return 1; // b is left of vector P0->a (counter-clockwise order)
    if (o == 1) return -1; // b is right of vector P0->a
    return 0; // Collinear, use distance tie breaker
}

function initializeGrahamScan() {
    P0 = points[0];
    points.sort(compareAngles);
    sortedPoints = points;
    hullStack = [];
    step = 0;
    running = true;
}

// Core logic step: processes the next point if possible
function runStep() {
    if (!running) return;

    if (sortedPoints.length === 0) {
        running = false;
        return;
    }

    if (hullStack.length < 2) {
        hullStack.push(sortedPoints[step]);
        step++;
        return;
    }

    const p_next = sortedPoints[step];
    let top1 = hullStack[hullStack.length - 2];
    let top2 = hullStack[hullStack.length - 1];

    if (orientation(top1, top2, p_next) == 2) { // Right turn detected
        // Pop operation: This point failed the left-turn condition
        hullStack.pop();
    }
    
    hullStack.push(p_next);
    step++;

    if (step >= sortedPoints.length && hullStack.length > 1) {
        // Final closure check/cleanup can happen here if needed, but for visualization, just stop the process loop
        running = false;
    }
}

function draw() {
    ctx.clearRect(0, 0, W, H);

    // 1. Draw all points
    ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
    points.forEach(p => ctx.beginPath());
    points.forEach(p => ctx.arc(p.x, p.y, 3, 0, Math.PI * 2));
    points.forEach(p => ctx.fill());

    // 2. Draw the current state (Hull Stack and Candidate Segment)
    ctx.strokeStyle = '#8affff';
    ctx.lineWidth = 2;
    if (hullStack.length >= 2) {
        for (let i = 0; i < hullStack.length - 1; i++) {
            ctx.beginPath();
            ctx.moveTo(hullStack[i].x, hullStack[i].y);
            ctx.lineTo(hullStack[i+1].x, hullStack[i+1].y);
            ctx.stroke();
        }
    }

    // 3. Draw the candidate segment (last point added vs next point)
    if (sortedPoints[step] && hullStack.length >= 2) {
         const p_next = sortedPoints[step];
         let top1 = hullStack[hullStack.length - 2];
         let top2 = hullStack[hullStack.length - 1];

         if (orientation(top1, top2, p_next) == 2) { // Right turn detected on the next step check
             ctx.strokeStyle = '#ff6b6b'; // Red for failure/pop
             ctx.beginPath();
             ctx.moveTo(top2.x, top2.y);
             ctx.lineTo(p_next.x, p_next.y);
             ctx.stroke();
         } else {
            ctx.strokeStyle = '#ffff8a'; // Yellow for successful check
            ctx.beginPath();
            ctx.moveTo(top2.x, top2.y);
            ctx.lineTo(p_next.x, p_next.y);
            ctx.stroke();
         }
    }

    // 4. Draw the final hull (closed loop)
    if (hullStack.length >= 3 && !running) {
        ctx.beginPath();
        ctx.fillStyle = 'rgba(70, 255, 100, 0.8)';
        ctx.moveTo(hullStack[0].x, hullStack[0].y);
        for (let i = 1; i < hullStack.length; i++) {
            ctx.lineTo(hullStack[i].x, hullStack[i].y);
        }
        ctx.closePath();
        ctx.fill();
    }

    // 5. Draw UI/Status
    ctx.fillStyle = '#fff';
    ctx.font = '16px monospace';
    ctx.fillText(`Step: ${step}/${N_POINTS} | Hull Size: ${hullStack.length}`, 20, 30);
}

function loop() {
    draw();
    if (running) {
        runStep();
        requestAnimationFrame(loop);
    } else if (!window.__finished__) {
        // Animation finished or needs restart
        setTimeout(() => {
            initializeGrahamScan();
            loop();
            window.__finished__ = true; // Flag to prevent continuous re-triggering on interval
        }, 1000); // Wait a second before restarting the loop
    }
}

// --- Initialization Sequence ---
generatePoints();
initializeGrahamScan();
requestAnimationFrame(loop);

</script>
</body>
</html>
```
MODEL Bqwen3:8bDNF
No usable answer reached the harness (ERR). Counted as a did-not-finish.
No usable answer reached the harness (TIMEOUT). Counted as a did-not-finish.
MODEL Dqwen3-coder:30bERRBLANKLIMFORMAT
Show code
```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Graham Scan Animation</title>
    <style>body {background: #111; color: white; font-family: sans-serif;} canvas {border: 1px solid #333; background: #000;}#info {margin-top: 10px;}</style>
</head>
<body>
    <canvas id="hullCanvas" width="800" height="600"></canvas>
    <div id="info">Step: <span id="stepCounter">0</span>/40</div>

<script>
    const canvas = document.getElementById('hullCanvas');
    const ctx = canvas.getContext('2d');
    const POINTS_COUNT = 40;
    let points = [];
    let sortedPoints = [];
    let hullStack = [];
    let stepCounter = 0;

    function generateRandomPoints() {
        points = [];
        for (let i = 0; i < POINTS_COUNT; i++) {
            points.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height });
        }
        stepCounter = 0;
    }

    function crossProduct(p1, p2, p3) {
        return (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x);
    }

    function distSq(p1, p2) {
        return Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2);
    }

    function comparePoints(a, b) {
        const cp = crossProduct(points[0], a, b);
        if (cp > 0) return -1; // CCW -> smaller angle/comes first
        if (cp < 0) return 1;  // CW -> larger angle/comes later
        return distSq(points[0], a) <= distSq(points[0], b) ? -1 : 1; // Collinear: closer first
    }

    function initializeScan() {
        if (points.length < 3) return true;

        // 1. Find Pivot (P0): Lowest Y, then leftmost X
        let p0 = points[0];
        let p0Index = 0;
        for(let i=1; i<points.length; i++){
            if(points[i].y < p0.y || (points[i].y === p0.y && points[i].x < p0.x)){
                p0 = points[i];
                p0Index = i;
            }
        }

        // Swap P0 to the start for ease of use
        [points[0], points[p0Index]] = [points[p0Index], points[0]];
        const pivot = p0;

        // 2. Sort remaining points by angle relative to P0
        sortedPoints = points.slice(1).sort(comparePoints);
        sortedPoints.unshift(pivot); // Add pivot back at start

        hullStack = [];
        if (sortedPoints.length > 0) {
            hullStack.push(sortedPoints[0], sortedPoints[1]);
        }
        return true;
    }

    function draw() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        
        // Draw all points
        points.forEach(p => { ctx.fillStyle = '#555'; ctx.beginPath(); ctx.arc(p.x, p.y, 3, 0, Math.PI * 2); ctx.fill(); });

        if (hullStack.length < 2) return;
        
        // Draw current candidate segment (last two points)
        ctx.strokeStyle = '#FFD700';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(hullStack[hullStack.length - 2].x, hullStack[hullStack.length - 2].y);
        ctx.lineTo(hullStack[hullStack.length - 1].x, hullStack[hullStack.length - 1].y);
        ctx.stroke();

        // Draw the current partial hull
        ctx.strokeStyle = '#00FFFF';
        ctx.lineWidth = 3;
        ctx.beginPath();
        for (let i = 0; i < hullStack.length - 1; i++) {
            let p = hullStack[i];
            if (i === 0) ctx.moveTo(p.x, p.y);
            else ctx.lineTo(p.x, p.y);
        }
        ctx.stroke();

        document.getElementById('stepCounter').textContent = stepCounter;
    }

    function scanStep() {
        if (hullStack.length < 2) return false;

        let p3 = sortedPoints[stepCounter]; // The point being considered next

        // Check for right turn: crossProduct(p_i-1, p_i, p_next) <= 0
        while (hullStack.length >= 2 && crossProduct(hullStack[hullStack.length - 2], hullStack[hullStack.length - 1], p3) <= 0) {
            // Right turn or collinear detected: POP the last point
            const poppedPoint = hullStack.pop();

            // Redraw to show pop visually (optional, but good practice)
            draw(); 
            currentSegment = `${hullStack[hullStack.length - 2].x},${hullStack[hullStack.length - 2].y} -> ${p3.x},${p3.y}`;

            // Increment step count for the POP action
            document.getElementById('stepCounter').textContent = Math.max(0, stepCounter) + 1;
        }

        if (crossProduct(hullStack[hullStack.length - 2], hullStack[hullStack.length - 1], p3) > 0) {
             // Left turn: Push the new point onto the stack
            hullStack.push(p3);
            draw();
        }

        stepCounter++;

        if (stepCounter < sortedPoints.length - 1) {
           requestAnimationFrame(scanStep);
        } else {
            // Finalization step: Close the polygon and reset/loop
            setTimeout(() => {
                ctx.strokeStyle = '#008000'; // Green for final hull
                ctx.lineWidth = 4;
                let path = new Path2D();
                for(let i=0; i<hullStack.length; i++){
                    path.lineTo(hullStack[i].x, hullStack[i].y);
                }
                path.closePath();
                ctx.stroke(path);

                // Loop/Reset after a delay
                setTimeout(() => {
                    generateRandomPoints();
                    if (initializeScan()) {
                        draw(); // Draw initial state
                        requestAnimationFrame(scanStep);
                    }
                }, 1500);
            }, 200);
        }
    }

    // --- Initialization ---
    window.onload = function() {
        generateRandomPoints();
        if (initializeScan()) {
             draw(); // Draw initial points and setup
             requestAnimationFrame(scanStep);
        } else {
            document.getElementById('info').textContent = "Need at least 3 points.";
        }
    };

</script>
</body>
</html>
```
MODEL Eqwen3.5:9bERRBLANKLIM
Show code
<!DOCTYPE html>
<html>
<head>
<title>Graham Scan Animation</title>
<style>body {margin:0;overflow:hidden;}canvas {display:block;background:#111;}</style>
</head>
<body>
<canvas id="c"></canvas><script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let W, H;

function initialize() {
    W = canvas.width = 800;
    H = canvas.height = 600;
    if (window.innerWidth < 800 || window.innerHeight < 600) {
        canvas.style.width = `${Math.min(800, window.innerWidth)}px`;
        canvas.style.height = `${Math.min(600, window.innerHeight)}px`;
    }
    points = [];
    for (let i = 0; i < 40; i++) {
        points.push({x: Math.random() * W, y: Math.random() * H});
    }
    hullStack = [];
    stepCounter = 0;
    scanIndex = 0;
    animationRunning = true;
    drawPoints();
    animate();
}

function crossProduct(p1, p2, p3) {
    return (p2.x - p1.x) * (p3.y - p1.y) - (p2.y - p1.y) * (p3.x - p1.x);
}

function distSq(p1, p2) {
    return Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2);
}

let points;
let hullStack = [];
let stepCounter = 0;
let scanIndex = 0;
let animationRunning = true;

function findPivotAndSort() {
    points.sort((a, b) => a.x - b.x);
    const p0 = points[0];
    const remainingPoints = points.slice(1);
    remainingPoints.sort((a, b) => {
        let cp = crossProduct(p0, a, b);
        if (cp > 0) return -1;
        if (cp < 0) return 1;
        return 0; // Collinear: closer point first
    });
    points = [p0, ...remainingPoints];
}

function drawScene(currentCandidate = null, poppedPoint = null) {
    ctx.clearRect(0, 0, W, H);
    
    // 1. Draw all initial points
    ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
    points.forEach(p => {
        ctx.beginPath();
        ctx.arc(p.x, p.y, 2, 0, Math.PI * 2);
        ctx.fill();
    });

    // 2. Draw the current hull stack (candidate)
    if (hullStack.length > 1) {
        ctx.strokeStyle = '#4CAF50';
        ctx.lineWidth = 2;
        ctx.beginPath();
        for(let i=0; i < hullStack.length; i++) {
            const p = hullStack[i];
            if (i === 0) ctx.moveTo(p.x, p.y);
            else ctx.lineTo(p.x, p.y);
        }
        ctx.stroke();
    }

    // 3. Draw the current segment being checked (H[-2] -> H[-1] -> P_i)
    if (currentCandidate && hullStack.length >= 2) {
        const A = hullStack[hullStack.length - 2];
        const B = hullStack[hullStack.length - 1];
        const C = currentCandidate;

        ctx.strokeStyle = '#FFD700';
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.moveTo(A.x, A.y);
        ctx.lineTo(B.x, B.y);
        ctx.lineTo(C.x, C.y);
        ctx.stroke();
    }

    // 4. Draw the popped point (if applicable)
    if (poppedPoint && !poppedPoint.isInitial) {
         ctx.fillStyle = 'red';
         ctx.beginPath();
         ctx.arc(poppedPoint.x, poppedPoint.y, 3, 0, Math.PI * 2);
         ctx.fill();
    }

    // 5. Draw step counter/status
    ctx.fillStyle = '#FFF';
    ctx.font = '16px Arial';
    ctx.fillText(`Step: ${stepCounter} | Points Left: ${scanIndex < points.length ? (points.length - scanIndex) : 0}`, 10, 25);
}

function grahamScan() {
    if (points.length < 3) return;

    hullStack = [points[0], points[1]]; // Initial stack with first two points

    for (let i = 2; i < points.length; i++) {
        const nextPoint = points[i];
        let popped = null;

        while (hullStack.length >= 2) {
            const p2 = hullStack[hullStack.length - 1];
            const p1 = hullStack[hullStack.length - 2];
            const cp = crossProduct(p1, p2, nextPoint);

            if (cp < 0) { // Right turn detected (POP required)
                popped = p2;
                hullStack.pop();
            } else if (Math.abs(cp) < 1e6) { // Collinear: Keep only the furthest point (already handled by sort/scan structure, but good safeguard)
                 if (distSq(p1, nextPoint) > distSq(p1, p2)) {
                     hullStack.pop();
                     // Note: A full implementation would replace p2 with nextPoint here if collinear and further away.
                 } else {
                    break; 
                 }
            } else { // Left turn (CCW) or straight path
                break;
            }
        }
        hullStack.push(nextPoint);
    }
    return hullStack;
}

function animate() {
    if (!animationRunning) return;

    drawScene();

    if (scanIndex < points.length - 1) {
        const p_i = points[scanIndex + 1];
        stepCounter++;
        
        let poppedPoint = null;
        let currentCandidate = p_i;

        while (hullStack.length >= 2) {
            const p2 = hullStack[hullStack.length - 1];
            const p1 = hullStack[hullStack.length - 2];
            const cp = crossProduct(p1, p2, p_i);

            if (cp < 0) { // Right turn
                poppedPoint = p2;
                hullStack.pop();
            } else if (Math.abs(cp) < 1e6 && hullStack.length > 0) {
                 // Collinear case handling (optional, but safe)
                 if (distSq(p1, p_i) > distSq(p1, p2)) {
                    poppedPoint = p2;
                    hullStack.pop();
                 } else {
                     break;
                 }
            } else { 
                break; // Left turn or collinear and further out (CCW maintained)
            }
        }

        // Update state for next step
        if (!poppedPoint || crossProduct(hullStack[hullStack.length - 2], hullStack[hullStack.length - 1], p_i) >= 0) {
             hullStack.push(p_i);
        } else if (poppedPoint) {
            // If a pop occurred, the stack already reflects it, and we push p_i.
            hullStack.push(p_i);
        }


        scanIndex++;

    } else {
        animationRunning = false;
        drawScene(); // Final draw
        
        ctx.strokeStyle = '#00FFFF';
        ctx.lineWidth = 3;
        ctx.beginPath();
        for (let i = 0; i < hullStack.length; i++) {
            const p = hullStack[i];
            if (i === 0) ctx.moveTo(p.x, p.y);
            else ctx.lineTo(p.x, p.y);
        }
        ctx.closePath(); // Close the loop for visual completion
        ctx.stroke();

        setTimeout(() => {
            initialize(); // Restart the process
        }, 1500);
    }

    requestAnimationFrame(animate);
}

window.onload = initialize;
</script>
</body>
</html>
MODEL Fgemma4:26bDNF
No usable answer reached the harness (TIMEOUT). Counted as a did-not-finish.
MODEL Gllama3.1:8bDNF
No usable answer reached the harness (TIMEOUT). Counted as a did-not-finish.
MODEL Hdeepseek-r1:14bDNF
No usable answer reached the harness (TIMEOUT). Counted as a did-not-finish.
Reveal the models— compare the answers first, then open. Names, blind scores and latencies are hidden until you do.

Who wrote what

Blind labelModelBlind scoreLatencyStatus
MODEL A qwen3:14b 36.1 sOK
MODEL B qwen3:8b DNF
MODEL C mistral-small:24b DNF
MODEL D qwen3-coder:30b 26.8 sOK
MODEL E qwen3.5:9b 60.4 sOK
MODEL F gemma4:26b DNF
MODEL G llama3.1:8b DNF
MODEL H deepseek-r1:14b DNF

Frontier reference — outside the blind field

These models answered the identical prompt outside the blind field. They are deliberately not scored and not part of the blind comparison — they are there so you can see where a frontier model lands on the same task.

Collected differently from the blind field: the local models run through a plain API harness, the frontier references through a context-free agent, one shot, no retries. Response times are therefore not comparable and are not shown. On two English tasks the reference answer came back in German — an artefact of that harness, flagged on the affected rounds.
Claude Opus 5frontier reference
<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>