Easy Transparent Animations with the EGI Player

Skip to main content (skip navigation menu)






Easy Transparent Animations with the EGI Player

 

Playing frame-based animations with transparent regions is one of the main features of EGI, and the toolkit has several methods to achieve it: bitmap masks, layered windows, multi-level masks (alpha channels) and region masks. Except, perhaps, for layered windows, transparent drawing in EGI requires help from the application that calls into the EGI player DLL (the "host program"). Below is a short table with various options and trade-offs:

Technique Environment Difficulty of implementation Notes
layered windows Windows 2000 and Windows XP only easy, the OS does everything The animation runs in its own window; performance is not very high.
See the application note "Transparent animations on the desktop" for an implementation.
region masks & a regional window any 32-bit version of Microsoft Windows easy, the OS does almost all The animation runs in its own window; low performance.
See the application note "Transparent animations on the desktop" for an implementation.
bitmap masks, GDI only any version of Microsoft Windows hard The classical GDI "masked blitting" technique requires that you do everything yourself: from building the complete picture from the frame, the mask and the background picture to managing "memory" DCs to perform off-screen blitting.
The EGI player programs EGIRUN and PTEST use this technique.
DirectDraw DirectDraw (any version) moderate Blitting with a colour key is easy in DirectDraw, but managing the off-screen bitmaps is still your task.
See the application note "Using EGI with DirectDraw" for more on the DirectDraw interface.
DirectX overlays DirectX 5+ and a video card that supports overlays with colour keying in hardware moderate/hard After setting up DirectX overlays, everything is (almost) automatic. The difficulty, though, is to cope with the various levels and varieties of hardware support for overlays. (The DirectX HAL is a thin layer over the hardware and the DirectX HEL does not emulate overlay functionality if the hardware lacks it.)
multi-level masks & AniSprite AniSprite easy The AniSprite manual has an example.
region masks, GDI only any version of Microsoft Windows easy EGI does the transparent drawing for you, and helps you to draw the background without causing flicker.

There could be more lines in the above table: products like EGO for Windows has native support for FLIC animations with transparency and EGI has special support for Macromedia Authorware. As EGI gives you access to nearly all internal information and data, a "roll-your-own" solution is feasible as well.

This application note discusses the last technique in the table: transparent animations with region masks and nothing but GDI. It works with every version of Microsoft Windows (including 16-bit), has a moderate performance (not as good as DirectDraw or AniSprite, but better than layered windows) and it is fairly easy to implement. The basis for the example program is the simple EGI player developed in the application note "An EGI Player in C++"; here I will show only the modifications that we need to make for transparent playing.

The complete source code for this application note can be downloaded from a link at the end of this article.

Loading and displaying the background image

To load and display a background image, various parts of the original program (see the application note "An EGI Player in C++") change: from the resource file (added menu) to the Cls_OnPaint() function which must display the background image.

I am skipping the step that shows you how to add yet another menu option to the menu resource, as this part is easily looked up in the original program. To load a bitmap image, I added a case to the Cls_OnCommand() function that first shows a common file open dialog and then calls LoadImage(). Outside the function, I declared the global variable hbmpBackground that keeps the handle to the image. Since this is a bitmap "handle" instead of an object (with a destructors), the program has to clean up this bitmap explicitly, both when replacing it with a different bitmap and when closing the program.

Cls_OnCommand() with the new IDM_OPENBKGND case
static HBITMAP hbmpBackground = NULL;

void Cls_OnCommand(HWND hwnd, int id, HWND /*hwndCtl*/, UINT /*codeNotify*/)
{
  switch (id) {

  (other cases ...)

  case IDM_OPENBKGND:
    { /* local */
      OPENFILENAME ofn = { 0 };
      char Filename[MAX_PATH] = "";
      ofn.lStructSize  = sizeof ofn;
      ofn.hwndOwner    = hwnd;
      ofn.lpstrFilter  = "Bitmap files\0*.bmp;\0";
      ofn.nFilterIndex = 1;
      ofn.lpstrFile    = Filename;
      ofn.nMaxFile     = sizeof Filename;
      ofn.Flags        = OFN_FILEMUSTEXIST;
      if (GetOpenFileName(&ofn)) {
        if (hbmpBackground)     /* delete current bitmap (if any) */
          DeleteObject(hbmpBackground);
        hbmpBackground = (HBITMAP)LoadImage(NULL, Filename, IMAGE_BITMAP, 0, 0,
                                            LR_LOADFROMFILE);
        if (hbmpBackground)
          InvalidateRect(hwnd, NULL, FALSE);
        else
          MessageBox(hwnd,
                     "Unable to open the requested file (probably due to an "
                     "invalid or unsupported file format).",
                     szAppName, MB_OK | MB_ICONERROR);
      } /* if */
    } /* local */
    break;
  } /* switch */
}

