Transparent animations on the desktop

Skip to main content (skip navigation menu)






Transparent animations on the desktop

 

Do you want to have an animated character move over the Windows desktop, or over any application's window without modifying the application? Sometimes you can get away with grabbing the screen, then doing the animation on that grabbed image; this is what some screen savers do. In case that you cannot grab the screen because the background may actually be dynamic (animated), you need either a "layered window" or a region attached to the window. Layered windows are a feature of Windows 2000 and Windows XP, window regions are available since Windows 95. This application note discusses both types, in the context of the EGI animation tool. At the end, this note presents an example that uses layered windows when available and window regions otherwise.

This application note refers to the EGI manual at several occasions. The EGI manual is included, in PDF format (Adobe Acrobat), in the evaluation version, which can downloaded from its own page.

Layered windows

Starting with Windows 2000, Microsoft Windows supports a transparent colour or an alpha channel for a window. Setting a transparent colour is a simple matter of setting WS_EX_LAYERED style and setting the colour with SetLayeredWindowAttributes().

If you use FlicCreateWindow() to play the animation in, it is usually more convenient to let the window made by FlicCreateWindow() call SetLayeredWindowAttributes(), as it checks for the availability of SetLayeredWindowAttributes(). To set the transparent colour, send the message FLIC_COMMAND with code FC_TRANSPARENT to the window. See the section on the FLIC_COMMAND message in the manual for details.

Another advantage of FLIC_COMMAND (with code FC_TRANSPARENT) is that it accepts the transparent colour in RGB, PALETTEINDEX or PALETTERGB formats; SetLayeredWindowAttributes() supports only the RGB format. Since many FLIC animations are in 256 colour mode, it is often convenient to specify a colour as a palette index.

Starting with version 4.0, EGI stores the transparent colour that was used to build the animation file. This colour value is particularly useful in combination with region masks. The colour is stored as a palette index for a 256-colour FLIC file and as an RGB colour for a HiColor FLIC file. The new parameter FLIC_PARAM_TRANSPCOLOR returns the transparent colour, if available. So basically, what we must do to play an animation with a transparent colour on Windows 2000 or Windows XP is:

  1. read the transparent colour from the FLIC file by calling FlicGetParam() with code FLIC_PARAM_TRANSPCOLOR
  2. set it by calling SendMessage() with message FLIC_COMMAND and code FC_TRANSPARENT

The EGI manual contains a listing of the "smallest EGI animation player" in C, "TINY.C" (by using the C++ class layer, you can make a smaller program still). The program in the manual plays a FLIC animation without any transparency. The example below adapts this little program to use a "layered window" with a transparency colour, if available. The text in bold face represents the changes from the original "TINY.C".

TINY.C with a layered window
    #include <windows.h>
    #include "eplay.h"

    int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                       LPSTR lpCmdLine, int nCmdShow)
    {
      LPFLIC lpFlic;
      int width, height;
      HWND hwnd;
      COLORREF color;

      if ((lpFlic=FlicOpenEx("fanni2.flc", 0, NULL)) != NULL) {

        width = FlicGetParam(lpFlic, FLIC_PARAM_WIDTH);
        height = FlicGetParam(lpFlic, FLIC_PARAM_HEIGHT);
        hwnd = FlicCreateWindow("", WS_POPUP | WS_VISIBLE,
                                0, 0, width, height, NULL, 0);

        /* check for a transparent colour; if present, set it */
        color = (COLORREF)FlicGetParam(lpFlic, FLIC_PARAM_TRANSPCOLOR);
        if (color != -1)
          SendMessage(hwnd, FLIC_COMMAND, FC_TRANSPARENT, color);

        SendMessage(hwnd, FLIC_COMMAND, FC_DRAGCLIENT, TRUE);
        FlicPlay(lpFlic, hwnd);

        MessageBox(NULL, "FLIC file now playing\nclick \"OK\" to stop.",
                   NULL, MB_OK);

        DestroyWindow(hwnd);

      } else {

        MessageBox(NULL, "Unable to open FLIC file", NULL, MB_OK);

      } /* if */
      return 0;
    }

The original TINY.C creates a window with a border. When using transparency, we do not wish to have a border around the animation, so in the call to FlicCreateWindow(), the style changes and the width and height parameters are no longer adjusted to include the size of the border. The most essential change is that the program subsequently queries the transparent colour defined for the animation and "activates" this transparent colour when it is set.

One more new feature in the new "TINY.C": the call to SendMessage() with code FLIC_COMMAND and parameter FC_DRAGCLIENT lets the user drag the animation by clicking anywhere inside the opaque area of the animation and dragging the animation to a new position. This feature (introduced with EGI 3.1) is not restricted to animations with region masks, but it is most useful here because these animation lack a window caption.

