Mandelbrot Explorer

Zoom into the Mandelbrot set in real time, GPU-rendered in your browser. Adjust iterations, cycle colours, switch to Julia sets, export PNG or video.

Mandelbrot Explorer

Live controls

0.32
180
3
0
0.2

Knobs patch the running animation live. A few (marked in the code as baked-in values like particle counts) restart the preview.

Escape-time fractal you can actually dive into. Scroll to zoom.

Your code runs in a sandboxed frame with no access to this page, and it is never sent to a server. three.js demos load the library from jsDelivr; the rest need nothing but the browser.

Pick an animation

All 40 animations →

Canvas 2D
three.js
WebGL
CSS / DOM
Advertisement

Mandelbrot explorer: zoom into the set in your browser, with the shader source open beside it

This is a GPU Mandelbrot renderer. Drag the picture to pan, scroll to zoom, and pull the sliders to change how deep the renderer looks and how the colours cycle. The whole thing is a single WebGL fragment shader running in a sandboxed frame in your browser — no image is fetched from a server, nothing you do is uploaded, and the frame has no access to the page around it. The right–hand panel is not a picture of the code, it is the actual editable source: change a line, hit Run, and the fractal changes.

It renders in real time because every pixel is independent. The GPU runs the same short loop on all of them at once, which is exactly the shape of problem a graphics card is built for.

What the set actually is, without the maths degree

Take a point on the screen. Treat its two coordinates as a single number with a horizontal part and a vertical part — a complex number, call it c. Now start from zero and apply one rule over and over:

z → z² + c

Square the current number, add c, repeat. That is the entire definition. Two things can happen. Either the value stays bounded forever, wandering around near the origin no matter how many times you repeat — or it eventually starts growing and runs away to infinity. The Mandelbrot set is simply the collection of points c for which it stays bounded. Those are drawn black. Everything else escapes.

"Square a complex number" is less exotic than it sounds. Written out in the coordinates the shader actually uses, it is two multiplications and a subtraction:

z = vec2(z.x*z.x - z.y*z.y, 2.0*z.x*z.y) + c;

You can see that line in the shader source on this page, inside a loop of at most 600 passes. Nothing else in the file is doing the fractal work. All the complexity in the image comes out of repeating those few operations.

What the colours mean

The colours are a stopwatch. For every escaping pixel the shader counts how many times it had to apply the rule before the value got large — specifically before |z| passed 16, which is the bailout radius this renderer uses — and paints the pixel according to that count. Points that escape almost immediately get one end of the palette; points that cling on for hundreds of iterations before finally flying off get the other. Points that never escape within your iteration budget are black.

So the bands you see are contour lines of escape speed. They crowd together near the boundary of the set because that is where the behaviour changes fastest, and they are what makes the edge look infinitely fringed rather than like a drawn outline.

There is one refinement worth knowing about, because it is the difference between a good fractal renderer and a crude one. A raw integer iteration count produces visible stair–steps: every pixel that escaped on pass 47 is one flat colour, every pixel that escaped on pass 48 is another. This renderer instead computes a continuous escape count, n + 1 − log(log|z|)/log 2, which turns the integer into a smoothly varying fraction using how far past the bailout radius the value overshot. The contours then blend into each other instead of banding. The colour itself comes from a cosine palette — three cosine waves at different phase offsets for red, green and blue — which is why the gradient loops seamlessly instead of hitting a hard seam when it wraps.

The controls this tool actually gives you

Press Controls under the preview to open the panel. These are the knobs, and their real ranges:

ControlRangeDefaultWhat it does
Drag to pan / scroll to zoomon / offonEnables mouse interaction on the canvas. Turn it off if scroll–wheel zoom is fighting your page scrolling.
Zoom0.1 – 4000.32Magnification. The slider reaches far deeper than the wheel does — see below.
Iterations30 – 600180How many times the rule is applied before the renderer gives up and calls a pixel "inside".
Colour cycles0.5 – 123How many times the palette repeats across the escape range. High values give tight rainbow banding, low values give broad washes.
Hue0 – 3600Rotates the palette. Purely cosmetic; it does not change which pixels are in the set.
Julia set (animated seed)on / offoffSwitches the rendering rule — see the Julia section.
Seed speed0 – 10.2How fast the Julia seed travels around its circle. Set it to 0 to freeze on one shape.

Two things about zoom that will save you confusion. First, the scroll wheel is clamped: each notch multiplies zoom by about 1.12, but the wheel will not take you below 0.15 or above 12. To go deeper than that you drag the Zoom slider, which goes to 400. Second, zooming is anchored to the centre of the canvas, not to your cursor. The working habit is therefore: drag the feature you want until it sits in the middle of the frame, then zoom. Trying to zoom toward a corner just pushes your target off the edge.

Around the preview there are four more buttons that are not fractal controls but are the reason people stay: Save PNG grabs the current frame, Record captures the canvas to a WebM video until you stop it, Fullscreen gives the shader the whole screen, and Console shows anything the code logs or any error it throws if you have been editing.

