thecodingidiot.com

The InfiniteThe View

The View

Implement the viewport: the mapping between pixel coordinates and complex coordinates, with pan and zoom. Replace the hard-coded coordinate range from the previous page with a proper camera model.


What the viewport does

The Mandelbrot set lives in the complex plane. The interesting region runs from roughly −2.5 to 1.0 on the real axis and −1.25 to 1.25 on the imaginary axis. The screen is 800 × 600 pixels. The viewport maps one to the other.

The mapping has two parameters:

  • Centre — which complex coordinate is at the centre of the screen. (center_re, center_im).
  • Scale — how many complex units fit across the width of the screen. Starting scale: 3.5 / WIDTH (the 3.5 covers the interesting range).

Given centre and scale, the complex coordinate for pixel (x, y) is:

re=center_re+(xW2)scale\text{re} = \text{center\_re} + \left(x - \tfrac{W}{2}\right) \cdot \text{scale}

im=center_im+(yH2)scale\text{im} = \text{center\_im} + \left(y - \tfrac{H}{2}\right) \cdot \text{scale}

Zoom multiplies scale by a factor. scale *= 0.9 zooms in (smaller scale = fewer complex units per pixel = more detail). scale *= 1.1 zooms out.

Pan shifts the centre. center_re += dre * scale moves it in complex coordinates proportional to the current zoom level, so panning speed is consistent regardless of zoom depth.


view.c

Replace the stub from the previous page:

#include "view.h"
 
void view_init(view_t *v)
{
    v->center_re = -0.5;      /* set spans -2.5 to 1.0; -0.5 centres it on screen */
    v->center_im = 0.0;
    v->scale = 3.5 / WIDTH;   /* 3.5 complex units covers the interesting width */
}
 
void view_pan(view_t *v, double dre, double dim)
{
    v->center_re += dre * WIDTH * v->scale;   /* dre=0.1 shifts 10% of screen width */
    v->center_im += dim * HEIGHT * v->scale;
}
 
void view_zoom(view_t *v, double factor)
{
    v->scale *= factor;   /* factor < 1 zooms in, factor > 1 zooms out */
}
 
double pixel_to_re(int x, view_t *v)
{
    /* pixel offset from screen centre, scaled to complex units, shifted to viewport */
    return (v->center_re + (x - WIDTH / 2) * v->scale);
}
 
double pixel_to_im(int y, view_t *v)
{
    return (v->center_im + (y - HEIGHT / 2) * v->scale);
}

pixel_to_re and pixel_to_im. These implement the formulas from the section above. (x - WIDTH / 2) is the pixel's horizontal distance from the screen centre — negative on the left half, zero at the centre column, positive on the right. Multiplying by v->scale converts that pixel distance into complex units. Adding v->center_re shifts the result into the current viewport position. pixel_to_im does the same on the vertical axis.

view_init. The centre starts at (-0.5, 0.0) rather than the origin because the Mandelbrot set is not symmetric on the real axis: it extends from roughly −2.5 on the left to 1.0 on the right, with the bulk of its structure to the left of zero. The scale 3.5 / WIDTH gives each pixel a width of 3.5/8000.0043.5 / 800 \approx 0.004 complex units — exactly enough to show the full interesting region at startup.

view_pan. dre and dim are normalised deltas — 0.1 means "move 10% of the screen". Multiplying by WIDTH * v->scale converts the fraction into complex units at the current zoom level. At 100× zoom, a 0.1 pan still moves 10% of what is visible, not 10% of the full set. Without this scaling, panning would feel sluggish at close-up and wild at the default view.

view_zoom. One multiplication. A factor below 1 shrinks the scale — fewer complex units per pixel, more detail in view. A factor above 1 zooms out. The arrow-key handlers in main.c pass 0.9 to zoom in and 1.1 to zoom out.


Remove the temporary mapping

In render.c, remove the hard-coded coordinate lines from the previous page:

/* remove these: */
re = -2.5 + (double)x / WIDTH * 3.5;
im = -1.25 + (double)y / HEIGHT * 2.5;

The existing calls to pixel_to_re and pixel_to_im now have real implementations and will return the correct complex coordinates.

make re && ./infinite

The Mandelbrot set appears, correctly mapped to the screen. Press the arrow keys to pan. Press = to zoom in — each keypress multiplies scale by 0.9, taking you deeper into the boundary. Press - to zoom out.


What you are seeing

The cardioid is the main body of the set — the region of complex numbers whose iteration converges to a fixed point. The circular discs attached to it are bulbs, each one a region where the iteration converges to a cycle of length 2, 3, 4, and so on.

The boundary between convergence and divergence is infinitely complex. Every zoom level reveals new structure — spirals within spirals, bulbs attached to bulbs, each junction hiding another copy of the whole.

The antennae extending left from the main cardioid are called the real axis spine. The tip at (−2.0, 0.0) is the leftmost point of the set. Zoom into the junction between any two bulbs and you will find a miniature copy of the entire set.

Zoom in until the render is visibly slow. With MAX_ITER = 100, deep zooms flatten out quickly — the iteration count reaches the ceiling before revealing fine detail. Raising MAX_ITER to 500 or 1000 reveals more structure at the cost of longer render times. The next page adds colour; the page after that makes it worth waiting for.