2D Cell Animation Part III: Creating a Color Palette

This is the third and final post about my WPF 2D Cell Animation program; Ink-A-Mator. Once you can get the source on the .NET 3.0 Community site. This week we are going to look at how we can create a color palette, since WPF doesn’t have one.

The first place to start with creating a color picker is to figure out what color is at a given x, y coordinate. If you are familiar with GDI or GDI+, then you might think there is a GetPixel method available on some object similar to a Graphics object. Unfortunately there is no such method or property. Instead we will have to create a bitmap from the UIElement we want to get the color from, create a copy of its pixels and then index into that copy to find the pixel we want.

// We are using 32 bit color.

int bytesPerPixel = 4;

 

// Where we are going to store our pixel information.

byte[] Pixels = new byte[(int) uiElement.Height * (int) uiElement.Width * bytesPerPixel];

 

// Create a render target which allows us to get a copy of any UI element as a bitmap

RenderTargetBitmap rt = new RenderTargetBitmap((int)uiElement.Width, (int) uiElement.Height, 96, 96, PixelFormats.Pbgra32);

 

// Render the image of the uiElement to a bitmap.

rt.Render(uiElement);

 

// Calculate the stride of the bitmap

int stride = (int) uiElement.Width * bytesPerPixel;

 

// Copy the pixels from our render target to the array of pixels.

rt.CopyPixels(Pixels, stride, 0);

// p is a Point so use p with formula (Y * Width * BPP + X * BPP) to figure out index in array.

int pixelIndex = (int)((int)(p.Y) * uiElement.Width * bytesPerPixel + (int)(p.X) * bytesPerPixel);

 

// extract the rgb components from the array. We use Pbgra32 here so our order is b g r.

byte b = Pixels[pixelIndex];

byte g = Pixels[pixelIndex + 1];

byte r = Pixels[pixelIndex + 2];

 

// Create a color from the rgb components.

Color color = Color.FromRgb(r, g, b);

Each pixel in our Pixel array is made up of four components; blue, green, red and alpha all of which are one byte, which is 32 bits or 4 bytes, this is known as the bit depth. Each entry in our Pixels array is going to hold one of these components. To get the total size of the array to hold the pixels we need to multiply the width, height and bytes per pixel.

Bitmap

Next we create our RenderTargetBitmap passing it the desired width, height, DPIX, DPIY and the desired pixel format. I have hard coded the DPI to be 96, which I thought might cause a problem when I changed the DPI, but WPF does a nice job of abstracting so that it performs and looks correct at different DPI settings. RenderTargetBitmap derives from BitmapSource which is the same type as the Source property on an Image control, so we can display a RenderTargetBitmap using an Image control.

Now we call Render on our RenderTargetBitmap instance passing in the uiELement. RenderTargetBitmap allows us to get a bitmap version of a WPF Visual. Once we compute the stride we call CopyPixels on our RenderTargetBitmap instance which as the name implies copies the pixels in the bitmap to our Pixels byte array.

All we have to do now is compute the index for the pixel and point x, y. Since our Pixels byte array represents a pixel component per index we need to multiply our y coordinate by the width and bit depth then add that to our x coordinate times the bit depth. This will put our index and the first component of the pixel. Since we are using BGR color ordering defined by the PixelFormat.Pbgra32, pixel components are in the order blue, green, red, alpha instead of red, green, blue, alpha. We don’t care about alpha so we are just going to ignore that component.

In the source code I have two different palettes that I create. The simpler one called Palette2 uses a LinearGradientBrush to create the look of the Palette. The other palette called Palette in source code takes four colors and blends them from the four corners of the palette.

Palette

To create Palette2 we need to create a Visual that will define look of our palette. To do this we are going to create a LinearGradientBrush and add some gradient stops.

LinearGradientBrush brush = new LinearGradientBrush();

 

brush.StartPoint = new Point(0.5, 0);

brush.EndPoint = new Point(0.5, 1);

 

brush.GradientStops.Add(new GradientStop(Colors.Orange, 0));

brush.GradientStops.Add(new GradientStop(Colors.Yellow, 0.15));

brush.GradientStops.Add(new GradientStop(Colors.Green, 0.25));

brush.GradientStops.Add(new GradientStop(Colors.Blue, 0.5));

brush.GradientStops.Add(new GradientStop(Colors.Red, 0.75));

brush.GradientStops.Add(new GradientStop(Colors.Black, 0.9));

brush.GradientStops.Add(new GradientStop(Colors.White, 1));

 

We then create a Rectangle and set its Fill property to the brush. We then use RenderTargetBitmap to make a bitmap copy of the Rectangle the first time the user clicks on it to generate the Pixels array. We then cache the Pixels array and index into when a user clicks somewhere else in the Rectangle.

The more complex Palette uses a bit of math to create the blending of the colors in the four corners. Palette.cs has the code for this palette. Unlike Palette2 which took a snapshot of a Visual and turned it into an image, Palette will create an image from scratch.

