$sloprun.dev

Estimating π with Darts

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

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

▶ 5 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

Darts rain onto a square with a circle drawn inside it. The circle covers exactly π/4 of the square, so four times the share of darts that land inside is π — arrived at by counting, with no trigonometry anywhere. It opens mid-throw: the running estimate wobbles in big type beside the true value, and the convergence trace narrows behind it. Hit Throw 10,000 a few times to watch the error collapse, then notice how stubborn the next decimal is — accuracy improves only with the square root of the darts, so ten times the precision costs a hundred times the throws.

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/pi-darts" width="100%" height="640" loading="lazy" allow="" style="border:1px solid #E3E2DC;border-radius:10px" title="Estimating π with Darts — 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/pi-darts

Source

the code you see is the code that just ran raw ↗
Source — the code you see is the code that just ran 806 lines 33.0 KB index.html
demos/pi-darts/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<link rel="icon" href="data:,">
<title>Estimating π with Darts</title>
<style>
/* sloprun design tokens — inline this block into every demo (self-contained rule).
   Identity: instrument-panel. Machine facts in mono; human words in sans.
   Green is EARNED: only for "it ran / it worked" states, never decoration. */
: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); }

/* ---------- base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { line-height: 1.5; font-size: 15px; overflow-x: hidden; -webkit-text-size-adjust: 100%; }
.wrap { max-width: 1120px; margin: 0 auto; padding: 18px 14px 30px; }
h1, h2 { margin: 0; font-weight: 600; letter-spacing: -0.01em; }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.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;
}

/* ---------- header ---------- */
.head {
  display: flex; flex-wrap: wrap; gap: 10px 16px; align-items: flex-start;
  justify-content: space-between; padding-bottom: 13px; margin-bottom: 14px;
  border-bottom: 1px solid var(--line);
}
.head h1 { font-size: 1.3rem; }
.lede { margin: 5px 0 0; color: var(--muted); max-width: 60ch; font-size: 0.95rem; }
.status {
  display: inline-flex; align-items: center; gap: 8px; flex: none;
  font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.04em;
  padding: 6px 11px; border: 1px solid var(--line); border-radius: 999px;
  background: var(--surface); color: var(--muted); white-space: nowrap;
}
.status .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); flex: none; }
.status[data-state="run"] { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
.status[data-state="run"] .dot { background: var(--accent); animation: pulse 1.15s ease-in-out infinite; }
@keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: 0.2 } }
@media (prefers-reduced-motion: reduce) { .status[data-state="run"] .dot { animation: none } }

/* ---------- layout ---------- */
.grid {
  display: grid; gap: 14px; align-items: start;
  grid-template-columns: minmax(0, 1fr) 336px;
  grid-template-rows: auto auto auto 1fr;
  grid-template-areas: "board readout" "board spark" "board controls" "board .";
}
@media (max-width: 920px) {
  .grid { grid-template-columns: minmax(0, 1fr); grid-template-rows: none;
          grid-template-areas: "readout" "board" "spark" "controls"; }
}
.a-board { grid-area: board; align-self: stretch; display: flex; flex-direction: column; justify-content: center }
.a-readout { grid-area: readout }
.a-spark { grid-area: spark } .a-controls { grid-area: controls }
.panel {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 13px; min-width: 0;
}
.panel > h2 {
  font-family: var(--font-mono); font-size: 0.68rem; text-transform: uppercase;
  letter-spacing: 0.1em; color: var(--muted); font-weight: 600; margin-bottom: 10px;
}

/* ---------- board ---------- */
.board { position: relative; width: 100%; max-width: 540px; margin: 0 auto; aspect-ratio: 1 / 1; flex: 0 0 auto; }
.board canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; border-radius: 7px; }
.board .fx { pointer-events: none; }
.legend {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px 14px;
  margin-top: 10px; font-size: 0.7rem; color: var(--muted); letter-spacing: 0.02em;
}
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.legend b { color: var(--ink); font-weight: 600; }
.sw { width: 9px; height: 9px; border-radius: 2px; flex: none; }
.sw.in { background: var(--accent) } .sw.out { background: var(--muted) }
.legend .grow { flex: 1 1 auto; min-width: 0 }
.caption {
  margin: 8px 0 0; font-family: var(--font-mono); font-size: 0.7rem;
  color: var(--muted); line-height: 1.6;
}

