Get a site

VC++ 6.0 ebook chapter index
homepage aranna.altervista.org
Free counters!

Palette Animation

If you saw the word "animation" in the title of this section and started thinking about kwazy wabbits running around your screen, your sights are probably set a little too high. Yes, you can do some animation using the Windows Palette Manager, but it is a rather specialized form of animation.

Usually, animation under Windows involves displaying a series of bitmaps in quick succession. Palette animation is quite different. You begin by drawing everything you need on the screen, and then you manipulate the palette to change the colors of these objects, perhaps rendering some of the images invisible against the screen background. In this way, you can get animation effects without redrawing anything. Palette animation is consequently very fast.

The initial creation of the palette for use in palette animation is a little different from what we've seen earlier: The peFlags field of the PALETTEENTRY structure must be set to PC_RESERVED for each RGB color value that will be changed during animation.

Normally, as we've seen, you set the peFlags flag to 0 when you create a logical palette. This allows the GDI to map identical colors from multiple logical palettes into the same system palette entry. For example, suppose two Windows programs create logical palettes containing the RGB entry 10-10-10. Windows needs only one 10-10-10 entry in the system palette table. But if one of these two programs is using palette animation, then you don't want GDI to do this. Palette animation is intended to be very fast—and it can only be fast if no redrawing occurs. When the program using palette animation changes the palette, it should not affect other programs or force GDI to reorganize the system palette table. The peFlags value of PC_RESERVED reserves the system palette entry for a single logical palette.

When using palette animation, you call SelectPalette and RealizePalette as normal during the WM_PAINT message. You specify color using the PALETTEINDEX macro. This macro takes an index into the logical palette table.

For animation, you probably want to change the palette in response to a WM_TIMER message. To change the RGB color values in the logical palette, you call the function AnimatePalette using an array of PALETTEENTRY structures. This function is fast because it needs to change entries in the system palette only and, consequently, the video board hardware palette table.

The Bouncing Ball

Figure 16-8 shows the components of the BOUNCE program, yet another program that displays a bouncing ball. For purposes of simplicity, this ball is drawn as an ellipse depending on the size of the client area. Because I have several palette animation programs in this chapter, the PALANIM.C ("palette animation") file contains some overhead common to all of them.

Figure 16-8. The BOUNCE program.

PALANIM.C

/*----------------------------------------------
   PALANIM.C -- Palette Animation Shell Program
                (c) Charles Petzold, 1998
  ----------------------------------------------*/

#include <windows.h>

extern HPALETTE CreateRoutine  (HWND) ;
extern void     PaintRoutine   (HDC, int, int) ;
extern void     TimerRoutine   (HDC, HPALETTE) ;
extern void     DestroyRoutine (HWND, HPALETTE) ;

LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;

extern TCHAR szAppName [] ;
extern TCHAR szTitle [] ;

int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
     HWND     hwnd ;
     MSG      msg ;
     WNDCLASS wndclass ;
     wndclass.style         = CS_HREDRAW | CS_VREDRAW ;
     wndclass.lpfnWndProc   = WndProc ;
     wndclass.cbClsExtra    = 0 ;
     wndclass.cbWndExtra    = 0 ;
     wndclass.hInstance     = hInstance ;
     wndclass.hIcon         = LoadIcon (NULL, IDI_APPLICATION) ;
     wndclass.hCursor       = LoadCursor (NULL, IDC_ARROW) ;
     wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
     wndclass.lpszMenuName  = NULL ;
     wndclass.lpszClassName = szAppName ;
     
     if (!RegisterClass (&wndclass))
     {
          MessageBox (NULL, TEXT ("This program requires Windows NT!"),
                      szAppName, MB_ICONERROR) ;
          return 0 ;
     }
     
     hwnd = CreateWindow (szAppName, szTitle, 
                          WS_OVERLAPPEDWINDOW, 
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          NULL, NULL, hInstance, NULL) ;

     if (!hwnd)
          return 0 ;

     ShowWindow (hwnd, iCmdShow) ;
     UpdateWindow (hwnd) ;

     while (GetMessage (&msg, NULL, 0, 0))
     {
          TranslateMessage (&msg) ;
          DispatchMessage (&msg) ;
     }
     return msg.wParam ;
}

