Friday, May 22, 2009

A PixelShader that means business

Maybe you heard that PixelShaders are a feature of Silverlight 3 … which is coming our way very soon.

Maybe you’re like I was, thinking that PixelShaders have no place in a business application. Hey, I love those rippling water demos as much as the next guy. But I’ll be looking for new employment the morning after I give my application a psychedelic makeover.

Then my buddy Joe Gershgorin introduced me to the “Grayscale Effect”, a PixelShader effect that de-saturates the colors of the targeted image, “graying” it out.

You say “So what?”

Here’s a hint: What’s the look of a disabled button? It’s gray.

I used to have one image for the active button state and another image for the disabled button state. If I’ve got 10 buttons, that’s 20 images to maintain. That’s double the payload winging over the wire to my Silverlight client.

It takes time (and skills I lack) to produce the first image; adding a disabled version is another step I can do without.

Now, instead of swapping button images when the enabled state changes, I add or remove the grayscale effect.

I’m not sure where Joe found the code for this effect. The first mention I can find is a blog post by Anders Bursjöö from June 2008. I don’t know if Anders post was first; I can confirm that it will take you through the details of the effect as it relates to WPF … which details are the same for Silverlight.

There is no need for me to repeat what Anders had to say or the many who followed him and took their turns at explaining it. I can also recommend Greg Schechter’s series of May 2008 posts on custom GPU-based effects for WPF.

Now put that knowledge to good use by shedding image bloat and waste your creativity on some other project.

Oh … you wanted code?

Dude ... it's XAML. We'll be here all day.

Ok, here are two snippets for your delectation. Imagine we're inside ResourceDictionary defining a style for the button.

Here is the image definition that will use template binding to pick up the image assigned to the button:

 <Image 
x:Name="ImagePresenter"
Margin="0,0,0,0"
Opacity="1"
Source="{TemplateBinding Image}"
Width="32"
Height="32"
HorizontalAlignment="Center"
Stretch="Uniform"
>
<Image.Effect>
<effects:GrayScaleEffect DesaturationFactor="1"/>
</Image.Effect>
</Image>


Notice how we added the effect, setting the DesaturationFactor to one (meaning … ahem … no desaturation). GrayScaleEffect is the class straight from Anders example.


Now let's look at the pertinent "Disabled" state governed by the VisualStateManager:


<VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimationUsingKeyFrames
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="ImagePresenter"
Storyboard.TargetProperty="(UIElement.Effect).(GrayScaleEffect.DesaturationFactor)">

<DiscreteDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>

Notice how we’re identifying the effect and its DesaturationFactor property using attached property syntax. Setting the factor to zero results in the gray effect. We’re not using animation so the duration is zero-time.


Wouldn’t it be great if there were a one-liner to specify a no-animation Storyboard or just to skip the Storyboard altogether? Maybe someday.

9 comments:

Jeff Handley said...

That is awesome!

gorda@mail.ru said...

Thanks for idea, but its too difficult(for me at least). So I've used Binding for GrayscaleEffect. Something like this works perfect
<Imagex:Name="ImagePresenter"
Source="{TemplateBinding Image}"
Width="32"
Height="32">
<Image.Effect>
<effects:GrayScaleEffect DesaturationFactor="{Binding Path=IsEnabled, RelativeSource={RelativeSource TemplatedParent}}"/>
</Image.Effect>
</Image>

Ward Bell said...

@gorda .. why didn't I think of that.

Ward Bell said...

@gorda -

For what it's worth, I cannot get your technique to work in Silverlight 3 RTM. Kept getting a parse error (you know how much fun those are!).I even tried introducting a ValueConverter to go from boolean to double. Nope.

Could easily be me. I'm lost in the XAML pretty easily.

I have to resort to the storyboard ... at least for now.

Anonymous said...

estuary http://cciworldwide.org/members/Garage-Door-Openers.aspx morethan http://cciworldwide.org/members/Area-Rugs.aspx wehr http://cciworldwide.org/members/Omeprazole.aspx taayushs http://cciworldwide.org/members/Vacuum-Cleaners.aspx domenico http://cciworldwide.org/members/Annuity-Calculator.aspx iwinland http://cciworldwide.org/members/Bariatric-Surgery.aspx pehn http://cciworldwide.org/members/Electric-Blankets.aspx mullet http://gotuc.net/members/Furnace-Filters/default.aspx onus http://gotuc.net/members/Vending-Machines/default.aspx nagpur http://gotuc.net/members/Kitchen-Cabinets/default.aspx programfy http://gotuc.net/members/Slipcovers/default.aspx joyeuse http://gotuc.net/members/Polar-Heart-Rate-Monitors/default.aspx booths http://gotuc.net/members/Popcorn-Machines/default.aspx carboxylase http://gotuc.net/members/Garage-Door-Openers/default.aspx brisbane http://gotuc.net/members/Area-Rugs/default.aspx florentine

Anonymous said...

Can also be done with a single style:

>Style x:Key="ToolbarButtonStyle" TargetType="Button"<
>Setter Property="Effect"<
>Setter.Value<
>gray:GrayscaleEffect x:Name="GrayscaleEffect" DesaturationFactor="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Button},Path=(gray:GrayscaleEffect.DesaturationFactor)}"<
>/gray:GrayscaleEffect<
>/Setter.Value<
>/Setter<
>Setter Property="gray:GrayscaleEffect.DesaturationFactor" Value="1" /<
>Style.Triggers<
>Trigger Property="IsEnabled" Value="False"<
>Setter Property="gray:GrayscaleEffect.DesaturationFactor" Value="0" /<
>/Trigger<
>/Style.Triggers<
>/Style<

Anonymous said...

Something that might also work is just using a single style (and a bit of attached-property abusing):


<Style x:Key="ToolbarButtonStyle" TargetType="Button">
<Setter Property="Effect">
<Setter.Value>
<gray:GrayscaleEffect x:Name="GrayscaleEffect" DesaturationFactor="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Button},Path=(gray:GrayscaleEffect.DesaturationFactor)}">
</gray:GrayscaleEffect>
</Setter.Value>
</Setter>
<Setter Property="gray:GrayscaleEffect.DesaturationFactor" Value="1" />
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="gray:GrayscaleEffect.DesaturationFactor" Value="0" />
</Trigger>
</Style.Triggers>
</Style>

Anonymous said...

Below is a simpler style, and this one will not set the effect if the image is "enabled" (big performance gain if you have many large images that are "enabled"). Replace parens with angle brackets.


(gray:GrayscaleEffect x:Key="grayscaleEffect" DesaturationFactor="0"/)

(Style TargetType="Image")
(Style.Triggers)
(Trigger Property="IsEnabled" Value="False")
(Setter Property="Effect" Value="{StaticResource grayscaleEffect}"/)
(/Trigger)
(/Style.Triggers)
(/Style)

Danilo da Silva said...

I've created a behavior using Ander's GrayscaleEffect.dll compiled targeting .NET Framework 4.0. For those interested, the download link is http://www.codelines.com/dicas/UsingApplyGrayScaleBehavior.rar .