/* ---------- readout ---------- */
.big {
  font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: clamp(2.5rem, 10vw, 3.5rem); font-weight: 600; line-height: 1.02;
  letter-spacing: -0.035em; word-break: keep-all;
}
.big .hit { color: var(--run) }
.formula {
  margin-top: 6px; font-family: var(--font-mono); font-variant-numeric: tabular-nums;
  font-size: 0.82rem; color: var(--accent); letter-spacing: 0.01em;
}
.stats { margin: 13px 0 0; padding: 11px 0 0; border-top: 1px solid var(--line);
         font-family: var(--font-mono); font-variant-numeric: tabular-nums; font-size: 0.78rem; }
.stats > div { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; padding: 3px 0; }
.stats dt { color: var(--muted) }
.stats dd { margin: 0; font-weight: 600; white-space: nowrap }
.stats dd.good { color: var(--run) }

/* ---------- sparkline ---------- */
.sparkwrap { position: relative; width: 100%; height: 142px; }
.sparkwrap canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.note { margin: 9px 0 0; font-size: 0.78rem; color: var(--muted); line-height: 1.45 }

/* ---------- controls ---------- */
.btns { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 14px }
button {
  font: inherit; color: var(--ink); background: var(--surface);
  border: 1px solid var(--line); border-radius: 7px; padding: 7px 12px;
  min-height: 36px; cursor: pointer; font-family: var(--font-mono); font-size: 0.75rem;
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
button:hover { border-color: var(--accent); color: var(--accent) }
button.primary { background: var(--accent-soft); border-color: var(--accent); color: var(--accent) }
.ctl-top { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; margin-bottom: 3px }
.ctl-top label { font-size: 0.88rem }
.ctl-val { font-family: var(--font-mono); font-variant-numeric: tabular-nums;
           font-size: 0.85rem; font-weight: 600; color: var(--accent); white-space: nowrap }
input[type=range] {
  -webkit-appearance: none; appearance: none; width: 100%; height: 24px;
  background: transparent; margin: 0; display: block; cursor: pointer;
}
input[type=range]::-webkit-slider-runnable-track { height: 4px; background: var(--line); border-radius: 2px }
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 16px; height: 16px; margin-top: -6px; 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; background: var(--line); border-radius: 2px }
input[type=range]::-moz-range-thumb {
  width: 16px; height: 16px; 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: none }
input[type=range]:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 3px var(--surface), 0 0 0 5px var(--accent) }
input[type=range]:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 3px var(--surface), 0 0 0 5px var(--accent) }
.scale { display: flex; justify-content: space-between; font-family: var(--font-mono);
         font-size: 0.65rem; color: var(--muted); margin-top: 1px }
.hint { margin: 13px 0 0; font-family: var(--font-mono); font-size: 0.68rem; color: var(--muted); letter-spacing: 0.02em }

