Simple Animation Techniques: Using Bitmaps and Timers

By Kelvin R. Lawrence

I have had the opportunity to attend numerous seminars and conferences to present programming lectures and workshops about one of my favorite OS/2 topics: graphical programming using the features of the OS/2 Graphical Programming Interface (GPI). I also try to keep up with the activity on the OS/2 forums related to Presentation Manager (PM) programming on CompuServe and other on-line services. I try very hard to factor the feedback into future GPI classes and articles such as this

Many of the questions concern the use and exploitation of bitmaps in GPI applications. This article addresses one of several bitmap related topics that we receive frequent questions about and provides sample code for as well. That topic is simple animation using bitmaps and timers. We will reference sample applications as we go through this examination of animation techniques. The code for these sample applications is on disc 2 of the Developer Connection OS/2 CD-ROM. Look for the animate1 and animate2 directories in source\devnews\vol12.

What is Meant By Animation?
For the purpose of this discussion, animation refers to creating an effect such that to the user of the application it appears that something on the screen is actually moving, (i.e., it is animated). The way we typically achieve animation is to display a series of bitmaps or frames one after the other with an appropriate time delay between each one. Each bitmap in the sequence would show the next step in the animation. This is really the computer equivalent of the jumping stick man that children often create. Each step of the stick man character jumping is drawn on a separate piece of paper. The animation is achieved by placing all of the pages on top of each other in the correct order and quickly flicking through the pages. The only difference in the computer version is that each page can be represented using a bitmap and instead of turning the page we replace the bitmap with the next one in the sequence periodically, such as every so many milliseconds.

PM Timers
A key part of programming that requires some amount of animation is getting notified at regular intervals when it is time to display the next frame in the animation. You can do this using PM timers. In case you are not familiar with how PM timers work, here is a very quick recap. A program can have many different timers each going off at different intervals. For the purpose of this discussion, each of the sample applications only requires one timer. A timer is created/started, using the WinStartTimer. For example the following code segment creates a timer with an id of 99 to go off every ten milliseconds. This example assumes that the anchor block and client window handle are stored in the variables hab and hwndClient. WinStartTimer( hab, hwndClient, ID_TIMER, 10 ); Each time the timer goes off, the application receives a WM_TIMER message. Because there may be more than one timer in use by an application, the id of the timer that has just gone off is passed to the application in the low word of the first message parameter for the WM_PAINT message. In the following example taken from the window procedure of the animate1.exe sample, a test is made on the message parameter mp1 to see if the timer is the one we want. case WM_TIMER: { if ( SHORT1FROMMP(mp1) == ID_TIMER ) { // Do something ... } } Now that we have a mechanism for generating our timer tick, often referred to as the frame rate when working with animation, we can concentrate on the code to actually draw some animated frames. I am going to describe two similar, but subtly different ways to actually display the animated frames. The first involves having one bitmap for each frame in the animation and simply replacing bitmaps one-by-one as the timer goes off. The second technique shows how the animation can be performed by having just one bitmap. However, in reality, the bitmap contains all of the animation frames and as each frame is the same size we can easily step through them one-by-one only displaying the relevant portion of the bitmap each time.
 * 1) define ID_TIMER 99

Technique 1 - One Frame Per Bitmap, Multiple Bitmaps
For our first example, we are going to create an application called animate1.exe. A sample program header and main routine for animate1 are shown in Source Code 3. This program uses sixteen bitmaps (or frames) to create its animation. The bitmaps exist on disk in files named frame1.bmp, frame2.bmp, frame3.bmp etc. up to frame16.bmp. These files are bound to the application's resources via the resource (.rc) file animate1.rc as shown in Source Code 1. The constant definitions used in the resource file are defined in the header file animate1.h as shown in Source Code 2. MENU ID_PROG PRELOAD BEGIN SUBMENU "Exit", -1 BEGIN MENUITEM "~Exit now", IDM_EXIT, MIS_TEXT, 0 MENUITEM "~Resume", IDM_RESUME, MIS_TEXT, 0 END END BITMAP ID_BITMAP01 "frame1.bmp" BITMAP ID_BITMAP02 "frame2.bmp" BITMAP ID_BITMAP03 "frame3.bmp" BITMAP ID_BITMAP04 "frame4.bmp" BITMAP ID_BITMAP05 "frame5.bmp" BITMAP ID_BITMAP06 "frame6.bmp" BITMAP ID_BITMAP07 "frame7.bmp" BITMAP ID_BITMAP08 "frame8.bmp" BITMAP ID_BITMAP09 "frame9.bmp" BITMAP ID_BITMAP10 "frame10.bmp" BITMAP ID_BITMAP11 "frame11.bmp" BITMAP ID_BITMAP12 "frame12.bmp" BITMAP ID_BITMAP13 "frame13.bmp" BITMAP ID_BITMAP14 "frame14.bmp" BITMAP ID_BITMAP15 "frame15.bmp" BITMAP ID_BITMAP16 "frame16.bmp" ''Source Code 1. String of animate1.rc. Resource definitions for the animate1 application.'' ''Source Code 2. The animate1.h file provides global constant definitions.''
 * 1) define INCL_WIN
 * 2) include 
 * 3) include "animate1.h"
 * 1) define ID_PROG 1
 * 2) define IDM_EXIT 100
 * 3) define IDM_RESUME 105
 * 4) define ID_TIMER 10
 * 5) define ID_BITMAP01 801
 * 6) define ID_BITMAP02 802
 * 7) define ID_BITMAP03 803
 * 8) define ID_BITMAP04 804
 * 9) define ID_BITMAP05 805
 * 10) define ID_BITMAP06 806
 * 11) define ID_BITMAP07 807
 * 12) define ID_BITMAP08 808
 * 13) define ID_BITMAP09 809
 * 14) define ID_BITMAP10 810
 * 15) define ID_BITMAP11 811
 * 16) define ID_BITMAP12 812
 * 17) define ID_BITMAP13 813
 * 18) define ID_BITMAP14 814
 * 19) define ID_BITMAP15 815
 * 20) define ID_BITMAP16 816

