Wednesday, March 10, 2010

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:

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:

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:

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.

If you like this please leave a comment. Any suggestions for improvement are welcome also.

5 comments:

  1. Yes this is a very good article with a great code example.

    ReplyDelete
  2. sadly an update to the Blogger template has screwed up the formatting of the code samples. You are best to just grab the source from the link.

    ReplyDelete
  3. Hey - by any chance do you have this updated for VS2010? When I try to load the project I get a slew of dependency errors.

    ReplyDelete
  4. Hi Jamie, I haven't looked at it for a while, but I will put it on my to-do list.

    ReplyDelete