0

I am trying to figure out a way to zoom in on my Mandelbrot set on click. I have it so when I click it slightly zooms in but it doesn't move the Mandelbrot accordingly.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Numerics;

namespace Project_V2
{
    public partial class FractalGen : Form
    {
        public double zoom = 2.4;
        public FractalGen()
        {
            InitializeComponent();
        }

        private void pictureBox1_Click(object sender, EventArgs e)
        {
            zoom -= 0.3;
            Mandelbrot();
        }

        private void button1_Click(object sender, EventArgs e)
        {
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Mandelbrot();
        }

        private void timer1_Tick(object sender, EventArgs e)
        {

        }

        private void Mandelbrot()
        {
            Bitmap bm = new Bitmap(pictureBox1.Width, pictureBox1.Height);
            DateTime StartT = DateTime.Now;
            for (int x = 0; x < pictureBox1.Width; x++)
            {
                for (int y = 0; y < pictureBox1.Height; y++)
                {
                    double a = (double)(x - (pictureBox1.Width / 1.25)) / (double)(pictureBox1.Width / zoom);
                    double b = (double)(y - (pictureBox1.Height / 2)) / (double)(pictureBox1.Height / zoom);
                    Complex C = new Complex(a, b);
                    Complex Z = new Complex(0, 0);
                    int u = 0;
                    do
                    {
                        u++;
                        Z = Z * Z;
                        Z = Z + C;
                        double Mag = Complex.Abs(Z);
                        if (Mag > 2.0) break;
                    } while (u < 255);
                    Color rgbInside = Color.FromArgb(0, 0, 0);
                    Color rgbOutside = Color.FromArgb(u >= 127 ? 255 : 2 * u, u >= 127 ? (u - 127) : 0, 0);
                    bm.SetPixel(x, y, u < 255 ? rgbOutside : rgbInside);
                }
            }
            pictureBox1.Image = bm;
            DateTime EndT = DateTime.Now;
            string Time = Convert.ToString((EndT - StartT).TotalSeconds);
            textBox1.Text = "Time Taken: " + Time + " Seconds";
        }

        private void button1_Click_1(object sender, EventArgs e)
        {
            zoom = 2.4;
            Mandelbrot();
        }

        private void button2_Click(object sender, EventArgs e)
        {
            saveFileDialog1.ShowDialog();
        }

        private void saveFileDialog1_FileOk(object sender, CancelEventArgs e)
        {
            string name = saveFileDialog1.FileName;
            pictureBox1.Image.Save(saveFileDialog1.FileName, System.Drawing.Imaging.ImageFormat.Png);
        }
    }
}

The current code divides the picturebox width and height by a value, but I want to have it so it zooms in on where I click. How could I scale the picturebox in relation to where I click?

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
James Barker
  • 11
  • 1
  • 3
  • 1
    You need to retrieve and work with the mouseposition. This answer has the solution, even ready for a picture box https://stackoverflow.com/questions/7055211/how-to-get-the-position-of-a-click – Markus Deibel Jun 21 '17 at 14:11
  • Forget about the picturebox and concentrate on the data! - Oh, I have told you that before and you have ignored it. Well, good luck then.. – TaW Jun 21 '17 at 14:52
  • 1
    @TaW: _"I have told you that before"_ -- in what context? Is there a deleted question where you provided that information? It'd help to know what advice has already been offered to the OP, to understand better what they might already have heard, and what they might be having difficulty understanding. – Peter Duniho Jun 22 '17 at 00:07
  • @Peter: In [this question](https://stackoverflow.com/questions/44573631/how-can-i-colour-my-mandelbrot-set-c) I told him to separate the calculation from the coloring code and to store the data ('k values') so one can apply/play with colors later. Not being able to zoom, he probably can't quite see how slow the calculations will get and how precious the results are.. – TaW Jun 22 '17 at 03:20
  • @TaW: ah, I see. thanks for the link. you're right...he'll probably soon find it's better to just do the calculations once. that said, zooming mostly negates the usefulness of past results anyway. but certainly he won't want to re-render fields with large amounts of in-set points just to fiddle with color mappings. – Peter Duniho Jun 22 '17 at 03:47
  • _zooming mostly negates the usefulness of past results_ that depends on how much you like the results ;-) - that being said the calculation boundaries are of course also cheap and interesting data to save.. ((I can hardly believe it is over 33 years now since I did my 1st fractals on my memotech in turbo pascal and soon on an IBM/370 in cobol..)) – TaW Jun 22 '17 at 06:57
  • @PeterDuniho if the zoom is a doubling, one quarter of the iterations are already known (even if the colouring scheme is more complex than a simple lookup). – Weather Vane Jun 22 '17 at 16:55
  • @WeatherVane: if you limit zooming to exact multiples of two, you can theoretically reuse 1/4 of the points. But that's not the case in the example above, nor in many viewers. For arbitrary zooming, due to rounding error, you almost never get the same complex plane points after zooming and so you're either accepting erroneous results (which admittedly are often not noticeable to a human looking at an image) or you're recomputing the whole field. I prefer the latter...there are other much more effective ways to improve performance, especially if you're willing to accept a little error. – Peter Duniho Jun 22 '17 at 17:10

1 Answers1

3

It would be helpful to understand what guidance has already been offered to you. I infer from this comment that there is probably an existing question you asked and then deleted, in which this problem was discussed. Without knowing what help has already been offered and what specifically it was you were unable to understand, it's hard to know how best to present a new answer.