BOOL CheckDisplay (HWND hwnd)
{
     HDC hdc ;
     int iPalSize ;

     hdc = GetDC (hwnd) ;
     iPalSize = GetDeviceCaps (hdc, SIZEPALETTE) ;
     ReleaseDC (hwnd, hdc) ;
     if (iPalSize != 256)
     {
          MessageBox (hwnd, TEXT ("This program requires that the video ")
                            TEXT ("display mode have a 256-color palette."),
                      szAppName, MB_ICONERROR) ;
          return FALSE ;
     }
     return TRUE ;
}

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     static HPALETTE hPalette ;
     static int      cxClient, cyClient ;
     HDC             hdc ;
     PAINTSTRUCT     ps ;

     switch (message)
     {
     case WM_CREATE:
          if (!CheckDisplay (hwnd))
               return -1 ;

          hPalette = CreateRoutine (hwnd) ;
          return 0 ;
     
     case WM_DISPLAYCHANGE:
          if (!CheckDisplay (hwnd))
               DestroyWindow (hwnd) ;

          return 0 ;

     case WM_SIZE:
          cxClient = LOWORD (lParam) ;
          cyClient = HIWORD (lParam) ;
          return 0 ;

     case WM_PAINT:
          hdc = BeginPaint (hwnd, &ps) ;

          SelectPalette (hdc, hPalette, FALSE) ;
          RealizePalette (hdc) ;

          PaintRoutine (hdc, cxClient, cyClient) ;

          EndPaint (hwnd, &ps) ;
          return 0 ;

     case WM_TIMER:
          hdc = GetDC (hwnd) ;

          SelectPalette (hdc, hPalette, FALSE) ;

          TimerRoutine (hdc, hPalette) ;

          ReleaseDC (hwnd, hdc) ;
          return 0 ;

     case WM_QUERYNEWPALETTE:
          if (!hPalette)
               return FALSE ;

          hdc = GetDC (hwnd) ;
          SelectPalette (hdc, hPalette, FALSE) ;
          RealizePalette (hdc) ;
          InvalidateRect (hwnd, NULL, TRUE) ;

          ReleaseDC (hwnd, hdc) ;
          return TRUE ;

     case WM_PALETTECHANGED:
          if (!hPalette || (HWND) wParam == hwnd)
               break ;

          hdc = GetDC (hwnd) ;
          SelectPalette (hdc, hPalette, FALSE) ;
          RealizePalette (hdc) ;
          UpdateColors (hdc) ;

          ReleaseDC (hwnd, hdc) ;
          break ;

     case WM_DESTROY:
          DestroyRoutine (hwnd, hPalette) ;
          PostQuitMessage (0) ;
          return 0 ;
     }
     return DefWindowProc (hwnd, message, wParam, lParam) ;
}

BOUNCE.C

/*---------------------------------------

   BOUNCE.C -- Palette Animation Demo
               (c) Charles Petzold, 1998
  ---------------------------------------*/

#include <windows.h>

#define ID_TIMER 1

TCHAR szAppName [] = TEXT ("Bounce") ;
TCHAR szTitle   [] = TEXT ("Bounce: Palette Animation Demo") ;

static LOGPALETTE * plp ;

HPALETTE CreateRoutine (HWND hwnd)
{
     HPALETTE hPalette ;
     int      i ;

     plp = malloc (sizeof (LOGPALETTE) + 33 * sizeof (PALETTEENTRY)) ;

     plp->palVersion    = 0x0300 ;
     plp->palNumEntries = 34 ;

     for (i = 0 ; i < 34 ; i++)
     {
          plp->palPalEntry[i].peRed   = 255 ;
          plp->palPalEntry[i].peGreen = (i == 0 ? 0 : 255) ;
          plp->palPalEntry[i].peBlue  = (i == 0 ? 0 : 255) ;
          plp->palPalEntry[i].peFlags = (i == 33 ? 0 : PC_RESERVED) ;
     }
     hPalette = CreatePalette (plp) ;

     SetTimer (hwnd, ID_TIMER, 50, NULL) ;
     return hPalette ;
}