Iterations: the setting that actually matters

The boundary of the set is infinitely detailed, and the iteration count is your budget for resolving it. This is the single control most people leave alone and then wonder why their deep zoom looks like mud.

Here is the mechanism. To decide a pixel is outside, the renderer only has to watch it escape — which for most pixels happens quickly. To decide a pixel is inside, it has to fail to escape for the entire budget, and then give up and call it black. Near the boundary there are enormous numbers of points that escape only after hundreds of iterations. At a low budget, all of those get lumped in with the genuinely-inside points and painted black. The fine filigree you came to see is drawn as a solid blob.

So: as you zoom in, raise iterations. At the default view 180 is plenty and costs nothing. At a zoom in the tens you will want to be pushing toward the 600 ceiling, and you may notice the frame rate drop, because the cost is roughly linear in the iteration count on every pixel at once. If detail is missing, raise it; if the animation is stuttering and you are not zoomed far in, lower it.

Why deep zooms eventually go blocky, and why that is not a bug

Keep zooming and at some point the smooth curves dissolve into flat rectangular tiles. That is not the renderer failing to iterate enough — raising the iteration count will not fix it.

It is arithmetic precision running out. The shader works in single–precision 32–bit floating point, which carries roughly seven significant decimal digits. Each pixel's c is computed from the screen coordinate divided by the zoom, so as zoom grows, adjacent pixels are asked to represent complex numbers that differ in the eighth or ninth significant digit. Eventually they cannot: neighbouring pixels round to the identical value, get the identical iteration count, and render as one flat block. The picture stops being wrong in a subtle way and starts being visibly quantised.

Every GPU fractal renderer hits this wall. Going deeper requires software arithmetic — double–double or arbitrary precision, with perturbation techniques to keep it affordable — which is a fundamentally different and much slower program. Deep–zoom fractal videos that dive to magnifications of 10100 are made that way, offline, over hours. A live shader is trading depth for the fact that it responds instantly.

Where to point it

The default view is centred at −0.5 on the real axis, which frames the whole set. From there, the landmarks:

  • The main cardioid — the big heart–shaped body on the right. Every point inside it is a value of c whose orbit settles down to a single fixed value.
  • The period–2 bulb — the large circle stuck to its left. Orbits here settle into a two–value cycle instead. Every smaller bulb around the rim corresponds to a longer repeating cycle, and the periods are arranged in a strict order along the boundary.
  • Seahorse valley — the pinch where the cardioid meets the period–2 bulb, at roughly −0.75. This is the best first zoom on the whole set: the spiral seahorse tails get more elaborate the further you go and the structure never repeats exactly.
  • Mini–brots — hunt anywhere along the filaments and you will find complete miniature copies of the entire set, each surrounded by its own decoration. They occur at every scale, which is the self–similarity people mean when they call the set fractal. Note that the copies are not exact: each one is distorted, and the tendrils around it look nothing like the tendrils around the original.
  • The needle — the thin spike running left along the real axis toward −2. It looks one–dimensional at the default zoom and is not.

A good sequence for a first session: leave iterations at 180 and drag until the cardioid/bulb pinch is centred, wheel–zoom in a few notches, raise iterations to about 400, then switch to the Zoom slider and keep centring the feature you want before each step.

The Julia toggle

Flipping Julia set changes which quantity the pixel represents. In the Mandelbrot rendering, each pixel is a different c and every orbit starts at zero. In the Julia rendering, the whole image shares one single c — the seed — and each pixel is a different starting z. Same iteration rule, same escape–time colouring, opposite question.

The seed here is not fixed: it travels around a circle of radius 0.7885 at a rate you set with Seed speed, so the shape morphs continuously as you watch. Set the speed to 0 to hold one form still. The two pictures are tied together — a seed taken from inside the Mandelbrot set gives a connected Julia set, and a seed from outside gives one that has shattered into dust. That correspondence is the reason the Mandelbrot set matters mathematically rather than just decoratively: it is a map of which Julia sets are connected.

Editing the shader

The panel on the right has HTML, CSS, JS and EXPLAIN tabs. The fractal lives in the JS tab, as a GLSL fragment shader inside a template string, alongside the small amount of WebGL plumbing that compiles it and draws a single full–screen triangle. Everything is legible; there is no framework in the way.

  • Edit any pane and press Run, or leave Auto–run ticked and it recompiles as you type.
  • Reset restores the original source. Copy takes the current pane, Download .html gives you a single self–contained file that runs offline, and Share copies a link.
  • If the shader fails to compile, the GLSL error appears in the Console panel with a line number.

Things worth trying: change the bailout test from dot(z, z) > 256.0 to a smaller threshold and watch the smooth colouring break down, because the continuous escape formula assumes the value has comfortably overshot. Swap the palette's phase offsets vec3(0.0, 0.6, 1.0) for other values to get an entirely different colour scheme. Or change the exponent — cube z instead of squaring it and you get a multibrot, which has two–fold rotational symmetry instead of the single cardioid (an exponent of d gives d − 1 lobes).

