Fractal Explorer
by sloprun seed
2026-08-14
0 reported it broke · the ▶ count is distinct visitors who ran it, counted once each, anonymously
The Mandelbrot set falls out of one absurdly short rule applied to every pixel — start at zero, square it, add the pixel's own coordinates, repeat — where the points that never run away form the black shape and everything else is coloured by how fast it left. The remarkable part is that the boundary never resolves: zoom a thousand times and the same crenellated edge is still there, rebuilt from detail that did not exist a moment earlier. Start with Seahorses, then click straight into one of the spirals and keep going — each click doubles the magnification, and the image is drawn in coarse blocks and sharpened over several passes so it never locks up. Push far enough and you hit a real wall near 1e-13 wide, where ordinary 64-bit numbers run out of digits, the picture goes blocky, and the demo says so rather than pretending.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Fractal Explorer — 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 (scrims need alpha, tokens are opaque hex) */
:root { --scrim: rgba(246,246,243,.9); --shadow: 0 1px 2px rgba(26,29,33,.05); }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --scrim: rgba(20,22,26,.9); --shadow: 0 1px 2px rgba(0,0,0,.35); }
}
:root[data-theme="dark"] { --scrim: rgba(20,22,26,.9); --shadow: 0 1px 2px rgba(0,0,0,.35); }
* { 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: 1140px; 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: 68ch; color: var(--muted); font-size: 14.5px; line-height: 1.55; }
.lede b { color: var(--ink); font-weight: 600; }
.lede code {
font-family: var(--font-mono); font-size: .87em; color: var(--accent);
background: var(--accent-soft); border-radius: 4px; padding: 1px 5px;
}
.stage { display: grid; grid-template-columns: minmax(0,1fr) 292px; gap: 16px; align-items: start; }
@media (max-width: 900px) { .stage { grid-template-columns: minmax(0,1fr); } }
.left { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
/* the canvas frame */
.field {
position: relative; overflow: hidden;
border: 1px solid var(--line); border-radius: var(--radius);
background: var(--surface); box-shadow: var(--shadow);
aspect-ratio: 4 / 3; max-height: 620px; width: 100%;
}
canvas.main { display: block; width: 100%; height: 100%; touch-action: none; cursor: crosshair; }
canvas.main.dragging { cursor: grabbing; }
canvas.main:focus { outline: none; }
canvas.main:focus-visible { outline: 2px solid var(--accent); outline-offset: -3px; }
canvas.main.ptr-focus:focus-visible { outline: none; }
/* progressive-render bar, pinned to the top edge of the frame */
.prog {
position: absolute; left: 0; right: 0; top: 0; height: 2px;
background: transparent; pointer-events: none;
opacity: 1; transition: opacity .35s ease .1s;
}
.prog.done { opacity: 0; }
.prog i { display: block; height: 100%; width: 0%; background: var(--accent); transition: width .08s linear; }
.chip {
position: absolute; pointer-events: none;
font-family: var(--font-mono); font-size: 11px; letter-spacing: .01em;
color: var(--muted); background: var(--scrim);
border: 1px solid var(--line); border-radius: 999px; padding: 5px 10px;
max-width: calc(100% - 20px);
}
.hint { left: 10px; bottom: 10px; transition: opacity .45s ease; }
@media (max-width: 520px) { .hint .wide { display: none; } }
.hint.gone { opacity: 0; }
.hint[hidden] { display: none; }
.limit { right: 10px; bottom: 10px; color: var(--accent); border-color: var(--accent); }
.limit[hidden] { display: none; }
/* panel */
.panel { display: flex; flex-direction: column; gap: 12px; }
.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;
}
.card h3.sub {
margin: 15px 0 8px; padding-top: 12px; border-top: 1px solid var(--line);
font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .1em;
text-transform: uppercase; color: var(--muted); font-weight: 600;
}
.toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px 12px; }
.tbsep { width: 1px; align-self: stretch; background: var(--line); margin: 0 2px; }
@media (max-width: 480px) { .tbsep { display: none; } }
/* stats */
.stats { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 11px 10px; }
.stat { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.stat .v {
font-family: var(--font-mono); font-size: 19px; line-height: 1.05;
font-variant-numeric: tabular-nums; letter-spacing: -.02em;
white-space: nowrap; overflow: hidden; text-overflow: clip;
}
.stat .v .u { font-size: 11px; color: var(--muted); margin-left: 2px; letter-spacing: 0; }
.stat .k {
font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .09em;
text-transform: uppercase; color: var(--muted);
}
/* coordinates */
.coords { margin-top: 13px; padding-top: 12px; border-top: 1px solid var(--line); display: grid; gap: 5px; }
.crow { display: flex; align-items: baseline; gap: 9px; min-width: 0; }
.crow .ck {
font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .09em;
text-transform: uppercase; color: var(--muted); width: 4.2em; flex: none;
}
.crow .cv {
font-family: var(--font-mono); font-size: 12px; font-variant-numeric: tabular-nums;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
}
.crow.dim .cv { color: var(--muted); }
/* locator map */
.map {
display: block; width: 100%; aspect-ratio: 4 / 3; margin-top: 13px;
border: 1px solid var(--line); border-radius: 6px; background: var(--bg);
}
.cap { margin: 8px 0 0; font-size: 11.5px; line-height: 1.4; color: var(--muted); }
.cap b { color: var(--ink); font-weight: 600; }
.cap .box {
display: inline-block; width: 9px; height: 8px; margin: 0 1px;
border: 1.5px solid var(--accent); border-radius: 2px; vertical-align: baseline;
}
/* pill */
.pill {
display: inline-flex; align-items: center; gap: 6px;
font-family: var(--font-mono); font-size: 11px; letter-spacing: .04em;
padding: 3px 9px 3px 7px; border-radius: 999px;
border: 1px solid var(--line); color: var(--muted); background: var(--bg);
white-space: nowrap;
}
.pill .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--muted); flex: none; }
.pill.on { color: var(--run); background: var(--run-soft); border-color: transparent; }
.pill.on .dot { background: var(--run); }
/* controls */
.ctl { display: block; }
.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; }
input[type=range] {
-webkit-appearance: none; appearance: none; display: block;
width: 100%; height: 20px; 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; }
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 .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:disabled { opacity: .42; cursor: not-allowed; }
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); }
.places { display: grid; grid-template-columns: repeat(2, minmax(0,1fr)); gap: 7px; }
.places button { padding: 7px 6px; }
.places button[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
/* switch */
.switch { display: flex; align-items: center; gap: 9px; cursor: pointer; font-size: 13px; margin-top: 11px; }
.switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.track {
width: 34px; height: 19px; border-radius: 999px; background: var(--line);
border: 1px solid var(--line); position: relative; flex: none; transition: background .15s ease;
}
.track::after {
content: ""; position: absolute; top: 1.5px; left: 2px; width: 14px; height: 14px;
border-radius: 50%; background: var(--surface); transition: transform .15s ease;
box-shadow: 0 1px 2px rgba(0,0,0,.25);
}
.switch input:checked + .track { background: var(--accent); border-color: var(--accent); }
.switch input:checked + .track::after { transform: translateX(15px); }
.switch input:focus-visible + .track { outline: 2px solid var(--accent); outline-offset: 2px; }
.keys { margin: 10px 0 0; font-family: var(--font-mono); font-size: 10.5px; line-height: 1.7; color: var(--muted); }
.keys kbd {
font-family: inherit; font-size: 10.5px; color: var(--ink);
border: 1px solid var(--line); border-bottom-width: 2px; border-radius: 4px;
padding: 0 4px; background: var(--bg);
}
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>Fractal Explorer</h1>
<p class="lede">
One rule, run on every pixel: start at zero, square it, add the pixel's own position,
repeat. Points that never run away are the black shape — everything else is coloured by
<b>how fast it left</b>. <b>Click to zoom in</b>, shift-click to back out, drag to slide
around. The edge never smooths out, no matter how far you go.
</p>
</header>
<main class="stage">
<div class="left">
<div class="field" id="field">
<canvas class="main" id="c" tabindex="0" role="img"
aria-describedby="canvasHelp"
aria-label="Fractal view. Full view, zoom 1 times.">
A drawing of the Mandelbrot set. Your browser does not support canvas.
</canvas>
<div class="prog" id="prog" aria-hidden="true"><i id="progBar"></i></div>
<div class="chip hint" id="hint">click to zoom in<span class="wide"> · shift-click to zoom out</span> · drag to pan</div>
<div class="chip limit" id="limitChip" hidden>as deep as 64-bit numbers go</div>
</div>
<p id="canvasHelp" class="sr-only">
Interactive fractal view. Click to zoom in on a point, shift-click to zoom out.
Drag to pan. With the view focused, use the arrow keys to pan, plus and minus to zoom,
and zero to return to the full view.
</p>
<div class="card toolbar" role="group" aria-label="View controls">
<button type="button" class="primary" id="zinBtn">Zoom in</button>
<button type="button" id="zoutBtn">Zoom out</button>
<span class="tbsep" aria-hidden="true"></span>
<button type="button" id="resetBtn">Reset view</button>
<span class="pill" id="pill"><span class="dot"></span><span id="pillText">rendering</span></span>
</div>
</div>
<div class="panel">
<section class="card" aria-label="View readout">
<h2>Readout</h2>
<div class="stats">
<div class="stat"><span class="v" id="sZoom">×1.00</span><span class="k">zoom</span></div>
<div class="stat"><span class="v" id="sIter">200</span><span class="k">iterations</span></div>
<div class="stat"><span class="v" id="sSpan">3.40e+0</span><span class="k">width on the plane</span></div>
<div class="stat"><span class="v" id="sTime">—<span class="u">ms</span></span><span class="k">last render</span></div>
</div>
<div class="coords">
<div class="crow"><span class="ck">re</span><span class="cv" id="cRe">-0.60000</span></div>
<div class="crow"><span class="ck">im</span><span class="cv" id="cIm">+0.00000</span></div>
<div class="crow dim"><span class="ck">cursor</span><span class="cv" id="cCur">—</span></div>
</div>
<canvas class="map" id="map" aria-hidden="true"></canvas>
<p class="cap">The <span class="box" role="img" aria-label="amber rectangle"></span> marks where you are on the whole shape. Below about <b>1e-13</b> wide, ordinary 64-bit numbers run out of digits and the picture turns blocky — that is the floor.</p>
</section>
<section class="card" aria-label="Detail controls">
<h2>Detail</h2>
<div class="ctl">
<div class="ctl-top">
<label for="iter">Iteration depth</label><span class="val" id="iterV" aria-hidden="true">200</span>
</div>
<input type="range" id="iter" min="50" max="2000" step="5" value="200">
</div>
<label class="switch" for="autoIter">
<input type="checkbox" id="autoIter" checked>
<span class="track" aria-hidden="true"></span>
<span>Raise depth as I zoom</span>
</label>
<p class="cap">Deeper zooms need more iterations before a point can be called “never leaves”. Too few and fine threads fill in solid black.</p>
<h3 class="sub">Places</h3>
<div class="places" id="places" role="group" aria-label="Preset places">
<button type="button" data-p="0" aria-pressed="true">Full view</button>
<button type="button" data-p="1" aria-pressed="false">Seahorses</button>
<button type="button" data-p="2" aria-pressed="false">Elephants</button>
<button type="button" data-p="3" aria-pressed="false">Triple spiral</button>
<button type="button" data-p="4" aria-pressed="false">Tiny twin</button>
<button type="button" data-p="5" aria-pressed="false">Frost</button>
</div>
<p class="keys">
<kbd>←↑↓→</kbd> pan <kbd>+</kbd> <kbd>−</kbd> zoom <kbd>0</kbd> full view
</p>
</section>
</div>
</main>
<footer>demo · sloprun.dev</footer>
</div>
<div class="sr-only" role="status" aria-live="polite" id="live"></div>
<script>
(function () {
"use strict";
/* ── dom ─────────────────────────────────────────────────────────────────── */
var root = document.documentElement;
var field = document.getElementById("field");
var cv = document.getElementById("c");
var ctx = cv.getContext("2d", { alpha: false, willReadFrequently: true });
var mapCv = document.getElementById("map");
var mapCtx = mapCv.getContext("2d", { alpha: false });
var progEl = document.getElementById("prog");
var progBar = document.getElementById("progBar");
var hintEl = document.getElementById("hint");
var limitEl = document.getElementById("limitChip");
var pill = document.getElementById("pill");
var pillTxt = document.getElementById("pillText");
var sZoom = document.getElementById("sZoom");
var sIter = document.getElementById("sIter");
var sSpan = document.getElementById("sSpan");
var sTime = document.getElementById("sTime");
var cRe = document.getElementById("cRe");
var cIm = document.getElementById("cIm");
var cCur = document.getElementById("cCur");
var iterEl = document.getElementById("iter");
var iterV = document.getElementById("iterV");
var autoEl = document.getElementById("autoIter");
var places = document.getElementById("places");
var live = document.getElementById("live");
/* ── constants ───────────────────────────────────────────────────────────── */
var HOME = { cx: -0.6, cy: 0, span: 3.4 };
var REF_SPAN = 3.4; /* zoom ×1 reference */
var MIN_SPAN = 1e-13; /* double-precision floor */
var MAX_SPAN = 8;
var BAIL = 65536; /* |z|^2 bailout, big for smooth colour */
var LN2 = Math.LN2;
var LUTN = 1024;
var BANDS = 8; /* colour cycles across the whole depth budget */
var BUDGET = 13; /* ms of compute per animation frame */
var PLACES = [
{ n: "Full view", cx: -0.6, cy: 0, span: 3.4 },
{ n: "Seahorses", cx: -0.7453, cy: 0.1127, span: 0.0065 },
{ n: "Elephants", cx: 0.274, cy: 0.0069, span: 0.009 },
{ n: "Triple spiral", cx: -0.088, cy: 0.654, span: 0.006 },
{ n: "Tiny twin", cx: -1.7597, cy: 0, span: 0.08 },
{ n: "Frost", cx: -0.235125, cy: 0.827215, span: 0.004 }
];
/* ── state ───────────────────────────────────────────────────────────────── */
var view = { cx: HOME.cx, cy: HOME.cy, span: HOME.span };
var maxIter = 200;
var autoOn = true;
var W = 0, H = 0, dpr = 1;
var snap = document.createElement("canvas");
var sctx = snap.getContext("2d", { alpha: false });
/* ── colour ──────────────────────────────────────────────────────────────── */
var LR = new Uint8Array(LUTN), LG = new Uint8Array(LUTN), LB = new Uint8Array(LUTN);
var IN_R = 12, IN_G = 13, IN_B = 15; /* the set itself */
var BLANK = "#ffffff"; /* not-yet-drawn ground */
var MAP_SET = [17,17,17], MAP_BG = [227,226,220], MAP_BOX = [192,86,0];
function hexToRgb(h) {
h = String(h).trim();
if (h.charAt(0) === "#") h = h.slice(1);
if (h.length === 3) h = h.charAt(0)+h.charAt(0)+h.charAt(1)+h.charAt(1)+h.charAt(2)+h.charAt(2);
var n = parseInt(h, 16);
if (h.length !== 6 || isNaN(n)) return null;
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}
function tok(name, fallback) {
var v = getComputedStyle(root).getPropertyValue(name);
return hexToRgb(v) || fallback;
}
function mix(a, b, t) {
return [a[0]+(b[0]-a[0])*t, a[1]+(b[1]-a[1])*t, a[2]+(b[2]-a[2])*t];
}
function css(c) { return "rgb(" + (c[0]|0) + "," + (c[1]|0) + "," + (c[2]|0) + ")"; }
function lum(c) { return (0.2126*c[0] + 0.7152*c[1] + 0.0722*c[2]) / 255; }
function buildPalette() {
var bg = tok("--bg", [246,246,243]);
var acc = tok("--accent", [192,86,0]);
var ink = tok("--ink", [26,29,33]);
var mut = tok("--muted", [90,96,104]);
var sur = tok("--surface", [255,255,255]);
var lin = tok("--line", [227,226,220]);
var dark = lum(bg) < 0.45;
/* one cyclic ramp, read straight off the tokens, so it re-tunes per theme:
light → pale grey, amber, ink, slate; dark → slate, amber, cream, grey */
var stops = [lin, acc, ink, mut];
var ns = stops.length;
for (var i = 0; i < LUTN; i++) {
var f = i / LUTN * ns;
var a = Math.floor(f), t = f - a;
var e = t * t * (3 - 2 * t); /* smoothstep: soft bands, no hard edges */
var c = mix(stops[a % ns], stops[(a + 1) % ns], e);
LR[i] = c[0] | 0; LG[i] = c[1] | 0; LB[i] = c[2] | 0;
}
var inner = dark ? mix(bg, [0,0,0], 0.62) : mix(ink, [0,0,0], 0.4);
IN_R = inner[0] | 0; IN_G = inner[1] | 0; IN_B = inner[2] | 0;
BLANK = css(mix(sur, bg, 0.5));
MAP_SET = inner;
/* on dark the line token is nearly as dark as the set — lift the map ground
until both the silhouette and the amber locator box clear 3:1 against it */
MAP_BG = dark ? mix(lin, ink, 0.30) : lin;
MAP_BOX = acc;
mapBase = null;
}
/* ── the kernel ──────────────────────────────────────────────────────────── */
function mandel(cr, ci) {
/* the two big interior lobes have closed forms — skip the loop entirely */
var q = cr - 0.25, y2 = ci * ci;
q = q * q + y2;
if (q * (q + (cr - 0.25)) <= 0.25 * y2) return -1;
var xp = cr + 1;
if (xp * xp + y2 <= 0.0625) return -1;
var zx = 0, zy = 0, zx2 = 0, zy2 = 0, i = 0, m = maxIter;
while (i < m && zx2 + zy2 <= BAIL) {
zy = 2 * zx * zy + ci;
zx = zx2 - zy2 + cr;
zx2 = zx * zx; zy2 = zy * zy;
i++;
}
if (i >= m) return -1;
/* smooth (fractional) iteration count → continuous colour */
var lg = Math.log(zx2 + zy2) * 0.5;
var sm = i + 1 - Math.log(lg / LN2) / LN2;
if (!(sm > 0)) sm = 0;
var t = sm * invL;
t = t - Math.floor(t);
var idx = (t * LUTN) | 0;
return idx >= LUTN ? LUTN - 1 : idx;
}
/* ── progressive renderer ────────────────────────────────────────────────── */
var img = null, data = null, passes = [], pinfo = [], pi = 0, bk = 0;
var rafId = null, workTotal = 1, workDone = 0, tCompute = 0, running = false;
var xmin = 0, ymax = 0, pxs = 1, invL = 1 / 26;
function stopRender() {
if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; }
running = false;
}
function startRender() {
stopRender();
if (!W || !H) return;
invL = 1 / Math.max(18, maxIter / BANDS); /* band width follows the depth */
xmin = view.cx - view.span / 2;
pxs = view.span / W;
ymax = view.cy + (H * pxs) / 2;
/* seed the buffer with whatever is on screen, so untouched areas keep the
zoom/pan preview instead of flashing empty */
img = ctx.getImageData(0, 0, W, H);
data = img.data;
var first = 16;
while (first > 1 && (W / first < 6 || H / first < 6)) first >>= 1;
passes = []; pinfo = [];
for (var s = first; s >= 1; s >>= 1) passes.push(s);
workTotal = 0;
for (var p = 0; p < passes.length; p++) {
var sp = passes[p];
var nx = Math.ceil(W / sp), ny = Math.ceil(H / sp);
var n = nx * ny;
var skip = p === 0 ? 0 : Math.ceil(nx / 2) * Math.ceil(ny / 2);
pinfo.push({ s: sp, nx: nx, n: n });
workTotal += n - skip;
}
if (workTotal < 1) workTotal = 1;
pi = 0; bk = 0; workDone = 0; tCompute = 0; running = true;
progEl.classList.remove("done");
progBar.style.width = "0%";
setPill(false, "rendering 0%");
rafId = requestAnimationFrame(step);
}
function fillBlock(bx, by, s, idx) {
var r, g, b;
if (idx < 0) { r = IN_R; g = IN_G; b = IN_B; }
else { r = LR[idx]; g = LG[idx]; b = LB[idx]; }
var x1 = bx + s; if (x1 > W) x1 = W;
var y1 = by + s; if (y1 > H) y1 = H;
for (var y = by; y < y1; y++) {
var o = (y * W + bx) * 4;
for (var x = bx; x < x1; x++) {
data[o] = r; data[o+1] = g; data[o+2] = b; data[o+3] = 255;
o += 4;
}
}
}
function step() {
rafId = null;
var t0 = performance.now(), dirty = false, guard = 0;
while (pi < passes.length) {
var P = pinfo[pi], s = P.s, nx = P.nx, half = s * 2, n = P.n;
var k = bk, out = false;
while (k < n) {
var bx = (k % nx) * s, by = ((k / nx) | 0) * s;
k++;
if (pi > 0 && (bx % half) === 0 && (by % half) === 0) continue; /* done last pass */
fillBlock(bx, by, s, mandel(xmin + (bx + 0.5) * pxs, ymax - (by + 0.5) * pxs));
workDone++; dirty = true;
if ((++guard & 31) === 0 && performance.now() - t0 >= BUDGET) { out = true; break; }
}
bk = k;
if (out) break;
pi++; bk = 0;
if (performance.now() - t0 >= BUDGET) break;
}
tCompute += performance.now() - t0;
if (dirty) ctx.putImageData(img, 0, 0);
var pct = Math.min(100, Math.round(workDone / workTotal * 100));
progBar.style.width = pct + "%";
if (pi < passes.length) {
setPill(false, "rendering " + pct + "%");
rafId = requestAnimationFrame(step);
} else {
running = false;
progBar.style.width = "100%";
progEl.classList.add("done");
var ms = Math.round(tCompute);
sTime.innerHTML = ms + '<span class="u">ms</span>';
setPill(true, "done · " + ms + " ms");
sctx.drawImage(cv, 0, 0);
cv.setAttribute("aria-label", describe());
}
}
function setPill(ok, text) {
pillTxt.textContent = text;
if (ok) pill.classList.add("on"); else pill.classList.remove("on");
}
/* ── previews: reuse the current pixels while the new view computes ──────── */
function preview(old) {
if (!W || !H) return;
sctx.drawImage(cv, 0, 0); /* freeze what is on screen */
var ns = view.span / W;
var k = (old.span / W) / ns;
var tx = (old.cx - old.span / 2 - (view.cx - view.span / 2)) / ns;
var oldYmax = old.cy + (H * (old.span / W)) / 2;
var ty = (view.cy + (H * ns) / 2 - oldYmax) / ns;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = BLANK;
ctx.fillRect(0, 0, W, H);
ctx.setTransform(k, 0, 0, k, tx, ty);
ctx.drawImage(snap, 0, 0);
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
/* ── view changes ────────────────────────────────────────────────────────── */
function clampSpan(s) { return Math.max(MIN_SPAN, Math.min(MAX_SPAN, s)); }
function autoIterFor(span) {
var z = REF_SPAN / span;
var v = Math.round((120 + 85 * Math.log(z + 1) / LN2) / 5) * 5;
return Math.max(120, Math.min(2000, v));
}
function applyAuto() {
if (!autoOn) return;
var v = autoIterFor(view.span);
if (v !== maxIter) { maxIter = v; iterEl.value = String(v); }
iterV.textContent = String(maxIter);
sIter.textContent = String(maxIter);
}
function setView(cx, cy, span, opts) {
var old = { cx: view.cx, cy: view.cy, span: view.span };
view.cx = cx; view.cy = cy; view.span = clampSpan(span);
preview(old);
applyAuto();
updateReadout();
drawMap();
startRender();
if (!opts || opts.announce !== false) announce();
markPlace();
dismissHint();
}
function zoomAt(cx, cy, factor) {
var span = clampSpan(view.span / factor);
if (span === view.span && Math.abs(cx - view.cx) < span * 1e-12 &&
Math.abs(cy - view.cy) < span * 1e-12) {
/* nothing would change (already at the floor or the ceiling) — but a
pointerdown may have paused a half-finished render, so resume that;
don't restart a render that already completed */
if (!running && pi < passes.length) startRender();
return;
}
setView(cx, cy, span);
}
function reset() {
autoOn = true; autoEl.checked = true;
setView(HOME.cx, HOME.cy, HOME.span);
live.textContent = "Back to the full view.";
}
/* ── readout ─────────────────────────────────────────────────────────────── */
function digits() {
var d = Math.ceil(-Math.log(view.span) / Math.LN10) + 5;
return Math.max(5, Math.min(15, d));
}
function fmtNum(v, d) {
var s = Math.abs(v).toFixed(d);
return (v < 0 ? "-" : "+") + s;
}
function fmtZoom(z) {
if (z < 9.995) return "×" + z.toFixed(2);
if (z < 1000) return "×" + Math.round(z);
var u = ["K", "M", "B", "T"], i = -1;
while (z >= 1000 && i < 3) { z /= 1000; i++; }
return "×" + (z < 10 ? z.toFixed(1) : Math.round(z)) + u[i];
}
function updateReadout() {
var d = digits();
sZoom.textContent = fmtZoom(REF_SPAN / view.span);
sSpan.textContent = view.span.toExponential(2);
sIter.textContent = String(maxIter);
cRe.textContent = fmtNum(view.cx, d);
cIm.textContent = fmtNum(view.cy, d);
var atFloor = view.span <= MIN_SPAN * 1.0001;
limitEl.hidden = !atFloor;
document.getElementById("zinBtn").disabled = atFloor;
document.getElementById("zoutBtn").disabled = view.span >= MAX_SPAN * 0.9999;
}
function describe() {
var d = Math.min(digits(), 9);
return "Fractal view centred at " + fmtNum(view.cx, d) + " " + fmtNum(view.cy, d) +
"i, zoom " + fmtZoom(REF_SPAN / view.span).replace("×", "") +
" times, " + maxIter + " iterations.";
}
var announceT = null;
function announce() {
if (announceT) clearTimeout(announceT);
announceT = setTimeout(function () { live.textContent = describe(); }, 350);
}
function markPlace() {
var btns = places.querySelectorAll("button");
for (var i = 0; i < btns.length; i++) {
var p = PLACES[+btns[i].getAttribute("data-p")];
var on = Math.abs(view.cx - p.cx) < p.span * 1e-3 &&
Math.abs(view.cy - p.cy) < p.span * 1e-3 &&
Math.abs(view.span / p.span - 1) < 1e-3;
btns[i].setAttribute("aria-pressed", on ? "true" : "false");
}
}
/* ── locator map ─────────────────────────────────────────────────────────── */
var MX0 = -2.66, MX1 = 1.14, MY0 = -1.425, MY1 = 1.425;
var mapBase = null, mw = 0, mh = 0;
function sizeMap() {
var r = mapCv.getBoundingClientRect();
var d = Math.min(window.devicePixelRatio || 1, 2);
var w = Math.max(80, Math.round(r.width * d));
var h = Math.max(60, Math.round(r.height * d));
if (w !== mw || h !== mh) { mw = w; mh = h; mapCv.width = mw; mapCv.height = mh; mapBase = null; }
}
function buildMap() {
var im = mapCtx.createImageData(mw, mh), dd = im.data;
var set = MAP_SET, bgc = MAP_BG;
var sx = (MX1 - MX0) / mw, sy = (MY1 - MY0) / mh, o = 0;
for (var j = 0; j < mh; j++) {
var ci = MY1 - (j + 0.5) * sy;
for (var i = 0; i < mw; i++) {
var cr = MX0 + (i + 0.5) * sx;
var q = cr - 0.25, y2 = ci * ci; q = q * q + y2;
var inside;
if (q * (q + (cr - 0.25)) <= 0.25 * y2) inside = true;
else if ((cr + 1) * (cr + 1) + y2 <= 0.0625) inside = true;
else {
var zx = 0, zy = 0, zx2 = 0, zy2 = 0, n = 0;
while (n < 60 && zx2 + zy2 <= 4) {
zy = 2 * zx * zy + ci; zx = zx2 - zy2 + cr;
zx2 = zx * zx; zy2 = zy * zy; n++;
}
inside = n >= 60;
}
var c = inside ? set : bgc;
dd[o] = c[0]; dd[o+1] = c[1]; dd[o+2] = c[2]; dd[o+3] = 255;
o += 4;
}
}
mapBase = im;
}
function drawMap() {
sizeMap();
if (!mw) return;
if (!mapBase) buildMap();
mapCtx.putImageData(mapBase, 0, 0);
var spanY = view.span * H / (W || 1);
var x = (view.cx - view.span / 2 - MX0) / (MX1 - MX0) * mw;
var y = (MY1 - (view.cy + spanY / 2)) / (MY1 - MY0) * mh;
var w = view.span / (MX1 - MX0) * mw;
var h = spanY / (MY1 - MY0) * mh;
mapCtx.strokeStyle = css(MAP_BOX);
mapCtx.lineWidth = Math.max(1.5, mw / 220);
if (w < 7 || h < 7) {
var px = x + w / 2, py = y + h / 2, r = Math.max(4, mw / 40);
mapCtx.beginPath(); mapCtx.arc(px, py, r, 0, Math.PI * 2); mapCtx.stroke();
mapCtx.beginPath();
mapCtx.moveTo(px - r * 2.1, py); mapCtx.lineTo(px - r * 1.2, py);
mapCtx.moveTo(px + r * 1.2, py); mapCtx.lineTo(px + r * 2.1, py);
mapCtx.moveTo(px, py - r * 2.1); mapCtx.lineTo(px, py - r * 1.2);
mapCtx.moveTo(px, py + r * 1.2); mapCtx.lineTo(px, py + r * 2.1);
mapCtx.stroke();
} else {
mapCtx.strokeRect(x, y, w, h);
}
}
/* ── layout ──────────────────────────────────────────────────────────────── */
/* returns true when the drawing buffer actually changed (and was cleared) */
function layout(force) {
var r = field.getBoundingClientRect();
var cw = Math.max(160, Math.round(r.width));
var ch = Math.max(120, Math.round(r.height));
var d = Math.min(window.devicePixelRatio || 1, 2);
var cap = Math.sqrt(1200000 / (cw * ch));
if (d > cap) d = Math.max(1, cap);
var w2 = Math.round(cw * d), h2 = Math.round(ch * d);
if (!force && w2 === W && h2 === H) return false;
dpr = d; W = w2; H = h2;
cv.width = W; cv.height = H;
snap.width = W; snap.height = H;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = BLANK;
ctx.fillRect(0, 0, W, H);
ctx.imageSmoothingEnabled = true;
return true;
}
/* ── pointer: drag to pan, click to zoom ─────────────────────────────────── */
var drag = null;
function localPos(e) {
var r = cv.getBoundingClientRect();
return { x: (e.clientX - r.left) / r.width, y: (e.clientY - r.top) / r.height };
}
function toComplex(fx, fy) {
var spanY = view.span * H / W;
return { re: view.cx + (fx - 0.5) * view.span, im: view.cy - (fy - 0.5) * spanY };
}
cv.addEventListener("pointerdown", function (e) {
if (e.button !== undefined && e.button !== 0) return;
cv.classList.add("ptr-focus");
try { cv.focus({ preventScroll: true }); } catch (err) { cv.focus(); }
stopRender();
sctx.drawImage(cv, 0, 0);
var p = localPos(e);
drag = { id: e.pointerId, sx: p.x, sy: p.y, x: p.x, y: p.y, moved: false, shift: e.shiftKey };
try { cv.setPointerCapture(e.pointerId); } catch (err) {}
e.preventDefault();
});
cv.addEventListener("pointermove", function (e) {
if (drag && e.pointerId !== drag.id) return; /* ignore a second finger */
var p = localPos(e);
if (!drag) {
var c = toComplex(p.x, p.y);
var d = Math.min(digits(), 8);
cCur.textContent = fmtNum(c.re, d) + " " + fmtNum(c.im, d) + "i";
return;
}
drag.x = p.x; drag.y = p.y;
var dx = (p.x - drag.sx), dy = (p.y - drag.sy);
if (!drag.moved && (Math.abs(dx) * W > 3 || Math.abs(dy) * H > 3)) {
drag.moved = true;
cv.classList.add("dragging");
dismissHint();
}
if (drag.moved) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = BLANK;
ctx.fillRect(0, 0, W, H);
ctx.drawImage(snap, dx * W, dy * H);
}
});
function endDrag(e) {
if (!drag) return;
if (e && e.pointerId !== undefined && e.pointerId !== drag.id) return;
var d = drag; drag = null;
cv.classList.remove("dragging");
try { cv.releasePointerCapture(d.id); } catch (err) {}
if (d.moved) {
var spanY = view.span * H / W;
/* put the un-shifted frame back first: preview() takes its own snapshot of
the canvas and applies the pan itself, so leaving the dragged frame in
place would offset the preview twice */
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.drawImage(snap, 0, 0);
setView(view.cx - (d.x - d.sx) * view.span, view.cy + (d.y - d.sy) * spanY, view.span);
} else {
var c = toComplex(d.x, d.y);
var out = d.shift || (e && e.shiftKey);
zoomAt(c.re, c.im, out ? 0.5 : 2);
}
}
cv.addEventListener("pointerup", endDrag);
cv.addEventListener("pointercancel", function (e) {
if (!drag) return;
if (e && e.pointerId !== undefined && e.pointerId !== drag.id) return;
drag = null;
cv.classList.remove("dragging");
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.drawImage(snap, 0, 0); /* undo the live drag offset */
startRender();
});
cv.addEventListener("pointerleave", function () { if (!drag) cCur.textContent = "—"; });
/* ── keyboard ────────────────────────────────────────────────────────────── */
cv.addEventListener("keydown", function (e) {
var k = e.key, spanY = view.span * H / W, s = 0.15, handled = true;
cv.classList.remove("ptr-focus");
if (k === "ArrowLeft") setView(view.cx - view.span * s, view.cy, view.span);
else if (k === "ArrowRight") setView(view.cx + view.span * s, view.cy, view.span);
else if (k === "ArrowUp") setView(view.cx, view.cy + spanY * s, view.span);
else if (k === "ArrowDown") setView(view.cx, view.cy - spanY * s, view.span);
else if (k === "+" || k === "=" || k === "Enter") zoomAt(view.cx, view.cy, 2);
else if (k === "-" || k === "_") zoomAt(view.cx, view.cy, 0.5);
else if (k === "0" || k === "Home") reset();
else handled = false;
if (handled) e.preventDefault();
});
cv.addEventListener("blur", function () { cv.classList.remove("ptr-focus"); });
function dismissHint() {
if (hintEl.hidden || hintEl.classList.contains("gone")) return;
hintEl.classList.add("gone");
setTimeout(function () { hintEl.hidden = true; }, 600);
}
/* ── controls ────────────────────────────────────────────────────────────── */
document.getElementById("zinBtn").addEventListener("click", function () {
zoomAt(view.cx, view.cy, 2);
});
document.getElementById("zoutBtn").addEventListener("click", function () {
zoomAt(view.cx, view.cy, 0.5);
});
document.getElementById("resetBtn").addEventListener("click", reset);
iterEl.addEventListener("input", function () {
maxIter = +iterEl.value;
iterV.textContent = String(maxIter);
sIter.textContent = String(maxIter);
if (autoOn) { autoOn = false; autoEl.checked = false; }
startRender();
});
iterEl.addEventListener("change", function () {
live.textContent = maxIter + " iterations.";
});
autoEl.addEventListener("change", function () {
autoOn = autoEl.checked;
if (autoOn) { applyAuto(); startRender(); live.textContent = "Depth follows the zoom."; }
else live.textContent = "Depth held at " + maxIter + ".";
});
places.addEventListener("click", function (e) {
var b = e.target.closest ? e.target.closest("button[data-p]") : null;
if (!b) return;
var p = PLACES[+b.getAttribute("data-p")];
autoOn = true; autoEl.checked = true;
setView(p.cx, p.cy, p.span, { announce: false });
live.textContent = p.n + " — zoom " + fmtZoom(REF_SPAN / view.span).replace("×", "") + " times.";
});
/* ── theme ───────────────────────────────────────────────────────────────── */
function onThemeChange() {
buildPalette();
drawMap();
startRender();
}
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);
onThemeChange();
}
});
var mq = window.matchMedia ? matchMedia("(prefers-color-scheme: dark)") : null;
if (mq) {
if (mq.addEventListener) mq.addEventListener("change", onThemeChange);
else if (mq.addListener) mq.addListener(onThemeChange);
}
/* ── resize ──────────────────────────────────────────────────────────────── */
var rT = null;
function onResize() {
if (rT) clearTimeout(rT);
rT = setTimeout(function () {
rT = null;
if (layout()) { drawMap(); startRender(); }
else { sizeMap(); drawMap(); }
}, 160);
}
if (window.ResizeObserver) new ResizeObserver(onResize).observe(field);
window.addEventListener("resize", onResize);
/* ── go ──────────────────────────────────────────────────────────────────── */
buildPalette();
layout(true);
applyAuto();
updateReadout();
drawMap();
markPlace();
startRender();
})();
</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.