void PaintRoutine (HDC hdc, int cxClient, int cyClient)
{
     HBRUSH hBrush ;
     int    i, x1, x2, y1, y2 ;
     RECT   rect ;
          // Draw window background using palette index 33

     SetRect (&rect, 0, 0, cxClient, cyClient) ;
     hBrush = CreateSolidBrush (PALETTEINDEX (33)) ;
     FillRect (hdc, &rect, hBrush) ;
     DeleteObject (hBrush) ;

          // Draw the 33 balls

     SelectObject (hdc, GetStockObject (NULL_PEN)) ;

     for (i = 0 ; i < 33 ; i++)
     {
          x1 =  i      * cxClient / 33 ;
          x2 = (i + 1) * cxClient / 33 ;

          if (i < 9)
          {
               y1  = i      * cyClient / 9 ;
               y2 = (i + 1) * cyClient / 9 ;
          }
          else if (i < 17)
          {
               y1 = (16 - i) * cyClient / 9 ;
               y2 = (17 - i) * cyClient / 9 ;
          }
          else if (i < 25)
          {
               y1 = (i - 16) * cyClient / 9 ;
               y2 = (i - 15) * cyClient / 9 ;
          }
          else 
          {
               y1 = (32 - i) * cyClient / 9 ;
               y2 = (33 - i) * cyClient / 9 ;
          }

          hBrush = CreateSolidBrush (PALETTEINDEX (i)) ;
          SelectObject (hdc, hBrush) ;
          Ellipse (hdc, x1, y1, x2, y2) ;
          DeleteObject (SelectObject (hdc, GetStockObject (WHITE_BRUSH))) ;
     }
     return ;
}
void TimerRoutine (HDC hdc, HPALETTE hPalette)
{
     static BOOL bLeftToRight = TRUE ;
     static int  iBall ;

          // Set old ball to white

     plp->palPalEntry[iBall].peGreen = 255 ;
     plp->palPalEntry[iBall].peBlue  = 255 ;

     iBall += (bLeftToRight ? 1 : -1) ;

     if (iBall == (bLeftToRight ? 33 : -1))
     {
          iBall = (bLeftToRight ? 31 : 1) ;
          bLeftToRight ^= TRUE ;
     }

          // Set new ball to red

     plp->palPalEntry[iBall].peGreen = 0 ;
     plp->palPalEntry[iBall].peBlue  = 0 ;

          // Animate the palette

     AnimatePalette (hPalette, 0, 33, plp->palPalEntry) ;
     return ;
}

void DestroyRoutine (HWND hwnd, HPALETTE hPalette)
{
     KillTimer (hwnd, ID_TIMER) ;
     DeleteObject (hPalette) ;
     free (plp) ;
     return ;
}

Palette animation will not work unless Windows is in a video mode that supports palettes. So, PALANIM.C begins WM_CREATE processing by calling its CheckDisplay function, the same function in the SYSPAL programs.

PALANIM.C calls four functions in BOUNCE.C: CreateRoutine during the WM_CREATE message (during which BOUNCE is expected to create a logical palette), PaintRoutine during the WM_PAINT message, TimerRoutine during the WM_TIMER message, and DestroyRoutine during the WM_DESTROY message (during which BOUNCE is expected to clean up). Prior to calling both PaintRoutine and TimerRoutine, PALANIM.C obtains a device context and selects the logical palette into it. Prior to calling PaintRoutine, it also realizes the palette. PALANIM.C expects TimerRoutine to call AnimatePalette. Although AnimatePalette requires the palette to be selected in the device context, it does not require a call to RealizePalette.

The ball in BOUNCE bounces back and forth in a "W" pattern within the client area. The background of the client area is white. The ball is red. At any time, the ball can be seen in one of 33 nonoverlapping positions. This requires 34 palette entries, one for the background and the other 33 for the different positions of the ball. In CreateRoutine, BOUNCE initializes an array of PALETTEENTRY structures by setting the first palette entry (corresponding to the position of the ball in the upper left corner) to red and the others to white. Notice that the peFlags field is set to PC_RESERVED for all entries except the background (the last palette entry). BOUNCE concludes CreateRoutine by setting a Windows timer with an interval of 50 msec.