The image below shows that we blend from left to right and then from top to bottom. We actually do the gradient math for the first line of the image and the last line of the image and then blend between the top and bottom lines.

Blend

First we calculate the amount each color component changes as pixels go from left to right. The method CalculatePixelSlope shows how we calculate this change. The first two parameters define the color components of the pixels we want to interpolate between and distance specifics the distance between these pixels. Notice that the return type is PixelAsDoubles which is important because the change in color will most likely be small and possibly negative.

PixelAsDoubles CalculatePixelSlope(Pixel p1, Pixel p2, int distance)

{

return new PixelAsDoubles((double)(p2.r - p1.r) / distance, (double)(p2.g - p1.g) / distance, (double)(p2.b - p1.b) / distance);

}

struct Pixel

{

public byte r;

public byte g;

public byte b;

}

struct PixelAsDoubles

{

public double r;

public double g;

public double b;

}

Next we use the pixel slope to fill in the pixels in our image. The CalculatePixelGradient method below takes the starting pixel color components and pixel color slope and figure out for a given distance what the color should be.

private static Pixel CalculatePixelGradient(Pixel startingPixel, int distance, PixelAsDoubles slope)

{

Pixel p = new Pixel();

 

// We don't want to start at zero so shift.

distance++;

 

p.r = (byte)(startingPixel.r + slope.r * distance);

p.g = (byte)(startingPixel.g + slope.g * distance);

p.b = (byte)(startingPixel.b + slope.b * distance);

 

return p;

}

Now let’s look at how we create an image from scratch and call these methods to create our four color palette. Again we need to create a byte array which will hold our pixel components. Then we create the four corner pixels and pass them to CalculatePixelSlope to figure out the gradient for the top and bottom lines of the image. We begin looping through the pixels calling CalculatePixelGradient which will give us the final pixel at the top and bottom. Once we have these pixels we calculate the pixel slope for the top to bottom gradient and call CalculatePixelGradient which returns the final pixel. Notice that our pixel order is now RGB instead of BGR, this is because we are going to use the PixelFormat.Rgb24 instead of the PixelFormat.Pbgra32 format as we did with Palette2. After the look we calculate our stride and then call the static method Bitmap.Create which creates a BitmapSource from an array of bytes which represent the pixels.

BitmapSource CreateFourColorPalette()

{

// Create a place to hold our pixel data.

pixels = new Byte[(int)(FourColorPaletteSize.Width * FourColorPaletteSize.Height * bytesPerPixel)];

 

int i = 0;

 

// Find Color Slopes

Pixel upperLeft = new Pixel(upperLeftColor.Color.R, upperLeftColor.Color.G, upperLeftColor.Color.B);

Pixel upperRight = new Pixel(upperRightColor.Color.R, upperRightColor.Color.G, upperRightColor.Color.B);

Pixel lowerLeft = new Pixel(lowerLeftColor.Color.R, lowerLeftColor.Color.G, lowerLeftColor.Color.B);

Pixel lowerRight = new Pixel(lowerRightColor.Color.R, lowerRightColor.Color.G, lowerRightColor.Color.B);

 

// Get the change in color for the distance the color is traveling

PixelAsDoubles LeftToRightTop = CalculatePixelSlope(upperLeft, upperRight, (int)FourColorPaletteSize.Width);

PixelAsDoubles LeftToRightBottom = CalculatePixelSlope(lowerLeft, lowerRight, (int)FourColorPaletteSize.Width);

 

// We interpolate from left to right and then top to bottom.

for (int y = 0; y < FourColorPaletteSize.Height; y++)

{

for (int x = 0; x < FourColorPaletteSize.Width; x++)

{

Pixel p1, p2;

 

p1 = CalculatePixelGradient(upperLeft, x, LeftToRightTop);

p2 = CalculatePixelGradient(lowerLeft, x, LeftToRightBottom);

 

PixelAsDoubles topToBottom = CalculatePixelSlope(p1, p2, (int)FourColorPaletteSize.Height);

 

Pixel p = CalculatePixelGradient(p1, y, topToBottom);

 

pixels[i++] = p.r;

pixels[i++] = p.g;

pixels[i++] = p.b;

}

}

 

// Figure out the stride.

int stride = (int)FourColorPaletteSize.Width * bytesPerPixel;

 

return BitmapImage.Create((int)FourColorPaletteSize.Width, (int)FourColorPaletteSize.Height, 96, 96, PixelFormats.Rgb24, null, pixels, stride);

}

 

The two main advantages of creating a palette this way is that we don’t have to wait for WPF to render the brush like we did with Palette2 and we have more control over the colors in the palette. When looking at the source code you will notice that Palette.cs actually creates two palettes. The master palette uses more advanced blending to create a palette that allows us to select the colors of the FourCornerPalette.

masterPalette

Tomorrow I will start a new series about an application I created using WPF, WCF and peer to peer which allows for content distribution without servers.