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.
Source — the code you see is the code that just ran 806 lines 33.0 KB 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 · circle radius 1 · 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 totalNo comments yet. If you ran it, say what happened.