BOUNCE does all its drawing in PaintRoutine. The background of the window is drawn with a solid brush with a color specified by a palette index of 33. The colors of the 33 balls are drawn with colors based on palette indices ranging from 0 to 32. When BOUNCE first draws on its client area, the palette index of 0 maps to red and the other palette indices map to white. This causes the ball to appear in the upper left corner.

The animation occurs when WndProc processes the WM_TIMER message and calls TimerRoutine. TimerRoutine concludes by calling AnimatePalette, which has the following syntax:

AnimatePalette (hPalette, uStart, uNum, &pe) ;

The first argument is a handle to the palette, and the last argument is a pointer to one or more PALETTEENTRY structures arranged as an array. The function alters one or more entries in the logical palette beginning with the uStart entry and continuing for uNum entries. The new uStart entry in the logical palette is taken from the first element in the PALETTEENTRY structure. Watch out! The uStart parameter is an index into the original logical palette table, not an index into the PALETTEENTRY array.

For convenience, BOUNCE uses the array of PALETTEENTRY structures that is part of the LOGPALETTE structure used when creating the logical palette. The current position of the ball (from 0 to 32) is stored as the static iBall variable. During TimerRoutine, BOUNCE sets that PALETTEENTRY element to white. It then calculates a new position of the ball and sets that element to red. The palette is changed with the call

AnimatePalette (hPalette, 0, 33, plp->palPalEntry) ;

GDI changes the first 33 logical palette entries (although only 2 actually change), makes the corresponding changes in the system palette table, and then changes the hardware palette table on the video board. The ball appears to move without any redrawing.

You may find it instructive to run SYSPAL2 or SYSPAL3 while BOUNCE is running.

Although AnimatePalette works very quickly, you should probably avoid changing all the logical palette entries when only one or two actually change. This is a little complicated in BOUNCE because the ball bounces back and forth—iBall is first incremented and then decremented. One approach would be to have two other variables called iBallOld (set to the previous position of the ball) and iBallMin (the lesser of iBall and iBallOld). You then call AnimatePalette like this to change just the two entries:

iBallMin = min (iBall, iBallOld) ;
AnimatePalette (hPal, iBallMin, 2, plp->palPalEntry + iBallMin) ;

Here's another approach: Let's suppose you first define a single PALETTEENTRY structure:

PALETTEENTRY pe ;

During TimerRoutine, you set the PALETTEENTRY fields for white and call AnimatePalette to change one entry at the iBall position in the logical palette:

     pe.peRed   = 255 ;
     pe.peGreen = 255 ;
     pe.peBlue  = 255 ;
     pe.peFlags = PC_RESERVED ;
     AnimatePalette (hPalette, iBall, 1, &pe) ;

You then calculate the new value of iBall as shown in BOUNCE, define the fields of the PALETTEENTRY structure for red, and call AnimatePalette again:

     pe.peRed   = 255 ;
     pe.peGreen = 0 ;
     pe.peBlue  = 0 ;
     pe.peFlags = PC_RESERVED ;
     AnimatePalette (hPalette, iBall, 1, &pe) ;

Although a bouncing ball is a traditional simple illustration of animation, it's really not suited for palette animation because all the possible positions of the ball must be drawn initially. Palette animation is more suited for showing repetitive patterns of movement.

One-Entry Palette Animation

One of the more interesting aspects of palette animation is that you can implement some interesting techniques using only one palette entry. This is illustrated in the FADER program show in Figure 16-9. This program also requires the PALANIM.C file shown earlier.

Figure 16-9. The FADER program.

FADER.C

/*--------------------------------------
   FADER.C -- Palette Animation Demo
              (c) Charles Petzold, 1998
  --------------------------------------*/

#include <windows.h>

#define ID_TIMER 1

TCHAR szAppName [] = TEXT ("Fader") ;
TCHAR szTitle   [] = TEXT ("Fader: Palette Animation Demo") ;

static LOGPALETTE lp ;

HPALETTE CreateRoutine (HWND hwnd)
{
     HPALETTE hPalette ;
     
     lp.palVersion             = 0x0300 ;
     lp.palNumEntries          = 1 ;
     lp.palPalEntry[0].peRed   = 255 ;
     lp.palPalEntry[0].peGreen = 255 ;
     lp.palPalEntry[0].peBlue  = 255 ;
     lp.palPalEntry[0].peFlags = PC_RESERVED ;
   
     hPalette = CreatePalette (&lp) ;
     
     SetTimer (hwnd, ID_TIMER, 50, NULL) ;
     return hPalette ;
}

