Summmary
I've gone ahead and started tackling the emulated fractal application I mentioned in past posts. The logic is essentially the same (from what I can remember) as a previous AS3 (actionscript3) version I read about and recreated about a year and a half ago.

The logic is the same, however the technology has presented a few different challenges. I remember using a custom animation helper class called... I can't remember...but I'd assume it is highly similar to a helper class like Tweener.
Technical Details: The application utilizes a Leaf control as an extended Grid control. Each Leaf contains a few basic geometrical shapes, an Ellipse and Rectangle, as well as an Point, anchor. Anchor is used to store the location in the grid where the child leaf ellipse center is translated. The entire control visually speaking consists of a Root (leaf), branches (sets of leaves) and leaves (children sets of leaves). In the main Page control, a set of public variables allow the user to set the number of leaves per branch tier and total branch reiterations. This is why I reference this app as emulating a fractal - finite iterations are preferential in our particular realm. Needless to say I have added a permeation cap for the total leaf count to the input console I have added. Users are able to reset a number of variables and observe the changes made in the application at runtime.
StoryBoards are used to manipulate the RotateTransform properties of each Leaf (Grid) control.
Controls used consist of a Page, CustInterface, and Leaf Silverlight control as well as two helper classes - CustomButton and CustomTransform.
Leaf
The Leaf control uses a Grid LayoutRoot which contains an Ellipse (Gear?) and Rectangle (Rod?), and Anchor Point as well as two functions that allow us to directly alter properties for each individual leaf.
public partial class Leaf : UserControl
{
We create a few public variables allowing us direct access to each Leaf’s major compontents. The anchor stores the point location for the ellipse center as you will see. We will use this to center each Leaf’s RotateTransform in order to give the appropriate visual effect.
public Point anchor;
public Ellipse ellipse;
public Rectangle rectangle;
public Leaf()
{
InitializeComponent();
this.Width = 200;
this.Height = 200;
LayoutRoot.Background = new SolidColorBrush(Colors.Transparent);
ellipse = new Ellipse();
ellipse.Width = 80;
ellipse.Height = 80;
ellipse.Fill = new SolidColorBrush(Color.FromArgb(255, 50, 50, 50));
ellipse.StrokeThickness = 25;
ellipse.Stroke = new SolidColorBrush(Color.FromArgb(255, 83, 83, 83));
ellipse.SetValue(Canvas.ZIndexProperty, 1);
LayoutRoot.Children.Add(ellipse);
rectangle = new Rectangle();
rectangle.Width = 100;
rectangle.Height = 25;
rectangle.Fill = new SolidColorBrush(Color.FromArgb(255, 83, 83, 83));
rectangle.SetValue(Canvas.ZIndexProperty, 0);
TranslateTransform tt_rectangle = new TranslateTransform();
tt_rectangle.X = rectangle.Width / 2;
tt_rectangle.Y = 0;
rectangle.RenderTransform = tt_rectangle;
LayoutRoot.Children.Add(rectangle);
anchor = new Point();
anchor.X = (this.Width / 2);
anchor.Y = 0;
}
Two functions allow us direct access to common control manipulation – Altering the color of the leaf as well as altering the dimensions of the leaf’s major components:
public void ChangeLeafColor(SolidColorBrush scb_newColor)
{
ellipse.Stroke = scb_newColor;
rectangle.Fill = scb_newColor;
}
public void ChangeLeafDim(double cRecWidth, double cRecHeight, double cEllDiameter)
{
double nWidth = rectangle.Width + cRecWidth;
double nHeight = rectangle.Height + cRecHeight;
if (nWidth > 0 && nWidth <= 100 && nHeight > 0 && nHeight <= 100)
{
rectangle.Width += cRecWidth;
rectangle.Height += cRecHeight;
}
nWidth = ellipse.Width + cEllDiameter;
nHeight = ellipse.Height + cEllDiameter;
if (nWidth > 0 && nWidth <= 100 && nHeight > 0 && nHeight <= 100)
{
ellipse.Width += cEllDiameter;
ellipse.Height += cEllDiameter;
}
}
}
Page
The Page control initializes the variables used to control the application and contains a RenderLeaves() function which iterates the number of times specified by the numLeaves and numBranches variables. These variables contain initialized values visible when the app is first loaded such as how big the ellipse and rectangle contained with each Leaf control should be, what color and opacity each control should be set as well as other settings.
public partial class Page : UserControl
{
A number of variables are setup which will be used to dictate how the application runs. We’ll explain each in detail in a second.
public int numBranches, numLeaves, minSpeed, maxSpeed, zindex;
public byte Red, cRed, Green, cGreen, Blue, cBlue;
public double rotate, cRotate, scale, cScale, cOpacity, opacity, rWidth, rHeight, eDiameter;
public Leaf cLeaf;
public List<Leaf> lastLeaves, tempLeaves;
public Random randSpeed;
public SolidColorBrush scl;
public Grid grid;
public Page()
{
zindex = 0; //-- Used to set ZIndex of each added Leaf
InitializeComponent();
We setup the basic application dimensions…
this.Width = App.Current.Host.Content.ActualWidth;
this.Height = App.Current.Host.Content.ActualHeight;
LayoutRoot.Width = this.Width;
LayoutRoot.Height = this.Height;
…Add a cool background image…
//-- Add a cool background image
Uri uri_img_back = new Uri("http://www.digitalboon.com/images/background.jpg", UriKind.Absolute);
BitmapImage bmi_img_back = new BitmapImage();
bmi_img_back.UriSource = uri_img_back;
Image img_back = new Image();
img_back.Source = bmi_img_back;
LayoutRoot.Children.Add(img_back);
…and then add a Grid control which will contain our Leaves (allows us to simply Grid.Children.Clear() when we want to reload a new set of application settings)
//-- Contains Root and its leaves (allows for translation)
grid = new Grid();
grid.Opacity = 0.0;
LayoutRoot.Children.Add(grid);
We attach a StoryBoard targeting the Grid’s Opacity to add a fade in load effect when new settings are applied.
//-- Add a StoryBoard for Load Fade Effect
Storyboard sb_grid = new Storyboard();
this.Resources.Add("sb_grid", sb_grid);
sb_grid.FillBehavior = FillBehavior.HoldEnd;
DoubleAnimation da_opacity = new DoubleAnimation();
da_opacity.From = 0;
da_opacity.To = 1;
da_opacity.Duration = TimeSpan.FromSeconds(1.5);
Storyboard.SetTarget(da_opacity, grid);
Storyboard.SetTargetProperty(da_opacity, new PropertyPath("(UIElement.Opacity)"));
sb_grid.Children.Add(da_opacity);
Then we attach an event handler that keeps content centered when the user resizes the browser.
//-- Keep the background image centered on host resize
App.Current.Host.Content.Resized += (s, e) =>
{
LayoutRoot.Width = App.Current.Host.Content.ActualWidth;
LayoutRoot.Height = App.Current.Host.Content.ActualHeight;
img_back.SetValue(Canvas.LeftProperty, Convert.ToDouble((App.Current.Host.Content.ActualWidth - img_back.ActualWidth) / 2));
grid.SetValue(Canvas.LeftProperty, Convert.ToDouble((App.Current.Host.Content.ActualWidth - grid.ActualWidth) / 2));
grid.SetValue(Canvas.TopProperty, 250D);//Convert.ToDouble((App.Current.Host.Content.ActualHeight - grid.ActualHeight) / 2));
};
Remember those variables we declared? Let’s go ahead and initialize some values. LastLeaves and tempLeaves are lists used to store the last branch iteration of added leaves so we have access to their anchor points. This as well stores the last number of leaves added and so we can gather how many new children leaves we will need to create for our next branch tier.
lastLeaves = new List<Leaf>(); //-- Last Set of Leaves added (Track num new anchor points and coords)
tempLeaves = new List<Leaf>(); //-- Stores next wave of new Leaves
numBranches = 3; //-- Number of Branches
numLeaves = 3; //-- Number Leaves per Branch
rWidth = 100; //-- Init Leaf Rectangle Width
rHeight = 25; //-- Init Leaf Rectangle Height
eDiameter = 80; //-- Init Leaf Ellipse Diameter
rotate = 0; //-- Initial Rotation
cRotate = 60; //-- Change in Rotation (degrees)
scale = 1; //-- Initial Scale
cScale = 0.15; //-- Change in Scale -= per Leaf addition
minSpeed = 4; //-- Minimum rotational speed (time for one full rotation in seconds)
maxSpeed = 18; //-- Maximum rotational speed (time for one full rotation in seconds)
randSpeed = new Random(); //-- Generates random rotation speed value using minSpeed and maxSpeed
The above settings are used to setup Leaf dimensional settings while the below are used to change their color and opacity per branch iteration.
//-- Byte data for initial color and color change over branch iterations
Red = 21; //-- Initial R value
cRed = 0; //-- Change in R value for next branch addition -=
Green = 93;
cGreen = 0;
Blue = 138;
cBlue = 1; //-- Change in RGB Blue value -=
scl = new SolidColorBrush(Color.FromArgb(255, Convert.ToByte(Red), Convert.ToByte(Green), Convert.ToByte(Blue))); //-- Base Fill Color
opacity = 1.0; //-- Initial leaf opacity
cOpacity = 0.15; //-- Change in leaf opacity per tier iteration -=
After we’ve setup our initial variables, we add our CustInterface (control panel) and initialize it’s input controls to reflect the default application settings. This control was built in Expression Blend and contains sliders and text boxes among other basic controls.
//-- Add Our Custom Interface Control and Set to Init App Values
CustInterface custInterface = new CustInterface();
custInterface.combo_branches.SelectedIndex = numBranches - 1;
custInterface.combo_leaves.SelectedIndex = numLeaves - 1;
custInterface.slider_recWidth.Value = rWidth;
custInterface.slider_recHeight.Value = rHeight;
custInterface.slider_ellipseDiam.Value = eDiameter;
custInterface.slider_Scale.Value = cScale;
custInterface.slider_cOpacity.Value = cOpacity;
custInterface.slider_R.Value = Red;
custInterface.slider_G.Value = Green;
custInterface.slider_B.Value = Blue;
custInterface.chkbx_B.IsChecked = true;
custInterface.slider_AlterColor.Value = cBlue;
LayoutRoot.Children.Add(custInterface)
custInterface.SetValue(Canvas.LeftProperty, 50D);
custInterface.SetValue(Canvas.TopProperty, 50D);
Finally, we call our RenderLeaves() function, see below.
RenderLeaves();
this.UpdateLayout();
}
public void RenderLeaves()
{
The first step in calling our RenderLeaves() function is to clear all previously added children to our Gird control which contains our Leaves. When we apply changes to application variables we’ll be glad we did.
grid.Children.Clear();
Next we add a Root (Leaf) to start us off. This will serve as a single leaf branch. Our CustomTransform.cs helper class simply returns a TransformGroup with added Translate, Rotate, Skew, and Scale Transform.
Leaf root = new Leaf(); //-- Primary Root or start of leaves/branches
TransformGroup tg_root = CustomTransform.createNew(0, 0, 0, 1, 1, 0, 0);
root.RenderTransformOrigin = new Point(0.5, 0.5);
root.RenderTransform = tg_root;
root.SetValue(Canvas.ZIndexProperty, 0);
root.ChangeLeafColor(scl);
grid.Children.Add(root);
Then we add a StoryBoard with a single DoubleAnimation targeting the RotateTransform.Angle property of our Leaf’s TranformGroup. We set the DoubleAnimation to rotate forever from the Leaf’s current initial rotation, to a negative rotational value equal to one full revolution counter-clockwise. I’ve setup the Root to make one full revolution in 18 seconds. Children leaves will rotate at random intervals.
Storyboard sb_root = new Storyboard();
sb_root.RepeatBehavior = RepeatBehavior.Forever;
DoubleAnimation da_rotateRoot = new DoubleAnimation();
da_rotateRoot.From = rotate;
da_rotateRoot.To = -(360 - rotate);
da_rotateRoot.Duration = TimeSpan.FromSeconds(18);
Storyboard.SetTarget(da_rotateRoot, root);
Storyboard.SetTargetProperty(da_rotateRoot, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[1].(RotateTransform.Angle)"));
sb_root.Children.Add(da_rotateRoot);
sb_root.Begin();
As our Root is our first Leaf to contain anchor data, we add it to the tempLeaves list for future anchor point reference. cLeaf is used to store the parent leaf in the current branch tier we are actively adding leaves to.
tempLeaves.Add(root);
cLeaf = root; //-- cLeaf references last added anchor leaf point
For the number of total branch tiers…
for (int x = 0; x < numBranches; x++) //-- Branches or sets of leaves
{
Store references to the last tier of leaves added
lastLeaves.Clear();
foreach (Leaf leaf in tempLeaves)
{
lastLeaves.Add(leaf);
}
tempLeaves.Clear();
For the number of leaves added in the last branch tier…
for (int y = 0; y < lastLeaves.Count; y++)
//-- Sets of leaves contain numLeaves (new branches)
{
For each leaf in the last branch tier…
cLeaf = lastLeaves[y];
for (int z = 0; z < numLeaves; z++)
//-- Add numLeaves for each (new branch)
{
Add a new leaf and attach it to that last leaf.
Leaf leaf = new Leaf();
TransformGroup tg_leaf = CustomTransform.createNew(lastLeaves[y].anchor.X, lastLeaves[y].anchor.Y, rotate, scale, scale, 0, 0);
leaf.RenderTransformOrigin = new Point(1, 0.5);
leaf.RenderTransform = tg_leaf;
leaf.SetValue(Canvas.ZIndexProperty, zindex);
leaf.ChangeLeafColor(scl);
leaf.Opacity = opacity;
leaf.rectangle.Width = rWidth;
leaf.rectangle.Height = rHeight;
leaf.ellipse.Width = eDiameter;
leaf.ellipse.Height = eDiameter;
Storyboard sb_leaf = new Storyboard();
sb_leaf.RepeatBehavior = RepeatBehavior.Forever;
DoubleAnimation da_rotate = new DoubleAnimation();
da_rotate.From = rotate;
da_rotate.To = -(360 - rotate);
da_rotate.Duration = TimeSpan.FromSeconds(Convert.ToDouble(randSpeed.Next(minSpeed, maxSpeed)));
Storyboard.SetTarget(da_rotate, leaf);
Storyboard.SetTargetProperty(da_rotate, new PropertyPath("(UIElement.RenderTransform).(TransformGroup.Children)[1].(RotateTransform.Angle)"));
sb_leaf.Children.Add(da_rotate);
sb_leaf.Begin();
rotate += cRotate;
cLeaf.LayoutRoot.Children.Add(leaf);
Remember to update our tempLeaves list so have parent data for our next child iteration of leaves.
tempLeaves.Add(leaf);
zindex++;
}
}
For each new branch we alter color variables according to last changed value -= change per
iteration value…
Red -= cRed;
Green -= cGreen;
Blue -= cBlue;
scl = new SolidColorBrush(Color.FromArgb(255, Red, Green, Blue))
..as well we alter our scale and opacity (checking to ensure they are not reduced to or below 0)
if(scale > 0)
{
scale -= cScale;
}
if(opacity > 0)
{
opacity -= cOpacity;
}
}
After all Leaves have been added to locate begin our Storyboard that fades the entire collection in and
clear our lists so they are ready for the next settings application.
Storyboard sb_grid = (Storyboard)this.Resources["sb_grid"];
if (sb_grid != null)
{
sb_grid.Begin();
}
lastLeaves.Clear();
tempLeaves.Clear();
I realize I have neglected to review the CustomButton.cs helper class in any detail but do not worry; I will cover this in perhaps my next post – Custom Buttons in Silverlight!
Challenges/Conclusions
So far the only point of aggravation concerns the way Silverlight renders Storyboard enabled transforms in relation to preexisting transforms (namely a TranslateTransform). I'm finding it difficult to pinpoint the appropriate rendertransformorigin point. I do know the original TranslateTranform X and Y values and so migrating these values to a percentage should be as simple as (and taking into account the Grid control places objects at its center):
Leaf.RenderTransformOrigin = new Point((0.5 + (tt.X / Leaf.Width) = (n / 100)) / 100) / ((0.5 + (tt.Y / Leaf.Height) = (n / 100)) / 100));
Update: This challenge has been overcome by simply altering the RenderTransformOrigin X coordinate to 1.0 and leaving the Y coordinate at 0.5. Our anchor points used to calculate the translate values for children leaves has been altered to equate to half the total width of the parent grid. In retrospect if I was to deploy this application again, I would remove the use of anchors and rely on the static translate point the application essentially already uses.
Other than this I found an odd glitch when adding my custom button to the CustInterface control. It gave the appearance of improper clipping of the “Apply” button seen in the app. I was unable to figure out what was disrupting its proper rendering and ended up working around the problem by adding a parent Grid with the same dimensions as the CustomButton. Crude by effective, I suppose.