Note that this new version of "TINY.C" only plays the animation with transparency on Windows 2000 and Windows XP. On other versions of Microsoft Windows, the animation will play, but without transparency.

Window regions

Non-rectangular windows have been a feature of Microsoft Windows since Windows 95 and Windows NT 4.0. The concept is quite simple: you create a region of any complexity and then set it with the Win32 function SetWindowRgn(). The issues that are left to resolve are: how do I create a region that represents the opaque areas of my "sprite" in an efficient way, and how do I combine the frame updating with the region updating so that visual artefacts are avoided.

The answer to the first question is that the EGI animation compiler creates and stores the region masks in the animation (FLIC) file in a format that is both compact and efficient to extract. The "hard" work, acquiring the most efficient region that masks off the transparent areas of the frames, is done at the compilation stage, so the playback stage does not suffer. Note that "region masks" are a EGI feature as of version 3.1.

The second question can be answered in the same way: use the features of the EGI player. However, most users of the EGI toolkit use custom playback routines or special interfaces to the frame decoder in the EGI API. Therefore, the purpose of this application note is twofold: show how to use the EGI functions for this particular goal, and give background information on how window regions work.

I will first talk about how to compile an animation so that it includes region masks. Then, I extensively cover the topic of displaying (i.e. playing) the animation with its region masks. The displaying can be done in two ways: by EGI and by you, and both ways are described in this note. The coverage of windows regions finishes off with an assortment of titbits —which may, or may not, be of importance.

Creating an animation with region masks

To create an animation with region masks, include instructions similar to those below in the animation script:

Setting the region mask option
    setting(
        mask( region_mask
              transparent(246)
        )
    )

The transparent colour is any colour of your choosing, of course, and either in RGB format or as a palette index. The preceding example uses palette index 246 as the transparent colour. Starting with EGI 4.0, you may also use the alpha channel in the source images to create the mask from. When using region masks, you can only specify one transparent colour (in contrast to the case of multilevel masks).

Using FlicCreateWindow() to play an animation with region masks

The easiest way to handling region masks is probably to use the internal window that FlicCreateWindow() makes. The internal window already handles updating the window region and the frame contents and, additionally, FlicPlay() knows about windows made with FlicCreateWindow() and it automatically sets the appropriate FLIC parameters.

Below is (again) the "TINY.C" example from the EGI manual that is adapted for animations with region masks. When you compare this little program with the one in the manual, you may notice that this time, the window is created without border. This is an important observation: the frame is drawn into the client area of a window but the region of the window that you set with SetWindowRgn() is on the complete window. If the window does not have the same size as its client area (in other words, if the window has a caption, a menu or a border) the window region will not match the contents of the frame.

TINY with window regions
    #include <windows.h>
    #include "eplay.h"

    int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                       LPSTR lpCmdLine, int nCmdShow)
    {
      LPFLIC lpFlic;
      int width, height;
      HWND hwnd;

      if ((lpFlic=FlicOpenEx("fanni2.flc", 0, NULL)) != NULL) {

        width = FlicGetParam(lpFlic, FLIC_PARAM_WIDTH);
        height = FlicGetParam(lpFlic, FLIC_PARAM_HEIGHT);
        hwnd = FlicCreateWindow("", WS_POPUP | WS_VISIBLE,
                                0, 0, width, height, NULL, 0);
        SendMessage(hwnd, FLIC_COMMAND, FC_DRAGCLIENT, TRUE);
        FlicPlay(lpFlic, hwnd);

        MessageBox(NULL, "FLIC file now playing\nclick \"OK\" to stop.",
                   NULL, MB_OK);

        DestroyWindow(hwnd);

      } else {

        MessageBox(NULL, "Unable to open FLIC file", NULL, MB_OK);

      } /* if */
      return 0;
    }

The call to SendMessage() with code FLIC_COMMAND and parameter FC_DRAGCLIENT was already discussed earlier in the section on layered windows.

When you call FlicPlay(), the routine detects whether the window, whose handle you pass in, was created by FlicCreateWindow(). If this is the case, and if the animation contains region masks, FlicPlay() switched the frame notification on (look up FLIC_PARAM_FRNOTIFY in the manual) and frame redrawing off (FLIC_PARAM_DRAWFRAME).

In case you want to play an animation without applying the region masks that it contains, you must switch the above mentioned "automatic detection" off by sending the window a FC_RGNMASK command (see the manual, message FLIC_COMMAND) before playing the animation.

The window region internals

The EGI player API supports a new code for its function FlicGetData(): FLIC_DATA_RGNDATA. The pointer returned is a pointer to a RGNDATA structure with a layout as defined in the Microsoft Windows SDK documentation. Basically, it is a header followed by a long list of rectangles. You can pass this pointer directly to ExtCreateRegion(), which creates a region that you pass to SetWindowRgn.