footer { font-family: var(--font-mono); font-size: 11px; color: var(--muted);
         letter-spacing: .05em; padding-top: 12px; margin-top: 16px;
         border-top: 1px solid var(--line) }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important;
      transition-duration: 0.001ms !important; }
}
</style>
</head>
<body>
<div class="wrap">

  <header class="head">
    <div>
      <h1>Estimating π with darts</h1>
      <p class="lede">Darts land at random on a square. The circle covers exactly π/4 of that square — so four times the share that lands inside is π, no geometry required.</p>
    </div>
    <div class="status" id="status" data-state="run"><span class="dot"></span><span id="statusTxt">throwing</span></div>
  </header>

  <div class="grid">

    <section class="panel a-board">
      <div class="board">
        <canvas id="plot" role="img" aria-label="Square dartboard with an inscribed circle; darts that land inside the circle are highlighted."></canvas>
        <canvas id="fx" class="fx" aria-hidden="true"></canvas>
      </div>
      <div class="legend mono">
        <span><i class="sw in"></i>inside <b id="lgIn">0</b></span>
        <span><i class="sw out"></i>outside <b id="lgOut">0</b></span>
        <span class="grow"></span>
        <span id="lgSeed">seed 0</span>
      </div>
      <p class="caption">square 2 × 2 &nbsp;·&nbsp; circle radius 1 &nbsp;·&nbsp; a dart is inside when x² + y² ≤ 1</p>
    </section>

    <section class="panel a-readout">
      <h2>running estimate</h2>
      <div class="big"><span id="estHit" class="hit"></span><span id="estRest">3.000000</span></div>
      <div class="formula" id="formula">4 × 0 ÷ 0</div>
      <dl class="stats">
        <div><dt>π, actually</dt><dd>3.141593</dd></div>
        <div><dt>off by</dt><dd id="err">—</dd></div>
        <div><dt>decimals right</dt><dd id="digits">—</dd></div>
        <div><dt>darts thrown</dt><dd id="nTot">0</dd></div>
        <div><dt>95 % band here</dt><dd id="typ">—</dd></div>
      </dl>
    </section>

    <section class="panel a-spark">
      <h2>convergence</h2>
      <div class="sparkwrap"><canvas id="spark" role="img" aria-label="Convergence chart of the estimate against the number of darts thrown."></canvas></div>
      <p class="note">The shaded band is where the estimate should usually land. Ten times more accuracy costs a hundred times more darts.</p>
    </section>

    <section class="panel a-controls">
      <h2>controls</h2>
      <div class="btns">
        <button id="btnPlay" class="primary" type="button">Pause</button>
        <button id="btnBulk" type="button">Throw 10,000</button>
        <button id="btnReset" type="button">Reset</button>
      </div>
      <div class="ctl">
        <div class="ctl-top"><label for="speed">Throw rate</label><span class="ctl-val" id="speedVal">241 / sec</span></div>
        <input id="speed" type="range" min="0" max="100" step="1" value="51">
        <div class="scale"><span>5</span><span>10,000</span></div>
      </div>
      <p class="hint">space pause · t throw 10k · r reset</p>
    </section>

  </div>

  <footer>demo · sloprun.dev</footer>
</div>

<div class="sr-only" role="status" aria-live="polite" id="live"></div>

