Fade in / Fade out Images using Palette animation



Consider that your window has a white background and you need to draw an image on it. You could either simply draw the image or for effect you could let the image fade in from the white background. On the other hand if you already have an image on the window and you want to remove it, you can fade out the image into the background.

The code below shows you how to use palette animation to achieve these fade in and fade out effects. You can start with any color to fade the image in or end with any color when fading an image out. Note that palette animation is supported only on 256 color displays (actually 16 colors too). If you run the same code on a high color display, you won’t see any fading effect but the image would draw properly. This code has another limitation that it will work only with bitmaps that have 256 colors or less. The reason is that bitmaps with greater number of colors do not have color tables so you would have to write more code to transform it into a 256 color bitmap.

Function 1: Create a palette with reserved palette entries

For palette animation to work, the palette selected into the device has to be somewhat different than regular palettes. Any color entry that can change has to be marked with the PC_RESERVED flag. Only these colors can change when we animate the palette. There is another issue that we have to address when we create this special palette. During the palette animation we do not want to affect the system colors used by windows. There are 20 such colors. We, therefore, reduce the number of colors in the palette used by the image by upto 20 colors, thus resulting in a palette with at most 236 colors.

Here’s what the CreateReservedPalette() function does. If the color table has more than 236 colors, it scans through the bitmap bits and creates a count of how frequently each color is used. Based on this count, it removes the least used colors so that it ends up with only 236 colors. It then creates a palette with the PC_RESERVED flag for all the palette entries. If the bitmap has 236 or less colors then it simply uses all of them to create the palette.

The CreateReservedPalette() function needs a device-independent bitmap because the DIB has the color information and it also provides simple access to all the bitmap bits without having to worry about color planes. Scanning through a DDB would be too cumbersome.

