Friday, February 26, 2010

A Scalable Orb Radio Button

I saw this great looking glassy "uber" orb on the Expression Gallery page a while ago and got inspired to have a go creating something similar.

I had a search around for a Photoshop tutorial for something along the same lines and found this one.

The end result can be found (and downloaded) here, and looks like this:


The one thing I didn't like about the orb button on expression gallery was that it was not scalable. Trying to resize it to make a normal size radio button just didn't work.

So my first goal was simply to try and reproduce the look of the button from the Photoshop tutorial.

I was able to pretty much follow the Photoshop tutorial instructions, but the main difference was when the tutorial said to feather a selection, I just used the Blur effect in Blend.

The interesting part came when it got to step 7/8 in the tutorial. To get the reflection effect, I downloaded and compiled the WPF Pixel Shader Effect Library from Codedplex. The compiled assembly is included in the sample download from the Microsoft Expression Gallery.

It's hardly worth a step by step description of how to make this button since the Photoshop tutorial can be referenced for the general approach. However, there were a couple of interesting techniques that I will cover here in order to make this behave nicely as a scalable button.

Blur is Your Friend

Sometimes it can just be painful getting a gradient brush to do exactly what you want it to do. Trying to get a nice graduated fade into the background using a gradient brush seems to have mixed results depending on which colours I'm using. The blur does a great job here however. Applying a blur with a radius of 10 smooths the front ellipse into the background ellipse, and only requires a solid brush for the fill instead of trying to tweak a radial gradient brush.

This also comes in handy when you already have a gradient fill that you want to apply, as shown in the second image. The top ellipse has a linear gradient brush and the blend smooths it into the background quite nicely.

This will scale nicely too, providing a fixed blend area as the button grows or shrinks. This may sound like it is going against the principle of having proportional scaling, but I prefer this particular part of it not to scale proportionally - I want the same appearance of a thin curved surface at the edge regardless of the size of the button.

Getting it to Scale Nicely

As mentioned above, the "uber" orb button that got me interested in the first place, didn't scale well. It was initially constructed in a Canvas container so resizing didn't do anything. Changing it's container to a Grid allowed me to resize, but the resize didn't make the contents scale nicely.

None of this is a reflection on the designer (Thebirdbath) - since the goal of having it scale nicely was mine, not the designers.

So the trick to having it scale nicely is to use a grid with lots of columns and rows specified in star (*) widths, which are proportional. The way I did this was to:
  1. Build the button at a fixed size inside a grid with horizontal and vertical alignment set to stretch and using margins to position everything
  2. Calculate the offsets for each object inside the grid
  3. Turn those offsets into proportional columns and rows.
  4. Position each object in the appropriate column and row, with the appropriate column and row spans - and remove the margins
Sometimes when drawing a shape, Blend will set one of the alignments to something other than stretch. Trying to set the alignment on the properties sheet will usually change the position or size of the shape. The easiest way to convert the alignment is to use the little link icons that appear between a shape and the grid edges when you select the shape.

The measurement and conversion is not difficult, just tedious, So, for example, the second ellipse is 3 pixels in from all edges, and the whole button is 172 pixels wide and high, as I have created it initially. 3 pixels is 1.74% of 172 which converts into a column of width="0.0174*".

The next shape has a margin of 6 pixels all sides, but since we already have a column spanning the equivalent of 3 pixels we only need to make the second column 3 pixels also. The way I actually calculated this was to write all the margins down on the piece of paper, going in from both sides (doing columns first) and letting the middle remaining column take up the slack. I then calculated the pixels differences between the margins and converted them to a percentage for the column width. Rinse and repeat for the rows. Boring, but it does the job.

The columns were especially important for the "reflection" in the button.

The Reflection

The reflection was tricky. I toyed with the idea of just creating the deformed rectangular shapes by hand as paths, but I didn't think I would be able to achieve such a natural look (I'm not much of an artist). That's when I remembered the WPF Pixel Shader Effect Library on Codeplex. It has a SmoothMagnifyEffect that worked nicely. I combined the eight rectangles into a combined path and applied the effect, fiddling with the settings until I was happy with it. However it didn't seem to scale nicely at first, even though it was in the grid. As I scaled the button the changing size of the path would cause the effect to make it go completely out of shape. In the end, I put the path inside another grid along with a transparent ellipse. The path was positioned using rows and columns and the ellipse was allowed to fill the whole grid, which was the same size as the parent grid. This allowed the SmoothMagnifyEffect to be applied to the whole button size, affecting the window shape path roughly the same way at different sizes.

The Result

So with all the bits and pieces positioned in the grid, I end up with a button that scales well.

Blend makes it ulta simple to turn my grid with shapes into a button. The technique is covered on many other web sites, but it's as easy as right clicking the container (probably easiest in the object tree), selecting "Make into Control", and selecting Radio Button from the list of controls.

The Uber Button was a normal button, but I've always considered round buttons like this to work better as a Radio Button.

So the only thing left to do is to create the changes for the various states such as MouseOver and Checked etc.


A couple of things to remember when recording states:
  • Don't change the property of an object in two states from different state groups. With a Button this is usually not an issue, but with a Radio Button you have the CommonStates AND the CheckedStates. I had to duplicate an object that I wanted to appear differently in those two state groups.
  • Changing the easing function from the default can make a difference. I didn't do that in this example, but have done many other times - it can make a simple animation look natural and sophisticated.
Feel free to grab the radio button and use it for your own projects. It would be nice if you left me a comment below to let me know if you find it useful.

And remember that you can put whatever content you like inside the button! It doesn't have to be simple text - in fact if I was a better artist I would have made some nice icons to go inside them.

7 comments:

  1. Awesome - thank you for sharing this!

    David Roh

    ReplyDelete
  2. REALLY Cool !!! Thank you for Sharing !!!

    ReplyDelete
  3. Excellent article. Thanks!

    ReplyDelete
  4. very nice blog!

    ReplyDelete
  5. thebirdbath here, thanks for the kind comments. Your tut is great and I will experiment in making these with your ideas in mind.

    This is the kind of communication and peer review that makes us all better silverlight gurus.

    ReplyDelete
  6. Hi Zachery, thanks for stopping by. I totally agree with your comment about peer review.

    Two things thing I would possible change if I did the button again would be:
    1) I would just draw the light reflections instead of using an effect (for better performance)
    2) I would use less columns and rows - where a row or column is only 1 or two pixels I would use margins instead.

    ReplyDelete