<script>
(function () {
  "use strict";

  var PI = Math.PI;
  var SIGMA = 1.642317;          /* std dev of the estimate for one dart */
  var BAND  = 1.96 * SIGMA;      /* 95% band half-width, times 1/sqrt(n) */
  var MONO  = 'ui-monospace, "SF Mono", Menlo, Consolas, monospace';

  var root = document.documentElement;
  var $ = function (id) { return document.getElementById(id); };

  var plot = $("plot"), pctx = plot.getContext("2d");
  var fx = $("fx"), fctx = fx.getContext("2d");
  var spark = $("spark"), sctx = spark.getContext("2d");
  var elStatus = $("status"), elStatusTxt = $("statusTxt");
  var elHit = $("estHit"), elRest = $("estRest"), elFormula = $("formula");
  var elErr = $("err"), elDigits = $("digits"), elN = $("nTot"), elTyp = $("typ");
  var elIn = $("lgIn"), elOut = $("lgOut"), elSeed = $("lgSeed");
  var elLive = $("live"), elSpeed = $("speed"), elSpeedVal = $("speedVal");
  var btnPlay = $("btnPlay"), btnBulk = $("btnBulk"), btnReset = $("btnReset");

  var mqDark = window.matchMedia ? matchMedia("(prefers-color-scheme: dark)") : null;
  var mqRM = window.matchMedia ? matchMedia("(prefers-reduced-motion: reduce)") : null;
  var reduce = !!(mqRM && mqRM.matches);

  /* ── random ─────────────────────────────────────────────────────────────── */
  function mulberry32(a) {
    return function () {
      a |= 0; a = a + 0x6D2B79F5 | 0;
      var t = Math.imul(a ^ a >>> 15, 1 | a);
      t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
      return ((t ^ t >>> 14) >>> 0) / 4294967296;
    };
  }
  var seed = 1, rng = mulberry32(1);

  /* ── state ──────────────────────────────────────────────────────────────── */
  var n = 0, hits = 0, running = false, rate = 240, acc = 0;
  var CAP = 24000;                       /* darts kept for repainting */
  var bx = new Float32Array(CAP), by = new Float32Array(CAP);
  var bi = new Uint8Array(CAP), bHead = 0, bCount = 0;
  var samples = [];                      /* flat [n, est, n, est, ...] */
  var nextSample = 8;
  var flights = [], pulses = [];
  var FADE_EVERY = 2500, FADE_A = 0.12, sinceFade = 0;
  var halfCur = 0.4, sparkFirst = true;
  var S = 0, ox = 0, oy = 0, sc = 1, dr = 2, inset = 2;
  var dirty = true, rafId = 0, lastT = 0;

  /* ── colours ────────────────────────────────────────────────────────────── */
  var C = {};
  function tok(name) { return getComputedStyle(root).getPropertyValue(name).trim(); }
  function toRGB(c) {
    if (!c) return [128, 128, 128];
    if (c.charAt(0) === "#") {
      var h = c.slice(1);
      if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
      var v = parseInt(h, 16);
      return [(v >> 16) & 255, (v >> 8) & 255, v & 255];
    }
    var m = c.match(/-?\d+(\.\d+)?/g);
    return m ? [+m[0], +m[1], +m[2]] : [128, 128, 128];
  }
  function rgba(rgb, a) { return "rgba(" + rgb[0] + "," + rgb[1] + "," + rgb[2] + "," + a + ")"; }
  function readTheme() {
    C.surface = toRGB(tok("--surface"));
    C.ink = toRGB(tok("--ink"));
    C.muted = toRGB(tok("--muted"));
    C.line = toRGB(tok("--line"));
    C.accent = toRGB(tok("--accent"));
    C.soft = toRGB(tok("--accent-soft"));
  }

  /* ── formatting ─────────────────────────────────────────────────────────── */
  function grp(v) { return Math.round(v).toLocaleString("en-US"); }
  function pct(p) {
    return (p >= 1 ? p.toFixed(2) : p >= 0.1 ? p.toFixed(3) : p.toFixed(4)) + " %";
  }
  function estimate() { return n ? 4 * hits / n : 0; }

  /* ── canvas sizing ──────────────────────────────────────────────────────── */
  function fit(cv) {
    var r = cv.getBoundingClientRect();
    var dpr = Math.min(2, window.devicePixelRatio || 1);
    var w = Math.max(1, Math.round(r.width * dpr));
    var h = Math.max(1, Math.round(r.height * dpr));
    var changed = cv.width !== w || cv.height !== h;
    if (changed) { cv.width = w; cv.height = h; }
    return { dpr: dpr, w: r.width, h: r.height, changed: changed };
  }

  function layout() {
    fit(plot); fit(fx);
    S = plot.width;
    inset = Math.max(1, Math.round(S / 320));
    sc = (S - 2 * inset) / 2;
    ox = S / 2; oy = S / 2;
    var cssW = plot.clientWidth || S;
    var ratio = S / Math.max(1, cssW);
    var dotCss = Math.max(1.4, Math.min(2.4, cssW / 260));
    dr = Math.max(1, Math.round(dotCss * ratio));
    repaintBoard();
    var sp = fit(spark);
    sctx.setTransform(sp.dpr, 0, 0, sp.dpr, 0, 0);
    drawSpark();
  }

  /* ── board painting ─────────────────────────────────────────────────────── */
  function guides(alpha) {
    pctx.globalAlpha = alpha;
    pctx.fillStyle = rgba(C.soft, 1);
    pctx.beginPath(); pctx.arc(ox, oy, sc, 0, 6.2831853); pctx.fill();
    pctx.lineWidth = Math.max(1, S / 480);
    pctx.strokeStyle = rgba(C.accent, 0.55);
    pctx.beginPath(); pctx.arc(ox, oy, sc, 0, 6.2831853); pctx.stroke();
    pctx.strokeStyle = rgba(C.line, 1);
    pctx.strokeRect(inset, inset, S - 2 * inset, S - 2 * inset);
    pctx.globalAlpha = 1;
  }
  function clearBoard() {
    pctx.globalAlpha = 1;
    pctx.fillStyle = rgba(C.surface, 1);
    pctx.fillRect(0, 0, S, S);
    guides(1);
  }
  function fade() {
    pctx.globalAlpha = FADE_A;
    pctx.fillStyle = rgba(C.surface, 1);
    pctx.fillRect(0, 0, S, S);
    guides(FADE_A);
    pctx.globalAlpha = 1;
  }
  function batch(arr, rgb, alpha) {
    if (!arr.length) return;
    pctx.globalAlpha = alpha;
    pctx.fillStyle = rgba(rgb, 1);
    var h = dr / 2;
    for (var i = 0; i < arr.length; i += 2) pctx.fillRect(arr[i] - h | 0, arr[i + 1] - h | 0, dr, dr);
    pctx.globalAlpha = 1;
  }
  function repaintBoard() {
    if (!S) return;
    clearBoard();
    var ins = [], out = [];
    var start = bCount < CAP ? 0 : bHead;
    for (var k = 0; k < bCount; k++) {
      var i = (start + k) % CAP;
      var px = ox + bx[i] * sc, py = oy - by[i] * sc;
      if (bi[i]) ins.push(px, py); else out.push(px, py);
    }
    batch(out, C.muted, 0.42);
    batch(ins, C.accent, 0.9);
  }
  function store(x, y, ins) {
    bx[bHead] = x; by[bHead] = y; bi[bHead] = ins ? 1 : 0;
    bHead = (bHead + 1) % CAP;
    if (bCount < CAP) bCount++;
  }
  function afterStamp(k) {
    sinceFade += k;
    var guard = 0;
    while (sinceFade >= FADE_EVERY && guard++ < 6) { fade(); sinceFade -= FADE_EVERY; }
    if (guard >= 6) sinceFade = 0;
  }

  var tin = [], tout = [];
  function bulk(k) {
    tin.length = 0; tout.length = 0;
    for (var i = 0; i < k; i++) {
      var x = rng() * 2 - 1, y = rng() * 2 - 1;
      var ins = x * x + y * y <= 1;
      n++; if (ins) hits++;
      store(x, y, ins);
      var px = ox + x * sc, py = oy - y * sc;
      if (ins) tin.push(px, py); else tout.push(px, py);
    }
    batch(tout, C.muted, 0.42);
    batch(tin, C.accent, 0.9);
    afterStamp(k);
    dirty = true;
  }
  function stampOne(x, y, ins) {
    n++; if (ins) hits++;
    store(x, y, ins);
    var px = ox + x * sc, py = oy - y * sc;
    pctx.globalAlpha = ins ? 0.9 : 0.42;
    pctx.fillStyle = rgba(ins ? C.accent : C.muted, 1);
    pctx.fillRect(px - dr / 2 | 0, py - dr / 2 | 0, dr, dr);
    pctx.globalAlpha = 1;
    if (pulses.length < 60) pulses.push({ x: px, y: py, ins: ins, t: 0 });
    afterStamp(1);
    dirty = true;
  }

  /* ── samples ────────────────────────────────────────────────────────────── */
  function note() {
    if (n < nextSample) return;
    samples.push(n, estimate());
    while (nextSample <= n) nextSample = Math.max(nextSample + 1, Math.ceil(nextSample * 1.03));
    if (samples.length > 2400) {
      var half = [];
      for (var i = 0; i < samples.length - 400; i += 4) half.push(samples[i], samples[i + 1]);
      for (var j = Math.max(0, samples.length - 400); j < samples.length; j += 2) half.push(samples[j], samples[j + 1]);
      samples = half;
    }
  }

  /* ── sparkline ──────────────────────────────────────────────────────────── */
  function drawSpark() {
    var w = spark.width / (Math.min(2, window.devicePixelRatio || 1)), h = spark.height / (Math.min(2, window.devicePixelRatio || 1));
    if (!w || !h) return;
    sctx.clearRect(0, 0, w, h);
    var padL = 8, padR = 17, padT = 12, padB = 15;
    var pw = w - padL - padR, ph = h - padT - padB;
    if (pw <= 10 || ph <= 10) return;

    var nMax = Math.max(n, 12);
    var nMin = Math.max(8, nMax / 10000);
    if (nMax < nMin * 1.4) nMax = nMin * 1.4;
    var lMin = Math.log(nMin) / Math.LN10, lMax = Math.log(nMax) / Math.LN10;
    var X = function (v) { return padL + (Math.log(v) / Math.LN10 - lMin) / (lMax - lMin) * pw; };

    var dev = 0;
    for (var i = 0; i < samples.length; i += 2) {
      if (samples[i] < nMin) continue;
      var d = Math.abs(samples[i + 1] - PI);
      if (d > dev) dev = d;
    }
    var target = Math.max(dev * 1.15, BAND / Math.sqrt(nMax) * 1.4, 0.004);
    if (target > 1.1) target = 1.1;
    if (sparkFirst || reduce) { halfCur = target; sparkFirst = false; }
    else halfCur += (target - halfCur) * 0.12;
    var half = halfCur;
    var cy = padT + ph / 2;
    var Y = function (v) { return cy - (v - PI) / half * (ph / 2); };

    /* grid: decade ticks */
    sctx.font = "9px " + MONO;
    sctx.textBaseline = "top";
    sctx.textAlign = "center";
    var p0 = Math.ceil(lMin), p1 = Math.floor(lMax);
    for (var p = p0; p <= p1; p++) {
      var xv = X(Math.pow(10, p));
      sctx.strokeStyle = rgba(C.line, 1);
      sctx.lineWidth = 1;
      sctx.beginPath(); sctx.moveTo(xv + 0.5, padT); sctx.lineTo(xv + 0.5, padT + ph); sctx.stroke();
      var lbl = p >= 6 ? Math.pow(10, p - 6) + "M" : p >= 3 ? Math.pow(10, p - 3) + "k" : String(Math.pow(10, p));
      sctx.fillStyle = rgba(C.muted, 0.9);
      sctx.fillText(lbl, xv, padT + ph + 3);
    }

    sctx.save();
    sctx.beginPath(); sctx.rect(padL, padT, pw, ph); sctx.clip();

    /* expected error band */
    sctx.beginPath();
    var steps = 60, k, nv;
    for (k = 0; k <= steps; k++) {
      nv = Math.pow(10, lMin + (lMax - lMin) * k / steps);
      var yv = Y(PI + BAND / Math.sqrt(nv));
      if (k === 0) sctx.moveTo(X(nv), yv); else sctx.lineTo(X(nv), yv);
    }
    for (k = steps; k >= 0; k--) {
      nv = Math.pow(10, lMin + (lMax - lMin) * k / steps);
      sctx.lineTo(X(nv), Y(PI - BAND / Math.sqrt(nv)));
    }
    sctx.closePath();
    sctx.fillStyle = rgba(C.accent, 0.1);
    sctx.fill();

    /* π line */
    sctx.strokeStyle = rgba(C.ink, 0.45);
    sctx.lineWidth = 1;
    if (sctx.setLineDash) sctx.setLineDash([3, 3]);
    sctx.beginPath(); sctx.moveTo(padL, Y(PI)); sctx.lineTo(padL + pw, Y(PI)); sctx.stroke();
    if (sctx.setLineDash) sctx.setLineDash([]);

    /* estimate trace */
    sctx.strokeStyle = rgba(C.accent, 1);
    sctx.lineWidth = 1.6;
    sctx.lineJoin = "round";
    sctx.beginPath();
    var started = false, lx = 0, ly = 0;
    for (var s = 0; s < samples.length; s += 2) {
      if (samples[s] < nMin) continue;
      lx = X(samples[s]); ly = Y(samples[s + 1]);
      if (!started) { sctx.moveTo(lx, ly); started = true; } else sctx.lineTo(lx, ly);
    }
    if (n >= nMin && n > 0) { lx = X(Math.max(n, nMin)); ly = Y(estimate()); if (started) sctx.lineTo(lx, ly); }
    if (started) sctx.stroke();
    if (started) {
      sctx.fillStyle = rgba(C.accent, 1);
      sctx.beginPath(); sctx.arc(lx, ly, 2.6, 0, 6.2831853); sctx.fill();
    }
    sctx.restore();

    /* labels */
    sctx.fillStyle = rgba(C.ink, 0.65);
    sctx.textAlign = "left";
    sctx.textBaseline = "middle";
    sctx.font = "10px " + MONO;
    sctx.fillText("π", padL + pw + 4, Y(PI));
    sctx.textAlign = "right";
    sctx.textBaseline = "top";
    sctx.font = "9px " + MONO;
    sctx.fillStyle = rgba(C.muted, 1);
    sctx.fillText("π ± " + half.toFixed(half < 0.01 ? 4 : 3), padL + pw, 1);
    sctx.textAlign = "left";
    sctx.fillText("darts", padL, 1);
  }

  /* ── readout ────────────────────────────────────────────────────────────── */
  function paintReadout() {
    var est = estimate();
    var a = n ? est.toFixed(6) : "0.000000";
    var b = PI.toFixed(6);
    var i = 0;
    while (i < a.length && i < b.length && a.charAt(i) === b.charAt(i)) i++;
    var dec = i >= 2 ? i - 2 : 0;
    if (dec < 1) i = 0;
    elHit.textContent = a.slice(0, i);
    elRest.textContent = a.slice(i);

    elFormula.textContent = "4 × " + grp(hits) + " ÷ " + grp(n);
    elN.textContent = grp(n);
    elIn.textContent = grp(hits);
    elOut.textContent = grp(n - hits);
    if (n) {
      elErr.textContent = pct(Math.abs(est - PI) / PI * 100);
      elDigits.textContent = dec === 1 ? "1 place" : dec + " places";
      elDigits.className = dec >= 1 ? "good" : "";
      elTyp.textContent = "± " + pct(BAND / Math.sqrt(n) / PI * 100);
    } else {
      elErr.textContent = "—"; elDigits.textContent = "—"; elDigits.className = ""; elTyp.textContent = "—";
    }
  }

  /* ── animation ──────────────────────────────────────────────────────────── */
  var FLIGHT = 0.34, MAXFLY = 150;
  function spawn() {
    var x = rng() * 2 - 1, y = rng() * 2 - 1;
    var ins = x * x + y * y <= 1;
    flights.push({ x: x, y: y, ins: ins, t: 0 });
  }
  function stepFx(dt) {
    var i;
    for (i = flights.length - 1; i >= 0; i--) {
      var f = flights[i];
      f.t += dt / FLIGHT;
      if (f.t >= 1) { flights.splice(i, 1); stampOne(f.x, f.y, f.ins); }
    }
    for (i = pulses.length - 1; i >= 0; i--) {
      pulses[i].t += dt / 0.36;
      if (pulses[i].t >= 1) pulses.splice(i, 1);
    }
  }
  function drawFx() {
    fctx.clearRect(0, 0, S, S);
    var u = S / 1000, i, p;
    if (flights.length) {
      fctx.lineCap = "round";
      fctx.lineWidth = Math.max(1, 2.2 * u);
      fctx.strokeStyle = rgba(C.muted, 0.5);
      fctx.beginPath();
      for (i = 0; i < flights.length; i++) {
        var f = flights[i];
        var ty = oy - f.y * sc, sy = -0.09 * S;
        var e = f.t * f.t;
        var py = sy + (ty - sy) * e;
        var px = ox + f.x * sc;
        var len = (14 + 40 * f.t) * u;
        fctx.moveTo(px, py - len); fctx.lineTo(px, py);
      }
      fctx.stroke();
    }
    for (i = 0; i < pulses.length; i++) {
      p = pulses[i];
      var r = (2 + 13 * p.t) * u * 1.6;
      fctx.strokeStyle = rgba(p.ins ? C.accent : C.muted, 0.5 * (1 - p.t));
      fctx.lineWidth = Math.max(1, 1.4 * u);
      fctx.beginPath(); fctx.arc(p.x, p.y, r, 0, 6.2831853); fctx.stroke();
    }
  }

  function ensureLoop() { if (!rafId) { lastT = 0; rafId = requestAnimationFrame(tick); } }
  function tick(ts) {
    rafId = 0;
    var dt = lastT ? Math.min(0.05, (ts - lastT) / 1000) : 0;
    lastT = ts;

    if (running && dt > 0) {
      acc += rate * dt;
      var k = Math.floor(acc);
      acc -= k;
      if (k > 30000) k = 30000;
      if (!reduce && rate <= 400) {
        var room = MAXFLY - flights.length;
        var anim = Math.min(k, Math.max(0, room));
        for (var i = 0; i < anim; i++) spawn();
        if (k - anim > 0) bulk(k - anim);
      } else if (k > 0) bulk(k);
    }
    stepFx(dt);
    if (dirty) { note(); paintReadout(); drawSpark(); dirty = false; }
    drawFx();

    if (running || flights.length || pulses.length) rafId = requestAnimationFrame(tick);
    else fctx.clearRect(0, 0, S, S);
  }

  /* ── actions ────────────────────────────────────────────────────────────── */
  function say(msg) { elLive.textContent = msg; }
  function summary() {
    return grp(n) + " darts, estimate " + estimate().toFixed(4) +
           ", off by " + pct(Math.abs(estimate() - PI) / PI * 100) + ".";
  }
  function setStatus() {
    elStatus.setAttribute("data-state", running ? "run" : "idle");
    elStatusTxt.textContent = running ? "throwing" : (reduce && !running ? "paused · reduced motion" : "paused");
    btnPlay.textContent = running ? "Pause" : "Resume";
    btnPlay.setAttribute("aria-label", running ? "Pause throwing" : "Resume throwing");
    syncLabels();
  }
  function syncLabels() {
    spark.setAttribute("aria-label", "Convergence chart. " + summary());
    plot.setAttribute("aria-label",
      "Dartboard. " + grp(hits) + " of " + grp(n) + " darts landed inside the circle, " +
      "highlighted; the rest are grey.");
  }
  function setRunning(v) {
    running = v;
    setStatus();
    if (running) ensureLoop();
  }
  function rateFromSlider(v) { return Math.round(5 * Math.pow(2000, v / 100)); }
  function applyRate() {
    rate = rateFromSlider(+elSpeed.value);
    elSpeedVal.textContent = grp(rate) + " / sec";
    elSpeed.setAttribute("aria-valuetext", grp(rate) + " darts per second");
  }
  /* Throw `total` darts in geometrically growing chunks so the convergence
     trace gets intermediate points instead of one straight jump. */
  function throwMany(total) {
    var chunk = Math.max(4, n * 0.03), done = 0;
    while (done < total) {
      var c = Math.min(Math.max(1, Math.round(chunk)), total - done);
      bulk(c); note();
      done += c; chunk *= 1.28;
    }
    dirty = true;
  }
  function reset(quiet) {
    seed = (Math.random() * 4294967295) >>> 0;
    rng = mulberry32(seed);
    elSeed.textContent = "seed " + seed.toString(16).padStart(8, "0");
    n = 0; hits = 0; acc = 0; bHead = 0; bCount = 0; sinceFade = 0;
    samples = []; nextSample = 8; flights.length = 0; pulses.length = 0;
    sparkFirst = true;
    clearBoard();
    fctx.clearRect(0, 0, S, S);
    throwMany(reduce ? 20000 : 380);
    paintReadout(); drawSpark();
    if (running) ensureLoop();
    if (!quiet) say("Reset. " + summary());
  }

  btnPlay.addEventListener("click", function () {
    setRunning(!running);
    say(running ? "Throwing at " + grp(rate) + " darts per second." : "Paused. " + summary());
  });
  btnBulk.addEventListener("click", function () {
    throwMany(10000); paintReadout(); drawSpark(); dirty = false;
    setStatus();
    say("Threw 10,000 darts. " + summary());
    if (running || pulses.length) ensureLoop();
  });
  btnReset.addEventListener("click", function () { reset(false); });
  elSpeed.addEventListener("input", function () {
    applyRate();
    syncLabels();
  });
  elSpeed.addEventListener("change", function () { say(grp(rate) + " darts per second."); });

  document.addEventListener("keydown", function (e) {
    if (e.ctrlKey || e.metaKey || e.altKey) return;
    var tag = e.target && e.target.tagName;
    if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
    var k = e.key;
    if (k === " " || k === "Spacebar") {
      if (tag === "BUTTON") return;
      e.preventDefault();
      setRunning(!running);
      say(running ? "Throwing." : "Paused. " + summary());
    } else if (k === "t" || k === "T") {
      btnBulk.click();
    } else if (k === "r" || k === "R") {
      reset(false);
    }
  });

  /* ── theme + resize wiring ──────────────────────────────────────────────── */
  function themeChanged() {
    readTheme();
    repaintBoard();
    drawSpark();
    drawFx();
  }
  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);
      themeChanged();
    }
  });
  if (mqDark) {
    if (mqDark.addEventListener) mqDark.addEventListener("change", themeChanged);
    else if (mqDark.addListener) mqDark.addListener(themeChanged);
  }
  if (mqRM) {
    var onRM = function () {
      reduce = mqRM.matches;
      if (reduce) { setRunning(false); flights.length = 0; pulses.length = 0; }
      setStatus();
    };
    if (mqRM.addEventListener) mqRM.addEventListener("change", onRM);
    else if (mqRM.addListener) mqRM.addListener(onRM);
  }

  var resizePending = false;
  function onResize() {
    if (resizePending) return;
    resizePending = true;
    requestAnimationFrame(function () { resizePending = false; layout(); });
  }
  if (window.ResizeObserver) {
    var ro = new ResizeObserver(onResize);
    ro.observe(plot.parentNode);
    ro.observe(spark.parentNode);
  } else {
    window.addEventListener("resize", onResize);
  }

  /* ── boot ───────────────────────────────────────────────────────────────── */
  readTheme();
  applyRate();
  layout();
  reset(true);
  setRunning(!reduce);
  if (reduce) say("Reduced motion is on, so the darts are paused. " + summary());
})();
</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