// CreateReservedPalette	- Create a palette using the PC_RESERVED flag
//				  Limit the colors to 236 colors
// Returns			- Handle to a palette object
// hDIB				- Handle to a device-independent bitmap
//
HPALETTE CreateReservedPalette(HANDLE hDIB)
{
	HPALETTE hPal = NULL;    // handle to a palette

	if (!hDIB)
		return NULL;

	BITMAPINFO &bmInfo = *(LPBITMAPINFO)hDIB ;

	int nColors = bmInfo.bmiHeader.biClrUsed ? bmInfo.bmiHeader.biClrUsed :
					1 << bmInfo.bmiHeader.biBitCount;

	if( nColors > 256 )
		return NULL;		// No Palette


	//allocate memory block for logical palette
	UINT nSize = sizeof(LOGPALETTE) + (sizeof(PALETTEENTRY) * nColors);
	LOGPALETTE *pLP = (LOGPALETTE *) new BYTE[nSize];

	// Initialize the palette version
	pLP->palVersion = 0x300;

	// If it is a 256 color DIB, then let's find the most used 236 colors 
	// and make a palette out of those colors
	if (nColors > 236)
	{
		typedef struct _tagBESTCOLORS
		{
			DWORD dwColorCnt;	//Count of how many times a color is used
			BOOL  bDontUse;	//Should we use this color?
		} BESTCOLORS;

		BESTCOLORS bc[256];
		BYTE dwLeastUsed[20];		// Least used color indices
		LPSTR lpBits;			// pointer to D.I. bits of a DIB
		int   nWidth, nHeight, nBytesPerLine, cx, cy;

		::ZeroMemory( bc, 256*sizeof(BESTCOLORS));

		lpBits = (LPSTR)(bmInfo.bmiColors + nColors);
		nWidth = bmInfo.bmiHeader.biWidth;
		nHeight = bmInfo.bmiHeader.biHeight;
		nBytesPerLine = ((((bmInfo.bmiHeader.biWidth *
					bmInfo.bmiHeader.biBitCount) + 31) & ~31) / 8);

		// Traverse through all of the bits in the bitmap and place the 
		// color count of each color in the BESTCOLORS array
		for (cy = 0; cy < nHeight; cy++)
			for (cx = 0; cx < nWidth; cx++)
				bc[*(LPBYTE)(lpBits+cy*nBytesPerLine+cx)].dwColorCnt++;

		// Let's arbitrarily place the first few colors in the "Least Used" list.
		int nReject = nColors - 236;
		for (cx=0; cx < nReject; cx++)
		{
			bc[cx].bDontUse = TRUE;
			dwLeastUsed[cx] = cx;
		}

		// Now, let's traverse through all of the colors and 
		// sort out the least used
		for (cx=0; cx < nColors; cx++)
		{
			cy = 0;
			while ((!(bc[cx].bDontUse)) && cy < nReject)
			{
				if (bc[cx].dwColorCnt < bc[dwLeastUsed[cy]].dwColorCnt)
				{
					bc[dwLeastUsed[cy]].bDontUse = FALSE;
					dwLeastUsed[cy] = cx;
					bc[cx].bDontUse = TRUE;
				}
				cy++;
			}
		}

		// We want only 236 colors, so that the 20 system colors 
		// are left untouched
		pLP->palNumEntries = 236;

		cx = 0;
		for(int i = 0; i < nColors; i++)
		{
			// Should we use this color?
			if (!((bc[i].bDontUse)))
			{
				pLP->palPalEntry[cx].peRed = bmInfo.bmiColors[i].rgbRed;
				pLP->palPalEntry[cx].peGreen =
								bmInfo.bmiColors[i].rgbGreen;
				pLP->palPalEntry[cx].peBlue = bmInfo.bmiColors[i].rgbBlue;
				pLP->palPalEntry[cx].peFlags = PC_RESERVED;
				cx++;
			}
		}

	}
	else if (nColors)
	{
		// We have enough room for all the colors

		pLP->palNumEntries = nColors;

		// Copy the colors
		for(int i = 0; i < nColors; i++)
		{
			pLP->palPalEntry[i].peRed = bmInfo.bmiColors[i].rgbRed;
			pLP->palPalEntry[i].peGreen = bmInfo.bmiColors[i].rgbGreen;
			pLP->palPalEntry[i].peBlue = bmInfo.bmiColors[i].rgbBlue;
			pLP->palPalEntry[i].peFlags = PC_RESERVED;
		}
	}


	hPal = CreatePalette( pLP );
	delete[] pLP;

	// return handle to DIB's palette 
	return hPal;
}

Function 2: Fade in an image starting with a given color


The FadeIn() function draws the start color and makes the image emerge from that color. As I have already explained, we need a special palette to work with palette animation. In this case we start with a palette in which all the entries are the start color. When the image is first displayed, it will cause the entire image to display in the only color in the palette.

There is, however, one problem with this. When we draw an image using BitBlt() or SetDIBitsToDevice(), all the colors in the bitmap are mapped to colors in the selected palette. We would therefore end up with all the pixels using the same palette entry. To overcome this hurdle, we use an optimization in the GDI code. This optimization works like this. If the source device context and the target device context have the same palette (an identity palette) then the function does not bother with remapping the colors. Here’s what we do. We create a compatible memory device context, select the palette and realize it and then draw the image in this device context. Since it’s the memory DC, nothing happens on the screen yet. We select the same palette into the target device and then animate the palette so that all the color entries are the same as the starting color we desire. Now we can copy the image from the memory DC to the target DC without the color getting remapped. When we have finished with the BitBlt() to transfer the image, the image appears in a single color in the target device.

Once we have the image in the target device, we simply animate the palette till all the entries in the palette are the colors needed to display the image properly.

