Click here to Skip to main content
15,891,902 members
Articles / Programming Languages / C#
Tip/Trick

C# Simple Mandelbrot with Flicker Free Zoom

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
8 Sep 2013CPOL2 min read 24.1K   941   6   7
C# Simple Mandelbrot with Zoom

Introduction

This is a very simple C# graphics program that displays the famous Mandelbrot set, lets you zoom in with a simple selection rectangle, and shows how to smooth the colours, and not flicker.

Background

Most people are familiar with the Mandelbrot Set, a easy to compute, but very pretty fractal. Typical source code looks like this. The linked code works fine, but for the C# programmer you will generally run into two problems: banded colours, and horrible flickering when you implement a selection rectangle.

I'm assuming you can create a project, drag tools onto a form, and program simple event handlers.

Using the Code

This is C# code framework 4.0 using Visual Studio 10.

My program is a simple form with a panel to draw the Mandelbrot set on, a draw button to re-draw the current image, and a reset button to start over and unzoom.

We want to set a few variables when our program starts:

C#
public FormMandelbrot()
	{
	InitializeComponent();
	cx0 = -2.0;  // these values show the full mandelbrot set
	cx1 = 0.5;
	cy0 = -1.0;
	cy1 = 1.0; 
	 // where we store the panel background
	Map = new Bitmap(panelMain.Width, panelMain.Height);
	} 

Draw button event code looks like:

C#
// draw the currently selected mandelbrot section
private void buttonDraw_Click(object sender, EventArgs e)
	{ 
	doDrawMandelbrot = true;
	panelMain.Refresh();
	} 

You never actually do any drawing in event handlers. You just let the program know what it needs to know to draw, and call refresh to do trigger actually drawing something in the Paint event.

Reset button event code looks like:

C#
private void buttonReset_Click(object sender, EventArgs e)
	{
	// set the rectangle to draw back to the whole thing
	cx0 = -2.0;
	cx1 = 0.5;
	cy0 = -1.0;
	cy1 = 1.0;
	doDrawMandelbrot = true;
	panelMain.Refresh();
	} 

And the Paint event handler:

C#
private void panelMain_Paint(object sender, PaintEventArgs e)
{
	if (doDrawMandelbrot)
		{
		DrawMandelbrot(cx0, cy0, cx1, cy1, e.Graphics);
		doDrawMandelbrot = false;
		} 
}  

Points of Interest

The only way to stop flicker is to make your own class from panel that has only draws when you draw in Paint. This is done by setting the "style" which can't be done in a normal Panel class.

C#
// this has to be at the bottom of the source file or the form designer won't work
	public class MyPanel : System.Windows.Forms.Panel
		{
		// a non-flickering panel. It doesn't draw its own background
		// if you don't do this the panel flickers like crazy when you resize th
		// selection rectangle
		public MyPanel()
			{ 
			this.SetStyle(
				System.Windows.Forms.ControlStyles.UserPaint |
				System.Windows.Forms.ControlStyles.AllPaintingInWmPaint |
				System.Windows.Forms.ControlStyles.OptimizedDoubleBuffer,
				true);
			}
		}  

Once you have this new class set up, you have to change the declaration of the panel to use this new one. In forma1.designer.cs, change panelMain from a normal panel to your new panel:

