Click anywhere on the image to zoom in — the demo re-renders the Mandelbrot set centered on your click. Keep zooming along the boundary and watch new structure keep appearing.
<p class="hint">Click anywhere on the image to zoom in centered on that point. Zoom along the boundary to watch infinite detail unfold. The black region is inside the set; colors show how quickly the iteration escapes.</p>
<canvas id="canvas" width="480" height="340"></canvas>
<div class="status" id="status">Ready — click to zoom in.</div>
<div class="btns">
<button id="reset" type="button">Reset</button>
<button id="random" type="button" class="ghost">Random spot</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
#canvas { display: block; cursor: crosshair; border-radius: 8px; width: 100%; max-width: 480px; border: 1px solid #cdd9e3; }
.status { font-size: .9rem; font-weight: 600; margin: .5rem 0; min-height: 1.3em; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Mandelbrot set renderer.
// For each pixel (px,py) on the canvas we map to a complex number c = (x + iy)
// in the current viewport, then iterate z -> z^2 + c from z=0.
// If |z| > 2 after at most MAX_ITER steps the point escapes; we color by
// iteration count using a smooth palette. Points that never escape are black (inside the Mandelbrot set).
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const MAX_ITER = 256;
// Default view: x in [-2.5, 1.0], y centered
let view = { cx: -0.7, cy: 0.0, r: 1.6 }; // cx,cy = center; r = half-width
// A set of interesting deep spots to jump to
const SPOTS = [
{ cx: -0.7269, cy: 0.1889, r: 0.003, label: 'Seahorse valley' },
{ cx: -0.1592, cy: 1.0317, r: 0.002, label: 'Lightning spiral' },
{ cx: -1.7687, cy: 0.0042, r: 0.004, label: 'Elephant valley tip' },
{ cx: -0.5251, cy: 0.5268, r: 0.005, label: 'Filament junction' },
];
function palette(t) {
// t in [0,1]: smooth gradient through hues
const r = Math.round(9*(1-t)*t*t*t*255);
const g = Math.round(15*(1-t)*(1-t)*t*t*255);
const b = Math.round(8.5*(1-t)*(1-t)*(1-t)*t*255);
return [r, g, b];
}
function render() {
const img = ctx.createImageData(W, H);
const { cx, cy, r } = view;
const aspect = H / W;
const xMin = cx - r, xMax = cx + r;
const yMin = cy - r * aspect, yMax = cy + r * aspect;
for (let py = 0; py < H; py++) {
const ci = yMin + (py / H) * (yMax - yMin);
for (let px = 0; px < W; px++) {
const cr = xMin + (px / W) * (xMax - xMin);
// iterate z = z^2 + c
let zr = 0, zi = 0, n = 0;
while (n < MAX_ITER) {
const zr2 = zr * zr, zi2 = zi * zi;
if (zr2 + zi2 > 4) break;
zi = 2 * zr * zi + ci;
zr = zr2 - zi2 + cr;
n++;
}
const idx = (py * W + px) * 4;
if (n === MAX_ITER) {
// inside: black
img.data[idx] = img.data[idx+1] = img.data[idx+2] = 0;
} else {
// smooth coloring
const t = n / MAX_ITER;
const [red, green, blue] = palette(t);
img.data[idx] = red;
img.data[idx+1] = green;
img.data[idx+2] = blue;
}
img.data[idx+3] = 255;
}
}
ctx.putImageData(img, 0, 0);
}
function setStatus(msg) { document.getElementById('status').textContent = msg; }
canvas.addEventListener('click', function(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const px = (e.clientX - rect.left) * scaleX;
const py = (e.clientY - rect.top) * scaleY;
const aspect = H / W;
const xMin = view.cx - view.r, xMax = view.cx + view.r;
const yMin = view.cy - view.r * aspect, yMax = view.cy + view.r * aspect;
view.cx = xMin + (px / W) * (xMax - xMin);
view.cy = yMin + (py / H) * (yMax - yMin);
view.r *= 0.35; // zoom in ~3×
setStatus(fmt('Zoom ×%round% — center (%cx%, %cy%i)', { round: Math.round(1.6 / view.r), cx: view.cx.toFixed(6), cy: view.cy.toFixed(6) }));
render();
});
document.getElementById('reset').addEventListener('click', function() {
view = { cx: -0.7, cy: 0.0, r: 1.6 };
setStatus('Reset to full view.');
render();
});
document.getElementById('random').addEventListener('click', function() {
const s = SPOTS[Math.floor(Math.random() * SPOTS.length)];
view = { cx: s.cx, cy: s.cy, r: s.r };
setStatus(fmt('Jumped to: %label%', { label: s.label }));
render();
});
render();
console.log(fmt('Mandelbrot set rendered. MAX_ITER=%MAX_ITER%, initial view r=%r%', { MAX_ITER: MAX_ITER, r: view.r }));
<p class="hint">Click anywhere on the image to zoom in centered on that point. Zoom along the boundary to watch infinite detail unfold. The black region is inside the set; colors show how quickly the iteration escapes.</p>
<canvas id="canvas" width="480" height="340"></canvas>
<div class="status" id="status">Ready — click to zoom in.</div>
<div class="btns">
<button id="reset" type="button">Reset</button>
<button id="random" type="button" class="ghost">Random spot</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
#canvas { display: block; cursor: crosshair; border-radius: 8px; width: 100%; max-width: 480px; border: 1px solid #cdd9e3; }
.status { font-size: .9rem; font-weight: 600; margin: .5rem 0; min-height: 1.3em; color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Mandelbrot set renderer.
// For each pixel (px,py) on the canvas we map to a complex number c = (x + iy)
// in the current viewport, then iterate z -> z^2 + c from z=0.
// If |z| > 2 after at most MAX_ITER steps the point escapes; we color by
// iteration count using a smooth palette. Points that never escape are black (inside the Mandelbrot set).
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const MAX_ITER = 256;
// Default view: x in [-2.5, 1.0], y centered
let view = { cx: -0.7, cy: 0.0, r: 1.6 }; // cx,cy = center; r = half-width
// A set of interesting deep spots to jump to
const SPOTS = [
{ cx: -0.7269, cy: 0.1889, r: 0.003, label: 'Seahorse valley' },
{ cx: -0.1592, cy: 1.0317, r: 0.002, label: 'Lightning spiral' },
{ cx: -1.7687, cy: 0.0042, r: 0.004, label: 'Elephant valley tip' },
{ cx: -0.5251, cy: 0.5268, r: 0.005, label: 'Filament junction' },
];
function palette(t) {
// t in [0,1]: smooth gradient through hues
const r = Math.round(9*(1-t)*t*t*t*255);
const g = Math.round(15*(1-t)*(1-t)*t*t*255);
const b = Math.round(8.5*(1-t)*(1-t)*(1-t)*t*255);
return [r, g, b];
}
function render() {
const img = ctx.createImageData(W, H);
const { cx, cy, r } = view;
const aspect = H / W;
const xMin = cx - r, xMax = cx + r;
const yMin = cy - r * aspect, yMax = cy + r * aspect;
for (let py = 0; py < H; py++) {
const ci = yMin + (py / H) * (yMax - yMin);
for (let px = 0; px < W; px++) {
const cr = xMin + (px / W) * (xMax - xMin);
// iterate z = z^2 + c
let zr = 0, zi = 0, n = 0;
while (n < MAX_ITER) {
const zr2 = zr * zr, zi2 = zi * zi;
if (zr2 + zi2 > 4) break;
zi = 2 * zr * zi + ci;
zr = zr2 - zi2 + cr;
n++;
}
const idx = (py * W + px) * 4;
if (n === MAX_ITER) {
// inside: black
img.data[idx] = img.data[idx+1] = img.data[idx+2] = 0;
} else {
// smooth coloring
const t = n / MAX_ITER;
const [red, green, blue] = palette(t);
img.data[idx] = red;
img.data[idx+1] = green;
img.data[idx+2] = blue;
}
img.data[idx+3] = 255;
}
}
ctx.putImageData(img, 0, 0);
}
function setStatus(msg) { document.getElementById('status').textContent = msg; }
canvas.addEventListener('click', function(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const px = (e.clientX - rect.left) * scaleX;
const py = (e.clientY - rect.top) * scaleY;
const aspect = H / W;
const xMin = view.cx - view.r, xMax = view.cx + view.r;
const yMin = view.cy - view.r * aspect, yMax = view.cy + view.r * aspect;
view.cx = xMin + (px / W) * (xMax - xMin);
view.cy = yMin + (py / H) * (yMax - yMin);
view.r *= 0.35; // zoom in ~3×
setStatus(fmt('Zoom ×%round% — center (%cx%, %cy%i)', { round: Math.round(1.6 / view.r), cx: view.cx.toFixed(6), cy: view.cy.toFixed(6) }));
render();
});
document.getElementById('reset').addEventListener('click', function() {
view = { cx: -0.7, cy: 0.0, r: 1.6 };
setStatus('Reset to full view.');
render();
});
document.getElementById('random').addEventListener('click', function() {
const s = SPOTS[Math.floor(Math.random() * SPOTS.length)];
view = { cx: s.cx, cy: s.cy, r: s.r };
setStatus(fmt('Jumped to: %label%', { label: s.label }));
render();
});
render();
console.log(fmt('Mandelbrot set rendered. MAX_ITER=%MAX_ITER%, initial view r=%r%', { MAX_ITER: MAX_ITER, r: view.r }));
Comments
Loading comments...