To display the background image, I modified the Cls_OnPaint() function. The changes from the original program are in bold face:

Cls_OnPaint() modifications
void Cls_OnPaint(HWND hwnd)
{
  PAINTSTRUCT PaintStruct;
  BeginPaint(hwnd, &PaintStruct);
  if (hbmpBackground) {
    RECT rect;
    GetClientRect(hwnd, &rect);
    TileImage(PaintStruct.hdc, hbmpBackground, rect.right, rect.bottom);
  } /* if */
  if (Anim)
    Anim.Paint(PaintStruct.hdc,
               FLIC_FLAG_PAINTALL | FLIC_FLAG_CLIPREGION);
  EndPaint(hwnd, &PaintStruct);
}

As you can see, the hard work for displaying the background image is done in the new function TileImage(), printed below:

TileImage()
void TileImage(HDC hdc, HBITMAP hbmp, int width, int height)
{
  int x, y;
  BITMAP bm;
  HDC hdcMem;

  GetObject(hbmp, sizeof bm, (LPSTR)&bm);
  hdcMem = CreateCompatibleDC(hdc);
  hbmp = (HBITMAP)SelectObject(hdcMem, hbmp);

  for (y = 0; y < height; y += bm.bmHeight)
    for (x = 0; x < width; x += bm.bmWidth)
      BitBlt(hdc, x, y, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY);

  SelectObject(hdcMem, hbmp);
  DeleteObject(hdcMem);
}

Setting the "transparent painting" option

The other change in the Cls_OnPaint() function are the two flags in the second parameter of the call to FlicAnim::Paint(). In fact, the first of these two, FLIC_FLAG_PAINTALL is the default value for that second parameter, so up to now it was passed implicitly to FlicAnim::Paint(). The new flag, FLIC_FLAG_CLIPREGION makes FlicAnim::Paint() use the region masks in the animation to "clip off" any transparent areas in each frame during the blitting operation.

For this to have any effect, we must also make a FLIC file with region masks. This is covered in the article "Transparent animations on the desktop", and more extensively in the EGI manual.

Screen grab of the player With these modification, after we load a background image and an animation (with a region mask), we will get the picture at the right. It looks okay on first sight, but as soon as we start playing the animation, the EGI player ignores the region masks and shows the white rectangular box around the Fanni puppet (in my particular case) for every frame. The cause of this (undesirable) behaviour is that the FLIC_FLAG_CLIPREGION option is only effective when you call FlicAnim::Paint() (or the plain API function FlicPaint()) from your program. The fix, which we will see in a moment, is to make the EGI engine route the frame redrawing through our custom Cls_OnPaint() function.

First, though, some thoughts on the why of this design.

On each new frame, the opaque parts of that frame must be drawn. When the outline of a frame changes from the previous frame, portions of the background image must be restored too. The EGI player engine does not make any attempt to restore the background because it does not known how to do this: —although I have put a simple bitmap below it at the moment, it background (i.e., everything that is below the FLIC animation) might be dynamic too. Your application, on the other hand, knows what should be in the background, and it also already contains the code to paint it all: the handler of the WM_PAINT event. The general idea is then to have the EGI engine invoke the WM_PAINT handler for each new frame. For animations that you want to play without transparency, this is overkill, though, so the EGI engine does not have it set by default.