void PaintRoutine (HDC hdc, int cxClient, int cyClient)
{
     static TCHAR szText [] = TEXT (" Fade In and Out ") ;
     int          x, y ;
     SIZE         sizeText ;

     SetTextColor (hdc, PALETTEINDEX (0)) ;
     GetTextExtentPoint32 (hdc, szText, lstrlen (szText), &sizeText) ;

     for (x = 0 ; x < cxClient ; x += sizeText.cx)
     for (y = 0 ; y < cyClient ; y += sizeText.cy)

     {
          TextOut (hdc, x, y, szText, lstrlen (szText)) ;
     }

     return ;
}

void TimerRoutine (HDC hdc, HPALETTE hPalette)
{
     static BOOL bFadeIn = TRUE ;

     if (bFadeIn)
     {
          lp.palPalEntry[0].peRed   -= 4 ;
          lp.palPalEntry[0].peGreen -= 4 ;

          if (lp.palPalEntry[0].peRed == 3)
               bFadeIn = FALSE ;
     }
     else
     {
          lp.palPalEntry[0].peRed   += 4 ;
          lp.palPalEntry[0].peGreen += 4 ;

          if (lp.palPalEntry[0].peRed == 255)
               bFadeIn = TRUE ;
     }

     AnimatePalette (hPalette, 0, 1, lp.palPalEntry) ;
     return ;
}

void DestroyRoutine (HWND hwnd, HPALETTE hPalette)
{
     KillTimer (hwnd, ID_TIMER) ;
     DeleteObject (hPalette) ;
     return ;
}

FADER displays the text string "Fade In And Out" all over its client area. This text is initially displayed in white and appears invisible against the white background of the window. By using palette animation, FADER gradually changes the color of the text to blue and then back to white, over and over again. The text appears as if it's fading in and out.

FADER creates a logical palette in its CreateRoutine function. It needs only one entry of the palette and initializes the color to white—red, green, and blue values all set to 255. In PaintRoutine (which, you'll recall, is called from PALANIM after the logical palette has been selected into the device context and realized), FADER calls SetTextColor to set the text color to PALETTEINDEX(0). This means that the text color is set to the first entry in the palette table, which initially is white. FADER then fills up its client area with the "Fade In And Out" text string. At this time, the window background is white and the text is white and hence invisible.

In the TimerRoutine function, FADER animates the palette by altering the PALETTEENTRY structure and passing it to AnimatePalette. The program initially decrements the red and green values by 4 for each WM_TIMER message until they reach a value of 3. Then the values are incremented by 4 until they get back up to 255. This causes the color of the text to fade from white to blue and back to white again.

The ALLCOLOR program shown in Figure 16-10 uses a single-entry logical palette to display all the colors that the video adapter can render. It doesn't show them simultaneously, of course, but sequentially. If your video adapter has an 18-bit resolution (in which case it's capable of 262,144 different colors), at the rate of one color every 55 milliseconds you need spend only four hours staring at the screen to see all the colors!

Figure 16-10. The ALLCOLOR program.

ALLCOLOR.C

/*-----------------------------------------
   ALLCOLOR.C -- Palette Animation Demo
                 (c) Charles Petzold, 1998
  -----------------------------------------*/

#include <windows.h>

#define ID_TIMER    1

TCHAR szAppName [] = TEXT ("AllColor") ;
TCHAR szTitle   [] = TEXT ("AllColor: Palette Animation Demo") ;

static int          iIncr ;
static PALETTEENTRY pe ;