The next sample, animate1.c, shows how a series of similar bitmaps can be used to create the illusion of animation when combined with a timer. This program loads a series of bitmaps and starts a timer. Each time the timer "ticks" a new bitmap is drawn in the same location as the previous one, giving the impression that objects are moving.

The GPI functions used are: The related functions used are: // Global handles HAB hab ; HWND hwndFrame ; HWND hwndMenu ; HPS hpsMemory ; // Memory presentation space HDC hdcMemory ; // Memory device context HDC hdcScreen ; // Screen device context for our window // Global variables and arrays HBITMAP abmph[NUM_BITMAPS] ; LONG idBitmap = 0 ; // Local function prototype declarations MRESULT EXPENTRY ClientWndProc( HWND hwnd ,USHORT msg ,MPARAM mp1 ,MPARAM mp2 ); VOID LoadBitmaps( HWND hwnd ); VOID DisplayNextBitmap( HWND hwnd, LONG idBitmap ); // main procedure. // _ Initialize PM for this process // _ Create our message queue // _ Create frame and client windows // _ Show the window // _ Enter our message dispatching loop int cdecl main(VOID) { HMQ hmq; HWND hwndClient; QMSG qmsg; ULONG flCreateFlags = FCF_BORDER   | FCF_TASKLIST  | FCF_TITLEBAR| FCF_SYSMENU | FCF_SIZEBORDER | FCF_MINMAX | FCF_MENU ; hab = WinInitialize( (USHORT)NULL ); hmq=WinCreateMsgQueue( hab,0 ); WinRegisterClass( hab  , CLASSNAME    , (PFNWP)ClientWndProc    , (ULONG)CS_SIZEREDRAW    , (USHORT)256 ); hwndFrame = WinCreateStdWindow( HWND_DESKTOP    , 0UL      , &flCreateFlags      , CLASSNAME      , TITLEBARTEXT      , WS_VISIBLE      , (HMODULE)0      , ID_PROG      , &hwndClient ); hwndMenu = WinWindowFromID( hwndFrame, FID_MENU ); WinShowWindow( hwndFrame, TRUE ); WinStartTimer( hab, hwndClient, ID_TIMER, 10 ); LoadBitmaps( hwndClient ); // Start message loop while ( WinGetMsg( hab,&qmsg, (HWND)0, 0, 0 ) ) { WinDispatchMsg( hab,&qmsg ); } // Clean up resources before we terminate. if ( hpsMemory ) { LONG i; GpiSetBitmap( hpsMemory, 0 ); for ( i=0; i NUM_BITMAPS; i++ ) { GpiDeleteBitmap( abmph[i] ); } GpiDestroyPS( hpsMemory ); } if ( hdcMemory ) { DevCloseDC( hdcMemory ); } if ( hwndFrame ) { WinDestroyWindow(hwndFrame); WinDestroyMsgQueue(hmq); WinTerminate(hab); } return 0; } ''Source Code 3. The program header and main routine for animate1.'' // Client window procedure. MRESULT EXPENTRY ClientWndProc(HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { switch (msg) { // WM_ACTIVATE // Force a complete repaint when we get the focus case WM_ACTIVATE: {   if ( SHORT1FROMMP( mp1 ) ) {     WinInvalidateRect( hwnd, 0L, FALSE ); } }  break ; // WM_COMMAND // Process the users menu choices case WM_COMMAND: {    switch( SHORT1FROMMP( mp1 ) ) {     case IDM_EXIT: {       WinPostMsg( hwnd, WM_CLOSE, (MPARAM)0L, (MPARAM)0L ); }     break; } }  break; // WM_PAINT // Process paint requests case WM_PAINT: {   HPS hpsPaint ; RECTL rectl; hpsPaint = WinBeginPaint( hwnd, (HPS)0, &rectl ); WinQueryWindowRect( hwnd, &rectl ); WinFillRect( hpsPaint, &rectl, CLR_BLACK ); WinEndPaint( hpsPaint ); } break; // WM_TIMER // Display the next bitmap in the sequence case WM_TIMER: {    DisplayNextBitmap( hwnd, idBitmap ); idBitmap++ ; if ( idBitmap = NUM_BITMAPS ) {   idBitmap = 0 ; }  }   break ; // default case // Let PM handle messages that we are not interested in  default: {  return( WinDefWindowProc( hwnd, msg, mp1, mp2 )) ; } } } ''Source Code 4. The animate1 application window procedure.''
 * GpiLoadBitmap
 * GpiSetBitmap
 * GpiBitBlt
 * GpiQueryBitmapInfoHeader
 * GpiDeleteBitmap
 * GpiCreatePS
 * GpiDestroyPS
 * DevOpenDC
 * DevCloseDC
 * WinQueryWindowDC
 * WinOpenWindowDC
 * WinQueryAnchorBlock
 * 1) define INCL_32
 * 2) define INCL_GPI
 * 3) define INCL_WIN
 * 4) include 
 * 5) include 
 * 6) include "animate1.h"
 * 7) define TITLEBARTEXT "animate1 - Using bitmaps and timers"
 * 8) define CLASSNAME "animate1"
 * 1) define NUM_BITMAPS 16