C#
private void InitializeComponent()
	{
	this.buttonDraw = new System.Windows.Forms.Button();
	this.buttonReset = new System.Windows.Forms.Button();
	this.panelMain = new Mandelbrot.MyPanel(); 

and farther down..

C#
private System.Windows.Forms.Button buttonDraw;
private System.Windows.Forms.Button buttonReset;
//private System.Windows.Forms.Panel panelMain;
private MyPanel panelMain;

But that is only half the no flicker battle. Drawing the Mandelbrot takes seconds which would cause a bit of flicker if it had to do that each time we moved the mouse for the selection rectangle. So we save it to a bitmap, draw to a bitmap, then copy the bitmap to the visible bitmap as we move the mouse. No flicker.

So mouse move event looks like:

C#
private void panelMain_MouseMove(object sender, MouseEventArgs e)
   {
   if (isMouseDown)
       {
       // get new coords of rect
       mx1 = e.X;
       my1 = e.Y;
       panelMain.Refresh();
       }
   }

and the paint now looks like this:

C#
private void panelMain_Paint(object sender, PaintEventArgs e)
{
Graphics g;
	if (isMouseDown)
		{
		Pen penYellow = new Pen(Color.ForestGreen);
		// restore background, then draw new rectangle, both to the offscreen bMap
		bMap = (Bitmap )bMapSaved.Clone();
		g = Graphics.FromImage(bMap);
		g.DrawRectangle(penYellow, mx0, my0, mx1-mx0, my1-my0);
		e.Graphics.DrawImageUnscaled(bMap, 0, 0); // copy whole thing to visible screen
		}
	else
		{
		if (doDrawMandelbrot)
			{
			g = Graphics.FromImage(bMap);
			// draw it to our background bitmap
			DrawMandelbrot(cx0, cy0, cx1, cy1, g); 
			// display it on the panel
			e.Graphics.DrawImageUnscaled(bMap, 0, 0); 
			 // save our background; the current mandelbrot image
			bMapSaved = (Bitmap)bMap.Clone(); 
			doDrawMandelbrot = false;
			}
		else
			{
			e.Graphics.DrawImageUnscaled(bMap, 0, 0);
			}
		}
	} 

The other point is how to scale/zoom. The main drawing function, not shown, takes as parameters which part of the set to draw. So all you have to do is convert the points you select with the mouse to the points in the set:

C#
private void panelMain_MouseUp(object sender, MouseEventArgs e)
	{
	// save where the end of the selection rect is at
	isMouseDown = false;
	mx1 = e.X;
	my1 = e.Y;
			
	/*
	 * cx0, cy0 and cx1, cy1 are the current extent of the set
	 * mx0,my0 and mx1,my1 are the part we selected
	 * do the math to draw the selected rectangle
	 * */
	double scaleX, scaleY;
	scaleX = (cx1 - cx0) / (double )panelMain.Width;
	scaleY = (cy1 - cy0) / (double)panelMain.Height;
	cx1 = (double )mx1*scaleX + cx0;
	cy1 = (double)my1*scaleY + cy0;
	cx0 = (double)mx0 * scaleX + cx0;
	cy0 = (double)my0 * scaleY + cy0;
	doDrawMandelbrot = true;
	panelMain.Refresh(); // force mandelbrot to redraw
	} 

And we don't want horrible banded colours, we want nice smooth colours. This odd bit of math makes a "smooth" number between 0 and 1 which we can use as the hue to get a nice colour. If you look at the math link it looks very complex, but it boils down to this:

C#
private Color MapColor(int i, double r, double c)
   {
   double di=(double )i;
   double zn;
   double hue;
       zn = Math.Sqrt(r + c);
        // 2 is escape radius
       hue = di + 1.0 - Math.Log(Math.Log(Math.Abs(zn))) / Math.Log(2.0);
       hue = 0.95 + 20.0 * hue; // adjust to make it prettier
       // the hsv function expects values from 0 to 360
       while (hue > 360.0)
           hue -= 360.0;
       while (hue < 0.0)
           hue += 360.0;
       return ColorFromHSV(hue, 0.8, 1.0);
       }

Please see the full source for a working version, and all the details.

History

  • Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Canada Canada
Professional Programmer living in Beautiful Vancouver, BC, Canada.

Comments and Discussions

 
PraiseThanks! I made optimizations to this to really speed it up Pin
Member 1247173720-Apr-16 17:14
Member 1247173720-Apr-16 17:14 
PraiseThanks! I made optimizations to this to really speed it up Pin
Member 1247173720-Apr-16 17:14
Member 1247173720-Apr-16 17:14 
SuggestionSlight adjustments, zoom out, keep aspect ratio, free box draw, speed. Pin
EskeRahn6-Nov-14 17:41
EskeRahn6-Nov-14 17:41 
Nice and simple program, thanks! Big Grin | :-D
I made a few modifications:

1) Added Zoom out
2) Added Mouse click on same position, zooms by 4x
3) Now zoom keeps aspect ratio - same pixel size in both directions
4) Allow zoom-box to be drawn in any direction, not only top right
5) Display coordinates of the mouse at the bottom via an added Label1
6) Speed up by pre-calculating an array of brushes.
7) Speed up by adding some simple elliptic shape-filters in the base figure.