Passing in a new region
    LPRGNDATA lpRgnData = FlicGetData(lpFlic, FLIC_DATA_RGNDATA);
    if (lpRgnData != NULL) {
      DWORD size = sizeof(RGNDATAHEADER) + (16 * lpRgnData->rdh.nCount);
      HRGN hrgn = ExtCreateRegion(NULL, size, lpRgnData);
      SetWindowRgn(hwnd, hrgn, TRUE);
    } /* if */

You need to execute the above snippet of code after a region changes. This can be detected with the values that FlicNextFrame() returns (you can get the same value with FlicGetParam() and code FLIC_PARAM_FRAMEBITS).

You cannot have FlicPlay() redraw the frames, because FlicPlay ignores region masks (it ignores the other mask types too, by the way). So must adjust the animation settings so that you will get a callback at the time each new frame is ready. This is exactly what you would also to for the other kinds of masks. That is, set the frame notification on and frame drawing of before calling FlicPlay(), as in:

Setting up for redrawing the frames
    if (FlicGetData(lpFlic, FLIC_DATA_RGNDATA) != NULL) {
      FlicSetParam(lpFlic, FLIC_PARAM_FRNOTIFY, TRUE);
      FlicSetParam(lpFlic, FLIC_PARAM_DRAWFRAME, FALSE);
    } /* if */
    FlicPlay(lpFlic, hwnd);

If you do a SetWindowRgn() on a window and the new region differs from the previous region, SetWindowRgn() issues a WM_PAINT message. The update region for the WM_PAINT is the difference between the new region and the previous region.

As side note: some tests that I did indicate that the WM_PAINT message is sent from inside SetWindowRgn(), but I have also seen the WM_PAINT message arrive immediately after SetWindowRgn() returned. That is, even if the message is sent after returning from SetWindowRgn(), it appears to have a higher priority than the usual WM_PAINT messages. Perhaps this is a scheduling phenomenon (EGI is a multi-threaded library). Nevertheless, it is still a good idea to force an immediate update with a call to UpdateWindow().

Apart from the area that needs refreshing due to the change of the region, you also need to update the areas of the frame that contain fresh data. After all, a new frame has been read in and it can contain a new image even if the outline (the region) did not change. Since there already is a WM_PAINT being generated (by SetWindowRgn()), I suggest that you invalidate the bounding box of changes in the frame immediately before calling SetWindowRgn(). The bounding box of changes between frames is given by FlicGetRect().

Window messages must be handled by the thread that created the window (this is a limitation/feature of Microsoft Windows). If you have set up a callback function (rather than a message) via FlicSetCallback(), this function is called on a second thread. Waiting for the WM_PAINT to be handled causes thread synchronization, which is precisely what you wanted to avoid by setting up a callback function. At first sight, setting the last parameter of SetWindowRgn() to FALSE would be appropriate, but this blocks the refreshing of all windows below the animation as well.

If you look up the SetWindowRgn() function in Microsoft's SDK documentation, you will read that after setting the region it is owned by the system and that you should no longer touch it; you are specifically told not to delete a region after passing it in to SetWindowRgn(). Deletion of the window region is handled by Windows itself, at an appropriate time. The documentation is right: do not delete the region after the call to SetWindowRgn(). The reason to bring this up is that a search on MSDN turns up source code of a few example routines that use SetWindowRgn() and that faultily delete the region after the call.

A final essential note (non-essential notes follow below) is that under Windows95/98 ExtCreateRegion() fails if the RGNDATA data structure is larger than 64 KiB, or a maximum of 4094 rectangles. The region functions are implemented on the 16-bit side of GDI in Windows95/98 and apparently this bug slipped through (and was left uncorrected) when adapting the ExtCreateRegion() function from the Windows NT source code. To avoid this upper limit, you can create multiple regions with ExtCreateRegion(), each using less than 4094 rectangles and then combine them with CombineRgn().

Miscellaneous window region notes

A region mask differs from the other two kinds of masks that EGI supports ("bitmap" and "multilevel" masks) in that it is no pixel map. Instead of having second "mask image" or "alpha channel" image, where the values of the pixels determine the opaqueness of the frame image, a region mask is a list of non-overlapping rectangles that, together, cover the opaque areas of the frame image. The format of the region mask is covered in detail in the FLIC file format description.

Every type of mask has its advantages and disadvantages. A region mask cannot indicate semi-transparent areas, for example, whereas multilevel masks can. Region masks are also computationally more expensive than bitmap masks. The prime feature of region masks is that the data structure maps to internal clip lists of the GUI sub-system in a fairly straightforward manner and that the computational expense is paid by the animation compiler (EGI) and the GUI, that is, Microsoft Windows.