Screen grab of the player To recapitulate, when you open a FLIC file, there is a call to InvalidateRect(), which causes Cls_OnPaint() to be invoked, which calls FlicAnim::Paint(). This is why the first frame is okay. When you start playing the animation, with FlicAnim::Play(), EGI draws the frames itself, bypassing the application-specific WM_PAINT handler or xxx_OnPaint() event function. So our program never gets the chance to call FlicAnim::Paint() with the FLIC_FLAG_CLIPREGION flag set. The net result is a picture similar as the one on the right.

Intercepting frame drawing

To intercept frame drawing, we must ask for the EGI engine to send our program a notification message for each frame that is decoded. In addition, we must also tell the EGI engine not to draw the frames onto the display itself. You use FlicAnim::SetParam() for both these settings. In this example, I issue the settings right after loading a FLIC file, if the FLIC file contains region masks. Below is the snippet for opening a FLIC file with the modifications in bold face:

Cls_OnCommand() modifications in IDM_OPEN
  case IDM_OPEN:
    { /* local */
      OPENFILENAME ofn = { 0 };
      char Filename[MAX_PATH] = "";
      ofn.lStructSize  = sizeof ofn;
      ofn.hwndOwner    = hwnd;
      ofn.lpstrFilter  = "FLIC files\0*.flc;*.fli;*.flx\0";
      ofn.nFilterIndex = 1;
      ofn.lpstrFile    = Filename;
      ofn.nMaxFile     = sizeof Filename;
      ofn.Flags        = OFN_FILEMUSTEXIST;
      if (GetOpenFileName(&ofn)) {
        Anim.Open(Filename);
        if (Anim) {
          Anim.NextFrame();
          InvalidateRect(hwnd, NULL, FALSE);
          if (hbmpBackground && Anim.GetData(FLIC_DATA_RGNDATA)) {
            Anim.SetParam(FLIC_PARAM_DRAWFRAME, FALSE);
            Anim.SetParam(FLIC_PARAM_FRNOTIFY, TRUE);
          } /* if */
        } else
          MessageBox(hwnd,
                     "Unable to open the requested file (probably due to an "
                     "invalid or unsupported file format).",
                     szAppName, MB_OK | MB_ICONERROR);
      } /* if */
    } /* local */
    break;

The next step is to create an event function for the message that the EGI engine will now start sending our program for every frame, and react on it. The window function gets an extra HANDLE_MSG() line (modifications in bold face):

Modifications in WndProc()
LRESULT CALLBACK WndProc(HWND hwnd, UINT Message,
                         WPARAM wParam, LPARAM lParam )
{
  switch (Message) {
    HANDLE_MSG(hwnd, WM_COMMAND,         Cls_OnCommand);
    HANDLE_MSG(hwnd, WM_DESTROY,         Cls_OnDestroy);
    HANDLE_MSG(hwnd, WM_PAINT,           Cls_OnPaint);
    HANDLE_MSG(hwnd, WM_PALETTECHANGED,  Cls_OnPaletteChanged);
    HANDLE_MSG(hwnd, WM_QUERYNEWPALETTE, Cls_OnQueryNewPalette);
    HANDLE_MSG(hwnd, FLIC_NOTIFY,        Cls_OnFlicNotify);
  default:
    return DefWindowProc(hwnd, Message, wParam, lParam);
  } /* switch */
}

An then there is the implementation of Cls_OnFlicNotify():

Cls_OnFlicNotify()
void Cls_OnFlicNotify(HWND hwnd, int /*id*/, UINT codeNotify, UINT /*codeParam*/)
{
  switch (codeNotify) {
  case FN_FRAME:
    InvalidateRect(hwnd, NULL, FALSE);
    UpdateWindow(hwnd);
    break;
  } /* switch */
}

Removing flicker

So far, our player can correctly play back animations of a background that our program paints onto the display just before painting the animation. There is one thing to solve left: flicker. As it is right now, the animation is not as stable as it should be, because the drawing of the tiled background image erases the foreground (frame) image, just before that frame image is redrawn. This tiny period that the frame image is erased from the screen, causes the flicker effect.

The standard advice to avoid flicker in animation is to perform off-screen rendering —sometimes also called "double-buffering". You build up the image in memory, where whatever erasures you do are invisible, and then "blit" the final image to the display in a single operation. With DirectDraw and a few other techniques, instead of blit'ing, you can "flip" the off-screen image with the displayed image, which is faster.

