DEV Community

Cover image for How I Built a 60 FPS Real-Time Fractal Explorer in WebGL 2.0 & Vectorized NumPy Library for 27 Complex Systems

How I Built a 60 FPS Real-Time Fractal Explorer in WebGL 2.0 & Vectorized NumPy Library for 27 Complex Systems

How I Built a 60 FPS Real-Time Fractal Explorer in WebGL 2.0 & Vectorized NumPy Library for 27 Complex Systems ๐ŸŒ€โšก

Fractals are infinite mathematical universes generated by deceptively simple rules. Most of us have seen the classical Mandelbrot set ($z_{n+1} = z_n^2 + c$), but the mathematical universe of dynamical systems is vastly largerโ€”spanning non-analytic systems, rational physics maps, and complex analytic continuations.

Over the past week, I set out to build FantasticFractals: a dual-engine project consisting of:

  1. A 60 FPS interactive web visualizer powered by custom WebGL 2.0 GPU fragment shaders running in under 3 milliseconds per frame.
  2. A standalone Python scientific library on PyPI (fantasticfractals) with vectorized NumPy compute kernels, Hausdorff box-counting dimension analysis, and 16K Ultra-HD export.

FantasticFractals Mandelbrot Real-Time WebGL 2.0 Explorer
The Mandelbrot set rendered in real-time at 60 FPS with 1ms GPU compute using the Deep Abyss palette and uniform evolution timeline.

Here is the engineering journey of how it was built, why standard canvas loops melt the CPU, and how GLSL shaders and NumPy array broadcasting make infinite mathematical art run seamlessly.


๐Ÿ’ฅ The Problem: Why Traditional Canvas Loops Fail

At 1080p resolution ($1920 \times 1080$), a single frame contains 2,073,600 pixels.

If you calculate an escape-time fractal with an iteration ceiling of 250 steps, computing one frame on a standard CPU thread requires up to 518,400,000 operations.

CPU Canvas 2D Loop:
  For every Y (1080):
    For every X (1920):
       While |z| < 2 and n < 250:
          z = z^2 + c
       -> Takes 8,000ms - 12,000ms (10+ seconds of UI freezing!)
Enter fullscreen mode Exit fullscreen mode

When I tested this with serverless edge functions and CPU workers, dragging the canvas or zooming triggered severe lag (10+ seconds per render). It was completely unusable for interactive exploration.


โšก The Solution: Hardware GPU Acceleration via WebGL 2.0 (2ms Compute)

To achieve silky-smooth 60 frames per second, we offloaded the entire mathematical calculation to the GPU using WebGL 2.0 fragment shaders.

In WebGL, your graphics card executes thousands of pixel computations concurrently across its stream processors. Instead of computing pixels one by one in a serial loop, every pixel on your screen computes its own escape trajectory simultaneously in under 3 milliseconds.

Interactive Julia Set with Dynamic Seed Constants
Interactive Julia Set Explorer: Scrubbing the complex seed constant $C = -0.7 + 0.2702i$ smoothly morphs the attractor in real time.

The Core GLSL Escape Loop

Here is how the complex arithmetic executes directly inside our fragment shader:

#version 300 es
precision highp float;

in vec2 vUv;
out vec4 fragColor;

uniform vec2 u_center;
uniform float u_zoom;
uniform int u_max_iterations;
uniform sampler2D u_palette_lut;