Since Windows must update the contents below the animation, you do not fully control the screen update. In other words, you do not know exactly when the background is repainted and how quick. The application below the animation, if any, has an impact as well. So, as a general guideline, try to keep animations that you use this way fairly small (say 128 x 128 pixels) and try to minimize changes of the outline of the animation.

 

Again, referring to the Microsoft SDK documentation, when you look up the description of the RGNDATA structure, you come across the statement that: "The rectangles are sorted top to bottom, left to right. They do not overlap." Yes, er... is the structure such that the top edges are sorted top to bottom or that the bottom edges are sorted top to bottom? The reference keeps mum, and so does MSDN. You are on your own.

As it turns out, the rectangles in the structure are laid out such that no rectangle has a vertical span that partially overlaps the vertical span of any other rectangle. The key word in is "partially": the vertical span may overlap that of another rectangle in the region exactly, or it may be completely disjoint. For example, if you create a region from two rectangles as in the figure below:

source for the region
the region that Windows creates consists of the four rectangles below (and in that order):
resulting region

Only rectangles 2 and 3 have an overlapping vertical span (they do not overlap in their horizontal spans, and hence, they do not overlap, but their vertical spans do overlap). More importantly, the vertical spans of the rectangles 2 and 3 are exactly the same.

As you may appreciate, the remark on the sorting order is obvious given the extra constraint on vertical spans. The region format used Windows is also sub-optimal in the sense that the set of rectangles that the RGNDATA requires is potentially quite a bit larger than the minimal set of rectangles needed to describe the region. In our example, we grow from 2 rectangles to 4. Unfortunately, this also costs in performance: Microsoft Windows, as is typical for GUI or graphic subsystems, implements a region as a set of non-overlapping rectangles. Each "blit" is clipped against each rectangle in the set, so the larger the set is, the more performance suffers. Windows itself does not create a minimal set of rectangles, but what is worse: Windows NT does not accept regions created by a better implementation. Windows 95/98/ME is less strict, but since EGI is designed to run on both Windows NT/2000/XP and Windows 95/98/ME, EGI uses the "lowest common denominator", which is the sub-optimal format required by Windows NT.

Playing with either layered windows or windows regions

Often, you will want an animation to use layered windows if available and to use window regions (explained below) when running on older versions of Microsoft Windows. Obviously, the animation should contain the region data; see the section "Creating an animation with region masks" for details.

Using region windows is automatic for FlicCreateWindow(), so you need to switch it off when setting up for layered windows is successful. The example below is an adaptation of the TINY.C with a layered window code snippet that does so. The changes relative to this earlier example are in bold face.

TINY.C with either a layered window or window regions
    #include <windows.h>
    #include "eplay.h"

    int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                       LPSTR lpCmdLine, int nCmdShow)
    {
      LPFLIC lpFlic;
      int width, height;
      HWND hwnd;
      COLORREF color;

      if ((lpFlic=FlicOpenEx("fanni2.flc", 0, NULL)) != NULL) {

        width = FlicGetParam(lpFlic, FLIC_PARAM_WIDTH);
        height = FlicGetParam(lpFlic, FLIC_PARAM_HEIGHT);
        hwnd = FlicCreateWindow("", WS_POPUP | WS_VISIBLE,
                                0, 0, width, height, NULL, 0);
        /* check for a transparent colour; if present, set it */
        color = (COLORREF)FlicGetParam(lpFlic, FLIC_PARAM_TRANSPCOLOR);
        if (color != -1) {
          if (SendMessage(hwnd, FLIC_COMMAND, FC_TRANSPARENT, color)) {
            /* do not use region data */
            SendMessage(hwnd, FLIC_COMMAND, FC_RGNMASK, FALSE);
          } /* if */
        } /* if */

        SendMessage(hwnd, FLIC_COMMAND, FC_DRAGCLIENT, TRUE);
        FlicPlay(lpFlic, hwnd);

        MessageBox(NULL, "FLIC file now playing\nclick \"OK\" to stop.",
                   NULL, MB_OK);

        DestroyWindow(hwnd);

      } else {

        MessageBox(NULL, "Unable to open FLIC file", NULL, MB_OK);

      } /* if */
      return 0;
    }

And that's all there is to it.

End notes

EGI supports several mask types. This application note discusses region masks because these masks make a good fit with the Microsoft Windows function SetWindowRgn(). Sprite animation engines use bi-level mask bitmaps or a multilevel alpha channel. To interface with such libraries, EGI supports "bitmap masks" and "multilevel" masks in addition to region masks. An example animation that exploits the use of multilevel masks for (semi-transparent) halos and cast shadows, in combination with a sprite library, is the "Sisters" demo.