Click here to Skip to main content
15,895,822 members
Articles / Programming Languages / C#

Dynamic Fractals using WPF Transforms

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
12 Mar 2017CPOL3 min read 8.5K   189   5  
Generation of dynamic and randomized fractals using recursion technique.

Introduction

Life uses DNA and proteins to create structures. Animals are very complicated but structures of plants and trees can be described in terms of fractals. A simple equation could describe the structure of a plant and trees. But not all plants of the same species look the same. The branches are randomized, so is the appearance of flowers. Hence, fractals with a probabilistic parameters can create dynamic structure of plants and trees.

I started looking at the plants and trees and realized that they can be described as a fractal with simple base shape. Hence, I was looking for ways to convert a simple shape into fractals and realized that there is no such application to make it ease. Hence I used WPF and C# to create a GUI to create fractals of any shape.

In WPF, Canvas can accommodate many FrameworkElements, hence, in the GUI, I need to be able to create a Path using PointControls. WPF Transforms such as ScaleTransform, RotateTransform and TranslateTransform can be used on FracmeworkElements.

Code and UI

UserControl: PointControl

A draggable Point, which make up the PointControl, can be created by using a Thumb Control which can be dragged in the Canvas. PointControl encapsulates the property Point which are updated when the Thumb moves, using DragDeltaevent handler.

C#
private void Thumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
{
    Canvas.SetLeft(this, Canvas.GetLeft(this) + e.HorizontalChange);
    Canvas.SetTop(this, Canvas.GetTop(this) + e.VerticalChange);
    Point = new Point(Canvas.GetLeft(this) + 5, Canvas.GetTop(this) + 5);

    OnMove(this, new EventArgs());
}

The properties of PointControl, which are binded are the following.

public ObservableCollection<TransformProperty> Transforms { get; set; }
public Point Point { get; set; }
public double Size { get; set; }
public string SegmentType { get; set; }

PointControl also has an Move and Change event handlers which can be used to update SelectedPath. The main Path is made up of ArcSegments or LineSegments, whose properties are binded to the PointControl’s Point and Type. Hence, real time update of Path is made possible.

C#
BindingBase sourceBinding = new Binding { Source = this, Path = new PropertyPath(PointControl.PointProperty) };
BindingOperations.SetBinding(arc, ArcSegment.PointProperty, sourceBinding);

sourceBinding = new Binding { Source = this, Converter = new SizeConverter(), ConverterParameter = new { PointControls = MainWindow.Current.shape.PointControls, Self=this } , Path = new PropertyPath(PointControl.SizeProperty) };
BindingOperations.SetBinding(arc, ArcSegment.SizeProperty, sourceBinding);

sourceBinding = new Binding { Source = this, Converter = new SweepSizeConverter(), ConverterParameter = new { PointControls = MainWindow.Current.shape.PointControls, Self = this }, Path = new PropertyPath(PointControl.SizeProperty) };
BindingOperations.SetBinding(arc, ArcSegment.SweepDirectionProperty, sourceBinding);

With that setup, a ContextMenu is used to display with UI for PointControl to change the binded parameters.

Transforms

Many Transforms can be associated with a Point, Hence an ObservableCollection of TransformPropertys, which are the following is included in the PointControl, which is binded to a ListBox within the ContextMenu.

Image 1

C#
public class TransformProperty
{
    public double SizeRatio;
    public bool Flip;
    public double BaseRotation;
    public Color FillBrush;
    public Color LineBrush;
    public double Probability;
    public bool EndHere;

    public int StartLevel;
    public int EndLevel;
}

Each property is binded to controls in DataTemplate of the ListBox. Fractals are rendered iteratively, hence, StartLevel and EndLevel can be used to control the activation range in the Fractals. Probability can be used to enable random creation of branches. Don’t Sprout makes the rendered child path final and no more branches will sprout from that.

Image 2

Code

Once the shape is created, along with TransformPropertys. Each TransformProperty is changed to WPF TransformGroup using the following function. Initial Path is taken and copied to a new path, and appropriate transformation are applied, to create the fractal branch.