The most complex addition is the zoom-OUT that puts the current image into the marked area of the screen.

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace Mandelbrot
{
	public partial class FormMandelbrot : Form
		{
		protected int mx0, my0;  // where the mouse down was clicked
		protected int mx1, my1;  // where the mouse was released
		protected Boolean isMouseDown = false;
		protected double cx0, cx1, cy0, cy1;   // the part of the mandelbrot set we will draw
		protected Bitmap bMap;                 // the bmap we draw to, to avoid flicker
		protected Bitmap bMapSaved;            // a copy of the current mandelbrot picture, also to help stop flickering
		protected Boolean doDrawMandelbrot = true;  // should we draw the mandelbrot on this paint?
		
		public FormMandelbrot()
		{
			InitializeComponent();

			cx0 = -2.0;  // these values show the full mandelbrot set
			cx1 = 0.5;
			cy0 = -1.15;
			cy1 = 1.15;

			bMap = new Bitmap(panelMain.Width, panelMain.Height); // where we store the panel background
		}

		// draw the currently selected mandelbrot section
		private void buttonDraw_Click(object sender, EventArgs e)
		{
			doDrawMandelbrot = true;
			panelMain.Refresh();
		}

		// convert a hsv color to a rgb colour
		// hue must be from 0 to 360
		protected Color ColorFromHSV(double hue, double saturation, double value)
		{
			int hi = Convert.ToInt32(Math.Floor(hue / 60)) % 6;
			double f = hue / 60 - Math.Floor(hue / 60);

			value = value * 255;
			int v = Convert.ToInt32(value);
			int p = Convert.ToInt32(value * (1 - saturation));
			int q = Convert.ToInt32(value * (1 - f * saturation));
			int t = Convert.ToInt32(value * (1 - (1 - f) * saturation));

			if (hi == 0)
				return Color.FromArgb(255, v, t, p);
			else if (hi == 1)
				return Color.FromArgb(255, q, v, p);
			else if (hi == 2)
				return Color.FromArgb(255, p, v, t);
			else if (hi == 3)
				return Color.FromArgb(255, p, q, v);
			else if (hi == 4)
				return Color.FromArgb(255, t, p, v);
			else
				return Color.FromArgb(255, v, p, q);
		}

		// convert how long it took to bail out into a pretty colour
		// this function smooths out the colours chosen. if you just pick a colour using i it is very banded.
		// this math smoothly spreads the colours over the expected values returned, and uses the radius that we escaped at
		// as well as the iteration we bailed out at
		private Color MapColor(int i, double r, double c)
		{
			double di=(double )i;
			double zn;
			double hue;

				zn = 100;//Math.Sqrt(r + c);
				hue = di + 1.0 - Math.Log(Math.Log(Math.Abs(zn))) / Math.Log(2.0);  // 2 is escape radius
				hue = 0.95 + 20.0 * hue; // adjust to make it prettier
				// the hsv function expects values from 0 to 360
				while (hue > 360.0)
					hue -= 360.0;
				while (hue < 0.0)
					hue += 360.0;

				return ColorFromHSV(hue, 0.8, 1.0);
		}

		// classic, vanilla, mandelbrot code
		// cxMin etc... is the part of the mandelbrot we are going draw, will be scaled to the current pictureBox
		// the whole mandelbrot fits in x -2 to 0.5 y -1 to +1
		private void DrawMandelbrot(ref double cxMin, ref double cyMin, ref double cxMax, ref double cyMax, Graphics g)
		{
		int ix, iy;  // where we draw a pixel
		double pixelWidth;
		double pixelHeight;
		double cx, cy;
		double zx, zy;
		double zx2, zy2; // squared
		int i;
		int iterationMax = 255*3; // hmmm...
		double escapeRadius = 2.0;
		double er2 = escapeRadius * escapeRadius;
		SolidBrush sbBlack = new SolidBrush(Color.Black);
		SolidBrush[] sbA=new SolidBrush[iterationMax];
		for(i=0;i<iterationMax;i++)sbA[i]=new SolidBrush(MapColor(i, 0, 0));

		//Adjust aspect ratio
		double AspectRatio=(double)panelMain.Width/(double)panelMain.Height;
		if ((cxMax-cxMin)<(cyMax-cyMin)*AspectRatio)
		     {cxMin=(cxMax+cxMin-(cyMax-cyMin)*AspectRatio)/2.0; cxMax=cxMin+(cyMax-cyMin)*AspectRatio;}
		else {cyMin=(cyMax+cyMin-(cxMax-cxMin)/AspectRatio)/2.0; cyMax=cyMin+(cxMax-cxMin)/AspectRatio;}

			Cursor.Current = Cursors.WaitCursor;

			// scale it to our current window and section to draw
			pixelWidth = (cxMax - cxMin) / (double)panelMain.Width;
			pixelHeight = (cyMax - cyMin) / (double)panelMain.Height;
		
			for ( iy=0; iy < panelMain.Height; iy++ ) {
				cy = cyMin + (double)iy * pixelHeight;
				if (Math.Abs(cyMin) < pixelHeight / 2.0)
					cy = 0.0;
				for (ix = 0; ix < panelMain.Width; ix++) {
					cx = cxMin + (double )ix * pixelWidth;
					// init this go 
					/* z(i+1) = z(i)² + z(0) => │z(i+1)│ ≤ │z(i)│² + │z(0)│
					   =>  │z(i+1)│≤.5 if │z(0)│<.25=√.0625
					 */
					if ((cx<0 && (cx+.25)*(cx+.25)+cy*cy<0.25) || ((cx+1)*(cx+1)+cy*cy<0.0625) || cx*cx+cy*cy/4<0.0625) g.FillRectangle(sbBlack, ix, iy, 1, 1); //Rough fill of the interior, by a few geometric figures
					else {
						zx = 0.0;
						zy = 0.0;
						zx2 = 0.0;
						zy2 = 0.0;
						for (i = 0; i < iterationMax && ((zx2 + zy2) < er2); i++) {
							zy = zx * zy * 2.0 + cy;
							zx = zx2 - zy2 + cx;
							zx2 = zx * zx;
							zy2 = zy * zy;
						}
						if (i == iterationMax) {
							// interior, part of set, black
							// set colour to black
							g.FillRectangle(sbBlack, ix, iy, 1, 1);
						}else{
							// outside, set colour proportional to time/distance it took to converge
							// set colour not black
							//SolidBrush sbNeato = new SolidBrush(MapColor(i, zx2, zy2));
							g.FillRectangle(sbA[i], ix, iy, 1, 1);
						}
					}
				}
			}

			Cursor.Current = Cursors.Default;
		}

		private void panelMain_Paint(object sender, PaintEventArgs e)
		{
			Graphics g;

			if (isMouseDown)
			{
				Pen penYellow = new Pen(Color.ForestGreen);
				bMap = (Bitmap )bMapSaved.Clone(); // restore background, then draw new rectangle, both to the offscreen bMap
				g = Graphics.FromImage(bMap);
				g.DrawRectangle(penYellow, Math.Min(mx0,mx1), Math.Min(my0,my1), Math.Abs(mx1-mx0), Math.Abs(my1-my0));
				e.Graphics.DrawImageUnscaled(bMap, 0, 0); // copy whole thing to visible screen
			}
			else
			{
				if (doDrawMandelbrot)
				{
					g = Graphics.FromImage(bMap);
					DrawMandelbrot(ref cx0, ref cy0, ref cx1, ref cy1, g); // draw it to our background bitmap
					e.Graphics.DrawImageUnscaled(bMap, 0, 0); // display it on the panel
					bMapSaved = (Bitmap)bMap.Clone();  // save our background; the current mandelbrot image
					doDrawMandelbrot = false;
				}
				else
				{
					e.Graphics.DrawImageUnscaled(bMap, 0, 0);
				}
			}
		}

		private void panelMain_MouseDown(object sender, MouseEventArgs e)
		{
			// save where the start of the selection rect is
			isMouseDown = true;
			mx0 = e.X;
			my0 = e.Y;
			mx1 = mx0;
			my1 = my0;
		}

		private void panelMain_MouseUp(object sender, MouseEventArgs e)
		{
			// save where the end of the selection rect is at
			isMouseDown = false;
			mx1 = e.X;
			my1 = e.Y;

			if (mx1<mx0){int t=mx0;mx0=mx1;mx1=t;}
			if (my1<my0){int t=my0;my0=my1;my1=t;}
			if (mx1==mx0){mx0-=panelMain.Width /8;mx1=mx0+panelMain.Width /4;}
			if (my1==my0){my0-=panelMain.Height/8;my1=my0+panelMain.Height/4;}

			/*
			 * cx0, cy0 and cx1, cy1 are the current extent of the set
			 * mx0,my0 and mx1,my1 are the part we selected
			 * do the math to draw the selected rectangle
			 * */

			double scaleX=(cx1-cx0) / (double)panelMain.Width;
			double scaleY=(cy1-cy0) / (double)panelMain.Height;

			double tx0, tx1, ty0, ty1;
			if (e.Button == MouseButtons.Right)
			{ //Zoom out
				double fX=(mx1-mx0) / (double)panelMain.Width ;
				double fY=(my1-my0) / (double)panelMain.Height;
				tx0=-mx0/fX; tx1=tx0+ (double)panelMain.Width  / fX;
				ty0=-my0/fY; ty1=ty0+ (double)panelMain.Height / fY;
			}else{
				tx0=mx0; tx1=mx1;
				ty0=my0; ty1=my1;
			}

			cx1 = tx1 * scaleX + cx0;
			cy1 = ty1 * scaleY + cy0;
			cx0 = tx0 * scaleX + cx0;
			cy0 = ty0 * scaleY + cy0;

			doDrawMandelbrot = true;
			panelMain.Refresh(); // force mandelbrot to redraw
		}

		private void buttonReset_Click(object sender, EventArgs e)
		{
			// set the rectanlge to draw back to the whole thing
			cx0 = -2.0;
			cx1 = +0.5;
			cy0 = -1.13;
			cy1 = +1.13;
			doDrawMandelbrot = true;
			panelMain.Refresh();
		}

		private void panelMain_MouseMove(object sender, MouseEventArgs e)
		{
				double cxM = (double)e.X * (cx1 - cx0) / (double)panelMain.Width  + cx0;
				double cyM = (double)e.Y * (cy1 - cy0) / (double)panelMain.Height + cy0;
				double Dig= Math.Truncate(3-Math.Log10(cx1-cx0));
				string F="F"+Dig.ToString();
				label1.Text="x="+cxM.ToString(F)+" , y="+(-cyM).ToString(F)+"   "+Dig.ToString();			
			if (isMouseDown)
			{
				// get new coords of rect
				mx1 = e.X;
				my1 = e.Y;
				panelMain.Refresh();
			}

		}

		private void FormMandelbrot_SizeChanged(object sender, EventArgs e)
		{
			// bmap and bmap clone have to be redone to match new size of the form
			bMap.Dispose();
			bMap = new Bitmap(panelMain.Width, panelMain.Height);
		}

	}

	// this has to be at the bottom of the source file or the form designer won't work
	public class MyPanel : System.Windows.Forms.Panel
	{
		// a non-flickering panel. It doesn't draw its own background
		// if you don't do this the panel flickers like crazy when you resize the selection rectangle
		public MyPanel()
		{
			this.SetStyle(
				System.Windows.Forms.ControlStyles.UserPaint |
				System.Windows.Forms.ControlStyles.AllPaintingInWmPaint |
				System.Windows.Forms.ControlStyles.OptimizedDoubleBuffer,
				true);
		}
	}
}


/Eske
GeneralRe: Slight adjustments, zoom out, keep aspect ratio, free box draw, speed. Pin
arussell7-Nov-14 11:23
professionalarussell7-Nov-14 11:23 
GeneralRe: Slight adjustments, zoom out, keep aspect ratio, free box draw, speed. Pin
arussell7-Nov-14 12:25
professionalarussell7-Nov-14 12:25 
GeneralRe: Slight adjustments, zoom out, keep aspect ratio, free box draw, speed. Pin
arussell10-Nov-14 5:34
professionalarussell10-Nov-14 5:34 
GeneralMy vote of 5 Pin
Volynsky Alex9-Sep-13 7:59
professionalVolynsky Alex9-Sep-13 7:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.