// FadeIn	- Draws a start color and fades the bitmap in.
// pDC		- Pointer to the device context to draw in
// hDIB		- Handle to a device-independent bitmap
// clrStart	- The start color of the bitmap
// xDest	- x-coordinate of upper-left corner of dest. rect. 
// yDest	- y-coordinate of upper-left corner of dest. rect. 
// nLoops	- How many loops to fade the image in
// nDelay	- Delay in milli-seconds between each loop
//
void FadeIn( CDC *pDC, HANDLE hDIB, COLORREF clrStart, int xDest, int yDest,
							int nLoops, int nDelay )
{
	HPALETTE hPal;
	PALETTEENTRY peAnimate[256];
	PALETTEENTRY peOriginal[256];
	CPalette pal;

	// Create a 236 colors or less logical palette with PC_RESERVED set
	if (!(hPal = CreateReservedPalette(hDIB)))
		return;

	// The palette will be deleted when the CPalette object is destroyed
	pal.Attach( hPal );

	BITMAPINFO &bmInfo = *(LPBITMAPINFO)hDIB ;

	int nColors = bmInfo.bmiHeader.biClrUsed ? bmInfo.bmiHeader.biClrUsed :
					1 << bmInfo.bmiHeader.biBitCount;
	int nWidth = bmInfo.bmiHeader.biWidth;
	int nHeight = bmInfo.bmiHeader.biHeight;

	// Obtain the original entries in the palette
	GetPaletteEntries(hPal, 0, nColors, (LPPALETTEENTRY)&peOriginal);

	// Change all the entries in the palette to the start color
	int clrRValue = GetRValue(clrStart);
	int clrGValue = GetGValue(clrStart);
	int clrBValue = GetBValue(clrStart);
	for (int j = 0; j < nColors; j++)
	{
		peAnimate[j].peRed = clrRValue;
		peAnimate[j].peGreen = clrGValue;
		peAnimate[j].peBlue = clrBValue;
		peAnimate[j].peFlags = PC_RESERVED;
	}

	// Select the palette
	CPalette *pOldPalette = pDC->SelectPalette(&pal, FALSE);
	pDC->RealizePalette();

	// We need to draw the image so that it appears as a rectangle
	// with the start color. At the same time we don't want the 
	// colors to be remapped, otherwise all the pixels would remain
	// the same color. We use a memory DC to achieve this.
	CDC memDC;
	memDC.CreateCompatibleDC( pDC );
	CBitmap bmp;
	bmp.CreateCompatibleBitmap( pDC, nWidth, nHeight );
	CBitmap *pOldBitmap = memDC.SelectObject( &bmp );
	CPalette *pOldMemPalette = memDC.SelectPalette(&pal, FALSE);
	memDC.RealizePalette();

	// Draw the image to memDC
	LPVOID lpDIBBits = (LPVOID)(bmInfo.bmiColors + nColors);

	::SetDIBitsToDevice(memDC.m_hDC,	// hDC
		0,				// XDest
		0,				// YDest
		nWidth,				// nDestWidth
		nHeight,			// nDestHeight
		0,				// XSrc
		0,				// YSrc
		0,				// nStartScan
		nHeight,			// nNumScans
		lpDIBBits,			// lpBits
		(LPBITMAPINFO)hDIB,		// lpBitsInfo
		DIB_RGB_COLORS);		// wUsage

	// Set the color to start color
	AnimatePalette(hPal, 0, nColors, (LPPALETTEENTRY)&peAnimate);

	// Now copy the image from the memory DC
	// Since the palettes in memDC and pDC are the same, no color mapping
	// takes place. The image will appear in the start color
	pDC->BitBlt(xDest, yDest, nWidth, nHeight, &memDC,0,0,SRCCOPY );

	// Fade in
	for( int i=1; i <= nLoops; i++ )
	{
		for (j = 0; j < nColors; j++)
		{
			peAnimate[j].peRed = clrRValue -
					((clrRValue-peOriginal[j].peRed)*i)/nLoops;
			peAnimate[j].peGreen = clrGValue -
					((clrGValue-peOriginal[j].peGreen)*i)/nLoops;
			peAnimate[j].peBlue = clrBValue -
					((clrBValue-peOriginal[j].peBlue)*i)/nLoops;
		}

		pal.AnimatePalette(0, nColors, (LPPALETTEENTRY)&peAnimate);

		// Delay...
		Sleep(nDelay);
	}

	// Reselect old objects into DC
	memDC.SelectPalette(pOldMemPalette, FALSE);
	memDC.SelectObject( pOldBitmap );
	pDC->SelectPalette(pOldPalette, FALSE);
}

