Fract'ol is the first computer graphics project of the Common Core curriculum.
It is a simple graphics program using
minilibx
, an opportunity to learn how to use the mathematical notion of complex numbers, have a first contact with the concept of optimization in computer graphics, and event handling.
-
General
- The program must take the type of the fractal to be displayed as a parameter and any other relevant option.
- The program must display the fractal in the window powered by
minilibx
. - The project must contain a
Makefile
that compiles all sources. It must not relink. - Global variables are forbidden.
-
Rendering
- The program must offer the Julia and Mandelbrot sets.
- The mouse wheel zooms in and out almost infinitely, within the limits of the computer.
- A different Julia set must be rendered if the program is passed the appropriate parameters.
- A parameter passed on startup must be the type of the fractal to be rendered.
- Adding more parameters is optional. If no parameter is provided, or the parameters are invalid, it displays the help page and exits cleanly.
- A few different color schemes must be implemented.
-
Graphic Management
- The program has to display the image in a window.
- The management of the window must remain smooth.
- Pressing
ESC
must close the window and exit the program in a clean way. - Clicking on the cross on the top window frame must have the same effect.
- It is mandatory to use
images
fromminilibx
.
- One extra fractal.
- The zoom follows the mouse position.
- Moving the view by pressing the arrow keys.
- Make the color range shift.
Before anything else, the main()
function declares a t_display
variable named display
that stores all the necessary data, conveniently packed to be passed around the program.
typedef struct s_display
{
// mlx Data
void *mlx_conn; // Stores pointer to mlx connection
void *mlx_win; // Stores pointer to mlx window
t_img img; // Stores the image data
int width; // Stores the width of the window
int height; // Stores the height of the window
t_range win_size; // Stores the size of the window
double x_offset; // Stores how much to shift when moving the view
double y_offset; // Stores how much to shift when moving the view
double zoom; // Stores the zoom factor
// Fractal Data
char *name; // Stores the name of the fractal
int set; // Stores the type of fractal
long iter; // Stores the number of iterations
t_complex z; // Stores z for Mandelbrot/Julia/Tricorn/Burning Ship
t_complex c; // Stores c for Mandelbrot/Tricorn/Burning Ship
t_complex c_julia; // Stores c for Julia
t_complex z_newton; // Stores z for Newton
t_range frac_range; // Stores the range of the complex plane
double escape; // Stores the complex plane escape value
double newton_esc; // Stores escape value for Newton
t_range color_iter; // Stores a range of 0 to n iterations
t_range color_range; // Stores a range from black to white
int color; // Stores a color for the Newton fractal
} t_display;
The main logic for argument parsing can be found inside the
ft_args.c
file.
ft_no_args()
and ft_args()
are used to parse the input arguments and ensure that if there is something wrong the program exits correctly (without memory leaks).
if (argc < 2)
return (ft_no_args());
else if (!ft_args(&display, argc, argv))
exit(EXIT_FAILURE);
If the program is passed no arguments:
- It prints an error to
stderr
; - Displays the help page and exits cleanly.
Checks if the arguments passed are valid.
int ft_args(t_display *d, int argc, char **argv)
{
if (!ft_select_fractal(d, argc, argv))
return (ft_invalid_args(argv[1]));
if (!ft_set_args(d, argc, argv))
return (0);
return (1);
}
- First checks if the fractal type selected is valid.
- Then attempts to set the input arguments:
This function checks if the fractal type is valid.
- It first converts the fractal type (first argument) to lowercase.
- If it is valid,
ft_set_fractal()
is called and the function outputs 1. - If it is NOT valid it outputs 0.
Here we make sure we got the right number of arguments and check if they are the right type before the program initializes anything.
-
First checks the iterations argument:
- If the 2nd argument is a valid input for the number of iterations, we set it to
d->iter
. In case it is a negative value a default value is set instead. - Otherwise the program prints an error to
stderr
and exits.
- If the 2nd argument is a valid input for the number of iterations, we set it to
-
Then we check for the Julia case in which we get a complex number as the third and fourth arguments.
- If the input arguments are a valid doubles we set them to
d->c_julia.r
andd->c_julia.i
. - Otherwise the program prints an error to
stderr
and exits.
- If the input arguments are a valid doubles we set them to
After all validation tests are passed, the program calls
ft_init_display()
.
ft_init_display(&display, argv);
It initializes:
- the
mlx
connection intod->mlx_conn
by callingmlx_init()
; - the
mlx
window intod->mlx_win
by callingmlx_new_window()
; - the image pointer into
d->img.img
by callingmlx_new_image()
; - the image pixels into
d->img.pix
by callingmlx_get_data_addr()
;
All these calls are properly protected by calls to cleanup functions in case a initialization error arises.
After everything is properly allocated we proceed to initialize the event handling functionality.
This function initializes three event handlers to be triggered when certain events are received:
- Listens for
DestroyNotify
event;- Destroys the image data;
- Destroys the
mlx
window; - Destroys the
mlx
connection; - Frees the
t_display
pointer to themlx_conn
;
- Listens for
KeyPress
events;- If Escape is received, it exits by calling
ft_kill_handle()
; - If the arrow keys are pressed,
ft_handle_offsets()
is called; - If PageUp or PageDown are pressed, the
d->iter
is increased or decreased by 1 respectively; - If Space, 1, 2, 3, 4, 5 are pressed,
ft_swith_set()
is called. - If Left-Shift, Right-Shift, r, g or b are pressed,
ft_switch_color()
is invoked. - Else if the key press received is not being handled, a message with the keysym value is printed to
stdout
. - If an event was successfully caught
ft_render()
is called causing a re-render of the window.
- If Escape is received, it exits by calling
- Listens for
ButtonPress
events;- If the left mouse button is pressed inside the window when on the Mandelbrot set the fractal settings are changed and a re-render is triggered with a Julia set with its
c
set to the current mouse position; - Else if the right button is pressed the window re-renders the Mandelbrot set.
- Else if the mouse wheel is scrolled up or down
ft_handle_zoom()
is called.
- If the left mouse button is pressed inside the window when on the Mandelbrot set the fractal settings are changed and a re-render is triggered with a Julia set with its
Note
Understanding ft_handle_zoom()
:
Centering & Scaling
The keys to zooming in computer graphics are :
- Adjusting the view's center, by changing the
d->x_offset
andd->y_offset
; - Adjusting the view's scale, by changing the
d->zoom
factor;
Mouse Position & Zoom Center
The x
and y
coordinates of the mouse are used to determine the zoom center;
- This is done by mapping the mouse position to the range of the complex plane;
Zoom Factor & Scaling
- The zoom factor (
SCALE_FACTOR
) determines how much the view is scaled with each zoom operation. - Increasing the zoom level, divides
d->zoom
value by theSCALE_FACTOR
, enlarging the view; - Decreasing the zoom level, multiplies
d->zoom
value by theSCALE_FACTOR
, shrinking the view. fabs()
is used to ensure that the scale factor is always positive, regardless of the current zoom level.
Offset Adjustment
- The offset adjustment (0.13 * fabs(d->zoom)) is a scaling factor that controls how much the view is moved in response to zooming.
- This factor is multiplied by the mapped mouse position to ensure that the zoom center is adjusted proportionally to the zoom level, providing a smoother and more controlled zooming.
Now that we got the X connection, the window and event handling up and running all there is left to do it the data initialization.
In this function we initialize the data inside the t_display
structure to be passed and used by the program.
ft_init_display(&display, argv);
Check out ft_init.c and fractol.h for a closer look at what is being initialized and to what values.
The ft_usage()
function prints the usage of the program and all available commands to stdout
.
ft_usage();
This is where the pixel-by-pixel drawing of the window takes place.
ft_render(&display);
- It iterates over each pixel in the window;
- Selects the rendering function based on the chosen fractal type;
- For each pixel it evaluates the function describing the selected set;
while (++y <= HEIGHT)
{
x = -1;
while (++x < WIDTH)
ft_select_set(d, x, y);
ft_printf("\r%sRendering:%s [%d%%]", YEL, NC, ((y * 100) / d->height));
}
ft_printf("\t%sComplete!%s\n", MAG, NC);
- Once the calculations are done
mlx_put_image_to_window()
is called to render the image to the window.
mlx_put_image_to_window(d->mlx_conn, d->mlx_win, d->img.img, 0, 0);
- Then
ft_render_ui()
is called to print a simple UI to the window.
ft_render_ui(d);
Note
This is a function that can produce memory leaks if the usage of ft_itoa()
and ft_strjoin()
are not handled correctly. Take a look for yourself at ft_ui.c for details.
Finally, the program enters an infinite loop, keeping the window open listening for user events.
mlx_loop(d->mlx_conn);
First, clone the contents of this repository over SSH:
git clone [email protected]:PedroZappa/42_fractol.git
Then, make sure that the program is compiled with all its dependencies using make
:
make
One way to find out all available startup options and keybindings, is to run the program without arguments:
./fractol
If you want to test the program with valgrind
, you can use the following make
rule:
make valgrind
There is also a convenient make
rule to run a Norminette
check:
make norm
MinilibX is a small library, a simplified version of XLib (X11R6) written in C , designed to introduce students to the X-Window System. 1
The X-Window System is an architecture independent windowing system for bitmap displays that provides a basic framework for creating graphical user interfaces. 2 It enables users to draw and move windows on a display using the mouse and keyboard.
Note
In computing, a bitmap
(also known as bit array
or bitmap index
) is a mapping from a given domain (for instance, a range of integers) to bits. 3
X is based on a client-server model:
- one X server connects to multiple X client programs.
flowchart TB
Display[Display]
Keys[Keyboard]
Mouse[Mouse]
Keys[Keyboard] --->|input| Xserv[X Server]
Mouse[Mouse] --->|input| Xserv
Display[Display] <---|output| Xserv
subgraph W[User Workstation]
Xserv[X Server]
Xserv --> X-client[X client1]
Xserv --> X-client2[X client2]
end
subgraph Remote Machine
Xserv -->|Network Conn| X-client3[X client3]
end
The X Server receives requests to output graphics on the display (through windows) and sends back user input (from a keyboard, mouse, etc).
Note
There are many implementations of the X Window System (Xlib), minilibx being just one among many following the X Consortium standard; 4
Complex numbers
are numbers in the form (a + bi)
where:
-
a
is the real part: -
b
is the imaginary part; -
i
is the imaginary unit, defined by the equation$i^2 = -1$ .
Note
Like with real numbers, we can perform arithmetic on complex numbers.
$(a + bi) + (c + di) = (a + c) + (b + d)i$
Example of how to add two complex numbers:
$((3 - 4i) + (2 + 5i)) =$
$((3 + 2) + (-4 + 5)i) =$
$(5 + i)$
$(a + bi) - (c + di) = (a - c) + (b - d)i$
Multiplication is similar to multiplying binomials but with complex numbers we work with the real and imaginary parts separately.
$c(a + bi) = (c * a) + (c * b)i$
Example:
$3(6 + 2i) =$
$(3 * 6) + (3 * 2i) =$ # Distribute
$(18 * 6i)$ # Simplify
$(a + bi)(c + di) = ac + adi + bci + bdi^2$
- Because
$i^2 = -1$ , we can simplify the expression to:
$(a + bi)(c + di) = ac + adi + bci - bd$
- Simplifying, we combine the real parts, and then the imaginary parts:
$(a + bi)(c + di) =$
$(ac - bd) + (ad + bc)i$
Example:
$(4 + 3i)(2 - 5i) =$
$(4 * 2) + (4 * (-5i)) + (3i * 2) + (3i * (-5i)) =$
$8 - 20i + 6i - 15i^2 =$
$8 + 15 - 20i + 6i =$
$(23 - 14i)$
Here is an example on how to expand a squared complex number:
$(a + bi)^2 =$
$(a * a) + (a * bi) + (a * bi) - (bi * bi)$
$(a^2 - bi^2) + 2(a * bi))$
- The real part is
$(a^2 - b^2)$ ; - The imaginary part is
$2(a * bi)$ ;
We can take complex numbers and plot them in a plane known as the Complex Plane
.
This plane is formed by the mapping of the real and imaginary parts of a complex number to a Cartesian coordinate system. The real part mapped to the
x
-axis and the imaginary part to they
-axis.
Fractals are infinitely complex self-similar patterns across multiple scales.
Generated by:
- Initializing a complex number
$z = (x + yi)$ where:$i^2 = -1$ -
x
andy
are image pixel coordinates mapped to a range between -2 to 2. - A formula is iterated until the value of
|z|
becomes greater than2
.- If the point never escapes the range it IS considered to be part of the set.
- If the point escapes the range it means it is NOT part of the set.
- The color of each pixel is determined by the number of iterations it took to escape the set.
Formula :
$f(z_{n+1}) = z_n^2 + c$
There are infinitely many Julia sets. To generate them, we use the same complex number c
for all pixels.
- For each pixel in the image:
-
z
is initially set to 0. -
z
is updated repeatedly following the formula$z_{n+1} = z^2 + c$ . -
c
is a complex number that seeds a specific Julia set.
-
Formula :
$f(z_{n+1}) = z_n^2 + c$
For the Mandelbrot set, we use different complex numbers for each pixel. It is the one map to all Julia sets.
- For each pixel in the image:
-
z
is initially set to 0. -
z
is updated repeatedly following the formula$z_{n+1} = z^2 + c$ . -
c
is a complex constant defined as:$c = (x + yi)$ where:$i^2 = -1$
-
$f(z_{n+1}) = (|{Re}(z_n)| + |{Im}(z_n)|i)^2 + c$
The Burning Ship Set is generated by the equation above where:
-
$z_n$ is the current complex number; -
c
is a complex constant (just like in the Julia Set formula); -
$z_{n+1}$ is the next complex number in the sequence; - The real and imaginary components are set to their absolute values before squaring at each iteration.
This modification results in the distinctive "burning ship" appearance of the fractal.
Formula :
$f(z_{n+1}) = \overline{z_n}^2 + c$
The Tricorn fractal is a variant of the Mandelbrot set and is characterized by its triangular shape. It is generated by using a slightly different formula where:
- The complex conjugate of
z
is squared instead ofz
itself. - The complex conjugate of
z
is represented by$\overline{z_n}$ -
c
is a complex constant that varies for each pixel in the image.
Note
To get the complex conjugate
of a complex number
For example: The conjugate of (4 + 7i)
is (4 - 7i)
.