That said, there are two fundamental problems you have to solve to achieve the behavior you're looking for:

  1. Identify where the mouse is clicked. Currently, you are using the Click event, which only reports to your code that the control was clicked, not where it was clicked.
  2. Fix your rendering code to accommodate changes in the boundaries of what should be drawn. Currently you always map the range of pixels in your bitmap to the range in the complex plane centered on (-0.72, 0), adjusting only how far from that center is rendered (i.e. the zoom variable.

To address #1, you need to subscribe to the MouseClick event instead. This will pass a MouseEventArgs object to your handler, which has a Location property that indicates the point within the control's client area that was actually clicked (i.e. the coordinates reported are relative to an origin at the top-left of the control).

To address #2, you need to add some variables to your class to keep track of the relative position where you want to draw. There are lots of ways to do this, but staying with your current convention of representing the current "zoom" factor as the width and height of the rendered area, it seems to make sense to track the center of the space being rendered, i.e. the location the mouse was clicked, in terms of the location on the complex plane that was drawn.

I have a strong objection to programs that lock up the UI while they do long-running tasks. So in the process of revising your code example, I modified it so that it would render the image in a worker thread, and would report the rendering progress by providing a status message on a Label that I added to the form. (This involved making your Mandelbrot() method an async method…I went ahead and used await when calling it to suppress compiler warnings, but of course in this context the awaits are not strictly needed.)

One thing I did not do was to add further optimizations to the code. These tend to obfuscate the changes that are directly related to your question. There are a number of things you could do to your code to help improve performance, by removing redundant calculations or by using mathematical relationships to perform computations less expensively than the literal translation of the Mandelbrot algorithm.

There is lots on the Internet about how to render Mandelbrot images, so you can look around to find that sort of advice, once you have gotten the basic idea working. Besides, for now, with only 255 iterations per pixel and the relatively small bitmap, performance isn't a serious consideration.

Here is the version of your code I came up with:

public partial class Form1 : Form
{
    double zoom = 2.4;
    double centerX = -0.72, centerY = 0;

    public Form1()
    {
        InitializeComponent();
    }

    private async void pictureBox1_MouseClick(object sender, MouseEventArgs e)
    {
        double minX = centerX - zoom / 2, minY = centerY - zoom / 2;
        centerX = minX + (double)e.Location.X / pictureBox1.Width * zoom;
        centerY = minY + (double)e.Location.Y / pictureBox1.Height * zoom;
        zoom -= 0.3;
        await Mandelbrot();
    }

    private async void Form1_Load(object sender, EventArgs e)
    {
        await Mandelbrot();
    }

    private async Task Mandelbrot()
    {
        IProgress<double> progress = new Progress<double>(x => label1.Text = $"{x * 100:0}% done");

        DateTime StartT = DateTime.UtcNow;
        pictureBox1.Image = await Task.Run(() => _GenerateBitmap(progress));
        DateTime EndT = DateTime.UtcNow;
        string Time = Convert.ToString((EndT - StartT).TotalSeconds);
        textBox1.Text = "Time Taken: " + Time + " Seconds";
    }

    private Bitmap _GenerateBitmap(IProgress<double> progress)
    {
        Bitmap bm = new Bitmap(pictureBox1.Width, pictureBox1.Height);
        double minX = centerX - zoom / 2, minY = centerY - zoom / 2;

        for (int x = 0; x < pictureBox1.Width; x++)
        {
            for (int y = 0; y < pictureBox1.Height; y++)
            {
                double a = minX + (double)x / pictureBox1.Width * zoom;
                double b = minY + (double)y / pictureBox1.Height * zoom;
                Complex C = new Complex(a, b);
                Complex Z = new Complex(0, 0);
                int u = 0;
                do
                {
                    u++;
                    Z = Z * Z;
                    Z = Z + C;
                    double Mag = Complex.Abs(Z);
                    if (Mag > 2.0) break;
                } while (u < 255);
                Color rgbInside = Color.FromArgb(0, 0, 0);
                Color rgbOutside = Color.FromArgb(u >= 127 ? 255 : 2 * u, u >= 127 ? (u - 127) : 0, 0);
                bm.SetPixel(x, y, u < 255 ? rgbOutside : rgbInside);
            }

            progress.Report((double)x / pictureBox1.Width);
        }

        return bm;
    }

    private async void button1_Click_1(object sender, EventArgs e)
    {
        zoom = 2.4;
        await Mandelbrot();
    }
}
Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • Could you please explain what this line of code is doing? IProgress progress = new Progress(x => label1.Text = $("{x * 100:0}% done"); – James Barker Jun 22 '17 at 08:26
  • That creates an instance of `Progress`, a generic class using `double` in this case, that when created in the UI thread will automatically execute the handler passed to it on that UI thread. The handler in this case is the lambda expression that receives the status value `x` and formats it to present it on-screen by assigning the formatted string to `label1.Text`. By passing this whole `Progress` instance (typed as `IProgress`, the progress-reporting interface that `Progress` implements) to the method that does work, it can report progress as it goes. – Peter Duniho Jun 22 '17 at 16:51
  • @FlorisMartens: with respect to your recent proposed edit, it was correctly rejected because it deviated from the original intent. Please note that the elements you modified were part of the original question's code. In addition, nothing about the question was in relation to the exact effect of those constants on the user experience, so changing them didn't improve the answer in any material way. Please make sure in the future that you do not make edits that do not follow community guidelines; this will be even more important as you gain reputation and are allowed to make edits without review. – Peter Duniho Mar 03 '19 at 21:54