Introduction
Ah. Aqua buttons. They're everywhere. Except on your website. Don't be frustrated anymore. You've just found the helping hand you need: The Aqualizer! The Aqualizer, or Aqualize, is a fantastic, excellent and downright amazing command line utility to create �aqua�-buttons like this one:

If you want a button like this, download The Aqualizer and type at the command prompt:
C:\>aqualize "The\nAqualizer" button.bmp
Or maybe you want a red button?
C:\>aqualize -c:FF0000 "The\nAqualizer" button.bmp
A black one?
C:\>aqualize -c:000000 -t:FFFFFF "The\nAqualizer" button.bmp
A small one?
C:\>aqualize -s:small -a:0 "Hi!" button.bmp
I found a GIMP tutorial on the net for a aqua button I liked, and thought it would be cool if people, especially programmers, with no knowledge of GIMP or Photoshop could put a nice aqua button on their website. I wrote all the code I needed to follow the steps in the tutorial. The Aqualizer is based on Shadow, my program to add drop shadows to images (see my site).
Composing The Image
Let's take a look at the code in Main()
. We're going to create an image with several layers, so we create a new LayeredImage
object and call it image
:
LayeredImage image = new LayeredImage(width, height);
The first layer we add is the background layer. We fill the layer with the background color.
Layer bkgnd = image.Layers.Add();
bkgnd.Clear(bkcolor);
If the user wants a drop shadow, we create a new layer, select a circle, fill that circle with black, apply a blur filter, move the layer a bit to the right and to the bottom, and set the opacity of the layer so that the shadow is not too dark. The blur filter in Shadow uses a Gauss curve to calculate the contribution of the color of a pixel to the �current� pixel. The blur filter in The Aqualizer uses a Sigmoid curve. A Sigmoid curve is defined by three parameters: alpha, beta and gamma. Alpha is the start point, beta is the inflexion point, and gamma is the end point. The value at alpha is zero and the value at gamma is 1. The advantage of a Sigmoid curve is that you know that the value at alpha is 0; when using a Gauss curve, you cannot be 100% sure that the value will be 0. If you want to know how a blur filter works, see the article on my site.
if (dropshadow) {
Layer shadow = image.Layers.Add();
image.ActiveLayer = shadow;
image.BackColor = Color.Black;
image.Selection.SelectEllipse(
margin,
margin,
buttonwidth,
buttonwidth,
SelectionMode.Replace);
image.Fill();
Blur.ApplyTo(shadow.Bitmap, blurx, blury);
shadow.OffsetX = shadowoffsetx;
shadow.OffsetY = shadowoffsety;
shadow.Opacity = shadowopacity;
}
Now, we're ready to paint the button. We begin by adding a layer for the border. We select a circle the size of the button and fill it with a radial gradient. Implementing the radial gradient code was easy: I just needed to calculate the distance from the current pixel to the center point, convert it to a value between 0 and 1, and mix the foreground and background color.
Layer borderlayer = image.Layers.Add();
image.Selection.SelectEllipse(
margin,
margin,
buttonwidth,
buttonwidth,
SelectionMode.Replace);
image.ActiveLayer = borderlayer;
image.ForeColor = bordercolor;
image.BackColor = Color.Black;
image.Fill(
borderfillx0,
borderfilly0,
borderfillx1,
borderfilly1,
FillType.Radial);
We fill the inner area of the button with the button color. When the method Fill()
is called with no parameters, it will fill the current selection with the background color.
image.Selection.SelectEllipse(
buttoncolorx,
buttoncolory,
buttoncolorwidth,
buttoncolorwidth,
SelectionMode.Replace);
image.BackColor = buttoncolor;
image.Fill();
We don't want our button to look flat. To achieve a nice effect, we will darken the top and left of the inner area a bit. To darken the left, we select a sickle shape by selecting the inner area and subtracting a circle from the current selection. The selection is feathered and filled with black. The method Feather()
applies a blur filter to the selection mask. The same effect could be achieved by growing the selection and then applying a blur filter. To darken the top, we do the same: we select the inner area and subtract a circle about the same size, feather the selection, and fill it with black. We also add a mask to the shade layer because the feathering of the selection influences pixels outside the button, and we don't want that.
Layer shadelayer = image.Layers.Add();
image.ActiveLayer = shadelayer;
image.Selection.SelectEllipse(
buttoncolorx - 2,
buttoncolory - 2,
buttoncolorwidth + 4,
buttoncolorwidth + 4,
SelectionMode.Replace);
image.Selection.SelectEllipse(
shadeleftx,
shadelefty,
shadeleftwidth,
shadeleftheight,
SelectionMode.Subtract);
image.Selection.Feather(featherleft);
image.BackColor = Color.Black;
image.Fill();
shadelayer.Opacity = shadeopacity;
image.Selection.SelectEllipse(
buttoncolorx - 2,
buttoncolory - 2,
buttoncolorwidth + 4,
buttoncolorwidth + 4,
SelectionMode.Replace);
image.Selection.SelectEllipse(
shadetopx,
shadetopy,
shadetopwidth,
shadetopheight,
SelectionMode.Subtract);
image.Selection.Feather(feathertop);
image.Fill();
FastBitmap mask = new FastBitmap(width, height,
PixelFormat.Format24bppRgb);
Graphics g = Graphics.FromImage(mask._bitmap);
g.SmoothingMode = SmoothingMode.AntiAlias;
SolidBrush blackbrush = new SolidBrush(Color.Black);
SolidBrush whitebrush = new SolidBrush(Color.White);
g.FillRectangle(
blackbrush,
0,
0,
width,
height);
g.FillEllipse(
whitebrush,
buttoncolorx,
buttoncolory,
buttoncolorwidth,
buttoncolorwidth);
whitebrush.Dispose();
blackbrush.Dispose();
g.Dispose();
shadelayer.Mask = mask;
Before we can put the text on the button, we have to create a Font
object and preprocess the text a bit. If we encounter a '\n' in the text, we replace it by a Environment.NewLine
string. If we encounter a double backslash ("\\"), we replace it with a single backslash.
Layer textlayer = image.Layers.Add();
FontStyle fs = FontStyle.Regular;
for (Int32 i = 0; i < fontstyle.Length; i++) {
switch (fontstyle[i]) {
case 'b':
fs |= FontStyle.Bold;
break;
case 'i':
fs |= FontStyle.Italic;
break;
case 'r':
fs |= FontStyle.Regular;
break;
case 's':
fs |= FontStyle.Strikeout;
break;
case 'u':
fs |= FontStyle.Underline;
break;
}
}
Font f = new Font(fontface, fontsize, fs);
StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;
format.Trimming = StringTrimming.None;
SolidBrush brush = new SolidBrush(textcolor);
StringBuilder sb = new StringBuilder(text.Length);
for (Int32 i = 0; i < text.Length; i++) {
if (text[i] == '\\') {
if (i + 1 < text.Length) {
if (text[i + 1] == 'n') {
sb.Append(Environment.NewLine);
i++;
} else if (text[i + 1] == '\\') {
sb.Append('\\');
i++;
} else {
sb.Append('\\');
}
} else {
sb.Append('\\');
}
} else {
sb.Append(text[i]);
}
}
textlayer.DrawText(0, 0, width, height,
sb.ToString(), f, brush, format);
brush.Dispose();
f.Dispose();
textlayer.Opacity = 1.0 - texttrans;
And now, ladies and gentlemen, the finishing touch: the highlighting. We select an ellipse and fill the selection with a linear gradient which changes gradually from white to transparent. After that, we flatten the image and we're done. Tadaa! A very nice aqua button!
Layer highlightlayer = image.Layers.Add();
image.ActiveLayer = highlightlayer;
image.ForeColor = Color.White;
image.BackColor = Color.Transparent;
image.Selection.SelectEllipse(
highlightx,
highlighty,
highlightwidth,
highlightheight,
SelectionMode.Replace);
image.Fill(
0,
highlightfilly0,
0,
highlightfilly1,
FillType.Linear);
FastBitmap result = image.Flatten();
Remarks
Remark 1. Maybe you're wondering why you have to specify �small�, �medium� or �large� to set the button size, instead of a width and height. The reason is Pixel Picking. Icon designers will probably know what I mean. When you scale down a vector image, it's possible the image doesn't look too good anymore: you have to manually change the pixels.
Remark 2. .NET code is slow. When the image width was set to 400, it took about 30 seconds to generate the image! To optimize the blur filter, I replaced the calls to GetPixel()
with GetPixelInt32()
. GetPixelInt32()
returns a color as a Int32
instead of a Color
structure. Allocating objects inside loops is a bad idea, even in .NET. Floating-point calculations are also slow. But I didn't want to rewrite the code to save an operation here and there, I find the readability of the code more important.