Silverlight is a UI development framework that requires a combination of software development skills with graphic design skills. That does not mean that every good Silverlight developer needs to be an artist (thank god..) but it does mean that knowing your way around graphic design software and being able to understand various concepts from the graphic design world helps you a lot in taking someone else’s graphic vision (hopefully, somebody with talent…) and turning it into a reality. The biggest challenges I encounter in my work usually involves taking a Photoshop illustration and building an application that looks as close as possible to the experience that the artist was trying to give.

The problem is, that in order to build these illustrations, the designers use every tool made available to them in Photoshop. That includes a lot of stuff that Silverlight simply does not have. Now, most designers understand that and will not expect you do be able to reproduce crazy Photoshop effects, but they often use the “simple stuff” like inner shadows, angle gradients, glows, textures and a lot of other stuff that Siverlight does not provide out of the box. So, I will try to describe how I improvise some of these in the next posts.

Today’s post, is about angle gradient.

First, let’s talk about what exactly an ANGLE GRADIENT is.. Lets start with a simple image of a black ring over a white background:

A black ring over white BG

 

Now, by double clicking the “Layer 2” layer, we can modify the layer settings, and apply a gradient overlay.

 

Notice that Photoshop lets us choose between 5 types of gradients. The first 2 are the most commonly used and they are both supported by Silverlight (LinearGradient and RadialGradient) . The last two are hardly ever used in real life rich UI applications, the middle one is where we fall. This one is very popular in progress bars, busy state indicators and so on (it usually rotates around itself). No matter what, there is no way to achieve this result with Silverlight gradients.

Can’t we just use an image?.

Well, of course we could use an image. But the thing we want to achieve is a reusable component, that looks well on any resolution, that fits to the required size of your “please wait” dialog, that looks well even when the user zooms in to 276%. We want to be able to pick any 2 colors (including transparent ones) and we want to be able to rotate it using a simple storyboard. We want to be able to spread it over a rectangle, circle, or any path, and perhaps apply opacity masks and clipping. basically, we want it to behave like any other Silverlight component.

So how do we do it?

After long searches, I found two possible approaches. The first was developed by Charles Petzold and is described in his blog (highly recommended, BTW). A Circular Gradient Brush for Silverlight. He uses 4 rectangles, that have been transformed using 3D transformation matrices into 4 trapezoids , laid around North, West, South and East of a center point, and with a clipping mask that makes it all look like ring. It’s a very creative solution and the best thing about it is that it can be achieved without any “nasty” means such as pixel shaders. Only basic vector (if somewhat complex…) Silverlight objects. The problem is that when you try to create a gradient where one of the colors is not fully opaque, there are areas where the 4 shapes overlap, and that makes the gradient not appear as sooth as it should be.

The pixel shader approach

The second approach, the one that I will describe here, uses the pixel shader mechanism. Now usually, I prefer to save the pixel shaders to serve as a last resort. Silverlight 4 still renders them using the CPU (I understand that in Silverlight 5 the shaders are rendered using the GPU, which would be great) and they can seriously affect your application’s frame rate if you do not know exactly what you are doing. In this case, assuming this shader is applied on a simple shape (Path, Rectangle, Ellipse, and so on) without any content, it should be OK.

So the rest of this post describes a pixel shader, that when applied on a shape, paints it with a smooth circular gradient. The shader is provided 3 arguments. A start color, finish color, and an angle (we will use this to achieve the rotation animation). What the shader does is for each pixel, calculates its angle relative to the center point. This angle is then normalized to a double between 0 and 1 and this is called the “ratio”. So every pixel that is 90 degrees from the center point, for example, is translated into a ratio of 0.25. 180 degress is 0.5, and so on. We then use a mixture of the 2 colors to build the result color. We take “ratio” amount from the first color, and “1 – ratio” amount from the second color. So when r = 0, the result color is identical to color1. When r = 1, its identical to color2, and at any point in between, its changes gradually from one to the other.