HPALETTE CreateRoutine (HWND hwnd)
{
     HDC        hdc ;
     HPALETTE   hPalette ;
     LOGPALETTE lp ;

          // Determine the color resolution and set iIncr

     hdc = GetDC (hwnd) ;
     iIncr = 1 << (8 - GetDeviceCaps (hdc, COLORRES) / 3) ;
     ReleaseDC (hwnd, hdc) ;

          // Create the logical palette
     
     lp.palVersion             = 0x0300 ;
     lp.palNumEntries          = 1 ;
     lp.palPalEntry[0].peRed   = 0 ;
     lp.palPalEntry[0].peGreen = 0 ;
     lp.palPalEntry[0].peBlue  = 0 ;
     lp.palPalEntry[0].peFlags = PC_RESERVED ;
   
     hPalette = CreatePalette (&lp) ;

          // Save global for less typing

     pe = lp.palPalEntry[0] ;
     
     SetTimer (hwnd, ID_TIMER, 10, NULL) ;
     return hPalette ;
}

void DisplayRGB (HDC hdc, PALETTEENTRY * ppe)
{
     TCHAR szBuffer [16] ;

     wsprintf (szBuffer, TEXT (" %02X-%02X-%02X "),
               ppe->peRed, ppe->peGreen, ppe->peBlue) ;

     TextOut (hdc, 0, 0, szBuffer, lstrlen (szBuffer)) ;
}

void PaintRoutine (HDC hdc, int cxClient, int cyClient)
{
     HBRUSH   hBrush ;
     RECT     rect ;

          // Draw Palette Index 0 on entire window

     hBrush = CreateSolidBrush (PALETTEINDEX (0)) ;
     SetRect (&rect, 0, 0, cxClient, cyClient) ;
     FillRect (hdc, &rect, hBrush) ;
     DeleteObject (SelectObject (hdc, GetStockObject (WHITE_BRUSH))) ;

          // Display the RGB value

     DisplayRGB (hdc, &pe) ;
     return ;
}

void TimerRoutine (HDC hdc, HPALETTE hPalette)
{
     static BOOL  bRedUp = TRUE, bGreenUp = TRUE, bBlueUp = TRUE ;

          // Define new color value

     pe.peBlue += (bBlueUp ? iIncr : -iIncr) ;

     if (pe.peBlue == (BYTE) (bBlueUp ? 0 : 256 - iIncr))
     {
          pe.peBlue = (bBlueUp ? 256 - iIncr : 0) ;
          bBlueUp ^= TRUE ;
          pe.peGreen += (bGreenUp ? iIncr : -iIncr) ;

          if (pe.peGreen == (BYTE) (bGreenUp ? 0 : 256 - iIncr))
          {
               pe.peGreen = (bGreenUp ? 256 - iIncr : 0) ;
               bGreenUp ^= TRUE ;
               pe.peRed += (bRedUp ? iIncr : -iIncr) ;

               if (pe.peRed == (BYTE) (bRedUp ? 0 : 256 - iIncr))
               {
                    pe.peRed = (bRedUp ? 256 - iIncr : 0) ;
                    bRedUp ^= TRUE ;
               }
          }
     }

          // Animate the palette
     
     AnimatePalette (hPalette, 0, 1, &pe) ;
     DisplayRGB (hdc, &pe) ;
     return ;
}

void DestroyRoutine (HWND hwnd, HPALETTE hPalette)
{
     KillTimer (hwnd, ID_TIMER) ;
     DeleteObject (hPalette) ;
     return ;
}

Structurally, ALLCOLOR is very similar to FADER. In CreateRoutine, ALLCOLOR creates a palette with only one palette entry whose color is set to black (the red, green, and blue fields of the PALETTEENTRY structure set to 0). In PaintRoutine, ALLCOLOR creates a solid brush using PALETTEINDEX(0) and calls FillRect to color the entire client area with that brush.

In TimerRoutine, ALLCOLOR animates the palette by changing the PALETTEENTRY color and calling AnimatePalette. I wrote ALLCOLOR so that the change in color is smooth. First, the blue value is progressively incremented. When it gets to the maximum, the green value is incremented and then the blue value is progressively decremented. The incrementing and decrementing of the red, green, and blue color values is based on the iIncr variable. This is calculated during CreateRoutine based on the value returned from GetDeviceCaps with the COLORRES argument. If GetDeviceCaps returns 18, for example, then iIncr is set to 4—the lowest value necessary to obtain all the colors.

ALLCOLOR also displays the current RGB color value in the upper left corner of the client area. I originally added this code for testing purposes, but it proved to be useful so I left it in.

Engineering Applications

