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:
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 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 && ./infiniteThe 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.