Pixel Shaders are developed in the HLSL language which has a syntax that is not dissimilar to  C++, and are usually made of a small chunk of code that is called for every pixel that is about to be rendered. The method gets the X and Y coordinates (normalized to a value between 0 and 1) and is expected to return the color that should be used for that pixel. If you want to develop, compile and test pixel shaders, Shazzam is the tool I would recommend (http://blog.shazzam-tool.com)

So here is the code for the pixel shader itself:

sampler2D ourImage : register(s0);
 
/// <summary>First Color.</summary>
/// <defaultValue>White</defaultValue>
/// <type>Color</type>
float4 Color1 : register(C1);
 
/// <summary>Second Color</summary>
/// <defaultValue>Black<defaultValue>
/// <type>Color<type>
float4 Color2 : register(C2);
 
/// <summary>Relative Angle offset between 0 and 1.</summary>
/// <defaultValue>0.0</defaultValue>
/// <type>float</type>
float Angle : register(C3);
 
float4 main(float2 uv : TEXCOORD) : COLOR
{
	float4 Source = tex2D(ourImage, uv.xy);
	float4 Color = 0;
	// fix alpha channel
	float4 c1 = Color1 * Color1.a;
	float4 c2 = Color2 * Color2.a;
 
	float a = Angle;
 
	float r = atan2(uv.x - 0.5, uv.y - 0.5) / 6.28 + 0.5 + a;
	r = r - (floor(r));
 
	if (r < 0.005) r = (0.005) + (0.005 - r) * 200;
 
	float s = 1 - r;
 
	Color = c1*r + c2*s;
	Color *= Source.a;
	return Color;
}

Lines 01, 06, 11, and 16 define the arguments. We are expecting 2 color arguments called Color1 and Color2. An Angle argument called Angle, and the original image, so we can use it as an opacity mask. In line 20, we sample the original pixel that the shader is going to replace with its own color. In line 21 we define the variable that will serve as place holder for the result color.

Lines 23 and 24 are there to fix a strange pixel shader behavior towards the alpha channel. For some reason, it ignores the provided alpha value and uses an alpha value that is the average of the R, G and B channels combined with the original alpha. So these 2 lines simply sorts them back to the provided alpha value.

Line 28 is where the magic is. We take the coordinates of the point we are currently calculating, and subtract 0.5 from each in order to normalize the coordinates system around the center. (so the center point becomes 0,0, the top left corner is -0.5, -0.5 and the bottom right is 0.5,0.5). We then use the atan2 function in order to extract the angle from the x,y coordinates. The value returned by the function is between -pi and pi so we divide by 2pi and to get it between -0.5 and 0.5 and then add 0.5 to get it between 0 and 1. Now we add the provided angle (we expect angle to be a value that describes complete circles. so 0.5 means 180 degrees). Finally, we subtract the whole part of r in order to normalize it again between 0 and 1.  Now r is the ratio of where we are within the gradient, so by taking r% of the first color and (100-r)%  of the second color we produce the final result.

Line 31 is a small addition that makes sure that the two edges of the gradient “blend” a little to avoid rough edges. it takes the first and last 1/200 of the circle and blends them. Line 33-35 create the mixed color, and line 36 multiply it by the opacity of the original pixel thus use the original graphic as an opacity mask.

Using the pixel shader in our silverlight application

To incorporate the shader into Silverlight we need to wrap it with C# code that exposes it as a subclass of the Effect class. I used  Shazzam to auto generate it. And here it is:

//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.17379
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
 
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Effects;
using System.Windows.Media.Media3D;
namespace Shazzam.Shaders
{
 
    public class AngleGradientEffect : ShaderEffect
    {
        public static readonly DependencyProperty InputProperty = 
            ShaderEffect.RegisterPixelShaderSamplerProperty(
                "Input", 
                typeof(AngleGradientEffect), 0);
        public static readonly DependencyProperty Color1Property = 
            DependencyProperty.Register(
                "Color1"typeof(Color), typeof(AngleGradientEffect), 
                new PropertyMetadata(Color.FromArgb(255, 255, 255, 255),            
                PixelShaderConstantCallback(1)));
        public static readonly DependencyProperty Color2Property = 
            DependencyProperty.Register(
                "Color2"typeof(Color), typeof(AngleGradientEffect), 
                new PropertyMetadata(Color.FromArgb(255, 0, 0, 0), 
                PixelShaderConstantCallback(2)));
        public static readonly DependencyProperty AngleProperty = 
            DependencyProperty.Register(
                "Angle"typeof(float), typeof(AngleGradientEffect), 
                new PropertyMetadata(((float)(0.2F)), 
                PixelShaderConstantCallback(3)));
        public AngleGradientEffect()
        {
            PixelShader pixelShader = new PixelShader();
            pixelShader.UriSource = new Uri("/Shazzam.Shaders;component/AngleGradient.ps", 
                                            UriKind.Relative);
            this.PixelShader = pixelShader;
 
            this.UpdateShaderValue(InputProperty);
            this.UpdateShaderValue(Color1Property);
            this.UpdateShaderValue(Color2Property);
            this.UpdateShaderValue(AngleProperty);
        }
        public Brush Input
        {
            get
            {
                return ((Brush)(this.GetValue(InputProperty)));
            }
            set
            {
                this.SetValue(InputProperty, value);
            }
        }
        /// <summary>Demonstrates the Color syntax.</summary>
        public Color Color1
        {
            get
            {
                return ((Color)(this.GetValue(Color1Property)));
            }
            set
            {
                this.SetValue(Color1Property, value);
            }
        }
        /// <summary>Demonstrates the Color syntax.</summary>
        public Color Color2
        {
            get
            {
                return ((Color)(this.GetValue(Color2Property)));
            }
            set
            {
                this.SetValue(Color2Property, value);
            }
        }
        /// <summary>Demonstrates the Color syntax.</summary>
        public float Angle
        {
            get
            {
                return ((float)(this.GetValue(AngleProperty)));
            }
            set
            {
                this.SetValue(AngleProperty, value);
            }
        }
    }
}
 
Once you get it compiled and of course add the compiled shader into your project, you can apply it onto any Silverlight element. To test it with transparent values I placed it over an image of wood texture and here are the results:

 

Notice that we use the AngleGradientEffect class as an effect. We apply it on an ellipse with transparent fill and a 10 pixel thick stroke. We even use blend’s behaviors in order to start an animation storyboard that changes the angle property from 0 to 1 over 2 seconds, here is how the storyboard is defined:

Voila! A circular gradient that can be used with animation, over any shape, with any 2 colors. It is, of course, not as flexible as Silverlight gradients are, providing you with the ability to define a list of gradient stops, but for most cases, 2 colors should be enough.