In engineering applications, animation can be useful for the display of mechanical or electrical processes. It's one thing to display a combustion engine on a computer screen, but animation can really make it come alive and show its workings with much greater clarity.

One possible process that's good for palette animation is showing fluids passing through a pipe. This is a case where the image doesn't have to be strictly accurate—in fact, if the image were accurate (as if you were looking at a transparent pipe), it might be difficult to tell how the contents of the pipe were moving. It's better to take a more symbolic approach here. The PIPES program shown in Figure 16-11 is a simple demonstration of this technique. It has two horizontal pipes in the client area. The contents of the pipes move from left to right in the top pipe and from right to left in the bottom pipe.

Figure 16-11. The PIPES program.

PIPES.C

/*--------------------------------------
   PIPES.C -- Palette Animation Demo
              (c) Charles Petzold, 1998
  --------------------------------------*/

#include <windows.h>

#define ID_TIMER 1

TCHAR szAppName [] = TEXT ("Pipes") ;
TCHAR szTitle   [] = TEXT ("Pipes: Palette Animation Demo") ;

static LOGPALETTE * plp ;

HPALETTE CreateRoutine (HWND hwnd)
{
     HPALETTE hPalette ;
     int      i ;

     plp = malloc (sizeof (LOGPALETTE) + 32 * sizeof (PALETTEENTRY)) ;
     
          // Initialize the fields of the LOGPALETTE structure
     
     plp->palVersion    = 0x300 ;
     plp->palNumEntries = 16 ;

     for (i = 0 ; i <= 8 ; i++)
     {
          plp->palPalEntry[i].peRed   = (BYTE) min (255, 0x20 * i) ;
          plp->palPalEntry[i].peGreen = 0 ;
          plp->palPalEntry[i].peBlue  = (BYTE) min (255, 0x20 * i) ;
          plp->palPalEntry[i].peFlags = PC_RESERVED ;

          plp->palPalEntry[16 - i] = plp->palPalEntry[i] ;
          plp->palPalEntry[16 + i] = plp->palPalEntry[i] ;
          plp->palPalEntry[32 - i] = plp->palPalEntry[i] ;
     }

     hPalette = CreatePalette (plp) ;
     
     SetTimer (hwnd, ID_TIMER, 100, NULL) ;
     return hPalette ;
}

void PaintRoutine (HDC hdc, int cxClient, int cyClient)
{
     HBRUSH hBrush ;
     int    i ;
     RECT   rect ;

          // Draw window background

     SetRect (&rect, 0, 0, cxClient, cyClient) ;
     hBrush = SelectObject (hdc, GetStockObject (WHITE_BRUSH)) ;
     FillRect (hdc, &rect, hBrush) ;
          // Draw the interiors of the pipes

     for (i = 0 ; i < 128 ; i++)
     {
          hBrush = CreateSolidBrush (PALETTEINDEX (i % 16)) ;
          SelectObject (hdc, hBrush) ;

          rect.left   = (127 - i) * cxClient / 128 ;
          rect.right  = (128 - i) * cxClient / 128 ;
          rect.top    = 4 * cyClient / 14 ;
          rect.bottom = 5 * cyClient / 14 ;

          FillRect (hdc, &rect, hBrush) ;

          rect.left   =  i      * cxClient / 128 ;
          rect.right  = (i + 1) * cxClient / 128 ;
          rect.top    =  9 * cyClient / 14 ;
          rect.bottom = 10 * cyClient / 14 ;

          FillRect (hdc, &rect, hBrush) ;

          DeleteObject (SelectObject (hdc, GetStockObject (WHITE_BRUSH))) ;
     }

          // Draw the edges of the pipes

     MoveToEx (hdc, 0,         4 * cyClient / 14, NULL) ;
     LineTo   (hdc, cxClient,  4 * cyClient / 14) ;

     MoveToEx (hdc, 0,         5 * cyClient / 14, NULL) ;
     LineTo   (hdc, cxClient,  5 * cyClient / 14) ;

     MoveToEx (hdc, 0,         9 * cyClient / 14, NULL) ;
     LineTo   (hdc, cxClient,  9 * cyClient / 14) ;

     MoveToEx (hdc, 0,        10 * cyClient / 14, NULL) ;
     LineTo   (hdc, cxClient, 10 * cyClient / 14) ;
     return ;
}