C#
public TransformGroup ConvertToTransform(Point rotationCenter, Point StartPoint)
{
    TransformGroup group = new TransformGroup();

    List<Transform> transforms = new List<Transform>();

    if (Flip)
        group.Children.Add(new ScaleTransform(-1, 1) { CenterX = StartPoint.X, CenterY = StartPoint.Y });

    group.Children.Add(new TranslateTransform() { X= rotationCenter.X - StartPoint.X, Y=rotationCenter.Y - StartPoint.Y  });
    group.Children.Add(new ScaleTransform( SizeRatio, SizeRatio) { CenterX = rotationCenter.X, CenterY = rotationCenter.Y });
    group.Children.Add(new RotateTransform(BaseRotation) { CenterX = rotationCenter.X, CenterY = rotationCenter.Y });

    return group;
}

The recursion with which the fractal is created is given below

C#
    public void GeneratePath(int step, PointControl currentPoint, PointControl originalReferencePoint, double prevRotation = 0, double prevSize = 1, TransformGroup prevTransform = null)
{
    foreach (TransformProperty transform in currentPoint.Transforms)
    {
        // Check if tranformation is active on this level and check probablity of activation
        if ((transform.StartLevel <= step && (transform.EndLevel >= step || transform.EndLevel == 0)) && transform.Probability >= Random.NextDouble())
        {
            //Create copy of the path
            Path currentPath = new Path() { Data = Path.Data };
            Point originalRefPoint = originalReferencePoint.Point;

            currentPath.Fill = new SolidColorBrush(transform.FillBrush);
            currentPath.Stroke = new SolidColorBrush(transform.LineBrush);

            //Set size and rotation, depending on the previous size and rotation
            double sizeRatio = prevSize * transform.SizeRatio;
            double rotation = prevRotation + transform.BaseRotation;

            //Create transform objects and add to group
            TransformGroup currentTransform = new TransformGroup();

            if (transform.Flip)
                currentTransform.Children.Add(new ScaleTransform(-1, 1) { CenterX = originalRefPoint.X, CenterY = originalRefPoint.Y });

            currentTransform.Children.Add(new ScaleTransform() { ScaleX = sizeRatio, ScaleY = sizeRatio, CenterX = originalRefPoint.X, CenterY = originalRefPoint.Y });
            currentTransform.Children.Add(new RotateTransform() { Angle = rotation, CenterX = originalRefPoint.X, CenterY = originalRefPoint.Y });

            if (prevTransform == null)
            {
                currentTransform.Children.Add(new TranslateTransform() { X = currentPoint.Point.X - originalRefPoint.X, Y = currentPoint.Point.Y - originalRefPoint.Y });
            }
            else
            {
                Point currentPointTransformed = prevTransform.Transform(currentPoint.Point);
                currentTransform.Children.Add(new TranslateTransform() { X = currentPointTransformed.X - originalRefPoint.X, Y = currentPointTransformed.Y - originalRefPoint.Y });
            }

            //Apply transform to path
            currentPath.RenderTransform = currentTransform;

            //Add path to canvas
            canvas.Children.Add(currentPath);

            //Go to the next level and repeat
            if (step < MaxSteps && !transform.EndHere)
                foreach (PointControl point in PointControls)
                    GeneratePath(step + 1, point, originalReferencePoint, rotation, sizeRatio, currentTransform);
        }
    }
}

Scale and rotate level in go according to a simple multiplication and addition

C#
double sizeRatio = prevSize * transform.SizeRatio;
double rotation = prevRotation + transform.BaseRotation;

But translate needs the point node to be sprouted after begin transformed by previous transformation.

Hence we use recursion variable prevTransform which is a TransformGroup to translate a node point with previous transformation to provide the current point, to which the path needs to be translated.

C#
Point currentPointTransformed = prevTransform.Transform(currentPoint.Point);

Weather the branch is to be sprouted or not is given by the following block within the recursive function.

