Mandelbrot, Julia, and Burning Ship Fractals in a Progressive-Render Canvas
Each pixel runs an iteration loop of
z ā z² + cuntil it escapes or hits the max. The iteration count becomes the color. That's the whole Mandelbrot algorithm. Julia is the same loop withzandcswapped. Burning Ship addsMath.absto the recurrence. Three legendary fractals, less than 30 lines of actual math.
Fractals are the kind of thing that look complicated until you implement them, then look almost offensively simple. Three lines inside a while loop produce the most-famous image in mathematics.
š Live demo: https://sen.ltd/portfolio/fractals/
š¦ GitHub: https://github.com/sen-ltd/fractals
Features:
- 3 fractal types: Mandelbrot, Julia, Burning Ship
- Canvas-based rendering with 5 color palettes
- Click-drag zoom with rectangle selection
- Scroll to zoom, drag to pan
- Progressive rendering (low-res preview ā full-res)
- Preset views (seahorse valley, mini Mandelbrot, elephant valley)
- PNG export
- Japanese / English UI
- Zero dependencies, 60 tests
The Mandelbrot iteration
export function mandelbrot(cx, cy, maxIter) {
let x = 0, y = 0;
let iter = 0;
while (x*x + y*y < 4 && iter < maxIter) {
const xt = x*x - y*y + cx; // real part of z² + c
y = 2*x*y + cy; // imaginary part
x = xt;
iter++;
}
return iter;
}
At each pixel (cx, cy), iterate z_{n+1} = z_n² + c starting from z_0 = 0. If |z| ever exceeds 2 (so |z|² > 4), the point escapes and is not in the Mandelbrot set. The number of iterations before escape gives the color ā points near the boundary take longer to escape than points far from it.
The Julia twist
The Julia set is the same recurrence, but with c held constant and z_0 = pixel:
export function julia(zx, zy, cx, cy, maxIter) {
let x = zx, y = zy;
let iter = 0;
while (x*x + y*y < 4 && iter < maxIter) {
const xt = x*x - y*y + cx;
y = 2*x*y + cy;
x = xt;
iter++;
}
return iter;
}
For any point c in the Mandelbrot set, there's a corresponding connected Julia set. Picking c at the edge of the Mandelbrot produces visually interesting Julia shapes. Classic choices: c = -0.7 + 0.27015i, c = 0.285 + 0.01i, c = -0.835 - 0.2321i.
Burning Ship uses abs
const xt = x*x - y*y + cx;
y = Math.abs(2*x*y) + cy; // abs here
x = Math.abs(xt); // abs here
Taking absolute value before recurrence produces a fractal that looks like a ship with rising flames. Discovered in 1992, much younger than Mandelbrot and Julia.
Progressive rendering
Rendering a high-res fractal image can take a second or two. Users expect feedback faster than that. Solution: render at 1/8 resolution first, display it stretched, then compute full res:
async function render() {
// Pass 1: 1/8 resolution for instant preview
await renderFractal(width / 8, height / 8);
drawScaled(); // scale up to full canvas size
// Pass 2: full resolution in chunks
await renderFractal(width, height, { chunked: true });
}
async function renderFractal(w, h, { chunked = false } = {}) {
const CHUNK_SIZE = 50; // rows per chunk
for (let y = 0; y < h; y += CHUNK_SIZE) {
const endY = Math.min(y + CHUNK_SIZE, h);
for (let py = y; py < endY; py++) {
for (let px = 0; px < w; px++) {
// compute pixel
}
}
if (chunked) await new Promise(r => setTimeout(r, 0));
}
}
Splitting work into chunks with await setTimeout(0) between them keeps the UI responsive. The canvas updates visibly row-by-row, which looks cool and signals progress.
Color palettes
Points inside the set (didn't escape within maxIter) are black. Points outside are colored by their escape count, mapped to a palette function:
export function classic(iter, maxIter) {
if (iter === maxIter) return { r: 0, g: 0, b: 0 };
const t = iter / maxIter;
return {
r: Math.floor(9 * (1 - t) * t * t * t * 255),
g: Math.floor(15 * (1 - t)*(1 - t) * t * t * 255),
b: Math.floor(8.5 * (1 - t)*(1 - t)*(1 - t) * t * 255),
};
}
These are Bezier-like weightings that produce smooth color gradients. Different palette functions give totally different aesthetics from the same underlying field.
Test vector notes
Two "known" points actually escape under the standard algorithm:
-
c = (-2, 0)ā the leftmost cusp of the Mandelbrot set. Theoretically on the boundary, but the escape-time algorithm classifies it as escaped (thewhilecondition checks|z|² < 4strictly). -
z = (0, 0)for Julia withc = (-0.7, 0.27015)ā not in the filled Julia set, escapes around iteration 96.
Reading Wikipedia's list of "points in the Mandelbrot set" vs "what the escape-time algorithm says" can trip you up. The tests verify the algorithm, not the pure mathematical membership.
Series
This is entry #59 in my 100+ public portfolio series.
- š¦ Repo: https://github.com/sen-ltd/fractals
- š Live: https://sen.ltd/portfolio/fractals/
- š¢ Company: https://sen.ltd/

Top comments (0)