void TimerRoutine (HDC hdc, HPALETTE hPalette)
{
     static int iIndex ;

     AnimatePalette (hPalette, 0, 16, plp->palPalEntry + iIndex) ;
     iIndex = (iIndex + 1) % 16 ;

     return ;
}

void DestroyRoutine (HWND hwnd, HPALETTE hPalette)
{
     KillTimer (hwnd, ID_TIMER) ;
     DeleteObject (hPalette) ;
     free (plp) ;
     return ;
}

PIPES uses 16 palette entries for the animation, but you could probably get by with fewer. At the minimum, all you really need are enough entries to show the direction of the flow. Even three palette entries would be better than a static arrow.

The TUNNEL program shown in Figure 16-12 is the piggiest program of this batch, using 128 palette entries for animation. But the effect is worth it.

Figure 16-12. The TUNNEL program.

TUNNEL.C

/*---------------------------------------
   TUNNEL.C -- Palette Animation Demo
               (c) Charles Petzold, 1998
  ---------------------------------------*/

#include <windows.h>

#define ID_TIMER 1

TCHAR szAppName [] = TEXT ("Tunnel") ;
TCHAR szTitle   [] = TEXT ("Tunnel: Palette Animation Demo") ;

static LOGPALETTE * plp ;

HPALETTE CreateRoutine (HWND hwnd)
{
     BYTE     byGrayLevel ;
     HPALETTE hPalette ;
     int      i ;

     plp = malloc (sizeof (LOGPALETTE) + 255 * sizeof (PALETTEENTRY)) ;

          // Initialize the fields of the LOGPALETTE structure
     
     plp->palVersion    = 0x0300 ;
     plp->palNumEntries = 128 ;
     
     for (i = 0 ; i < 128 ; i++)
     {
          if (i < 64)
               byGrayLevel = (BYTE) (4 * i) ;
          else
               byGrayLevel = (BYTE) min (255, 4 * (128 - i)) ;
          
          plp->palPalEntry[i].peRed   = byGrayLevel ;
          plp->palPalEntry[i].peGreen = byGrayLevel ;
          plp->palPalEntry[i].peBlue  = byGrayLevel ;
          plp->palPalEntry[i].peFlags = PC_RESERVED ;
          
          plp->palPalEntry[i + 128].peRed   = byGrayLevel ;
          plp->palPalEntry[i + 128].peGreen = byGrayLevel ;
          plp->palPalEntry[i + 128].peBlue  = byGrayLevel ;
          plp->palPalEntry[i + 128].peFlags = PC_RESERVED ;
     }
   
     hPalette = CreatePalette (plp) ;
     
     SetTimer (hwnd, ID_TIMER, 50, NULL) ;
     return hPalette ;
}

void PaintRoutine (HDC hdc, int cxClient, int cyClient)
{
     HBRUSH hBrush ;
     int    i ;
     RECT   rect ;
     
     for (i = 0 ; i < 127 ; i++)
     {
               // Use a RECT structure for each of 128 rectangles
          
          rect.left   =            i * cxClient / 255 ;
          rect.top    =            i * cyClient / 255 ;
          rect.right  = cxClient - i * cxClient / 255 ;
          rect.bottom = cyClient - i * cyClient / 255 ;
          
          hBrush = CreateSolidBrush (PALETTEINDEX (i)) ;

               // Fill the rectangle and delete the brush
          
          FillRect (hdc, &rect, hBrush) ;
          DeleteObject (hBrush) ;
     }
     return ;
}

void TimerRoutine (HDC hdc, HPALETTE hPalette)
{
     static int iLevel ;

     iLevel = (iLevel + 1) % 128 ;

     AnimatePalette (hPalette, 0, 128, plp->palPalEntry + iLevel) ;
     return ;
}

void DestroyRoutine (HWND hwnd, HPALETTE hPalette)
{
     KillTimer (hwnd, ID_TIMER) ;
     DeleteObject (hPalette) ;
     free (plp) ;
     return ;
}

TUNNEL uses 64 moving gray shades in the 128 palette entries—from black to white and back to black—to give the effect of traveling through a tunnel.