RUNS ✓quick DONE @488 cmp while bubble grinds at 1404
Tells the algorithmic story perfectly: finish states, pivot highlights, glowing done-panels. Textbook.
Three sorting algorithms — bubble, insertion, quick — racing side by side on the SAME shuffled 60-bar array. Animated, auto-starting, auto-restarting, live comparison counts, highlighted comparisons. NO user input.
Tells the algorithmic story perfectly: finish states, pivot highlights, glowing done-panels. Textbook.
Redemption after R3 — correct, brisk, minimal. Lacks finish-state drama.
Elegant generator architecture, slightly leisurely pacing on screen.
The only distinct layout of the round — but the hue-coded bars read noisy next to Sonnet's clarity.
Valid syntax, dead runtime — the shell renders, nothing sorts.
17.8 KB of ambition, one parse error, zero bars.
Same story, 22 KB heavier.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sorting Race — Bubble vs Insertion vs Quick</title>
<style>
:root{
--bg:#0b0f17;
--panel:#121826;
--edge:#1f2937;
--text:#e5e7eb;
--muted:#94a3b8;
--bar:#3b82f6;
--cmp:#f59e0b;
--swap:#ef4444;
--done:#22c55e;
--pivot:#a855f7;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
margin:0;
background:radial-gradient(1200px 700px at 50% -10%,#16203a 0%,var(--bg) 60%);
color:var(--text);
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
display:flex;flex-direction:column;min-height:100%;
}
header{
padding:18px 20px 6px;text-align:center;
}
h1{
margin:0;font-size:clamp(18px,2.4vw,26px);letter-spacing:.14em;text-transform:uppercase;font-weight:700;
}
.sub{margin:6px 0 0;color:var(--muted);font-size:12px;letter-spacing:.08em}
main{
flex:1;display:grid;gap:14px;padding:14px 16px 18px;
grid-template-columns:repeat(3,1fr);
}
@media (max-width:900px){ main{grid-template-columns:1fr} }
.lane{
background:linear-gradient(180deg,#131a29,#0f141f);
border:1px solid var(--edge);border-radius:14px;
display:flex;flex-direction:column;overflow:hidden;
box-shadow:0 10px 30px rgba(0,0,0,.35);
min-height:260px;
}
.lane.win{border-color:var(--done);box-shadow:0 0 0 1px var(--done),0 10px 30px rgba(34,197,94,.18)}
.lane-head{
display:flex;align-items:baseline;justify-content:space-between;gap:10px;
padding:10px 14px;border-bottom:1px solid var(--edge);background:rgba(255,255,255,.02);
}
.name{font-weight:700;letter-spacing:.12em;text-transform:uppercase;font-size:13px}
.name .dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:8px;vertical-align:middle}
.stats{display:flex;gap:14px;font-variant-numeric:tabular-nums;font-size:12px;color:var(--muted)}
.stats b{color:var(--text);font-weight:600}
.badge{font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--done);opacity:0;transition:opacity .25s}
.lane.done .badge{opacity:1}
.bars{
flex:1;display:flex;align-items:flex-end;gap:2px;padding:12px 12px 14px;
}
.bar{
flex:1;background:var(--bar);border-radius:2px 2px 0 0;
transition:height .06s linear,background-color .06s linear;
}
footer{
padding:0 20px 16px;text-align:center;color:var(--muted);font-size:11px;letter-spacing:.06em;
}
#round{color:var(--text);font-variant-numeric:tabular-nums}
</style>
</head>
<body>
<header>
<h1>Sorting Race</h1>
<p class="sub">Same shuffled 60-bar array · bubble vs insertion vs quick</p>
</header>
<main id="track"></main>
<footer>Round <span id="round">1</span> · auto-restarting</footer>
<script>
(() => {
const N = 60;
const ALGOS = [
{ key:'bubble', label:'Bubble Sort', color:'#3b82f6' },
{ key:'insertion', label:'Insertion Sort', color:'#14b8a6' },
{ key:'quick', label:'Quick Sort', color:'#a855f7' }
];
const CMP = '#f59e0b', SWAP = '#ef4444', DONE = '#22c55e', PIVOT = '#e879f9';
const STEPS_PER_FRAME = 90; // work per frame, shared by all lanes
const RESTART_DELAY = 2200;
const track = document.getElementById('track');
const roundEl = document.getElementById('round');
let round = 1, lanes = [], rafId = null, timerId = null;
// ---- generators: each yields after every comparison / swap ----
function* bubble(a){
const n = a.length;
for(let i=0;i<n-1;i++){
let swapped = false;
for(let j=0;j<n-1-i;j++){
yield { cmp:[j,j+1] };
if(a[j] > a[j+1]){
[a[j],a[j+1]] = [a[j+1],a[j]];
swapped = true;
yield { swap:[j,j+1] };
}
}
if(!swapped) return;
}
}
function* insertion(a){
const n = a.length;
for(let i=1;i<n;i++){
const v = a[i];
let j = i-1;
while(j >= 0){
yield { cmp:[j,j+1] };
if(a[j] <= v) break;
a[j+1] = a[j];
j--;
yield { swap:[j+1, j+2 <= n-1 ? j+2 : j+1] };
}
a[j+1] = v;
}
}
function* quick(a){
function* qs(lo,hi){
if(lo >= hi) return;
const pivot = a[hi];
let i = lo - 1;
for(let j=lo;j<hi;j++){
yield { cmp:[j,hi], pivot:hi };
if(a[j] <= pivot){
i++;
if(i !== j){
[a[i],a[j]] = [a[j],a[i]];
yield { swap:[i,j], pivot:hi };
}
}
}
i++;
if(i !== hi){
[a[i],a[hi]] = [a[hi],a[i]];
yield { swap:[i,hi], pivot:i };
}
yield* qs(lo, i-1);
yield* qs(i+1, hi);
}
yield* qs(0, a.length-1);
}
const RUNNERS = { bubble, insertion, quick };
// ---- lane construction ----
function buildLane(algo){
const el = document.createElement('section');
el.className = 'lane';
el.innerHTML = `
<div class="lane-head">
<span class="name"><span class="dot" style="background:${algo.color}"></span>${algo.label}</span>
<span class="stats">
<span>cmp <b class="c">0</b></span>
<span>swap <b class="s">0</b></span>
<span class="badge">done</span>
</span>
</div>
<div class="bars"></div>`;
track.appendChild(el);
const barsEl = el.querySelector('.bars');
const bars = [];
for(let i=0;i<N;i++){
const b = document.createElement('div');
b.className = 'bar';
barsEl.appendChild(b);
bars.push(b);
}
return {
algo, el, bars,
cEl: el.querySelector('.c'),
sEl: el.querySelector('.s'),
data:[], gen:null, comps:0, swaps:0,
finished:false, dirty:new Set(), marked:[]
};
}
function shuffled(){
const a = Array.from({length:N},(_,i)=>i+1);
for(let i=a.length-1;i>0;i--){
const j = Math.floor(Math.random()*(i+1));
[a[i],a[j]] = [a[j],a[i]];
}
return a;
}
function paint(lane, i){
const v = lane.data[i];
lane.bars[i].style.height = (v / N * 100) + '%';
}
function resetLane(lane, base){
lane.data = base.slice();
lane.gen = RUNNERS[lane.algo.key](lane.data);
lane.comps = 0; lane.swaps = 0;
lane.finished = false;
lane.marked = [];
lane.el.classList.remove('done','win');
lane.cEl.textContent = '0';
lane.sEl.textContent = '0';
for(let i=0;i<N;i++){
paint(lane, i);
lane.bars[i].style.backgroundColor = lane.algo.color;
}
}
function clearMarks(lane){
for(const i of lane.marked){
if(i >= 0 && i < N) lane.bars[i].style.backgroundColor = lane.algo.color;
}
lane.marked = [];
}
function stepLane(lane, budget){
if(lane.finished) return;
clearMarks(lane);
let last = null;
for(let k=0;k<budget;k++){
const r = lane.gen.next();
if(r.done){
lane.finished = true;
for(let i=0;i<N;i++){
paint(lane,i);
lane.bars[i].style.backgroundColor = DONE;
}
lane.el.classList.add('done');
lane.marked = [];
return;
}
const st = r.value;
if(st.cmp) lane.comps++;
if(st.swap) lane.swaps++;
last = st;
}
// repaint bars (cheap enough at N=60) and highlight the latest action
for(let i=0;i<N;i++) paint(lane,i);
lane.cEl.textContent = lane.comps.toLocaleString();
lane.sEl.textContent = lane.swaps.toLocaleString();
if(last){
const idx = last.cmp || last.swap || [];
const col = last.swap ? SWAP : CMP;
for(const i of idx){
if(i >= 0 && i < N){ lane.bars[i].style.backgroundColor = col; lane.marked.push(i); }
}
if(last.pivot !== undefined && last.pivot >= 0 && last.pivot < N){
lane.bars[last.pivot].style.backgroundColor = PIVOT;
lane.marked.push(last.pivot);
}
}
}
function tick(){
let allDone = true;
for(const lane of lanes){
if(!lane.finished){ stepLane(lane, STEPS_PER_FRAME); allDone = false; }
}
if(allDone){
rafId = null;
// crown the fewest comparisons
let best = lanes[0];
for(const l of lanes) if(l.comps < best.comps) best = l;
best.el.classList.add('win');
timerId = setTimeout(newRound, RESTART_DELAY);
return;
}
rafId = requestAnimationFrame(tick);
}
function newRound(){
if(rafId) cancelAnimationFrame(rafId);
if(timerId) clearTimeout(timerId);
rafId = null; timerId = null;
roundEl.textContent = round++;
const base = shuffled();
for(const lane of lanes) resetLane(lane, base);
rafId = requestAnimationFrame(tick);
}
// ---- boot ----
lanes = ALGOS.map(buildLane);
newRound();
})();
</script>
</body>
</html>