When the application is first started, before any windows are displayed, a ten millisecond timer is initialized using WinStartTimer. Once the timer is running, the next step is to load all the bitmaps into memory from the application's resources. To simplify this, I wrote the function LoadBitmaps shown in Source Code 5. The LoadBitmaps function creates a memory presentation space and then loads each bitmap in turn into memory using the GpiLoadBitmap function. The result is the bitmaps now are effectively cached; that is we just load them once at the start of the program and keep them around for the life of the application. GpiLoadBitmap returns to the caller a handle for the bitmap that it just loaded. These handles are stored in an array, abmph, so that when we need to start drawing bitmaps later we can just select bitmap handles from the array. Note that GpiLoadBitmap can only load bitmaps from the application's resources and cannot load .BMP files directly from the disk. Binding the bitmaps into the .RC file keeps this aspect of the program simple. Currently OS/2 does not provide a function to load bitmaps from .BMP files, but writing a function which does this is not difficult. // LoadBitmaps // Load a series of bitmaps from our resource table. VOID LoadBitmaps( HWND hwnd ) { SIZEL      sizl;  // Size of PS when created (zero = default) HAB        hab;  // Application anchor bock LONG       i;    // Loop counter DEVOPENSTRUC dev; // DevOpenDC parameters // Query the anchor block for our window and get a DC for it hab = WinQueryAnchorBlock( hwnd ); if ( !(hdcScreen = WinQueryWindowDC( hwnd )) ) { hdcScreen = WinOpenWindowDC( hwnd ) ; } // Create a memory DC that is compatible with the window DC memset( &dev, 0, sizeof(DEVOPENSTRUC) ); dev.pszDriverName = "DISPLAY" ; hdcMemory = DevOpenDC( hab   , OD_MEMORY     , "*"     , 2L     , (PDEVOPENDATA)&dev     , hdcScreen ); // Create micro PS and associate with memory dc sizl.cx = sizl.cy = 0; hpsMemory = GpiCreatePS( hab  , hdcMemory    , &sizl    , PU_PELS | GPIA_ASSOC | GPIT_MICRO ); // Load the bitmaps we intend to use from our resource file. for ( i = ID_BITMAP01; i = ID_BITMAP16; i++ ) { abmph[i-ID_BITMAP01] = GpiLoadBitmap ( hpsMemory  // PS for load   , (HMODULE) 0 // Module containing bitmap (0=.EXE)    , i          // Id of the bitmap to load    , 0          // Load at original width    , 0           // Load at original heaight   ); } } ''Source Code 5. Code sample showing the LoadBitmaps function.''

