0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
Bubble, insertion, merge and quick sort race side by side on the same shuffled array, each lane a live field of bars. Every lane is handed an identical number of array operations per second, so the finish order isn't a stopwatch artifact — it is literally who needed less work, and the mono counters show exactly how much less. Big-O usually arrives as a table to memorise; here it arrives as merge sort finishing while bubble sort is still grinding through its tail. Try Nearly sorted first: both "slow" O(n²) algorithms beat both O(n log n) ones outright, then Reversed at 160 values opens the gap back up to 25×.
Source — the code you see is the code that just ran 937 lines 37.2 KB index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Sorting Algorithm Race — sloprun.dev</title>
<style>
/* ── sloprun design tokens (verbatim from demos/tokens.css) ───────────────── */
:root {
--bg: #F6F6F3; /* neutral paper, not cream */
--surface: #FFFFFF;
--ink: #1A1D21;
--muted: #5A6068;
--line: #E3E2DC;
--accent: #C05600; /* phosphor amber, darkened for light bg */
--accent-soft: #FFF3E6;
--run: #1A7F37; /* earned green */
--run-soft: #E7F4EA;
--danger: #C93C37;
--radius: 10px;
--font-sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
}
:root[data-theme="dark"] {
--bg: #14161A; --surface: #1C1F26; --ink: #E8E6E1; --muted: #9AA0A8;
--line: #2A2E36; --accent: #FFB454; --accent-soft: #2A2118;
--run: #3FB950; --run-soft: #16281B; --danger: #F47067;
}
body { background: var(--bg); color: var(--ink); font-family: var(--font-sans); }
/* ── end tokens ───────────────────────────────────────────────────────────── */
/* demo-local additions (shadows need alpha; tokens are opaque hex;
color-scheme keeps UA chrome — scrollbars, native range parts — in theme) */
:root { --shadow: 0 1px 2px rgba(26,29,33,.05); color-scheme: light; }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --shadow: 0 1px 2px rgba(0,0,0,.35); color-scheme: dark; }
}
:root[data-theme="dark"] { --shadow: 0 1px 2px rgba(0,0,0,.35); color-scheme: dark; }
/* text-on-soft-chip variants: raw --accent on --accent-soft is 4.2:1 and raw
--run on --run-soft is 4.5:1 in light — both under AA for the 10–13px labels
that use them. Nudging toward --ink lifts light to 5.4:1 and leaves dark
(already 6–9:1) comfortably above. Falls back to the raw token if unsupported. */
:root { --accent-ink: var(--accent); --run-ink: var(--run); }
@supports (color: color-mix(in srgb, red 50%, blue)) {
:root {
--accent-ink: color-mix(in srgb, var(--accent) 82%, var(--ink));
--run-ink: color-mix(in srgb, var(--run) 86%, var(--ink));
}
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; }
body { min-height: 100%; overflow-x: hidden; }
.wrap {
max-width: 1180px; margin: 0 auto;
padding: 22px 16px 26px;
display: flex; flex-direction: column; gap: 16px;
}
h1 { margin: 0 0 6px; font-size: clamp(21px, 3.6vw, 29px); letter-spacing: -.021em; line-height: 1.15; }
.lede { margin: 0; max-width: 72ch; color: var(--muted); font-size: 14.5px; line-height: 1.55; }
.lede b { color: var(--ink); font-weight: 600; }
/* layout */
.stage { display: grid; grid-template-columns: minmax(0,1fr) 300px; gap: 16px; align-items: start; }
@media (max-width: 940px) { .stage { grid-template-columns: minmax(0,1fr); } }
.left { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
.card {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 12px 13px; box-shadow: var(--shadow);
}
.card h2 {
margin: 0 0 10px; font-family: var(--font-mono); font-size: 10.5px;
letter-spacing: .1em; text-transform: uppercase; color: var(--muted); font-weight: 600;
}
/* banner */
.banner {
display: grid; grid-template-columns: minmax(0,1fr) auto; align-items: center; gap: 9px 14px;
padding: 11px 12px;
border: 1px solid var(--line); border-radius: var(--radius);
background: var(--surface); box-shadow: var(--shadow);
transition: border-color .2s ease, background-color .2s ease;
}
.banner.won { background: var(--run-soft); border-color: transparent; }
.headline { display: flex; align-items: center; gap: 9px; min-width: 0; }
.headline .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
.banner.live .headline .dot { background: var(--accent); }
.banner.won .headline .dot { background: var(--run); }
.headline .txt { font-size: 14.5px; line-height: 1.3; }
.headline .txt b { font-weight: 600; }
.banner.won .headline .txt { color: var(--run-ink); font-weight: 600; }
.bctl { display: flex; gap: 7px; }
.standings { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: 6px; list-style: none; margin: 0; padding: 0; }
.standings:empty { display: none; }
.standings li {
display: inline-flex; align-items: baseline; gap: 6px;
border: 1px solid var(--line); border-radius: 999px; padding: 3px 10px;
background: var(--bg); font-size: 12px;
}
.banner.won .standings li { background: var(--surface); }
.standings .r {
font-family: var(--font-mono); font-size: 10px; color: var(--muted);
font-variant-numeric: tabular-nums;
}
.standings li.first .r, .standings li.first .v { color: var(--run-ink); }
.standings .nm { font-size: 12px; }
.standings .v { font-family: var(--font-mono); font-size: 11.5px; font-variant-numeric: tabular-nums; color: var(--muted); }
/* race grid */
.race { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 12px; }
@media (max-width: 620px) {
.race { grid-template-columns: minmax(0,1fr); }
.plot { aspect-ratio: 2.9 / 1; }
}
@media (max-width: 520px) {
.banner { grid-template-columns: minmax(0,1fr); }
.bctl > button { flex: 1 1 auto; }
}
.lane {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--radius); padding: 11px 12px 10px; box-shadow: var(--shadow);
min-width: 0; display: flex; flex-direction: column; gap: 9px;
transition: border-color .2s ease;
}
.lane.won { border-color: var(--run); }
.lane-top { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.lane-top h2 {
margin: 0; font-family: var(--font-sans); font-size: 14.5px; font-weight: 600;
letter-spacing: -.01em; text-transform: none; color: var(--ink);
}
.lane-top .big {
font-family: var(--font-mono); font-size: 10.5px; color: var(--muted);
letter-spacing: .02em;
}
.badge {
margin-left: auto; font-family: var(--font-mono); font-size: 10px;
letter-spacing: .07em; text-transform: uppercase;
padding: 2px 8px; border-radius: 999px;
border: 1px solid var(--line); color: var(--muted); background: var(--bg); white-space: nowrap;
}
.badge.live { color: var(--accent-ink); border-color: transparent; background: var(--accent-soft); }
.badge.ok { color: var(--run-ink); border-color: transparent; background: var(--run-soft); }
.badge.bad { color: var(--danger); border-color: transparent; }
.plot {
position: relative; border: 1px solid var(--line); border-radius: 7px;
background: var(--bg); overflow: hidden; aspect-ratio: 2.5 / 1; min-height: 104px;
}
canvas { display: block; width: 100%; height: 100%; }
.lane-stats { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 8px; margin: 0; }
.lane-stats > div { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.lane-stats dt {
font-family: var(--font-mono); font-size: 9px; letter-spacing: .09em;
text-transform: uppercase; color: var(--muted);
}
.lane-stats dd {
margin: 0; font-family: var(--font-mono); font-size: 15px; line-height: 1.15;
font-variant-numeric: tabular-nums; letter-spacing: -.02em;
}
.lane.ok .lane-stats dd.total { color: var(--run); }
/* panel */
.panel { display: flex; flex-direction: column; gap: 12px; }
button {
font: inherit; font-size: 13px; font-family: var(--font-sans);
color: var(--ink); background: var(--surface);
border: 1px solid var(--line); border-radius: 8px;
padding: 7px 11px; cursor: pointer; line-height: 1.2;
transition: border-color .12s ease, color .12s ease, background-color .12s ease;
}
button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button.primary { background: var(--accent); color: var(--surface); border-color: var(--accent); font-weight: 600; }
button.primary:hover:not(:disabled) { color: var(--surface); filter: brightness(1.07); }
.row { display: flex; flex-wrap: wrap; gap: 7px; }
.row > button { flex: 1 1 auto; }
.shapes { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 7px; }
.shapes button[aria-pressed="true"] {
background: var(--accent-soft); color: var(--accent-ink); border-color: var(--accent);
}
.ctl { display: block; margin-bottom: 13px; }
.ctl:last-child { margin-bottom: 2px; }
.ctl-top { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 3px; }
.ctl-top label { font-size: 13px; }
.ctl-top .val { font-family: var(--font-mono); font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.ctl .desc { display: block; font-size: 11.5px; color: var(--muted); line-height: 1.35; margin-top: 2px; }
input[type=range] {
-webkit-appearance: none; appearance: none; display: block;
width: 100%; height: 24px; margin: 0; background: transparent; cursor: pointer;
}
input[type=range]::-webkit-slider-runnable-track { height: 4px; border-radius: 2px; background: var(--line); }
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; width: 15px; height: 15px; margin-top: -5.5px;
border-radius: 50%; background: var(--accent); border: 2px solid var(--surface);
box-shadow: 0 0 0 1px var(--line);
}
input[type=range]::-moz-range-track { height: 4px; border-radius: 2px; background: var(--line); }
input[type=range]::-moz-range-thumb {
width: 13px; height: 13px; border-radius: 50%; background: var(--accent);
border: 2px solid var(--surface); box-shadow: 0 0 0 1px var(--line);
}
input[type=range]:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
.legend { display: flex; flex-direction: column; gap: 6px; }
.legend div { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--muted); }
.legend .sw { width: 11px; height: 11px; border-radius: 3px; flex: none; }
.note { margin: 10px 0 0; font-size: 11.5px; line-height: 1.45; color: var(--muted); }
.note b { color: var(--ink); font-weight: 600; }
footer { font-family: var(--font-mono); font-size: 11px; color: var(--muted); letter-spacing: .05em; padding-top: 2px; }
.sr-only {
position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition-duration: .001ms !important; animation-duration: .001ms !important; }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<h1>Sorting Algorithm Race</h1>
<p class="lede">
Four sorting algorithms, one shuffled array, all four running at once. Every lane gets the
<b>same number of array operations per second</b>, so the finish order is simply who needs
less work. Try <b>Nearly sorted</b> — the two "slow" algorithms suddenly win.
</p>
</header>
<main class="stage">
<div class="left">
<div class="banner" id="banner">
<div class="headline">
<span class="dot" aria-hidden="true"></span>
<span class="txt" id="headline">Shuffling…</span>
</div>
<div class="bctl">
<button type="button" class="primary" id="playBtn">Start race</button>
<button type="button" id="resetBtn">New shuffle</button>
</div>
<ol class="standings" id="standings" role="list" aria-label="Finish order"></ol>
</div>
<div class="race" id="race">
<section class="lane" data-key="bubble">
<div class="lane-top">
<h2>Bubble sort</h2>
<span class="big">O(n²)</span>
<span class="badge" data-badge>ready</span>
</div>
<div class="plot"><canvas data-canvas role="img" aria-label="Bar chart of the array while bubble sort runs"></canvas></div>
<dl class="lane-stats">
<div><dt>compares</dt><dd data-c>0</dd></div>
<div><dt>writes</dt><dd data-w>0</dd></div>
<div><dt>total ops</dt><dd class="total" data-o>0</dd></div>
</dl>
</section>
<section class="lane" data-key="insertion">
<div class="lane-top">
<h2>Insertion sort</h2>
<span class="big">O(n²)</span>
<span class="badge" data-badge>ready</span>
</div>
<div class="plot"><canvas data-canvas role="img" aria-label="Bar chart of the array while insertion sort runs"></canvas></div>
<dl class="lane-stats">
<div><dt>compares</dt><dd data-c>0</dd></div>
<div><dt>writes</dt><dd data-w>0</dd></div>
<div><dt>total ops</dt><dd class="total" data-o>0</dd></div>
</dl>
</section>
<section class="lane" data-key="merge">
<div class="lane-top">
<h2>Merge sort</h2>
<span class="big">O(n log n)</span>
<span class="badge" data-badge>ready</span>
</div>
<div class="plot"><canvas data-canvas role="img" aria-label="Bar chart of the array while merge sort runs"></canvas></div>
<dl class="lane-stats">
<div><dt>compares</dt><dd data-c>0</dd></div>
<div><dt>writes</dt><dd data-w>0</dd></div>
<div><dt>total ops</dt><dd class="total" data-o>0</dd></div>
</dl>
</section>
<section class="lane" data-key="quick">
<div class="lane-top">
<h2>Quick sort</h2>
<span class="big">O(n log n)</span>
<span class="badge" data-badge>ready</span>
</div>
<div class="plot"><canvas data-canvas role="img" aria-label="Bar chart of the array while quick sort runs"></canvas></div>
<dl class="lane-stats">
<div><dt>compares</dt><dd data-c>0</dd></div>
<div><dt>writes</dt><dd data-w>0</dd></div>
<div><dt>total ops</dt><dd class="total" data-o>0</dd></div>
</dl>
</section>
</div>
</div>
<div class="panel">
<section class="card" aria-label="Starting array">
<h2>Starting array</h2>
<div class="shapes" id="shapes" role="group" aria-label="Shape of the starting array">
<button type="button" data-shape="random" aria-pressed="true">Shuffled</button>
<button type="button" data-shape="nearly" aria-pressed="false">Nearly sorted</button>
<button type="button" data-shape="reversed" aria-pressed="false">Reversed</button>
<button type="button" data-shape="few" aria-pressed="false">Few values</button>
</div>
<div class="ctl" style="margin-top:13px">
<div class="ctl-top">
<label for="size">Array size</label><span class="val" id="sizeV" aria-hidden="true">88</span>
</div>
<input type="range" id="size" min="16" max="160" step="4" value="88" aria-valuetext="88 values">
</div>
<div class="ctl">
<div class="ctl-top">
<label for="speed">Speed</label><span class="val" id="speedV" aria-hidden="true">3,177 ops/s</span>
</div>
<input type="range" id="speed" min="0" max="100" step="1" value="45" aria-valuetext="3,177 operations per second">
<span class="desc">Operations handed to every lane each second — identical for all four.</span>
</div>
</section>
<section class="card" aria-label="Colour key">
<h2>Key</h2>
<div class="legend">
<div><span class="sw" id="swCmp"></span> comparing two values</div>
<div><span class="sw" id="swWr"></span> writing a value</div>
<div><span class="sw" id="swOk"></span> finished, checked sorted</div>
</div>
<p class="note">Every lane starts from the same array. <b>Total ops</b> counts each comparison plus each element written — a swap is two writes.</p>
</section>
</div>
</main>
<footer>demo · sloprun.dev</footer>
</div>
<p class="sr-only" id="live" role="status" aria-live="polite"></p>
<script>
(function () {
"use strict";
/* ── theme plumbing ─────────────────────────────────────────────────────── */
var root = document.documentElement;
var mqDark = window.matchMedia ? matchMedia("(prefers-color-scheme: dark)") : null;
var mqRM = window.matchMedia ? matchMedia("(prefers-reduced-motion: reduce)") : null;
var reduced = mqRM ? mqRM.matches : false;
window.addEventListener("message", function (e) {
var d = e.data;
if (d && d.type === "sloprun:theme" && (d.theme === "light" || d.theme === "dark")) {
root.setAttribute("data-theme", d.theme);
readTheme(); drawAll();
}
});
if (mqDark) {
var onScheme = function () { readTheme(); drawAll(); };
if (mqDark.addEventListener) mqDark.addEventListener("change", onScheme);
else if (mqDark.addListener) mqDark.addListener(onScheme);
}
/* ── colour helpers ─────────────────────────────────────────────────────── */
function cssv(name) {
var v = getComputedStyle(root).getPropertyValue(name);
return v ? v.trim() : "";
}
function hexRGB(s, fallback) {
if (!s || s.charAt(0) !== "#") return fallback;
s = s.slice(1);
if (s.length === 3) s = s.charAt(0)+s.charAt(0)+s.charAt(1)+s.charAt(1)+s.charAt(2)+s.charAt(2);
if (s.length !== 6) return fallback;
var n = parseInt(s, 16);
if (isNaN(n)) return fallback;
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
function mix(a, b, t) {
return [Math.round(a[0]+(b[0]-a[0])*t), Math.round(a[1]+(b[1]-a[1])*t), Math.round(a[2]+(b[2]-a[2])*t)];
}
function rgb(c) { return "rgb(" + c[0] + "," + c[1] + "," + c[2] + ")"; }
var TH = {}, PAL_RUN = [], PAL_IDLE = [], STEPS = 48;
function readTheme() {
TH.bg = hexRGB(cssv("--bg"), [246,246,243]);
TH.surface = hexRGB(cssv("--surface"), [255,255,255]);
TH.ink = hexRGB(cssv("--ink"), [26,29,33]);
TH.muted = hexRGB(cssv("--muted"), [90,96,104]);
TH.line = hexRGB(cssv("--line"), [227,226,220]);
TH.accent = hexRGB(cssv("--accent"), [192,86,0]);
TH.run = hexRGB(cssv("--run"), [26,127,55]);
TH.inkS = rgb(TH.ink);
TH.accentS = rgb(TH.accent);
/* highlight column: --accent-soft is a chip colour for --surface and is
invisible on --bg (1.01:1 in light), so tint the plot ground directly */
TH.hiliteS = rgb(mix(TH.accent, TH.bg, 0.78));
TH.plotS = rgb(TH.bg);
TH.lineS = rgb(TH.line);
TH.runS = rgb(TH.run);
var idleLo = mix(TH.muted, TH.bg, 0.28); /* >=3:1 vs plot ground, both themes */
var idleHi = mix(TH.accent, TH.muted, 0.42);
var runLo = mix(TH.run, TH.bg, 0.22); /* >=3:1 vs plot ground, both themes */
PAL_IDLE = []; PAL_RUN = [];
for (var i = 0; i < STEPS; i++) {
var t = i / (STEPS - 1);
PAL_IDLE.push(rgb(mix(idleLo, idleHi, t)));
PAL_RUN.push(rgb(mix(runLo, TH.run, t)));
}
var sc = document.getElementById("swCmp"), sw = document.getElementById("swWr"), so = document.getElementById("swOk");
if (sc) sc.style.background = TH.inkS;
if (sw) sw.style.background = TH.accentS;
if (so) so.style.background = TH.runS;
}
/* ── algorithms (generators; each yield mutates the shared op record) ────── */
/* op record o: t = 0 compare / 1 write, i & j = highlighted slots, c = cost */
function* bubbleSort(a, o) {
var n = a.length, i, j, tmp, swapped;
for (i = 0; i < n - 1; i++) {
swapped = false;
for (j = 0; j + 1 < n - i; j++) {
o.t = 0; o.i = j; o.j = j + 1; o.c = 1; yield;
if (a[j] > a[j + 1]) {
tmp = a[j]; a[j] = a[j + 1]; a[j + 1] = tmp;
o.t = 1; o.i = j; o.j = j + 1; o.c = 2; yield;
swapped = true;
}
}
if (!swapped) return;
}
}
function* insertionSort(a, o) {
var n = a.length, i, j, v;
for (i = 1; i < n; i++) {
v = a[i]; j = i - 1;
while (j >= 0) {
o.t = 0; o.i = j; o.j = i; o.c = 1; yield;
if (a[j] <= v) break;
a[j + 1] = a[j];
o.t = 1; o.i = j; o.j = j + 1; o.c = 1; yield;
j--;
}
if (j + 1 !== i) {
a[j + 1] = v;
o.t = 1; o.i = j + 1; o.j = j + 1; o.c = 1; yield;
}
}
}
function* mergeSort(a, o, buf, lo, hi) {
if (hi - lo < 2) return;
var mid = (lo + hi) >> 1, k, i, j, takeRight;
yield* mergeSort(a, o, buf, lo, mid);
yield* mergeSort(a, o, buf, mid, hi);
for (k = lo; k < hi; k++) {
buf[k] = a[k];
o.t = 1; o.i = k; o.j = k; o.c = 1; yield;
}
i = lo; j = mid;
for (k = lo; k < hi; k++) {
if (i >= mid) takeRight = true;
else if (j >= hi) takeRight = false;
else {
o.t = 0; o.i = i; o.j = j; o.c = 1; yield;
takeRight = buf[j] < buf[i];
}
a[k] = takeRight ? buf[j++] : buf[i++];
o.t = 1; o.i = k; o.j = k; o.c = 1; yield;
}
}
/* Hoare partition, middle-element pivot, recursion only on the smaller side */
function* quickSort(a, o, lo, hi) {
var mid, p, i, j, q, tmp;
while (lo < hi) {
mid = (lo + hi) >> 1; p = a[mid]; i = lo - 1; j = hi + 1;
for (;;) {
do { i++; o.t = 0; o.i = i; o.j = mid; o.c = 1; yield; } while (a[i] < p);
do { j--; o.t = 0; o.i = j; o.j = mid; o.c = 1; yield; } while (a[j] > p);
if (i >= j) { q = j; break; }
tmp = a[i]; a[i] = a[j]; a[j] = tmp;
o.t = 1; o.i = i; o.j = j; o.c = 2; yield;
}
if (q - lo < hi - q - 1) { yield* quickSort(a, o, lo, q); lo = q + 1; }
else { yield* quickSort(a, o, q + 1, hi); hi = q; }
}
}
/* ── lanes ──────────────────────────────────────────────────────────────── */
var NAMES = { bubble: "Bubble sort", insertion: "Insertion sort", merge: "Merge sort", quick: "Quick sort" };
var lanes = [];
var nodes = document.querySelectorAll(".lane");
for (var li = 0; li < nodes.length; li++) {
var el = nodes[li];
lanes.push({
key: el.getAttribute("data-key"), el: el,
canvas: el.querySelector("[data-canvas]"),
ctx: el.querySelector("[data-canvas]").getContext("2d"),
badge: el.querySelector("[data-badge]"),
cEl: el.querySelector("[data-c]"), wEl: el.querySelector("[data-w]"), oEl: el.querySelector("[data-o]"),
a: null, buf: null, max: 1, gen: null, o: { t: 0, i: -1, j: -1, c: 1 },
cmp: 0, wrt: 0, ops: 0, hi: -1, hj: -1, ht: 0,
done: false, ok: false, rank: 0, w: 0, h: 0, dpr: 1
});
}
var banner = document.getElementById("banner");
var headline = document.getElementById("headline");
var standings = document.getElementById("standings");
var live = document.getElementById("live");
var playBtn = document.getElementById("playBtn");
var resetBtn = document.getElementById("resetBtn");
var shapesBox = document.getElementById("shapes");
var sizeEl = document.getElementById("size"), sizeV = document.getElementById("sizeV");
var speedEl = document.getElementById("speed"), speedV = document.getElementById("speedV");
var shape = "random";
var n = +sizeEl.value;
var state = "ready"; /* ready | running | paused | done */
var animOptIn = !reduced;
var announceFirst = true; /* muted while a race is resolved in one tick */
var finishedCount = 0;
var raf = 0, lastTs = 0, acc = 0;
var baseArray = null;
function opsPerSec() {
var v = +speedEl.value / 100;
return Math.round(400 * Math.pow(100, v));
}
function fmt(x) { return String(x).replace(/\B(?=(\d{3})+(?!\d))/g, ","); }
/* ── array shapes ───────────────────────────────────────────────────────── */
function swapAt(a, i, j) { var t = a[i]; a[i] = a[j]; a[j] = t; }
function buildArray(len, kind) {
var a = new Int32Array(len), i;
for (i = 0; i < len; i++) a[i] = i + 1;
if (kind === "random") {
for (i = len - 1; i > 0; i--) swapAt(a, i, (Math.random() * (i + 1)) | 0);
} else if (kind === "nearly") {
var nudges = Math.max(2, Math.round(len / 11));
for (i = 0; i < nudges; i++) {
var p = (Math.random() * (len - 1)) | 0;
swapAt(a, p, p + 1);
}
} else if (kind === "reversed") {
for (i = 0; i < len >> 1; i++) swapAt(a, i, len - 1 - i);
} else if (kind === "few") {
var k = 5;
for (i = 0; i < len; i++) a[i] = Math.round(((((Math.random() * k) | 0) + 1) / k) * len);
}
return a;
}
function makeGen(L) {
if (L.key === "bubble") return bubbleSort(L.a, L.o);
if (L.key === "insertion") return insertionSort(L.a, L.o);
if (L.key === "merge") return mergeSort(L.a, L.o, L.buf, 0, L.a.length);
return quickSort(L.a, L.o, 0, L.a.length - 1);
}
function isSorted(a) {
for (var i = 1; i < a.length; i++) if (a[i - 1] > a[i]) return false;
return true;
}
/* ── reset / build ──────────────────────────────────────────────────────── */
/* fresh = deal a new array; otherwise replay the one already on the table */
function reset(keepRunning, fresh) {
stopLoop();
if (fresh || !baseArray || baseArray.length !== n) baseArray = buildArray(n, shape);
var base = baseArray, max = 1, i;
for (i = 0; i < base.length; i++) if (base[i] > max) max = base[i];
finishedCount = 0;
lanes.forEach(function (L) {
L.a = new Int32Array(base);
L.buf = new Int32Array(base.length);
L.max = max;
L.o = { t: 0, i: -1, j: -1, c: 1 };
L.gen = makeGen(L);
L.cmp = 0; L.wrt = 0; L.ops = 0;
L.hi = -1; L.hj = -1; L.ht = 0;
L.done = false; L.ok = false; L.rank = 0;
L.el.classList.remove("done"); L.el.classList.remove("ok"); L.el.classList.remove("won");
});
state = keepRunning ? "running" : "ready";
syncStats(); syncBadges(); syncStandings(); syncButton(); syncBanner();
drawAll();
if (state === "running") startLoop();
}
/* ── stepping ───────────────────────────────────────────────────────────── */
function stepLane(L, budget) {
var o = L.o, gen = L.gen, r;
while (budget > 0) {
r = gen.next();
if (r.done) { finishLane(L); return; }
if (o.t === 0) { L.cmp++; L.ops++; budget--; }
else { L.wrt += o.c; L.ops += o.c; budget -= o.c; }
L.hi = o.i; L.hj = o.j; L.ht = o.t;
}
}
function finishLane(L) {
L.done = true;
L.ok = isSorted(L.a);
L.hi = -1; L.hj = -1;
L.el.classList.add("done");
finishedCount++;
rankFinished();
if (announceFirst && finishedCount === 1) {
say(NAMES[L.key] + " finished first, with " + fmt(L.ops) + " array operations.");
}
}
function rankFinished() {
var fin = lanes.filter(function (L) { return L.done; });
fin.sort(function (x, y) { return x.ops - y.ops; });
fin.forEach(function (L, i) { L.rank = i + 1; });
}
function allDone() { return finishedCount === lanes.length; }
/* ── loop ───────────────────────────────────────────────────────────────── */
function startLoop() {
if (raf) return;
lastTs = 0; acc = 0;
raf = requestAnimationFrame(frame);
}
function stopLoop() {
if (raf) { cancelAnimationFrame(raf); raf = 0; }
}
function frame(ts) {
raf = requestAnimationFrame(frame);
if (!lastTs) { lastTs = ts; return; }
var dt = (ts - lastTs) / 1000;
lastTs = ts;
if (dt > 0.1) dt = 0.1;
acc += opsPerSec() * dt;
var budget = acc | 0;
acc -= budget;
var before = finishedCount;
if (budget > 0) {
for (var i = 0; i < lanes.length; i++) {
if (!lanes[i].done) stepLane(lanes[i], budget);
}
}
drawAll();
syncStats();
if (finishedCount !== before) { syncBadges(); syncStandings(); syncBanner(); }
if (allDone()) {
stopLoop();
state = "done";
syncButton(); syncBanner();
var order = lanes.slice().sort(function (x, y) { return x.rank - y.rank; })
.map(function (L) { return NAMES[L.key]; }).join(", ");
say("All four lanes finished. Order: " + order + ".");
}
}
/* run to completion with no animation (reduced-motion first paint) */
function runInstantly() {
stopLoop();
announceFirst = false;
lanes.forEach(function (L) {
var guard = 8000000;
while (guard-- > 0) {
var r = L.gen.next();
if (r.done) break;
if (L.o.t === 0) { L.cmp++; L.ops++; }
else { L.wrt += L.o.c; L.ops += L.o.c; }
}
finishLane(L);
});
announceFirst = true;
state = "done";
drawAll(); syncStats(); syncBadges(); syncStandings(); syncButton(); syncBanner();
var order = lanes.slice().sort(function (x, y) { return x.rank - y.rank; })
.map(function (L) { return NAMES[L.key]; }).join(", ");
say("Race resolved without animation. Order: " + order + ".");
}
/* ── drawing ────────────────────────────────────────────────────────────── */
function sizeCanvas(L) {
var rect = L.canvas.getBoundingClientRect();
var dpr = Math.min(window.devicePixelRatio || 1, 2);
var w = Math.max(1, Math.round(rect.width));
var h = Math.max(1, Math.round(rect.height));
if (L.w === w && L.h === h && L.dpr === dpr) return;
L.w = w; L.h = h; L.dpr = dpr;
L.canvas.width = Math.round(w * dpr);
L.canvas.height = Math.round(h * dpr);
L.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function drawLane(L) {
var ctx = L.ctx, W = L.w, H = L.h;
if (!W || !H || !L.a) return;
ctx.fillStyle = TH.plotS;
ctx.fillRect(0, 0, W, H);
var len = L.a.length, bw = W / len;
var gap = bw > 4 ? 1 : (bw > 2 ? 0.5 : 0);
var barW = Math.max(0.8, bw - gap);
var pal = L.done ? PAL_RUN : PAL_IDLE;
var floor = H - 3, span = floor - 4;
var i, v, h, x, idx;
/* soft columns behind the two slots this algorithm is touching */
if (!L.done && (L.hi >= 0 || L.hj >= 0)) {
ctx.fillStyle = TH.hiliteS;
if (L.hi >= 0 && L.hi < len) ctx.fillRect(L.hi * bw, 0, Math.max(1.5, barW), H);
if (L.hj >= 0 && L.hj < len && L.hj !== L.hi) ctx.fillRect(L.hj * bw, 0, Math.max(1.5, barW), H);
}
for (i = 0; i < len; i++) {
v = L.a[i] / L.max;
h = 2 + v * span;
x = i * bw;
if (!L.done && (i === L.hi || i === L.hj)) {
ctx.fillStyle = L.ht === 0 ? TH.inkS : TH.accentS;
} else {
idx = (v * (STEPS - 1)) | 0;
if (idx < 0) idx = 0; else if (idx > STEPS - 1) idx = STEPS - 1;
ctx.fillStyle = pal[idx];
}
ctx.fillRect(x, floor - h, barW, h);
}
ctx.fillStyle = L.done ? TH.runS : TH.lineS;
ctx.fillRect(0, H - (L.done ? 2 : 1), W, L.done ? 2 : 1);
}
function drawAll() {
for (var i = 0; i < lanes.length; i++) drawLane(lanes[i]);
}
function measureAll() {
for (var i = 0; i < lanes.length; i++) sizeCanvas(lanes[i]);
drawAll();
}
/* ── DOM sync ───────────────────────────────────────────────────────────── */
function syncStats() {
lanes.forEach(function (L) {
L.cEl.textContent = fmt(L.cmp);
L.wEl.textContent = fmt(L.wrt);
L.oEl.textContent = fmt(L.ops);
});
}
function syncBadges() {
lanes.forEach(function (L) {
var b = L.badge;
L.el.classList.toggle("ok", L.done && L.ok);
L.el.classList.toggle("won", L.done && L.ok && L.rank === 1);
b.className = "badge";
if (L.done) {
if (L.ok) { b.classList.add("ok"); b.textContent = "#" + L.rank + " · sorted ✓"; }
else { b.classList.add("bad"); b.textContent = "not sorted"; }
} else if (state === "running") {
b.classList.add("live"); b.textContent = "running";
} else if (state === "paused") {
b.textContent = "paused";
} else {
b.textContent = "ready";
}
});
}
function syncStandings() {
var fin = lanes.filter(function (L) { return L.done; });
if (!fin.length) { standings.innerHTML = ""; return; }
fin.sort(function (x, y) { return x.rank - y.rank; });
var html = "";
for (var i = 0; i < fin.length; i++) {
var L = fin[i];
html += '<li' + (L.rank === 1 ? ' class="first"' : '') + '><span class="r">' + L.rank +
'</span><span class="nm">' + NAMES[L.key] + '</span><span class="v">' + fmt(L.ops) + '</span></li>';
}
standings.innerHTML = html;
}
function syncBanner() {
banner.classList.remove("won", "live");
var first = lanes.filter(function (L) { return L.rank === 1; })[0];
if (allDone() && first) {
banner.classList.add("won");
var slowest = lanes.slice().sort(function (x, y) { return y.ops - x.ops; })[0];
var ratio = first.ops > 0 ? (slowest.ops / first.ops) : 1;
headline.textContent = NAMES[first.key] + " wins — " + fmt(first.ops) + " ops, " +
(ratio >= 1.15 ? ratio.toFixed(1) + "× less work than " + NAMES[slowest.key].toLowerCase() : "a near dead heat");
} else if (first) {
banner.classList.add("won");
var left = lanes.length - finishedCount;
headline.textContent = NAMES[first.key] + " finished first — " + fmt(first.ops) + " ops · " +
(state === "paused" ? "paused, " : "") + left + (left === 1 ? " lane still running" : " lanes still running");
} else if (state === "running") {
banner.classList.add("live");
headline.textContent = "Racing " + n + " values · " + fmt(opsPerSec()) + " ops/s per lane";
} else if (state === "paused") {
headline.textContent = "Paused · " + n + " values";
} else {
headline.textContent = n + " values ready · press start";
}
}
function syncButton() {
if (state === "running") playBtn.textContent = "Pause";
else if (state === "paused") playBtn.textContent = "Resume";
else if (state === "done") playBtn.textContent = "Run it again";
else playBtn.textContent = "Start race";
}
var sayTimer = 0;
function say(msg) {
clearTimeout(sayTimer);
sayTimer = setTimeout(function () { live.textContent = msg; }, 140);
}
/* ── controls ───────────────────────────────────────────────────────────── */
playBtn.addEventListener("click", function () {
animOptIn = true;
if (state === "running") {
state = "paused"; stopLoop();
} else if (state === "done") {
reset(true, false);
} else {
state = "running"; startLoop();
}
syncButton(); syncBadges(); syncBanner();
});
function rebuild() {
if (!animOptIn) { reset(false, true); runInstantly(); return; }
reset(state === "running" || state === "done", true);
}
resetBtn.addEventListener("click", rebuild);
shapesBox.addEventListener("click", function (e) {
var b = e.target.closest ? e.target.closest("button[data-shape]") : null;
if (!b) return;
shape = b.getAttribute("data-shape");
var all = shapesBox.querySelectorAll("button[data-shape]");
for (var i = 0; i < all.length; i++) all[i].setAttribute("aria-pressed", all[i] === b ? "true" : "false");
rebuild();
});
function syncSizeLabel() {
sizeV.textContent = String(n);
sizeEl.setAttribute("aria-valuetext", n + " values");
}
function syncSpeedLabel() {
speedV.textContent = fmt(opsPerSec()) + " ops/s";
speedEl.setAttribute("aria-valuetext", fmt(opsPerSec()) + " operations per second");
}
sizeEl.addEventListener("input", function () {
n = +sizeEl.value;
syncSizeLabel();
rebuild();
});
speedEl.addEventListener("input", function () {
syncSpeedLabel();
if (state === "running") syncBanner();
});
document.addEventListener("visibilitychange", function () {
if (document.hidden) stopLoop();
else if (state === "running") startLoop();
});
if (window.ResizeObserver) {
var ro = new ResizeObserver(function () { measureAll(); });
ro.observe(document.getElementById("race"));
}
window.addEventListener("resize", measureAll);
/* ── boot ───────────────────────────────────────────────────────────────── */
readTheme();
measureAll();
syncSizeLabel();
syncSpeedLabel();
reset(false, true);
if (reduced) {
runInstantly();
playBtn.textContent = "Watch it run";
} else {
state = "running";
syncButton(); syncBadges(); syncBanner();
startLoop();
}
})();
</script>
</body>
</html>
source-visible by construction · nothing is published here without its code
Comments
0 totalNo comments yet. If you ran it, say what happened.