Function 3: Fade out an image to a given color

Fading out an image is much simpler than fading it in. We don’t have to bother with an identity palette. After creating the reserved palette, we can select it into the device context, realize and then draw the image onto the device context. Now we have the image on the target device and we need to animate the palette so that at the end the colors in the palette are all the same.

// FadeOut	- Draws a bitmap and fades it out to the desired end color
// pDC		- Pointer to the device context to draw in
// hDIB		- Handle to a device-independent bitmap
// clrEnd	- The final color of the bitmap
// xDest	- x-coordinate of upper-left corner of dest. rect. 
// yDest	- y-coordinate of upper-left corner of dest. rect. 
// nLoops	- How many loops to fade the image out.
// nDelay	- Delay in milli-seconds between each loop
//
void FadeOut( CDC *pDC, HANDLE hDIB, COLORREF clrEnd, int xDest, int yDest,
							int nLoops, int nDelay )
{
	HPALETTE hPal;
	PALETTEENTRY peAnimate[256];
	PALETTEENTRY peOriginal[256];
	CPalette pal;

	// Create a 236 colors or less logical palette with PC_RESERVED set
	if (!(hPal = CreateReservedPalette(hDIB)))
		return;

	// The palette will be deleted when the CPalette object is destroyed
	pal.Attach( hPal );

	BITMAPINFO &bmInfo = *(LPBITMAPINFO)hDIB ;

	int nColors = bmInfo.bmiHeader.biClrUsed ? bmInfo.bmiHeader.biClrUsed :
					1 << bmInfo.bmiHeader.biBitCount;
	int nWidth = bmInfo.bmiHeader.biWidth;
	int nHeight = bmInfo.bmiHeader.biHeight;

	// Obtain the original entries in the palette 
	GetPaletteEntries(hPal, 0, nColors, (LPPALETTEENTRY)&peAnimate);
	GetPaletteEntries(hPal, 0, nColors, (LPPALETTEENTRY)&peOriginal);

	// Get end color values
	int clrRValue = GetRValue(clrEnd);
	int clrGValue = GetGValue(clrEnd);
	int clrBValue = GetBValue(clrEnd);

	// Select the palette
	CPalette *pOldPalette = pDC->SelectPalette(&pal, FALSE);
	pDC->RealizePalette();

	// Draw the image 
	LPVOID lpDIBBits = (LPVOID)(bmInfo.bmiColors + nColors);

	::SetDIBitsToDevice(pDC->m_hDC,	// hDC
		xDest,				// XDest
		yDest,				// YDest
		nWidth,				// nDestWidth
		nHeight,			// nDestHeight
		0,				// XSrc
		0,				// YSrc
		0,				// nStartScan
		nHeight,			// nNumScans
		lpDIBBits,			// lpBits
		(LPBITMAPINFO)hDIB,		// lpBitsInfo
		DIB_RGB_COLORS);		// wUsage


	// Fade out
	for( int i=1; i <= nLoops; i++ )
	{
		for (int j = 0; j < nColors; j++)
		{
			peAnimate[j].peRed = peOriginal[j].peRed -
				((peOriginal[j].peRed - clrRValue)*i)/nLoops;
			peAnimate[j].peGreen = peOriginal[j].peGreen -
				((peOriginal[j].peGreen - clrGValue)*i)/nLoops;
			peAnimate[j].peBlue = peOriginal[j].peBlue -
				((peOriginal[j].peBlue - clrBValue)*i)/nLoops;
		}

		pal.AnimatePalette(0, nColors, (LPPALETTEENTRY)&peAnimate);

		// Delay...
		Sleep(nDelay);
	}

	// Reselect old objects into DC
	pDC->SelectPalette(pOldPalette, FALSE);
}

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read