Each time the WM_TIMER message comes in the application needs to display the next bitmap in the sequence. This is done by calling the function DisplayNextBitmap and passing as one of the parameters the index into the array of bitmaps of the bitmap to be displayed. The code that handles the WM_TIMER processing also resets the count of which bitmap to display next back to zero, if all of the bitmaps have been displayed. In other words, the application displays the same sixteen frames over and over again to give the impression of continuous animation. The DisplayNextBitmap function, as shown in Source Code 6, is very simple. It selects the required bitmap from the cached array of bitmap handles into a memory presentation space and then uses GpiBitBlt to display the bitmap. To make things a little more interesting, I set up the call so that the bitmap is stretched to twice its normal size before it is displayed.

If you have ever used the IBM WebExplorer shipped with OS/2 to browse the World Wide Web, then the animation sequence will look familiar to you when you run the animate1.exe program. // DiaplayNextBitmap // Display the next bitmap in the series. VOID DisplayNextBitmap( HWND hwnd, LONG idBitmap ) { POINTL aptl[5] ;       // Points used to perform blt BITMAPINFOHEADER2 bmp2 ; // Describes our bitmap LONG i,j,xDelta,yDelta ; // Loop counters and increments LONG lWidth,lHeight ;   // Width and height of loaded bitmap HPS hpsWindow ; // Query information about the bitmap so that we can obtain its // width and height. memset( &bmp2, 0, sizeof( BITMAPINFOHEADER2 ) ); bmp2.cbFix = sizeof( BITMAPINFOHEADER2 ) ; GpiQueryBitmapInfoHeader( abmph[idBitmap], &bmp2 ); lWidth = bmp2.cx ; lHeight = bmp2.cy ; // Select bitmap into memory PS GpiSetBitmap( hpsMemory, abmph[idBitmap] ); // Set up the initial coordinates for our blt. We have to provide // both coordinates of the target rectangle and the source as we // are going to double the size of the source bitmap when we do the BLT. aptl[0].x = 0 ;    // Lower left x of target bitmap (the screen) aptl[0].y = 0 ;    // Lower left y of target bitmap (the screen) aptl[1].x = lWidth*2 ;// Upper right x of target bitmap (the screen) aptl[1].y = lHeight*2;// Upper right y of target bitmap (the screen) aptl[2].x = 0 ;    // Lower left of x source (the memory bitmap) aptl[2].y = 0 ;    // Lower left of y source (the memory bitmap) aptl[3].x = lWidth ; // Upper right x of source aptl[3].y = lHeight ;// Upper right y of source // Now blt the bitmap to the screen hpsWindow = WinGetPS( hwnd ); GpiBitBlt( hpsWindow // Target PS   , hpsMemory    // Source PS     , 4           // Number of points in point array     , aptl         // Source and target coordinates for the BLT     , ROP_SRCCOPY  // Mix mode for BLT     , 0          // Options bits     ); WinReleasePS( hpsWindow ); } ''Source Code 6. Code sample for the DisplayNextBitmap function as used in animate1.''

Technique 2 - Multiple Frames in a Single Bitmap
An alternative approach to using multiple bitmaps, one per frame, to achieve animation, is to combine many frames together in a single bitmap. Consider a roll of film where each frame is physically located right beside the next one in the sequence. We can use this same technique when animating bitmaps. If the bitmaps are fairly large, then using single bitmaps or moving to one of the more multimedia-like file formats is probably the way to go. However, for animating a series of small size bitmaps the multi-frame approach works well. In effect, what you create is one bitmap file that contains all of the frames for the animation. As long as the size of each frame is the same, we can easily calculate which part of the bitmap represents the frame we need to display next and iterate through the bitmap displaying one frame after the other. One advantage of this approach is that we only have to bind one bitmap into our application's resources and keep only one bitmap in memory, thus saving on some system resource overhead.

