Julia Set
A real-time WebGL fractal explorer. Every pixel iterates z ↦ z² + c until it escapes or survives long enough to be considered "inside". Drag to pan, scroll to zoom, and tweak c below — or pick it straight from the Mandelbrot set.
Your browser doesn't support WebGL, so this simulation can't run here.
Pick c from the Mandelbrot set
Click anywhere on the mini map to set c to that point. The Julia set for a given c is connected exactly when c lies inside the Mandelbrot set (the dark region) — points picked from the wispy filaments outside it produce dust-like, disconnected Julia sets.
z_(n+1) = z_n² + c
Introduced by Gaston Julia in 1918, the Julia set of a complex parameter c is the boundary between points that stay bounded under repeated squaring-and-adding and points that fly off to infinity. Each pixel here is colored by an escape-time count — how many iterations it survives before crossing the bailout radius — smoothed to avoid banding. Filled-in points that never escape (within the iteration budget) are rendered as the dark background.
How this page works
The fractal is computed entirely on the GPU, one pixel at a time, inside a WebGL fragment shader — there's no per-pixel loop on the CPU. Here's the code broken into its main blocks.
1. Map each pixel to a point in the complex plane
The canvas is a single fullscreen quad (two triangles, four vertices). The vertex shader just
forwards each corner's [-1, 1]
coordinate to the fragment shader as vUv.
The fragment shader then turns that into a complex number using the current pan/zoom state:
vec2 coord = uCenter + vec2(vUv.x * uAspect, vUv.y) * uScale;
2. Simulate the escape: iterate z ↦ z² + c until it flies off
For the main view, z starts
at that pixel's coordinate and c is
the slider/preset value — that's the Julia set. For the Mandelbrot mini-map it's flipped:
z starts at 0 and
c is the pixel's coordinate.
Both share one shader, switched by a uMode uniform.
This loop runs independently for every pixel, in parallel, every frame — that's the "simulation":
for (int i = 0; i < MAX_ITERATIONS; i++) {
if (float(i) >= uMaxIter) break;
float x2 = z.x * z.x - z.y * z.y + c.x;
float y2 = 2.0 * z.x * z.y + c.y;
z = vec2(x2, y2);
if (dot(z, z) > 4.0) break; // escaped past radius 2
iter += 1.0;
} 3. Turn the iteration count into a color
A raw integer count bands harshly, so it's smoothed with a logarithmic correction
(smoothIter) and fed through
a 5-stop gradient (palette()).
Pixels that never escape within the iteration budget are painted the solid dark background instead.
4. Drive the shader from TypeScript
createFractalRenderer() compiles
and links the shader once per canvas, then exposes a single
draw(uniforms) call that resizes
the canvas, uploads c,
center,
scale and the rest as
uniforms, and issues one draw call. The exact same function renders both the main canvas
(mode 0) and the Mandelbrot mini-map (mode 1) — only the uniforms passed in differ.
A requestAnimationFrame loop
redraws the main canvas every frame. Pointer drag events convert pixel deltas into a
complex-plane pan; the wheel handler zooms by scaling
scale while re-solving
center so the point under the
cursor doesn't move. Sliders and preset buttons just update local variables that get
re-uploaded next frame, the "Animate c" checkbox walks c around a fixed-radius circle over
time, and clicking the mini-map converts the click position back into a complex number to
set c directly.