void main() {
    // Map normalized screen coordinates to complex plane
    vec2 c = u_center + (vUv - 0.5) * (3.0 / u_zoom);
    vec2 z = c;

    int iter = 0;
    float smoothVal = 0.0;

    for (int i = 0; i < 500; i++) {
        if (i >= u_max_iterations) break;

        // Complex multiplication: (x + iy)^2 = (x^2 - y^2) + i(2xy)
        float x2 = z.x * z.x;
        float y2 = z.y * z.y;

        if (x2 + y2 > 4.0) {
            // Smooth continuous exterior potential:
            // nu = n + 1 - ln(ln|z|) / ln(2)
            float log_zn = log(x2 + y2) / 2.0;
            float nu = log(log_zn / log(2.0)) / log(2.0);
            smoothVal = float(i) + 1.0 - nu;
            iter = i;
            break;
        }

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

    // Sample color from precomputed 256-color LUT texture
    float u = fract(smoothVal / 40.0);
    fragColor = texture(u_palette_lut, vec2(u, 0.5));
}
Enter fullscreen mode Exit fullscreen mode

By uploading a 1D palette Lookup Table (LUT) as a GPU texture, color interpolation and continuous shading happen at native hardware speed.


๐ŸŒ€ Beyond Mandelbrot: 27 Mathematical Fractals

Most visualizers stop at Mandelbrot and Julia. FantasticFractals provides complete analytical support for 27 distinct mathematical fractal systems organized into 7 mathematical families:

Burning Ship Non-Analytic Fractal at 500 Iterations
The Burning Ship non-analytic fractal rendered with 500 iterations in Prismatic Spectra palette.

Category Famous Examples Mathematical Formulation
Classical Escape Mandelbrot, Julia $z_{n+1} = z_n^2 + c$
Non-Analytic Burning Ship, Buffalo, Celtic $z_{n+1} = (
Conjugate Escape Tricorn (Mandelbar), Multibar $z_{n+1} = \bar{z}_n^2 + c$
Higher Order Multibrot 3, Multibrot 4 $z_{n+1} = z_n^d + c$ ($d=3, 4$)
Convergence Basins Newton-Raphson, Nova $z_{n+1} = z_n - R \frac{z_n^3 - 1}{3z_n^2} + c$
Physics & Rational Magnet 1 & 2 (Potts model), Feather $z_{n+1} = \left(\frac{z^2 + c - 1}{2z + c - 2}\right)^2$
Geometric & Dynamical Sierpinski, Koch, Collatz, Lyapunov Analytic continuation of $3n+1$ into $\mathbb{C}$

๐ŸŽฌ The Evolution Slideshow Engine

Fractals are iterative limits ($n \to \infty$). But what does a fractal look like at iteration 1? At iteration 5? At iteration 35?

We built a uniform Evolution Slideshow Engine that lets you scrub through time:

  • As the iteration ceiling steps forward ($1 \to 2 \to 3 \to \dots \to 250$), you watch geometric boundaries fold, branch, and crystallize into chaotic attractors.
  • Includes an interactive scrubber, variable speed playback (5 to 60 iterations/sec), and Ping-Pong oscillation mode.

๐Ÿ Building the Python Scientific Library (pip install fantasticfractals)

To bring these mathematical systems to data science notebooks, research pipelines, and high-resolution rendering, we packaged a standalone Python library and published it to PyPI:

pip install fantasticfractals
Enter fullscreen mode Exit fullscreen mode

1. Vectorized NumPy Broadcasting

Instead of slow nested Python loops, algorithms are implemented using NumPy array broadcasting:

from fantasticfractals import render_fractal, export_image

# Render a 4K Burning Ship wallpaper
img = render_fractal(
    fractal_type="burning_ship",
    width=3840,
    height=2160,
    palette="cyberpunk",
    max_iterations=300
)
img.save("burning_ship_4k.png")
Enter fullscreen mode Exit fullscreen mode

2. Empirical Hausdorff / Minkowski-Bouligand Box Dimension

The library includes automated box-counting dimension analysis:

from fantasticfractals import FractalEngine, compute_box_dimension

engine = FractalEngine()
grid = engine.render_raw("sierpinski_triangle", width=1024, height=1024, max_iter=25)
dim, r_squared, scales, counts = compute_box_dimension(grid)

print(f"Calculated Box Dimension: {dim:.4f} (Rยฒ = {r_squared:.4f})")
# Expected for Sierpinski Triangle: log(3)/log(2) โ‰ˆ 1.5850
Enter fullscreen mode Exit fullscreen mode

๐ŸŒ Try It Out (Links & Open Source)

Everything is open source under the MIT License:

If you enjoy mathematical art and creative coding, feel free to give the repository a star or try zooming into the Burning Ship antenna! What fractal system should we implement next? Let me know in the comments below!

Top comments (0)