Off-screen rendering is an option with EGI, (it gives you access to all the data that you need to perform it), but you have to manage the off-screen images yourself. There is an easier option: re-use the region mask. The FlicAnim::Paint() method limits painting outside the opaque areas of the frame by applying the region mask as a clip region. When the program tiles the background image, it can avoid drawing inside the opaque areas of the frame by setting a clip region that is the inverse of the region mask. Below is the adjusted Cls_OnPaint() function with the modifications in bold face:

Cls_OnPaint() without flicker
void Cls_OnPaint(HWND hwnd)
{
  PAINTSTRUCT PaintStruct;
  BeginPaint(hwnd, &PaintStruct);
  if (hbmpBackground) {
    RECT rect;
    GetClientRect(hwnd, &rect);
    SaveDC(PaintStruct.hdc);
    if (Anim)
      ExtSelectClipRgn(PaintStruct.hdc,
                      (HRGN)Anim.GetHandle(NULL, FLIC_HANDLE_RGN), RGN_DIFF);
    TileImage(PaintStruct.hdc, hbmpBackground, rect.right, rect.bottom);
    RestoreDC(PaintStruct.hdc, -1);
  } /* if */
  if (Anim)
    Anim.Paint(PaintStruct.hdc,
               FLIC_FLAG_PAINTALL | FLIC_FLAG_CLIPREGION);
  EndPaint(hwnd, &PaintStruct);
}

Note that the region handle returned by FlicAnim::GetHandle() is owned by the EGI player engine. You should not modify this region and you should not delete it. The calls to SaveDC() and RestoreDC() are necessary to make it possible for FlicAnim::Paint() to draw inside the opaque areas of the frame; FlicAnim::Paint() combines the clipping area with its region mask (if FLIC_FLAG_CLIPREGION is set), it does not replace it. This means that if the HDC has the inverse of the region mask set as its clipping region and you call FlicAnim::Paint() with FLIC_FLAG_CLIPREGION, the combined region will be the empty region, and nothing will show.

As an aside, instead of saving and restoring the display context, you can also explicitly remove the clipping region with a call like:

SelectClipRgn(PaintStruct.hdc, NULL);

Compiling and linking

The player developed so far has little requirements and/or dependencies on a particular compiler. Specifically, it does not use MFC or another object library. As a result, compiling it is fairly simple. With the source code for this sample, you will find three batch files:

Batch file Use for
build_vc.bat    For Microsoft Visual C/C++ 6.0 or higher
build_bcc.bat    For Borland C++ 5.x or or Borland C++ Builder
build_wcl.bat    For Watcom C/C++ 11.0 or OpenWatcom

If you are using another C++ compiler than the ones listed, you are probably able to adapt one of these batch files to suit your compiler. As you may verify, the batch files contain quite simple commands. For example, the batch file for Microsoft Visual C/C++ contains the instructions:

rc egiplay.rc
cl -I..\..\include egiplay.cpp egiplay.res ..\..\lib\eplay32m.lib

End notes

EGI supports notification messages, which are sent to a window function, and callback functions. This example used messages, as it did not set a callback function. A callback function gives higher performance, by bypassing the Windows message processing subsystem and by avoiding the thread synchronization that occurs during message processing.

There are more optimization opportunities in this program. For example, instead of making Cls_OnFlicNotify() invoke (or cause an invocation of) Cls_OnPaint() which then draws the background and the frame, you could instead do the actual drawing in a separate function, e.g. DisplayAll() and call this function from both Cls_OnPaint() and Cls_OnFlicNotify(). This saves and intermediate step through the Windows event dispatcher.

Although the amount of code to get animations play with transparency may seem impressive at first sight, note that most of the code existed already; it was only shown as a context for the modifications. Also, most of the new code is involved with loading and displaying the background image, and unrelated to the EGI engine. The number of lines specific to playing transparent animations in EGI is 22, which is not much, really.

Downloads

To run this example, you also need the development files of the EGI animation engine. The EGI evaluation version will do fine.