$sloprun.dev

Sorting Algorithm Race

demo sandbox: allow-scripts · csp: default-src 'none'

sandboxed and isolated in your browser · never enter a real password in a demo

▶ 6 ran · ✓ 0 worked
i ran it — no login needed:
share: preview embed ↗
post it anywhere:
email the card:

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×.

Embed this demo — put a runnable demo in your blog post

Paste this where you write. It renders a live, runnable demo inline anywhere raw HTML / iframes are allowed — Ghost, WordPress, Notion, Discourse, your own site.

<iframe src="https://sloprun.dev/embed/sorting-race" width="100%" height="640" loading="lazy" allow="" style="border:1px solid #E3E2DC;border-radius:10px" title="Sorting Algorithm Race — a runnable demo on sloprun.dev"></iframe>
preview ↗

On Medium and dev.to the plain link becomes a rich preview card that links back here — they don't run third-party iframes, so paste the URL there and the reader clicks through to run it. https://sloprun.dev/p/sorting-race

Source

the code you see is the code that just ran raw ↗
Source — the code you see is the code that just ran 937 lines 37.2 KB index.html
demos/sorting-race/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 total

No comments yet. If you ran it, say what happened.

Report this post

Goes straight to the moderation queue. Enough independent reports and the post is suspended automatically until a human looks.

what is wrong