Snippets from my development projects, and my mind

From the Blog

Jul
11

Faking a fast flood fill in Silverlight

Posted by infosekr on July 11th, 2011 at 4:37 am

I was recently asked how I achieved some of the features in my Paint The World web app. In this post I’ll cover how I perform the inexpensive faked flood fill operation on arbitrary shapes in Silverlight. To see the effect in action check out app here, and click anywhere on the design to paint a section red.

As you paint different areas of the design, you’ll notice that the flood fill occurs at different speeds. This was by design as I wanted the operation to always complete in the same amount of time regardless of the area being filled. So both small are large shapes are completely filled in 0.5 seconds.

Flood fill in action

How it’s done

Rather than developing an expensive flood fill routine, I do the following to generate the flood-fill-like effect: First I create a temporary canvas that is placed above the shape you want to fill, then I clip this canvas using the path data from the shape underneath, add a circle to this canvas at the point of your mouse cursor, animate the circle growing until it completely fills the canvas, and then completely remove the temporary canvas from the view. Just before removing the canvas however, I take the color that is being filled and apply it to the shape underneath. So the flood fill animation is just done for effect and the actual coloring happens instantly by changing the background color of the shape.

The details

The designs themselves were traced in Adobe Illustrator from line drawings and photographs using Object -> Live Trace. I then exported them to XAML using the XAML Export Plug-In. Then since XAML is the format for UI design in WPF/Silverlight, I can quickly load the design using:

Stream s = GetType().Assembly.GetManifestResourceStream(filename);
Canvas canvas = (Canvas)XamlReader.Load(new StreamReader(s).ReadToEnd());

With the design loaded, I wanted an easy way to get the data from each shape in order to provide clipping information. Unfortunately there is a limitation in Silverlight where you can’t extract the detailed path data from a shape. The path, which may consist of segments of straight lines or different bezier curves, is stored in a condensed markup format. This format can only be applied in XAML at load time and not directly at runtime. Fortunately there is the XamlReader to dynamically create objects from XAML at runtime. Since I can’t extract the path format at runtime, I store a copy of the data in the Tag property of every path object at the time I load the design file. Then later I use this property to recreate the shape at runtime.

So when the user clicks the mouse to paint a shape, I create a new transparent canvas that is sized to the extent of the shape. The clipping information for this canvas is obtained by getting the path data that I’ve hidden away in the Tag property of the path object.

// Create a new canvas that is clipped by the current path shape
String xaml = "<Canvas xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" " +
    String.Format("Canvas.Left=\"{0}\" Canvas.Top=\"{1}\" Width=\"{2}\" Height=\"{3}\" " +
                  "Background=\"Transparent\" Clip=\"{4}\"/>",
                  Canvas.GetLeft(path), Canvas.GetTop(path), path.Width, path.Height, path.Tag);
Canvas clippedCanvas = (Canvas)XamlReader.Load(xaml);

I then create the circle that will flood this canvas at the position of the mouse pointer, and add it to this new canvas.

// create ellipse
Ellipse ellipse = new Ellipse() { Fill = new SolidColorBrush(color), Width = 1, Height = 1 };
ellipse.SetValue(Canvas.LeftProperty, position.X);
ellipse.SetValue(Canvas.TopProperty, position.Y);
// add to the canvas
clippedCanvas.Children.Add(ellipse);

Next, I create a ScaleTransform, and an animation (only one axis shown) that is used to stretch the circle.

ScaleTransform scale = new ScaleTransform() { CenterX = 0.5, CenterY = 0.5 };
ellipse.RenderTransform = scale;
double size = Math.Sqrt(Math.Pow(path.Width, 2) + Math.Pow(path.Height, 2)) * 2;
// Animate the expansion of the the ellipse
DoubleAnimation doubleAnimation1 = new DoubleAnimation() { From = 1, To = size };
doubleAnimation1.Duration = new Duration(TimeSpan.FromSeconds(0.5));
Storyboard.SetTarget(doubleAnimation1, scale);
Storyboard.SetTargetProperty(doubleAnimation1, new PropertyPath("(ScaleX)"));

With the animation ready, we need a Storyboard to play it with.

// start the animation
Storyboard storyboard = new Storyboard();
storyboard.Children.Add(doubleAnimation1);
storyboard.Begin();

But before calling Begin(), you’ll want to attach an event to the Storyboard’s completion so that you can apply the flood fill color to the shape underneath, and remove our temporary canvas.

storyboard.Completed += (s, e) =>
{
    // set the color of the underlying path object to the flood fill color
    path.Fill = new SolidColorBrush(color);

    // remove our temporary canvas from the main canvas, garbage collection will handle the rest
    canvas.Children.Remove(clippedCanvas);
};

And with that we have a really fast a cheap flood fill that looks just a good as the real thing. If you want to make it fill faster or slower you can change the duration of the animation, or set it relative to the path size in order to get a constant fill speed.

Leave a Reply

7 Responses to Faking a fast flood fill in Silverlight

  1.  

    |

  2. Gregory Baytler |

    Thank you Aaron for this great post.
    I do have a question though: how do you dynamicaly retrieve and store a copy of the data in the Tag property of every path object at the time you load the design file? -Gregory.

  3. I use a LINQ statement to transform the XAML into the structure I need and prune off elements I don’t. At the same time that I’m selecting the path data, I also select the path to be stored in the Tag property. So I clone the Path property into the Path and Tag on the new objects. I could write a short post on filtering XAML using LINQ if you’d like.

  4. Gregory Baytler |

    Aaron, I am sorry for this late reply, I did not look inside the spam folder until now, and did not know that you have replied. I saved your email address in my Yahoo contacts – hopefully this may help in the future.
    Yes, could you please write a short post on filtering XAML using LINQ specific to this application? Thank you! -Gregory.

  5. Will do. I’m away from the internet this weekend. Will post something next week.

  6. It’s part of a much larger application, so I decided to only share snippets with descriptions. If you are having any troubles building your application I’m happy to help.