This project lets you zoom into, modify, and explore several fractal formulas in the browser. The standard renderer is fast and interactive, but it still hits floating point precision limits at relatively shallow zoom depths. An experimental deep zoom path is now wired in on top of ShaderPad and GMP-WASM.
This project is just for fun and learning. Pull requests are welcome as long as the app stays performant and fun to explore.
- Julia, Mandelbrot, Burning Ship, and Mandala formulas, each at any integer exponent from 2 to 16
- Arbitrary-depth zoom via perturbation theory
- Keyboard, mouse, and touch controls
- Animated palettes, stripe-average coloring, slope shading, distance-estimate anti-aliasing
- URL hash persistence for shareable views
- PNG frame export
- Works offline as a PWA
Rendering is a two-pass pipeline: an iteration pass writes a per-pixel metric (smooth escape time, stripe value, packed visual data, coverage) to an RGBA32F framebuffer, and a display pass maps it through the palette, lighting, and an ACES tone map. Palette animation and visual toggles only re-run the cheap display pass.
Past the deep-zoom threshold the iteration pass switches to perturbation: gmp-wasm computes a high-precision reference orbit (uploaded as a texture), and the shader iterates only each pixel's tiny delta from it, carried in mantissa/exponent form so it survives any depth.
- Quadratic Mandelbrot and Julia take the fast path: Newton-on-period reference search plus hierarchical bivariate linear approximation (BLA), which skips up to 2048 iterations at a time under a rigorous validity bound.
- Burning Ship and Mandala perturb their
|z|folds exactly (diffabs, correct across fold crossings); exponents above 2 use the binomial expansion of(Z + Ξ΄)α΄Ί β Zα΄Ί. These variants use grid-sampled references and scalar perturbation, with Brent cycle detection so interior pixels exit early. - The fractal type and exponent are baked into the generated shader source β fully unrolled, branch-free inner loops with literal coefficients. Changing either recreates the renderer (ShaderPad recreation on a shared canvas is nearly free).
- While you zoom or pan, the display pass reprojects the last finished metric frame (with bilinear color blending) and fresh iteration passes are scheduled only as the view outgrows it, so interaction stays continuous even when one iteration pass takes seconds.
Z/Shift+Z: zoom in / out- Arrow keys: pan (
Shiftfor larger steps) F/E: change fractal type / exponentR/I: change the constant's real / imaginary component (Julia and Mandala)C/G: change palette / color densityA: toggle stripe-average coloringN,L,K,J: slope shading toggle, light angle, height, intensityQ: change escape radiusD: change render densityS: change animation speed;Spacepauses;Shift+SpacereversesO/X: return to origin / reset everythingEnter: export the current frame as PNGP: toggle the profiler overlay?: show help- Hold
Shiftwith any stepped control above to reverse or shrink the step
- Click / tap: set the image center
- Scroll / swipe: zoom
- Multi-finger swipe: change palette and tweak parameters
src/main.jsβ application state, URL persistence, inputs, renderer lifecycle, zoom-preview schedulingsrc/standardShader.js/src/perturbationShader.jsβ generators for the iteration pass (standard float and deep perturbation variants)src/deepDisplayShader.jsβ display/compositor passsrc/shaderCommon.jsβ GLSL shared by all passes (metric packing, coloring math)src/deepZoom.js/src/gmpUtils.jsβ reference-orbit search and high-precision arithmeticsrc/deepZoomTables.jsβ BLA hierarchy and orbit prefix tables
git clone git@github.com:rileyjshaw/fractal.git
cd fractal
npm install
npm run dev # local dev server
npm test # unit tests (perturbation math, shader structure, GMP orbits)
npm run build # production build- ShaderPad β fullscreen WebGL2 rendering, uniforms, textures, framebuffers
- gmp-wasm β arbitrary-precision reference orbits (MPFR)
- tween.js β camera easing
- tinykeys β keyboard shortcuts
- Vite β dev server and bundling
- Variants without BLA (Burning Ship, Mandala, and exponents above 2) run every iteration scalar, so they render slower than quadratic Mandelbrot/Julia at equal depth; per-iteration cost also grows with the exponent.
- Reference-orbit computation runs on the main thread and can take seconds at extreme depth.
- Pan/center state ultimately derives from JS doubles; true arbitrary-depth navigation needs a high-range view state.