if ((transform.StartLevel <= step && (transform.EndLevel >= step || transform.EndLevel == 0)) && transform.Probability >= Random.NextDouble())

File Format

Each Fractal is generated by converting the PointControls and TransformPropertys to a serializable format, and saving it as XML. Extension is *.frac. Here, SaveFormat object is populated with the Path, Points and Transforms and then serialized using XMLSerializer. Retrival is done in the same way, creating PointControls out of SavePoints and Transforms out of SaveTransforms.

Saving is done in the following way

private void SaveFractalFile()
    {
        if (shape.PointControls.Count > 0)
        {
            SaveFormat format = new SaveFormat();

            format.CanvasColor = CanvasColor;
            format.FillColor = shapeViewModel.FillColor;
            format.LineColor = shapeViewModel.LineColor;
            format.PointControls = SaveFormat.SavePointFromControls(shape.PointControls);
            format.ReferencePoint = shapeViewModel.ReferencePoint.Point;
            format.Steps = Steps;

            SaveFileDialog save = new SaveFileDialog();
            save.AddExtension = true;
            save.DefaultExt = ".frac";
            save.Filter = "Fractal Files | *.frac";

            save.FileOk += (o, e) =>
            {
                var stream = File.Open(save.FileName, FileMode.Create);

                XmlSerializer serializer = new XmlSerializer(typeof(SaveFormat));
                serializer.Serialize(stream, format);

                stream.Close();
            };

            save.ShowDialog();
        }
    }

Retrival in the following way

    private void ReadFractalFile(string path)
{
    XmlSerializer serializer = new XmlSerializer(typeof(SaveFormat));
    SaveFormat format = (SaveFormat)serializer.Deserialize(File.Open(path, FileMode.Open));


    List<UIElement> removethese = new List<UIElement>();
    for (int i = 0; i < canvas.Children.Count; i++)
    {
        if (canvas.Children[i] != selectedPath)
            removethese.Add(canvas.Children[i]);
    }
    removethese.ForEach(r =>
    {
        canvas.Children.Remove(r);
    });
    removethese.Clear();
    ControlsStack.Children.Remove(shape);
    shapeViewModel = null;
    shape = null;
    Steps = 0;
    CanvasColor = Colors.Transparent;


    Steps = format.Steps;
    CanvasColor = format.CanvasColor;
    shapeViewModel = new ViewModels.ShapeViewModel();
    shapeViewModel.SelectedPath = selectedPath;


    shape = new Shape(canvas) { DataContext = shapeViewModel };
    shapeViewModel.Path = shape.path;

    ControlsStack.Children.Add(shape);

    shapeViewModel.FillColor = format.FillColor;
    shapeViewModel.LineColor = format.LineColor;

    foreach (SavePoint point in format.PointControls)
    {
        List<TransformProperty> transforms = new List<TransformProperty>();
        foreach(SaveTransform transform in point.Transforms)
        {
            transforms.Add(new TransformProperty(null) {
                 BaseRotation = transform.BaseRotation,
                 EndHere = transform.EndHere,
                 EndLevel = transform.EndLevel,
                 FillBrush = transform.FillBrush,
                 Flip = transform.Flip,
                 LineBrush = transform.LineBrush,
                 Probability = transform.Probability,
                 SizeRatio = transform.SizeRatio,
                 StartLevel = transform.StartLevel
            });
        }

        shape.AddPoint(point.Point, point.SegementType, point.ArcRadius, transforms);
    }
}

Examples

Randomized branch generation

Image 3

Few examples

Image 4

GIT

https://gitlab.com/hemanthk119/fractaldesigner

Tips

The number of steps to execute should be kept low to make sure the calculations don’t require much time, but high enough to maintain some detail. For 4 branches and 15 steps will require creation of 1073741824 paths and appropriate transformations. Just keep it low. Application also has some *.frac files in Examples folder, which can be opened by the application via menu to see some examples. Press run button to execute the Fractal.

License

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


Written By
Software Developer
India India
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
-- There are no messages in this forum --