This falls into the category of “just for fun”
For a little while now I’ve been dropping small Silverlight tutorials onto the ActiveTuts site which is, primarily, geared towards Flash development.
The ActiveTuts guys have been a real pleasure to work with, have a very large following and are really good at making your work look professional At this point I have 12 tutorials up on the site and there are some more in the publishing pipeline.
If you’re a regular reader of this blog, these videos might be too “introductory” for you but while I was browsing the ActiveTuts+ site the other day I came across a really nice little tutorial for Flash;
which I really liked – it’s originally from 2009 and it’s simple enough that a non-Flash person like me could follow along with it and it was inspirational in the sense that it made me wonder whether I could follow similar steps to create a Silverlight version of the same effect and how well/badly that would work out.
And so that’s what I did…
Silverlight Version of the Tutorial
I made a new project in Visual Studio;
and set up my main page to host a Canvas at 640×480 and set the background to the same gradient used in the Flash example;
from a XAML perspective this is;
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:SilverlightApplication4" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="SilverlightApplication4.MainPage"> <Canvas x:Name="LayoutRoot" Width="640" Height="480"> <Canvas.Background> <RadialGradientBrush> <GradientStop Color="#FF1F63B4" Offset="1" /> <GradientStop Color="#FF02C7FB" /> </RadialGradientBrush> </Canvas.Background> </Canvas> </UserControl>
and then figured that I would try and encapsulate as much of my drawing code into a UserControl so I added a new user control into the project and then drew an Ellipse onto my control before editing its XAML to add a few bits and pieces that Visual Studio can’t easily add;
and so the definition of my control ends up being;
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="SilverlightApplication4.UserControl1" d:DesignWidth="96" d:DesignHeight="96"> <UserControl.Resources> <Storyboard x:Name="movement" Completed="OnMovementCompleted"> <DoubleAnimation x:Name="animationX" From="0" To="0" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateX)" Storyboard.TargetName="ellipse" d:IsOptimized="True" /> <DoubleAnimation x:Name="animationY" From="0" To="0" Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.TranslateY)" Storyboard.TargetName="ellipse" d:IsOptimized="True" /> </Storyboard> </UserControl.Resources> <Ellipse x:Name="ellipse" Fill="#FFF4F4F5" Width="96" Height="96" RenderTransformOrigin="0.5,0.5"> <Ellipse.RenderTransform> <CompositeTransform x:Name="transform" ScaleX="1" ScaleY="1"/> </Ellipse.RenderTransform> <Ellipse.Effect> <BlurEffect x:Name="effect" /> </Ellipse.Effect> </Ellipse> </UserControl>
This is more declarative than the Flash version in that I’m creating a control that contains an Ellipse and the Ellipse is already pretty much set up to accept blur, scale values and it already has a Storyboard set up to animate the Ellipse from point A to point B.
Note that the Ellipse, CompositeTransform, Effect and my 2 DoubleAnimations are all named so that I can easily code against them and that lets me write code which will set up various values to random numbers when the control is first used. Here’s the code that forms the rest of the control;
namespace SilverlightApplication4 { using System; using System.Windows; using System.Windows.Controls; public partial class UserControl1 : UserControl { double parentWidth; double parentHeight; public UserControl1(double parentWidth, double parentHeight) { // Required to initialize variables InitializeComponent(); this.parentWidth = parentWidth; this.parentHeight = parentHeight; this.Loaded += (s,e) => { RandomiseAndStart(); }; } void RandomiseAndStart() { // Randomise our opacity this.ellipse.Opacity = Randomise(0.2, 0.8); // Randomise our blur effect radius this.effect.Radius = Randomise(20.0, 30.0); // Randomise our size double scale = Randomise(0.2, 1.0); this.transform.ScaleX = this.transform.ScaleY = scale; // Randomise the duration of our animations this.animationX.Duration = this.animationY.Duration = new Duration(new TimeSpan(0, 0, (int)Randomise(3, 10))); // Randomise our start and end positions on the X axis this.animationX.From = Randomise(0, parentWidth - (scale * this.ellipse.Width)); this.animationX.To = Randomise(0, parentWidth - (scale * this.ellipse.Width)); // And for the Y axis this.animationY.To = 0 -this.ellipse.Height - ((scale * this.ellipse.Height) / 2); this.animationY.From = this.parentHeight - ((this.ellipse.Height - (scale * this.ellipse.Height)) / 2); // And start the animation this.movement.Begin(); } void OnMovementCompleted(object sender, EventArgs e) { this.movement.Stop(); // Go again... RandomiseAndStart(); } static UserControl1() { _random = new Random((int)DateTime.Now.Ticks); } static double Randomise(double lower, double higher) { return (lower + (_random.NextDouble() * (higher - lower))); } static Random _random; } }
and so what’s this doing?
- We capture the width and height of our parent control when we are created.
- When we are loaded we;
- Randomise the opacity value of our ellipse between 0.2 and 0.8
- Randomise the blur radius on the blur effect placed onto the ellipse between 20 and 30
- Randomise the scale of our ellipse between 0.2 and 1.0.
- Randomise the duration of the animation that moves the ellipse so that it lasts between 3 and 10 seconds.
- Set up the start points (X,Y) and end points (X,Y) for the animation to move the ellipse
- Start the animation.
With that all encapsulated into a control with a mixture of declarative behaviour and some code to set up the initial random values, I just need the main page of my application to contain some code to create a set of these controls when we first run up and add them onto the parent Canvas;
public partial class MainPage : UserControl { public MainPage() { // Required to initialize variables InitializeComponent(); this.Loaded += (s, e) => { for (int i = 0; i < 30; i++) { UserControl1 uc = new UserControl1( this.LayoutRoot.Width, this.LayoutRoot.Height); this.LayoutRoot.Children.Add(uc); } }; } }
and that’s pretty much it. I get my nice effect and it all looks very pretty;
but there’s a little bit of “a problem” – here’s my CPU usage while running this version;
Ouch – that’s 2 very busy CPUs – not really likely to be acceptable. What to do?
Performance Optimisation 1 – Remove the Blur Effect
I suspected that the blur effect was hurting me. In Silverlight, effects like this are rendered in software rather than hardware ( if I remember correctly this is mainly for security reasons to avoid runaway effects from the internet going crazy on the GPU ) and so I imagined that the effect was expensive.
I took the effect away to see what pain it was causing me and saw;
That’s a significant “win” but the problem is that my background now looks like;
and I’ve lost a lot of the subtlety of the original. Not to worry – I figure that I can achieve the same thing that the Blur effect was giving me simply by using a multi-stepped gradient fill and randomising that fill in order to provide the randomised “blur” effect that I had previously. I replaced the solid white fill on the Ellipse with this gradient;
<RadialGradientBrush> <GradientStop Color="White" Offset="0" /> <GradientStop x:Name="gradientStop" Color="White" Offset="0" /> <GradientStop Color="#00FFFFFF" Offset="1" /> </RadialGradientBrush>
and then made sure that my RandomiseAndStart function had some code to set this up to a random value;
// Randomise our gradient offset this.gradientStop.Offset = Randomise(75.0, 95.0);
and that seems to give me a decent enough look-and-feel;
and runs at about 50% of my 2 CPUs. I moved from Task Manager at this point as I wanted a slightly more accurate breakdown focused directly on the iexplore.exe process itself;
which is still quite a lot of CPU but is an improvement over the original. Can it be improved?
Performance Optimisation 2 – GPU Acceleration
I figured I’d try my hand at switching on hardware acceleration for my background and so went to the hosting HTML page and switched the settings on;
the 2nd setting highlighted there is just a debug flag that lets me see what is not being GPU accelerated and when I run my UI I can see;
which suggests that the GPU isn’t involved here ( not that I’d expect it to be at this point ) and so I switched on GPU acceleration for my UserControl;
and I wasn’t at all sure that this would help me in this scenario but it does seem to give me a slightly better graph;
dropping to an average of about 40% rather than 53% on the previous graph.
But This is Still Way Too High 
I’m still at 40% whereas the original Flash version looks like this;
and so was running at around 7% and I’m still ticking away at 40%. This is not good
Performance Optimisation 3 – Frame Rates
The next question has to be whether I’m running to stand still – i.e. am I working harder than I need to for this particular effect? I figured I’d switch on my frame rate counter and see how many frames the plug-in is rendering;
and what I see is;
Wow, 63 frames per second is probably a little more than I need for this particular animated background so I figure I’ll gate it down a little by setting a maxFrameRate on the plug-in;
and my perfmon trace of CPU utilisation is looking a lot more healthy;
but, still, this felt a little bit like cheating. I wonder if it’s that UserControl that’s hurting me?
Performance Optimisation 4 – Taking Away the UserControl
I wondered what overhead I was paying for making use of a UserControl that contained an Ellipse rather than just using an Ellipse and so I went down that route and removed my UserControl and just manually created Ellipses from code.
I wrote a second version of my main loop which currently created 30 UserControls and made it create 30 Ellipses directly;
public MainPage() { // Required to initialize variables InitializeComponent(); this.Loaded += (s, e) => { for (int i = 0; i < 30; i++) { #if UC UserControl1 uc = new UserControl1( this.LayoutRoot.Width, this.LayoutRoot.Height); this.LayoutRoot.Children.Add(uc); #else Ellipse ellipse = CreateEllipse(); this.LayoutRoot.Children.Add(ellipse); RandomiseAndBegin(ellipse); #endif } }; }
with the routine CreateEllipse doing the work that I had previously left to the definition of the UserControl – i.e. setting up the Ellipse with a Fill, RenderTransform, Storyboard etc.
Ellipse CreateEllipse() { Ellipse ellipse = new Ellipse(); ellipse.CacheMode = new BitmapCache(); ellipse.Width = 96; ellipse.Height = 96; CompositeTransform transform = new CompositeTransform(); ellipse.RenderTransformOrigin = new Point(0.5, 0.5); ellipse.RenderTransform = transform; RadialGradientBrush brush = new RadialGradientBrush(); brush.GradientStops.Add(new GradientStop() { Color = Colors.White, Offset = 0 }); brush.GradientStops.Add(new GradientStop() { Color = Colors.White, Offset = 0 }); brush.GradientStops.Add(new GradientStop() { Color = Color.FromArgb(0, 0xFF, 0xFF, 0xFF), Offset = 1 }); ellipse.Fill = brush; DoubleAnimation xAnim = new DoubleAnimation(); Storyboard.SetTarget(xAnim, ellipse); Storyboard.SetTargetProperty(xAnim, new PropertyPath("(UIElement.RenderTransform).(CompositeTransform.TranslateX)")); DoubleAnimation yAnim = new DoubleAnimation(); Storyboard.SetTarget(yAnim, ellipse); Storyboard.SetTargetProperty(yAnim, new PropertyPath("(UIElement.RenderTransform).(CompositeTransform.TranslateY)")); Storyboard sb = new Storyboard(); sb.Children.Add(xAnim); sb.Children.Add(yAnim); ellipse.Resources.Add("sb", sb); sb.Completed += (s, e) => { sb.Stop(); RandomiseAndBegin(ellipse); }; return (ellipse); }
and the routine RandomiseAndBegin doing the randomisation work and starting the animations for the first time;
void RandomiseAndBegin(Ellipse e) { double scale = Utility.Randomise(0.2, 1.0); CompositeTransform transform = e.RenderTransform as CompositeTransform; transform.ScaleX = transform.ScaleY = scale; RadialGradientBrush brush = (RadialGradientBrush)e.Fill; brush.GradientStops[1].Offset = Utility.Randomise(0.5, 0.90); e.Opacity = Utility.Randomise(0.2, 0.8); Duration duration = new Duration(new TimeSpan(0, 0, (int)Utility.Randomise(3, 10))); Storyboard sb = e.Resources["sb"] as Storyboard; sb.Children[0].Duration = duration; sb.Children[1].Duration = duration; DoubleAnimation xAnim = (DoubleAnimation)sb.Children[0]; xAnim.From = Utility.Randomise(0, this.LayoutRoot.Width - (scale * e.Width)); xAnim.To = Utility.Randomise(0, this.LayoutRoot.Width - (scale * e.Width)); DoubleAnimation yAnim = (DoubleAnimation)sb.Children[1]; yAnim.From = this.LayoutRoot.Height - ((e.Height - (scale * e.Height)) / 2); yAnim.To = 0 - e.Height - ((scale * e.Height) / 2); sb.Begin(); }
and I took another look at this instance running
( I noticed while doing this that I’d previously been randomising my gradient offsets to be between 75 and 95 rather than 0.75 and 0.95 but I hadn’t noticed before
and it actually looked more like the original Flash version when configured that way );
Wow, that’s quite some difference
Now I’m averaging 0.9% CPU utilisation and I find that if I take away my artificial limit on the framerate then that does little to affect the performance I’m seeing
( by the way – turning off enableGPUAcceleration has a huge difference so I left that one switched on ).
I ramped up from 30 ellipses on the screen to 350 ellipses on the screen and my performance is still better than it was previously even with an order of magnitude more for the plug-in to draw;
although the effect gets a little messy;
Taking the number of Ellipse instances back down to 30, I wondered if I could now put my BlurEffect back and I found that I could do that without seeming to significantly alter the performance characteristics.
The Example
Here’s the example running in the page;
The Source
Here’s the source for download containing both versions ( via conditional compilation )