Contents
When you watch the shapes made by this software, you may imagine that there are huge and complex mathematical formulas behind it, but it’s not true. The program uses only some simple formulas (like SIN and COS) with a recursive method, and that’s all. All other parts of the program are to make the shapes look better and natural.
In the previous version (DotNet Real Tree), I explained some mathematical logic, and now I wish to add some new features that make it more funny (like real leaves and flowers), but I removed some parts of the program that I thought may make some issues (such as Swastika!). I also removed some other controls and scales to make the algorithm easier and more useful.
A fractal is a shape made by repeating a mathematical formula. Every time, it does a similar action and when you start it from anywhere, it will make the same shape. There are some famous formulas for this job, such as Mandelbrot set or Julia set.
The technique that is used in this application is very similar to the file system in your computer. When you are working on your hard disk drive (e.g. Drive D:\ as in the following figure), you see some files and directories. When you go to a directory, you will see some other files and directories, and when you continue to go into new directories, you will find more; eventually there is a final point that there are no more directories, so you have to go back and search another place.
And, the directory structure:
In this application, the directories are similar to the branches, and files are equal to leaves, flowers or fruits on the tree. As your directories may contain different files, your tree’s branches may contain different objects.
What About Recursion?
Our main function in this application gets information of the current branch and calculates and draws new branches. There is a control for choosing your ideal divisions:
And the result is shown with 3 different selections:
|
|
|
Division per Step = 2 |
Division per Step = 4 |
Division per Step = 10 |
But the procedure makes only one level (step) of the tree, what about the others? This is what we want to speak about.
This is our main procedure in brief:
private void nextBranch(float startX, float startY, float startAngle)
{
if (myTreeInfo.myStep >= myTreeInfo.totalSteps) return;
myTreeInfo.myStep++;
endX = startX + (float)Math.Cos(startAngle * Math.PI / 180) * branchSize;
endY = startY - (float)Math.Sin(startAngle * Math.PI / 180) * branchSize;
DrawImage();
for (int j = 1; j <= myTreeInfo.divisionPerStep; j++)
{
newAngle = startAngle - maxAngle/2+ maxAngle*(j-1);
nextBranch(endX, endY, newAngle);
}
myTreeInfo.myStep--;
}
And I simulate it practically with 4 steps of generation for branches, you can download the Flash version here:
The algorithm is simple, something like the following flowchart:
Some parts of the code are as follows:
Initially I declared some Lists and Classes. objectCollection
class is for collecting all information about flowers, leaves and fruits (in calculation time). And BranchCollection
class is for collecting information of branches (the items for drawing a branch are different from a flower or a fruit). There is also a treeInfo
class for holding general information of the tree, such as maximum Tree size, angle between branches and more.
private List<string> imageTypes = new List<string>(new string[]
{ "*.gif", "*.png", "*.jpg", "*.bmp" });
private List<Image> picsBackground=new List<Image> ();
private List<Image> picsBase=new List<Image> ();
private List<Image> picsLeaves=new List<Image> ();
private List<Image> picsFlowers=new List<Image> ();
private List<Image> picsFruits=new List<Image>();
private class objectCollection
{
internal Image myImage = null;
internal float myX = 0;
internal float myY = 0;
internal float myWidth = 0;
internal float myHeight = 0;
internal int myStep = 0;
}
private objectCollection myObjectInfo;
private List<objectCollection> myAllObjectsCollection;
private class BranchCollection
{
internal float startX = 0;
internal float startY = 0;
internal float endX = 0;
internal float endY = 0;
internal int myStep = 0;
internal float myWidth = 0;
internal Color myColor;
}
private BranchCollection myBranchInfo;
private List<BranchCollection> myAllBranchCollection;
private class treeInfo
{
internal int myStep, totalSteps;
internal bool fixedSize, fixedAngle, brokenBranches;
internal float divisionPerStep, startingBranch, maxSize, maxAngle, maxBrokenBranches;
internal float leafLevel, trunkHeight, widthSize;
internal float myProgress, flowerPercent, fruitPercent, leafPercent;
}
private treeInfo myTreeInfo=new treeInfo() ;
private Bitmap myBitmapTree;
private Pen myPen = new Pen(Color.Black);
private Random myRandom = new Random();
private static bool plzStopCalculation;
private static bool plzStopDrawing;
Then I load some images from hard disk and internal resources and select random images for different parts of the application (Background, Fruits...) .
private void firstStart()
{
loadPictures();
picGround.BackgroundImage = picsBase[myRandom.Next(picsBase.Count)];
picBack.BackgroundImage = picsBackground[myRandom.Next(picsBackground.Count)];
picFlower.BackgroundImage = picsFlowers[myRandom.Next(picsFlowers.Count)];
picFruit.BackgroundImage = picsFruits[myRandom.Next(picsFruits.Count)];
picLeaf.BackgroundImage = picsLeaves[myRandom.Next(picsLeaves.Count)];
picTree.Image = Properties.Resources.about;
try
{
DirectoryInfo myDir = new DirectoryInfo(Application.StartupPath + "\\Save\\");
if (!myDir.Exists) myDir.Create();
}
catch (Exception e)
{
MessageBox.Show(e.Message);
}
}
// at the start of the program, load some pictures and shapes
private void loadPictures()
{
// add some pictures from internal resources
picsBase.Add(Properties.Resources.ground1);
picsBackground.Add(Properties.Resources.Edinburgh_Castle__Edinburgh__Scotland );
picsFlowers.Add(Properties.Resources.Flower__30_);
picsFlowers.Add(Properties.Resources.Flower__32_);
picsFruits.Add(Properties.Resources.icons6362);
picsFruits.Add(Properties.Resources.icons6367);
picsLeaves.Add(Properties.Resources.leaf_31);
//load some pictures from Hard disk (different sub directories)
loadFromHard("Ground", picsBase);
loadFromHard("Back", picsBackground);
loadFromHard("Fruits", picsFruits);
loadFromHard("Flowers", picsFlowers);
loadFromHard("Leaves", picsLeaves);
}
// load pictures from the hard disk
private void loadFromHard(string myDir, List<Image> myPicList)
{
DirectoryInfo myFileDir = new DirectoryInfo(Application.StartupPath + "\\" + myDir);
if (myFileDir.Exists)
{
// For each image extension (.jpg, .png, etc.)
foreach (string imageType in imageTypes)
{
// all graphic files in the directory
foreach (FileInfo myFile in myFileDir.GetFiles(imageType))
{
// add image
try
{
Image image = Image.FromFile(myFile.FullName);
myPicList.Add(image);
}
catch (OutOfMemoryException)
{
continue;
}
}
}
}
}
This button has 3 different actions:
- Starting a new process.
- Stopping the calculation.
- Stopping the drawing.
The following procedures control this button:
private void btnOK_Click(object sender, System.EventArgs e)
{
if (btnOK.Text == "Run")
{
goRun();
}
else if (btnOK.Text == "Stop Calculation") buttonsStatus(1);
else if (btnOK.Text == "Stop Drawing") buttonsStatus(2);
}
private void goRun()
{
setPictures();
Application.DoEvents();
getTreeInfo();
buttonsStatus(0);
calculateTree();
buttonsStatus(1);
if (!plzStopDrawing) startPaint();
if (mnuAutoSave.Checked) ImageSave();
buttonsStatus(2);
}
private void buttonsStatus(byte myStatus)
{
if (myStatus == 0)
{
plzStopCalculation = false;
plzStopDrawing = false;
progressBar.Visible = true;
btnOK.Text = "Stop Calculation";
}
else if (myStatus == 1)
{
plzStopCalculation = true;
btnOK.Text = "Stop Drawing";
}
else
{
plzStopCalculation = true;
plzStopDrawing = true;
progressBar.Visible = false;
btnOK.Text = "Run";
}
}
At the beginning of a new shape, the following procedures change random images and set control values to variables.
private void setPictures()
{
if (rdoBackgroundTexture.Checked)
{
if (chkRandomGround.Checked) picGround.BackgroundImage =
picsBase[myRandom.Next(picsBase.Count)];
if (chkRandomBack.Checked) picBack.BackgroundImage =
picsBackground[myRandom.Next(picsBackground.Count)];
}
else
{
if (chkRandomColor.Checked)lblBackgroundColor.BackColor =
Color.FromArgb(myRandom.Next(255), myRandom.Next(255), myRandom.Next(255));
}
if (chkRandomFlower.Checked && chkFlowerObjects.Checked)
picFlower.BackgroundImage = picsFlowers[myRandom.Next(picsFlowers.Count)];
if (chkRandomFruit.Checked && chkFruitObjects.Checked)
picFruit.BackgroundImage = picsFruits[myRandom.Next(picsFruits.Count)];
if (chkRandomLeaf.Checked && chkLeafObjects.Checked)
picLeaf.BackgroundImage = picsLeaves[myRandom.Next(picsLeaves.Count)];
}
private void getTreeInfo()
{
myTreeInfo.totalSteps = (int)updTotalSteps.Value;
myTreeInfo.divisionPerStep = (int)updDivisionPerStep.Value;
myTreeInfo.startingBranch = (int)updStartingBranch.Value;
myTreeInfo.maxSize = (float)updMaxSize.Value;
myTreeInfo.maxAngle = (float)updMaxAngle.Value;
myTreeInfo.maxBrokenBranches = (float)updBrokenBranches.Value;
myTreeInfo.fixedSize = chkFixedSize.Checked;
myTreeInfo.fixedAngle = chkFixedAngle.Checked;
myTreeInfo.brokenBranches = chkBrokenBranches.Checked;
myTreeInfo.leafLevel = (float)trbBranchLevel.Value;
myTreeInfo.trunkHeight = (float)trbTrunkHeight.Value;
myTreeInfo.widthSize = (float)trbWidthSize.Value;
myTreeInfo.flowerPercent = (float)updFlowerObjects.Value;
myTreeInfo.fruitPercent = (float)updFruitObjects.Value;
myTreeInfo.leafPercent = (float)updLeafObjects.Value;
if (chkFixedAngle.Checked) myTreeInfo.maxAngle *= 2 / myTreeInfo.divisionPerStep;
myTreeInfo.maxSize -= (myTreeInfo.trunkHeight / 5 - 8) * 1.6F;
if (myTreeInfo.maxSize < 1) myTreeInfo.maxSize = 1;
}
This is the main part; the following procedure starts the recursion method.
private void calculateTree()
{
myTreeInfo.myStep = 0;
myTreeInfo.myProgress = 0;
myAllBranchCollection = new List<BranchCollection>();
myAllObjectsCollection = new List<objectCollection>();
nextBranch(picTree.Width / 2, picTree.Height *4 / 5, 90);
}
And this is the recursion part:
private void nextBranch(float startX, float startY, float startAngle)
{
float endX, endY, newAngle, angleGrow, branchSize;
if (!plzStopCalculation && myTreeInfo.myStep < myTreeInfo.totalSteps)
{
if (myTreeInfo.myStep == 3)
{
myTreeInfo.myProgress += (float)(100 /
Math.Pow(myTreeInfo.divisionPerStep, myTreeInfo.myStep));
if (myTreeInfo.myProgress > 100) myTreeInfo.myProgress = 100;
progressBar.Value = (int)myTreeInfo.myProgress;
}
if (myTreeInfo.brokenBranches && myTreeInfo.myStep > 2 &&
myRandom.NextDouble ()*100 < myTreeInfo.maxBrokenBranches +
(myTreeInfo.maxBrokenBranches * ((myTreeInfo.myStep * 2 -
myTreeInfo.totalSteps) / myTreeInfo.totalSteps)) * 0.7) return;
myTreeInfo.myStep++;
myPen.Color = Color.FromArgb(100, (int)(255 *
myTreeInfo.myStep / myTreeInfo.totalSteps), 35);
myPen.Width = 10 * myTreeInfo.widthSize *
(float)Math.Pow((myTreeInfo.totalSteps - myTreeInfo.myStep), 3) /
(float)Math.Pow(myTreeInfo.totalSteps, 4);
if (myPen.Width < 1) myPen.Width = 1;
branchSize = (myTreeInfo.totalSteps - myTreeInfo.myStep *
myTreeInfo.leafLevel / 50);
if (myTreeInfo.leafLevel >= 50) branchSize *= myTreeInfo.leafLevel / 50;
else branchSize *= (myTreeInfo.leafLevel + 50) / 100;
if (branchSize <= 0) branchSize = 1;
branchSize *= (float)(picTree.Height /
Math.Pow(myTreeInfo.totalSteps, 1.9)) * myTreeInfo.maxSize / 80;
if (!myTreeInfo.fixedSize) branchSize *=
(float)(myRandom.NextDouble() * 2 + 0.1);
if (myTreeInfo.myStep < 3) branchSize +=
picTree.Height / (30 * myTreeInfo.myStep) + branchSize *
(myTreeInfo.trunkHeight / 10 - 4) / (myTreeInfo.myStep + 1.5F) *
myTreeInfo.totalSteps / 15;
endX = startX + (float)Math.Cos(startAngle * Math.PI / 180) * branchSize;
endY = startY - (float)Math.Sin(startAngle * Math.PI / 180) * branchSize;
try
{
if (myTreeInfo.myStep >= myTreeInfo.startingBranch)
{
myBranchInfo = new BranchCollection();
myBranchInfo.startX = startX;
myBranchInfo.startY = startY;
myBranchInfo.endX = endX;
myBranchInfo.endY = endY;
myBranchInfo.myStep = myTreeInfo.myStep;
myBranchInfo.myColor = myPen.Color;
myBranchInfo.myWidth = myPen.Width;
myAllBranchCollection.Add(myBranchInfo);
}
if (chkLeafObjects.Checked)
{
if (myTreeInfo.myStep > myTreeInfo.totalSteps / 4)
{
if (myRandom.Next(100000) * myTreeInfo.myStep <
Math.Pow(myTreeInfo.leafPercent, 4))
{
myObjectInfo = new objectCollection();
myObjectInfo.myImage = picLeaf.BackgroundImage;
myObjectInfo.myStep = myBranchInfo.myStep;
float myScale = (float)myRandom.Next(13) /
myObjectInfo.myImage.Width;
myObjectInfo.myWidth = myObjectInfo.myImage.Width * myScale;
myObjectInfo.myHeight = myObjectInfo.myImage.Height * myScale;
myObjectInfo.myX = startX - myObjectInfo.myWidth / 2;
myObjectInfo.myY = startY - myObjectInfo.myHeight / 2;
myAllObjectsCollection.Add(myObjectInfo);
}
}
}
if (chkFlowerObjects.Checked)
{
if (myTreeInfo.myStep > myTreeInfo.totalSteps / 2)
{
if (myRandom.Next(100000) * myTreeInfo.myStep <
Math.Pow(myTreeInfo.flowerPercent, 3.5))
{
myObjectInfo = new objectCollection();
myObjectInfo.myImage = picFlower.BackgroundImage;
myObjectInfo.myStep = myBranchInfo.myStep;
float myScale = (float)myRandom.Next(12) /
myObjectInfo.myImage.Width;
myObjectInfo.myWidth = myObjectInfo.myImage.Width * myScale;
myObjectInfo.myHeight = myObjectInfo.myImage.Height * myScale;
myObjectInfo.myX = startX - myObjectInfo.myWidth / 2;
myObjectInfo.myY = startY - myObjectInfo.myHeight / 2;
myAllObjectsCollection.Add(myObjectInfo);
}
}
}
if (chkFruitObjects.Checked)
{
if (myTreeInfo.myStep > myTreeInfo.totalSteps * 4 / 5)
{
if (myRandom.Next(100000) * myTreeInfo.myStep <
Math.Pow(myTreeInfo.fruitPercent, 3))
{
myObjectInfo = new objectCollection();
myObjectInfo.myImage = picFruit.BackgroundImage;
myObjectInfo.myStep = myBranchInfo.myStep;
float myScale = (float)myRandom.Next(15) /
myObjectInfo.myImage.Width;
myObjectInfo.myWidth = myObjectInfo.myImage.Width * myScale;
myObjectInfo.myHeight = myObjectInfo.myImage.Height * myScale;
myObjectInfo.myX = startX - myObjectInfo.myWidth / 2;
myObjectInfo.myY = startY - myObjectInfo.myHeight / 2;
myAllObjectsCollection.Add(myObjectInfo);
}
}
}
}
catch (Exception)
{
}
for (int j = 1; j <= myTreeInfo.divisionPerStep; j++)
{
angleGrow = myTreeInfo.maxAngle;
if (myTreeInfo.fixedAngle) angleGrow =
angleGrow / 2 - angleGrow * (j - myTreeInfo.divisionPerStep / 2);
else angleGrow *= (float)(myRandom.NextDouble() * 2 - 1);
newAngle = (startAngle + angleGrow) % 360;
nextBranch(endX, endY, newAngle);
}
myTreeInfo.myStep--;
Application.DoEvents();
}
}
In the previous version, drawing and calculation parts were in the same procedure, but here, I divided them into 2 parts. Drawing of objects is in order by their levels (steps).
private void startPaint()
{
try
{
myBitmapTree = new Bitmap(picTree.Width , picTree.Height );
Graphics gTree=Graphics.FromImage(myBitmapTree );
gTree.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
if (rdoBackgroundTexture.Checked)
{
gTree.DrawImage(picBack.BackgroundImage,
new Rectangle(0, 0, picTree.Width, picTree.Height),
new Rectangle(0, 0, picBack.BackgroundImage.Width,
picBack.BackgroundImage.Height), GraphicsUnit.Pixel);
gTree.DrawImage(picGround.BackgroundImage,
new Rectangle(0, picTree.Height * 2 / 3, picTree.Width,
picTree.Height / 3), new Rectangle(0, 0,
picGround.BackgroundImage.Width,
picGround.BackgroundImage.Height), GraphicsUnit.Pixel);
}
else
{
gTree.FillRectangle(new SolidBrush(lblBackgroundColor.BackColor),
0, 0, picTree.Width, picTree.Height);
}
progressBar.Value = 0;
for (int i = 0; i <= myTreeInfo.totalSteps ; i++)
{
foreach (BranchCollection myB in myAllBranchCollection)
{
if (i == myB.myStep-1 ) DrawTree(gTree, myB);
}
picTree.Image = myBitmapTree;
foreach (objectCollection myP in myAllObjectsCollection)
{
if (i == myP.myStep ) DrawPicture(gTree, myP);
}
picTree.Image = myBitmapTree;
progressBar.Value = i * 100 / myTreeInfo.totalSteps;
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
private void DrawTree(Graphics gTree, BranchCollection myB)
{
Application.DoEvents();
if(!plzStopDrawing) gTree.DrawLine(new Pen(myB.myColor, myB.myWidth),
myB.startX, myB.startY, myB.endX, myB.endY);
}
private void DrawPicture(Graphics gTree, objectCollection myP)
{
if (!plzStopDrawing) gTree.DrawImage
(myP.myImage, myP.myX, myP.myY, myP.myWidth, myP.myHeight);
}
That's it.
The GUI is very simple.
There are some predefined samples that you can access from the main menu:
You can add your own samples in the menu; add values in Click event of the item as follows:
private void mnuFlower6_Click(object sender, EventArgs e)
{
putInfo(150, 49, 8, 0, 55, 0, 38, 1, 9800, 40, 40, 20, 50, 1,
0, 30, 1, 1, 0, 50, 1, 0, 0, 0, 0);
}
The values are in order by placing Controls on the form. And following the procedure sets the values on the controls.
private void putInfo(params int[] infoArray)
{
updTotalSteps.Value = infoArray[0];
updDivisionPerStep.Value = infoArray[1];
updStartingBranch.Value = infoArray[2];
chkFixedSize.Checked = Convert.ToBoolean(infoArray[3]);
updMaxSize.Value = infoArray[4];
chkFixedAngle.Checked = Convert.ToBoolean(infoArray[5]);
updMaxAngle.Value = infoArray[6];
chkBrokenBranches.Checked = Convert.ToBoolean(infoArray[7]);
updBrokenBranches.Value = (decimal)infoArray[8] / 100;
trbBranchLevel.Value = infoArray[9];
trbTrunkHeight.Value = infoArray[10];
trbWidthSize.Value = infoArray[11];
chkLeafObjects.Checked = Convert.ToBoolean(infoArray[13]);
chkFlowerObjects.Checked = Convert.ToBoolean(infoArray[17]);
chkFruitObjects.Checked = Convert.ToBoolean(infoArray[21]);
if (chkLeafObjects.Checked)
{
if (infoArray[14] > 0) picLeaf.BackgroundImage = picsLeaves[infoArray[14] - 1];
if (infoArray[15] > 0) updLeafObjects.Value = infoArray[15];
chkRandomLeaf.Checked = Convert.ToBoolean(infoArray[16]);
}
if (chkFlowerObjects.Checked)
{
if (infoArray[18] > 0) picFlower.BackgroundImage = picsFlowers[infoArray[18] - 1];
if (infoArray[19] > 0) updFlowerObjects.Value = infoArray[19];
chkRandomFlower.Checked = Convert.ToBoolean(infoArray[20]);
}
if (chkFruitObjects.Checked)
{
if (infoArray[22] > 0) picFruit.BackgroundImage = picsFruits[infoArray[22] - 1];
if (infoArray[23] > 0) updFruitObjects.Value = infoArray[23];
chkRandomFruit.Checked = Convert.ToBoolean(infoArray[24]);
}
Also you can change textures and pictures of objects by yourself. There are several directories in the executable file's place that contain these images and you can change or add more.
- First release (Feb 9, 2009)
- Update 1 (Feb 16, 2009): Added Timer
- Update 2 (Feb 25, 2009): Added Save options
- Update 3 (Mar 23, 2009): Some small changes