This renderer is one preset of a larger animation playground; the gallery under the preview switches to the other WebGL, canvas, CSS and three.js demos, all with their source open the same way.

How the renderer works

Everything you see is one fragment shader. The GPU runs it once per pixel, with thousands of pixels evaluated in parallel, and nothing is stored between frames — the colour of a pixel is a pure function of its coordinates, the zoom, and the iteration budget. That is why panning stays interactive instead of taking seconds per frame the way a CPU renderer does.

Complex multiplication, written out

GLSL has no complex number type, so z^2 + c is expanded by hand into a two-component vector:

z = vec2(z.x * z.x - z.y * z.y, 2.0 * z.x * z.y) + c;

The real part is x^2 - y^2 and the imaginary part is 2xy. That single line, repeated, is the entire Mandelbrot set.

Why the loop looks strange

for (int k = 0; k < 600; k++) {
  if (float(k) >= uIter) break;
  ...
}

WebGL 1 requires loop bounds to be compile-time constants — you cannot write k < uIter where uIter is a uniform, because the shader compiler needs to be able to unroll the loop. The workaround is to loop to a fixed maximum and break early on the real limit. The constant 600 is therefore a hard ceiling on the iteration slider, not an arbitrary number.

Escaping without a square root

The escape test is written as dot(z, z) > 256.0 rather than length(z) > 16.0. Both say the same thing, but length computes a square root and this runs on every iteration of every pixel. Comparing squared magnitudes avoids millions of unnecessary square roots per frame.

The generous escape radius of 16 (rather than the mathematically sufficient 2) also improves the smooth-colouring formula: the continuous escape estimate is more accurate the further past the boundary the point has travelled when you sample it.

Precision, performance, and where it breaks down

The zoom wall

GPUs work in 32-bit floating point, which carries roughly 7 significant decimal digits. Every pixel is computed from screen position / zoom, so as the zoom climbs, adjacent pixels start resolving to the same representable number. Around 10^6 to 10^7 magnification the image goes blocky and stops gaining detail no matter how many iterations you allow.

Renderers that go deeper use double precision (available on some hardware at a large speed penalty) or arbitrary-precision arithmetic combined with perturbation theory, where one high-precision reference orbit is computed on the CPU and nearby pixels are calculated as small offsets from it. That is how the famous deep-zoom videos are made, and it is far too slow to be interactive.

What actually costs you frames

Cost scales with pixels multiplied by iterations. Doubling the iteration slider roughly halves the frame rate; so does moving to a display with twice the pixel ratio. Points inside the set are the expensive ones, because they never escape and always run the full loop — which is why zooming into a large black region feels heavier than skimming the boundary.

If the preview stutters, drop the iterations before anything else. You will lose fine boundary detail and gain frame rate immediately.

A note on the black region

Points that never escape are drawn black because the escape count carries no information for them. There are other conventions — colouring by how close the orbit came to the origin, or by the period of the cycle it settled into — which reveal structure inside the set rather than a flat silhouette.

Frequently Asked Questions

How deep can I zoom?+

Until 32-bit floating point precision runs out on the GPU, which in practice is somewhere around 10^6 to 10^7 magnification depending on your hardware. Past that the image goes blocky - neighbouring pixels compute identical values because the numbers can no longer represent the difference between them. That is a hardware limit, not a bug. Renderers that go deeper switch to double precision or arbitrary-precision arithmetic, which is far slower.

Why does the image turn to mush when I zoom in?+

You need more iterations. The boundary of the set is infinitely detailed, and the iteration count is your budget for resolving it - too few and the renderer gives up before it can tell an escaping point from a trapped one, so everything blurs into a single band. Raise the Iterations slider as you descend. The trade-off is frame rate, since every pixel runs the loop.

What is the difference between the Mandelbrot set and a Julia set?+

They are two views of the same equation. For the Mandelbrot set, each pixel supplies the constant c and the sequence always starts at zero. For a Julia set, c is one fixed value for the whole image and each pixel supplies the starting z. Every point in the Mandelbrot set corresponds to a Julia set that is connected; every point outside corresponds to one that is scattered dust. Turning on the Julia toggle animates c around a circle so you can watch the family morph.

Can I save the image?+

Yes. Save PNG captures the current frame at the canvas resolution, and Record captures a WebM video, which is the better choice for a zoom or a Julia morph. Both happen entirely in your browser - nothing is uploaded.

Is this doing the maths on my graphics card?+

Yes. The whole image is a fragment shader, so the escape-time loop runs once per pixel with thousands of pixels computed in parallel on the GPU. That is why panning and zooming stay interactive instead of taking seconds per frame the way a CPU renderer would.

Related tools

This tool is provided for informational and educational purposes only. All processing happens in your browser — no data is sent to or stored on our servers. While we strive for accuracy, we make no warranties about the completeness or reliability of results.