I created a second example application program based on animate1 (imaginatively named animate2). Animate2 uses the single bitmap containing multiple frames approach to create the same animation effect as displayed by animate1. I changed the DisplayNextBitmap function and renamed it DisplayNextFrame, as shown in Source Code 8. Instead of taking the id of the bitmap to display next as input, it now takes the frame number to display and the handle for the bitmap containing all of the frames. Also, to give a different example of how to draw the bitmap frames to the screen, I used GpiWCBitBlt (world coordinate bit blt) to draw the frames. This allows a source bitmap to be drawn directly to the screen without first creating a memory bitmap. This approach, or the one used in animate1, is acceptable in this case. The key thing to note about this version of the DrawNextBitmap function is that it now uses the two defined constants, FRAME_WIDTH and FRAME_HEIGHT, to calculate where the next frame to draw is located in the bitmap passed in. The function assumes that all the frames are the same height and therefore finds the starting x coordinate of the next frame to draw by multiplying the frame number passed in by the FRAME_WIDTH constant.

The LoadBitmap function (derived from the LoadBitmaps function in animate1) now is very simple. There is only one bitmap to load and there is no bitmap handle management/array management overhead to keep track of all the bitmaps. To save space, the sample listing of animate2, shown in Source Code 7, depicts these changes. For the most part, the remainder of the program is unchanged from animate1. The entire source code is included on the DevCon for OS/2 CD-ROM. // LoadBitmap // Load the bitmap containing the animation frames. HBITMAP LoadBitmap( HWND hwnd, LONG idBitmap ) { HBITMAP hbmp ; HPS hps = WinGetPS( hwnd ) ; hbmp = GpiLoadBitmap( hps // PS for load , (HMODULE) 0          // Module containing bitmap (0=.EXE)   , idBitmap            // Id of the bitmap to load   , 0                 // Load at original width   , 0                // Load at original heaight   ); WinReleasePS( hps ) ; return( hbmp ) ; } Sample Code 7. Sample listing of the LoadBitmap function as used in animate2. // DisplayNextFrame // Display the next frame in the series. VOID DisplayNextFrame( HWND hwnd, LONG lFrameNumber, HBITMAP hbmpFrames ) { POINTL aptl[4] ; // Points used to perform blt LONG x1,y1,x2,y2 ; HPS hps ; x1 = lFrameNumber * FRAME_WIDTH ; y1 = 0 ; x2 = x1 + FRAME_WIDTH -1 ; y2 = y1 + FRAME_HEIGHT -1 ; // Set up the initial coordinates for our blt. We have to provide // both coordinates of the target rectangle and the source, as we // are going to double the size of the source bitmap when we do // the BLT. aptl[0].x = 0 ; // Lower left x of target bitmap (the screen) aptl[0].y = 0 ; // Lower left y of target bitmap (the screen) aptl[1].x = 2 * FRAME_WIDTH-1 ;// Upper right x of target bitmap (the screen) aptl[1].y = 2 * FRAME_HEIGHT-1;// Upper right y of target bitmap (the screen) aptl[2].x = x1 ; // Lower left of x source (the memory bitmap) aptl[2].y = 0 ; // Lower left of y source (the memory bitmap) aptl[3].x = x2 ; // Upper right x of source aptl[3].y = y2 ; // Upper right y of source // Now blt the bitmap to the screen hps = WinGetPS( hwnd ); GpiWCBitBlt( hps // Target PS , hbmpFrames    // Source PS  , 4      // Number of points in point array  , aptl   // Source and target coordinates for the BLT  , ROP_SRCCOPY  // Mix mode for BLT  , 0          // Options bits  ); WinReleasePS( hps ); } ''Source Code 8. Sample listing of the DisplayNextBitmap function as used in animate2.''

For The More Serious Animator
The techniques described here are simple and are intended to be included easily in your own applications to achieve animations of a fairly simple nature. It is possible and often necessary, especially with complex images involving lots of animation and lots of colors, to derive complex and sophisticated algorithms to only update the pieces of the image that need updating and not redraw the whole image each time. Multimedia applications like Multi Media Presentation Manager (MMPM) do this. Such algorithms are necessary to achive the frame rates (number of frames per second) needed to show real software motion video (i.e. movies). I welcome your comments and feedback about this article or OS/2 graphics programming in general.