Animating the Silverlight opacity mask

I used to use Adobe Flash a few years ago. Not much - just enough to get a couple of little projects done. Silverlight competes with Flash and Flex. That may or may not be the official Microsoft position - but it is true nevertheless.

Most of my work involves creating corporate level Silverlight apps. There is still much room for creativity, but only to a point. Animations are usually restricted to a logo, or transitioning between screens etc. So when I get the time to play with the more creative side of Silverlight development I often think about the artistic tools that were available in Flash.
 
In the context of animation, one of the biggest features in Flash that is missing from Silverlight 3 is an Opacity Mask layer. I haven't seen any mention of this in to Silverlight 4, but I could be mistaken. The only feature that we have in Silverlight 3 is the OpacityMask on each element, which is either a Brush or an image - (Image Brushes don't tile either, but that's a post for another day).
 
An Opacity Mask "layer" allows for animation, so you can do things like a moving spot light revealing parts of an image, or have a foggy border animating around something.

An Opacity Mask "Layer" Behavior for Silverlight

I've written a Behavior for Silverlight that lets you turn a FrameworkElement into an opacity mask for it's parent container. Before I discuss the code, let's have a look at it in action:
 

 
You can grab the source here.
 
The behavior is attached to a UIElement - for example, one of the containers in the demo above looks like this:

The grid named "Island" holds a border with an ImageBrush for it's fill. It has a child grid called "SpotLightMask" with the ElementMaskBehavior attached.
 
The ElementMaskBehavior will turn the whole "SpotLightMask" grid into an opacity mask (at run time) that will be applied to the parent "Island" grid.
 
As the ellipse is animated, the opacity mask is updated. As the SpotLightMask layer is turned into the opacity mask, it's flattened image's opacity is inverted, so if an area on the mask grid is transparent, it will mask out the parent layer. If an area on the mask grid is opaque it will allow the parent layer to show through. This means that you only have to add and animate the part of the mask that you want the image to show through. This is generally easier than creating a shape and cookie-cutting a mask out of it.
 
The ElementMaskBehavior has the following property:

The UpdateBehavior exists because the behavior can use up a lot of CPU power if it is left to continuously update the opacity mask. The property can be set to one of the following values:
  1. Disabled - no mask is generated.
  2. SingleUpdate - the mask is generated once and not updated after that unless the UpdateBehavior property is changed to one of the following two values.
  3. ContinuousUpdate - the mask is continually updated. This may max out your CPU depending on your computer's ability, so if you set the UpdateBehavior to this value, you may want to restrict the frame rate of your Silverlight app.
  4. LinkedToHitTestVisible - the mask will be updated while the mask layer has it's IsHitTestVisible property set to true. This option is discussed in more detail below.

Performance issues

The behavior works by attaching an event to the CompositionTarget.Rendering event and checking to see if it should update the opacity mask as each frame is about to be rendered. For a static opacity mask, the SingleUpdate is all I need. But if I'm animating the opacity mask, I don't want to have the behavior to continue demandanding so much CPU usage after the animation is finished. Ideally, I would like the Storyboard to change a property on the behavior itself to turn it on and off during the Storyboard playing out. Unfortunately in Silverlight 3, you can only animate properties on a FrameworkElement object.
 
Since the layer being used as a mask is not going to be visible anyway, I've provided the LinkedToHitTestVisible enumeration value that synchronises the updating of the opacity mask to the checked state of the IsHitTestVisible property on the behaviors associated object. The IsHitTestVisible property can be changed during a Storyboard so it provides a behavior controlling mechanism. It's a compromise, but it's the best solution I could find.
 
So how does it work?

How it works

The behavior itself is pretty small. There are only three methods that do anything worth discussing. The first method is the LoadedEvent handler for the behavior's associated object:

01 private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
02 {
03     DependencyObject parent = this.AssociatedObject.Parent;
04     this.container = parent as Panel;
05     if (null == this.container)
06     {
07         return;
08     }
09  
10     if (null != this.container.OpacityMask)
11     {
12         // we won't override the existing mask
13         return;
14     }
15  
16     // replace the mask if either the container or the associated object change size
17     this.container.SizeChanged += (s, args) => this.isOpacityBrushCreated =false;
18     this.AssociatedObject.SizeChanged += (s, args) =>this.isOpacityBrushCreated = false;
19  
20     // hook into the rendering loop
21     CompositionTarget.Rendering += this.CompositionTargetRendering;
22 }
The code attaches an event handler to the SizeChanged event of both the parent and the associated object - if either of these objects changes shape we should regenerate the mask. The other event it hooks up is the CompositionTarget.Rendering event, which is called as each frame is about to be rendered. That handler looks like this:
01 private void CompositionTargetRendering(object sender, EventArgs e)
02 {
03     // create the opacity mask if we need to
04     if (!this.isOpacityBrushCreated)
05     {
06         this.isMasked = false;
07         this.AttachOpacityMask();
08     }
09  
10     // obey the UpdateBehavior property
11     switch (this.UpdateBehavior)
12     {
13         case MaskUpdateBehaviorType.Disabled: