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:
14             return;
15         case MaskUpdateBehaviorType.LinkedToHitTestVisible:
16             if (!this.AssociatedObject.IsHitTestVisible)
17             {
18                 return;
19             }
20  
21             break;
22         case MaskUpdateBehaviorType.SingleUpdate:
23             if (this.isMasked)
24             {
25                 return;
26             }
27  
28             this.isMasked = true;
29             break;
30     }
31  
32     // clear any existing mask
33     Array.Clear(this.writeableBitmap.Pixels, 0,this.writeableBitmap.Pixels.Length);
34  
35     // update the mask to using the associated object
36     this.writeableBitmap.Render(this.AssociatedObject, this.transform);
37     this.writeableBitmap.Invalidate();
38 }

This code calls the method to attach the opacity mask (if it's not already attached) and then checks the UpdateBehavior property. If the value of the UpdateBehavior property allows it, the method clears the mask and re-renders it using the associated object.

The AttachOpacityMask method looks like this:
01 private void AttachOpacityMask()
02 {
03     if (this.isOpacityBrushCreated)
04     {
05         // already created
06         return;
07     }
08  
09     this.isOpacityBrushCreated = true;
10  
11     // move the mask out of the container - effectively masking itself out
12     TranslateTransform translate = new TranslateTransform { X = -this.container.ActualWidth, Y = this.container.ActualHeight };
13     this.AssociatedObject.RenderTransform = translate;
14  
15     // create the opacity mask using a writeable bitmap
16     this.writeableBitmap = newWriteableBitmap((int)this.AssociatedObject.ActualWidth, (int)this.AssociatedObject.ActualHeight);
17     this.brush = new ImageBrush { ImageSource = this.writeableBitmap };
18     this.container.OpacityMask = this.brush;
19 }

The attached object is moved out of view of the parent's masked content and then used as the source for an ImageBrush applied to the parent object's OpacityMask. A WriteableBitmap is used as the source for the ImageBrush, and it is that WriteableBitmap that is updated during the Rendering event.

Improvements
 
There is plenty of scope for enhancing this behavior. If I get the time I would like to add a property to allow the mask to be inverted. I understand that in Silverlight 4 binding both ways on DependencyProperties will be allowed down to the DependencyObject level instead of just down to the FrameworkElement level. If this flows through into being able to animate such properties then this will allow me to get rid of the link to the associated object's IsHitTestVisible property and just animate the UpdateBehavior property directly.
 
Source here.

About the Author

About Phil Middlemiss

Phil Middlemiss has been a software developer for 17 years, and has lately been moving into Interaction Design work. He has worked with Silverlight for the last 12 months as he has been studying design. Currently he fills the roles of both Design Lead and UI Development for the company he works for in Auckland, New Zealand. Phil started blogging on Silverlight topics in February this year at http://silverscratch.blogspot.com/

#1 bharti on 5.12.2010 at 11:37 PM

Excellent tut!!!

I was looking for such effects.

#2 Mike on 5.28.2010 at 10:26 PM

Thanks for the great tutorial! Greatly appreciated.

#3 Adrian on 9.05.2010 at 1:25 AM

Nice tutorial, simple and easy. Thanks :)

#4 Zachary Bauer on 10.18.2010 at 10:25 PM

Neat workaround but Blend needs a true masking effect before it can really draw in designers who will create designs and animations that compete with what Flash can do.

#5 silverlight developer on 9.21.2011 at 1:27 AM

much room for creativity

#6 logical on 11.13.2011 at 10:29 PM

Althought silverlight is not helping SEO I am going to use your template in my mobile links for mobile users.

gr8 work!!

#7 Security Software on 12.14.2011 at 7:38 AM

As long as you can import and export data between the two platforms you should be able to get the desired effect. I have tested many types of animation software and it has happened often for me to have problems with compatibility when I switched to another software provider.

#8 florist busselton on 4.15.2012 at 12:40 AM

Hello. Really what I needed. Thanks I have been looking for this sort of info for a while. I have bookmarked your blog to enable me to read more on the topic.

Leave a Comment