1
votes

I'm writing a C program to render a Mandelbrot set and currently, I'm stuck with trying out to figure out how to zoom in properly.

I want for the zoom to be able to follow the mouse pointer on the screen - so that the fractal zooms in into the cursor position.

I have a window defined by:

# define WIDTH 800
# define HEIGHT 600

My Re_max, Re_min, Im_Max, Im_Min are defined and initialized as follows:

man->re_max = 2.0;
man->re_min = -2.0;
man->im_max = 2.0;
man->im_min = -2.0;

The interpolation value (more on in later) is defined and initialized as follows:

pos->interp = 1.0;

To map the pixel coordinates to the center of the screen, I'm using the position function:

void        position(int x, int y, t_mandel *man)
{
    double  *s_x;
    double  *s_y;

    s_x = &man->pos->shift_x;
    s_y = &man->pos->shift_y;
    man->c_re = (x / (WIDTH / (man->re_max - man->re_min)) + man->re_min) + *s_x;
    man->c_im =(y / (HEIGHT / (man->im_max - man->re_min)) + man->im_min) + *s_y;
    man->c_im *= 0.8;
}

To zoom in, I first get the coordinates of the mouse pointer and map them to the visible area given by the rectangle defined by the (Re_Max, Re_Min, Im_Max, Im_Min) using this function, where x and y are coordinates of the pointer on a screen:

int                     mouse_move(int x, int y, void *p)
{
    t_fract         *fract;
    t_mandel        *man;

    fract = (t_fract *)p;
    man = fract->mandel;
    fract->mouse->Re = x / (WIDTH / (man->re_max - man->re_min)) + man->re_min;
    fract->mouse->Im = y / (HEIGHT / (man->im_max - man->re_min)) + man->im_min;
    return (0);
}

This function is called when a mouse wheel scroll is registered. The actual zooming is achieved by this function:

void        zoom_control(int key, t_fract *fract)
{
    double      *interp;

    interp = &fract->mandel->pos->interp;
    if (key == 5)    // zoom in
    {
        *interp = 1.0 / 1.03;
        apply_zoom(fract->mandel, fract->mouse->Re, fract->mouse->Im, *interp);
    }
    else if (key == 4)    // zoom out
    {
        *interp = 1.0 * 1.03;
        apply_zoom(fract->mandel, fract->mouse->Re, fract->mouse->Im, *interp);
    }
}

Which calls this:

void        apply_zoom(t_mandel *man, double m_re, double m_im, double interp)
{
    man->re_min = interpolate(m_re, man->re_min, interp);
    man->im_min = interpolate(m_im, man->im_min, interp);
    man->re_max = interpolate(m_re, man->re_max, interp);
    man->im_max = interpolate(m_im, man->im_max, interp);
}

I have a simple interpolate function to redefine the area bounding rectangle:

double      interpolate(double start, double end, double interp)
{
    return (start + ((end - start) * interp));
}

So the problem is:

My code renders the fractal like this - Mandelbrot set

But when I try to zoom in as described with the mouse, instead of going nicely "in", it just distorts like this, the image just sort of collapses onto itself instead of actually diving into the fractal.

I would really appreciate help with this one as I've been stuck on it for a while now.

If you please could also explain the actual math behind your solutions, I would be overjoyed!

Thank you!

2
Using standard C complex numbers might help simplify your code a bit, btw.Shawn
If you start by controlling the first plot from the origin and the scale, then when you make the zoom, you can convert the click position to a graph coordinate and set a new origin and, presumably, double the scale. The first plot is better not at (y=0.0, x=0.0) but off-centre anyway.Weather Vane
Your apply_zoom function scales the real and imaginary axes with different scaling factor. Your current mouse->real position is a scaling for real axis, and mouse->imag is a scaling factor for imaginary axis. That's why you get the image zoomed unproportionally. With your current code you can get the proportional zooming only if you click on the points where real part = imaginary.Alex
@Alex, thank you for noticing this! Out of curiosity, I've made my mouse->re and mouse->im mapping calculations similar: fract->mouse->Re = x / (WIDTH / (man->re_max - man->re_min)) + man->re_min; fract->mouse->Im = x / (WIDTH / (man->re_max - man->re_min)) + man->re_min; This way, zoom and panning towards mouse x works perfectly, but I still can't figure out how to take y (im) axis into account. Could you please point me in the right direction? Thank you!Artur

2 Answers

2
votes

After quite a bit of headache and a lot of paper wasted on recalculation interpolation methods, I've realized that the way I've mapped my complex numbers on-screen was incorrect, to begin with. Reworking my mapping method solved my problem, so I'll share what have I done.

-------------------------------OLD WAY--------------------------------------

I've initialized my Re_max, Re_min, Im_Max, Im_Min values, which define the visible area in the following way:

re_max = 2.0;
re_min = -2.0;
im_max = 2.0;
im_min = -2.0;

Then, I used this method to convert my on-screen coordinates to the complex numbers used to calculate the fractal (note that the coordinates used for mapping the mouse position for zoom interpolation and coordinates used to calculate the fractal itself use the same method):

Re = x / (WIDTH / (re_max - re_min)) + re_min;
Im = y / (HEIGHT / (im_max - re_min)) + im_min;

However, this way I didn't take the screen ratio into account and I've neglected the fact (due to a lack of knowledge) that the y coordinate on-screen is inverse (at least in my program) - negative direction is up, positive is down.

This way, when I tried to zoom in with my interpolation, naturally, the image distorted.

------------------------------CORRECT WAY-----------------------------------

When defining the bounding rectangle of the set, maximum imaginary im_max) part should be calculated, based on the screen ratio, to avoid image distortion when the display window isn't a square:

re_max = 2.0;
re_min = -2.0;
im_min = -2.0;
im_max = im_min + (re_max - re_min) * HEIGHT / WIDTH;

To map the on-screen coordinates to the complex numbers, I first found the "coordinate-to-number* ratio, which is equal to *rectangle length / screen width*:

re_factor = (re_max - re_min) / (WIDTH - 1);
im_factor = (im_max - im_min) / (HEIGHT - 1);

Then, I've mapped my pixel coordinates to the real and imaginary part of a complex number used in calculations like so:

c_re = re_min + x * re_factor;
c_im = im_max - y * im_factor;

After implementing those changes, I was finally able to smoothly zoom into the mouse position without any distortion or image "jumps".

1
votes

If I understand you correctly, you want to make the point where the mouse is located a new center of the image, and change the scale of the image by a factor of 1.03. I would try something like that:

  • Your position() and mouse_move() functions remain the same.

  • in zoom_control() just change the way how you set the new value of interpolation, it should not be a fixed constant, but should be based on its current value. Also, pass the new scaling factor to the apply_zoom():

void zoom_control(int key, t_fract *fract)
{
    double *interp;
    interp = &fract->mandel->pos->interp;
    double zoom_factor = 1.03;

    if (key == 5)    // zoom in
    {
        *interp /=  zoom_factor;
        apply_zoom(fract->mandel, fract->mouse->Re, fract->mouse->Im, 1.0 / zoom_factor);
    }
    else if (key == 4)    // zoom out
    {
        *interp *= zoom_factor;
        apply_zoom(fract->mandel, fract->mouse->Re, fract->mouse->Im, zoom_factor);
    }
}
  • modify the apply zoom function:
void        apply_zoom(t_mandel *man, double m_re, double m_im, double zoom_factor)
{
    // Calculate the new ranges along the real and imaginary axes.
    // They are equal to the current ranges multiplied by the zoom_factor.
    double re_range = (man->re_max - man->re_min) * zoom_factor;
    double im_range = (man->im_max - man->im_min) * zoom_factor;

    // Set the new min/max values for real and imaginary axes with the center at 
    // mouse coordinates m_re and m_im.
    man->re_min = m_re - re_range / 2;
    man->re_max = m_re + re_range / 2;
    man->im_min = m_im - im_range / 2;
    man->im_max = m_im + im_range / 2;
}