🎨 Interactive Mandelbrot Set in Rust

A Complete Tutorial Using Macroquad

In this tutorial, we'll build an interactive Mandelbrot set visualizer from scratch. You'll learn about fractals, complex number mathematics, and real-time graphics rendering in Rust.

What You'll Build: A clickable fractal explorer where you can zoom infinitely into the beautiful Mandelbrot set, revealing endless self-similar patterns.

📚 What is the Mandelbrot Set?

The Mandelbrot set is a famous fractal defined by a simple iterative formula. For each point c in the complex plane, we repeatedly apply:

zn+1 = zn² + c
Starting with z0 = 0

If this sequence remains bounded (doesn't escape to infinity), the point c is in the Mandelbrot set. The beautiful colors come from counting how many iterations it takes for points to escape.

🚀 Setup

1Create a New Project

cargo new mandelbrot_zoom
cd mandelbrot_zoom

2Add Dependencies

Edit Cargo.toml and add:

[dependencies]
macroquad = "0.4"

Macroquad is a simple game library that makes it easy to draw graphics and handle input.

🧩 Building the Program Step by Step

Download main.rs if you want to skip the steps below.

3Define Constants

use macroquad::prelude::*;

const WIDTH: usize = 800;
const HEIGHT: usize = 600;
const MAX_ITER: u32 = 256;

WIDTH/HEIGHT: Window dimensions
MAX_ITER: Maximum iterations before we assume a point is in the set (higher = more detail but slower)

4Create the View Structure

struct View {
    center_x: f64,
    center_y: f64,
    zoom: f64,
}

impl View {
    fn new() -> Self {
        Self {
            center_x: -0.5,
            center_y: 0.0,
            zoom: 1.0,
        }
    }
}

The View tracks where we're looking in the complex plane. The Mandelbrot set is centered around (-0.5, 0) in the complex plane.

5Convert Screen Coordinates to Complex Numbers

fn screen_to_complex(&self, x: f32, y: f32) -> (f64, f64) {
    let aspect = WIDTH as f64 / HEIGHT as f64;
    let range = 3.5 / self.zoom;
    
    let real = self.center_x + (x as f64 / WIDTH as f64 - 0.5) * range * aspect;
    let imag = self.center_y + (y as f64 / HEIGHT as f64 - 0.5) * range;
    
    (real, imag)
}

This function maps screen pixels to complex numbers. The aspect ratio ensures circles stay circular, and range controls how much of the complex plane we see (smaller range = more zoomed in).

6Implement the Mandelbrot Algorithm

fn mandelbrot(c_real: f64, c_imag: f64, max_iter: u32) -> u32 {
    let mut z_real = 0.0;
    let mut z_imag = 0.0;
    let mut iter = 0;

    while z_real * z_real + z_imag * z_imag <= 4.0 && iter < max_iter {
        let temp = z_real * z_real - z_imag * z_imag + c_real;
        z_imag = 2.0 * z_real * z_imag + c_imag;
        z_real = temp;
        iter += 1;
    }

    iter
}

How it works:

Complex multiplication: (a + bi)² = (a² - b²) + (2ab)i

7Create Beautiful Colors

fn color_from_iter(iter: u32, max_iter: u32) -> Color {
    if iter == max_iter {
        BLACK  // Point is in the set
    } else {
        let t = iter as f32 / max_iter as f32;
        let hue = t * 360.0;
        
        let s = 0.8;  // Saturation
        let v = if t < 0.5 { t * 2.0 } else { 1.0 };  // Value
        
        // HSV to RGB conversion
        let c = v * s;
        let x = c * (1.0 - ((hue / 60.0) % 2.0 - 1.0).abs());
        let m = v - c;
        
        let (r, g, b) = match (hue / 60.0) as i32 {
            0 => (c, x, 0.0),
            1 => (x, c, 0.0),
            2 => (0.0, c, x),
            3 => (0.0, x, c),
            4 => (x, 0.0, c),
            _ => (c, 0.0, x),
        };
        
        Color::new(r + m, g + m, b + m, 1.0)
    }
}

We use HSV color space to create a smooth rainbow gradient based on escape time. Points in the set are black, while points outside get colors based on how quickly they escape.

8Render the Mandelbrot Set

fn render_mandelbrot(view: &View) -> Image {
    let mut img = Image::gen_image_color(WIDTH as u16, HEIGHT as u16, BLACK);
    
    for y in 0..HEIGHT {
        for x in 0..WIDTH {
            let (c_real, c_imag) = view.screen_to_complex(x as f32, y as f32);
            let iter = mandelbrot(c_real, c_imag, MAX_ITER);
            let color = color_from_iter(iter, MAX_ITER);
            img.set_pixel(x as u32, y as u32, color);
        }
    }
    
    img
}

For every pixel, we convert it to a complex number, calculate its Mandelbrot value, and set its color.

Performance Note: This is intentionally simple. For better performance, consider using parallel iterators with Rayon or GPU shaders.

9Create the Main Loop

#[macroquad::main("Mandelbrot Zoom")]
async fn main() {
    let mut view = View::new();
    let mut texture = Texture2D::from_image(&render_mandelbrot(&view));
    let mut rendering = false;
    
    loop {
        clear_background(BLACK);

        // Left click to zoom in
        if is_mouse_button_pressed(MouseButton::Left) && !rendering {
            rendering = true;
            let (mx, my) = mouse_position();
            let (new_x, new_y) = view.screen_to_complex(mx, my);
            view.center_x = new_x;
            view.center_y = new_y;
            view.zoom *= 2.0;
            
            let img = render_mandelbrot(&view);
            texture = Texture2D::from_image(&img);
            rendering = false;
        }
        
        // Right click to zoom out
        if is_mouse_button_pressed(MouseButton::Right) && !rendering {
            rendering = true;
            let (mx, my) = mouse_position();
            let (new_x, new_y) = view.screen_to_complex(mx, my);
            view.center_x = new_x;
            view.center_y = new_y;
            view.zoom /= 2.0;
            
            let img = render_mandelbrot(&view);
            texture = Texture2D::from_image(&img);
            rendering = false;
        }
        
        // R to reset
        if is_key_pressed(KeyCode::R) && !rendering {
            rendering = true;
            view = View::new();
            let img = render_mandelbrot(&view);
            texture = Texture2D::from_image(&img);
            rendering = false;
        }

        draw_texture_ex(
            &texture,
            0.0, 0.0,
            WHITE,
            DrawTextureParams {
                dest_size: Some(vec2(WIDTH as f32, HEIGHT as f32)),
                ..Default::default()
            },
        );
        
        // UI
        draw_text("Left Click: Zoom In", 10.0, 20.0, 20.0, WHITE);
        draw_text("Right Click: Zoom Out", 10.0, 40.0, 20.0, WHITE);
        draw_text("R: Reset", 10.0, 60.0, 20.0, WHITE);
        draw_text(&format!("Zoom: {:.1}x", view.zoom), 10.0, 80.0, 20.0, WHITE);

        next_frame().await
    }
}

Key concepts:

🎮 Running Your Program

cargo run --release

Use --release for much better performance! Debug builds are very slow for this kind of computation.

🎨 Enhancement Ideas

  1. Smooth coloring: Use fractional iteration counts for gradient smoothing
  2. Different color schemes: Try different color palettes
  3. Julia sets: Similar algorithm but fix c and vary starting z
  4. Parallel rendering: Use Rayon to compute multiple pixels simultaneously
  5. Progressive rendering: Show low-res preview while rendering high-res
  6. Save images: Export your favorite views as PNG files

🔬 Mathematical Deep Dive

Why does this work?

The Mandelbrot set is the set of complex numbers c for which the iteration zn+1 = zn² + c (starting with z0 = 0) remains bounded.

We test for |z| > 2 because if any iterate exceeds this, the sequence will escape to infinity. We square this threshold to avoid square root calculations: |z|² = z_real² + z_imag² > 4.

🐛 Troubleshooting

Program is slow:

Want to learn more?

✨ Conclusion

You've now built a fully interactive fractal explorer! This project combines mathematics, graphics programming, and user interaction. The Mandelbrot set has infinite detail—you can zoom forever and always find new patterns.

The same techniques apply to rendering other fractals like Julia sets, the Burning Ship fractal, or Newton fractals. Experiment and have fun exploring the infinite!