Color Animation II
Volume Number: | | 10
|
Issue Number: | | 4
|
Column Tag: | | Getting Started
|
Related Info: Color Quickdraw Quickdraw
Finally - Color Animation!!!
Flying color bits around flicker-free
By Dave Mark, MacTech Magazine Regular Contributing Author
Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.
A while ago, we got into bitmap animation using BitMapper. BitMapper was based on QuickDraws BitMap data structure. Since BitMaps are always 1 pixel deep, BitMapper was limited to black and white animation. This month, well colorize BitMapper, taking advantage of Color QuickDraws PixMap data structure.
PixMaps and GWorlds
The PixMap is much more complex than the BitMap. With BitMap animation, all you had to do was fill out the relatively simple BitMap structure, create a GrafPort, then connect the two via a call to SetPortBits(). Once thats done, you are ready to copy the BitMap from port to port via a call to CopyBits(). If you need a little refresher, check back to the BitMapper column (September, 93).
On the other hand, PixMaps are complicated beasties. Pop open your copy of Inside Macintosh, Volume V (or THINK Reference, if you prefer) and look up the PixMap data structure. To create a PixMap by hand, youd have to initialize all of those fields. Notice the pmTable field, listed as a handle to a color table (also known as a color lookup table, or CLUT). In addition to the PixMap fields, youve also got to create and initialize a ColorTable structure. Look it up.
This stuff is non-trivial. Fortunately, the Toolbox offers a high-level set of functions that make the creation of off-screen PixMaps relatively simple. These off-screen PixMaps are known as GWorlds. Off-screen simply means that you are creating something that is not drawn in a window on the screen. GWorlds are created in memory and typically drawn to a window via a call to CopyBits(), just as we did with BitMaps in BitMapper.
PixMapper
This months program is called PixMapper. PixMapper creates a window the size of the main screen and fills it in with a randomly generated sequence of colored squares. Next, PixMapper loads a PICT resource and uses a series of GWorlds to smoothly animate the PICT over the colored background.
As usual, well create the program this month, then go over the program details in next months column.
Creating the PixMapper Resources
Start by creating a folder named PixMapper inside your Development folder. Fire up ResEdit and create a file named PixMapper.Π.rsrc inside your PixMapper folder.
Now create an ALRT resource (along with a corresponding DITL resource) for our error alert. The ALRT resource should have a Top: of 40, a Left: of 30, a Height: of 116, and a Width: of 292. Be sure to set the DITL ID: to 128.
Next, create a DITL with an id of 128. Use the specifications in Figure 2 to create the OK button and the specifications in Figure 3 to create the error alerts static text item.
Figure 2. Specifications for the error alerts OK button.
Figure 3. Specifications for the error alerts static text item.
Next, create an MBAR resource with an id of 128. The MBAR should list four MENU ids: 128, 129, 130 and 131. Create four MENU resources according to the specifications in Figure 4. Be sure to include a separator line as the second item in the File menu. Also, be sure to mark the appropriate items in the Colors menu with a check mark (). Use the popup menu in the Mark: field to do this.
Figure 4. The four MENU resources.
Finally, go into the scrapbook (or your favorite graphics application) and use Copy and Paste to create a PICT resource in the resource file. Be sure that the PICT has a resource id of 128. When choosing your PICT resource, heres a couple of things to keep in mind. First of all, smaller is probably better. A smaller picture takes up less memory and contains less pixels to copy around in memory. Something around the size of a color icon is probably a good size to start with.
Next, you might want to create a picture that is non-rectangular or that has a hole in it, then use the lasso tool to select only the pixels in the picture (and not the background). Though a rectangular picture will work just fine, a non-rectangular picture (like an X or an O shape) produces much more impressive results.
Creating the PixMapper Project
Save your changes and quit ResEdit. Launch THINK C and create a new project named PixMapper.Π in the PixMapper folder. Add MacTraps to the project. Next, create a new source code window, save it as PixMapper.c and add it to the project.
Type in the following source code:
/* 1 */
#include <QDOffscreen.h>
#include <Picker.h>
#include <GestaltEqu.h>
#define kMBARResID 128
#define kSleep 0L
#define kMoveToFront (WindowPtr)-1L
#define kEmptyString "\p"
#define kEmptyTitlekEmptyString
#define kVisible true
#define kNoGoAwayfalse
#define kNilRefCon (long)nil
#define kErrorAlertID128
#define kNilFilterProc nil
#define kSquareSize16
#define kForegroundPICT 128
#define kIgnored nil
#define kUseMaxDepth 0
#define kNoFlags (GWorldFlags)0
#define mApple 128
#define iAbout 1
#define mFile 129
#define iRedraw 1
#define iQuit 3
#define mColors 131
#define iUseRGB 1
#define iUseHSV 2
#define iRed4
#define iGreen 5
#define iBlue 6
#define iHue4
#define iSaturation5
#define iBrightness6
/* Globals */
Boolean gDone;
Boolean gIsRGB = true, gRandomReds = true,
gRandomGreens = true, gRandomBlues = true;
Boolean gRandomHue = true, gRandomSaturation = true, gRandomBrightness
= true;
WindowPtr gMainWindow;
short gXBump = 1, gYBump = 1; // <--Try changing these numbers
GWorldPtr gPictWorld, gSaveWorld, gSaveMixWorld;
PixMapHandlegPixMapSave, gPixMapSaveMix, gPixMapPict;
Rect gSavedFloaterRect, gPictWorldRect, gWorldRect;
short gGlobalHue;
/* Functions */
void ToolboxInit( void );
void MenuBarInit( void );
Boolean HasGWorlds( void );
void WindowInit( void );
void PaintWindow( void );
void RandomForeColor( void );
void GWorldInit( void );
GWorldPtr MakeGWorld( Rect *boundsPtr );
void EventLoop( void );
void DoEvent( EventRecord *eventPtr );
void HandleMouseDown( EventRecord *eventPtr );
void HandleMenuChoice( long menuChoice );
void HandleAppleChoice( short item );
void HandleFileChoice( short item );
void HandleColorsChoice( short item );
void MainLoop( void );
void DrawFirstFloater( void );
void MoveFloater( void );
void CalcNewFloaterPosition( void );
void DoError( Str255 errorString );
/****************** main ***************************/
void main( void )
{
ToolboxInit();
MenuBarInit();
if ( ! HasGWorlds() )
DoError( "\pDeep GWorlds not supported by this machine!" );
WindowInit();
GWorldInit();
DrawFirstFloater();
EventLoop();
}
/****************** ToolboxInit *********************/
void ToolboxInit( void )
{
InitGraf( &thePort );
InitFonts();
InitWindows();
InitMenus();
TEInit();
InitDialogs( NULL );
InitCursor();
}
/****************** MenuBarInit ***********************/
void MenuBarInit( void )
{
Handle menuBar;
MenuHandle menu;
menuBar = GetNewMBar( kMBARResID );
SetMenuBar( menuBar );
menu = GetMHandle( mApple );
AddResMenu( menu, 'DRVR' );
DrawMenuBar();
}
/****************** HasGWorlds *****************/
Boolean HasGWorlds( void )
{
long feature;
OSErr err;
err = Gestalt( gestaltQuickdrawFeatures, &feature );
if ( err != noErr )
DoError( "\pError calling Gestalt!" );
if ( feature & 0x0004 )
return true;
else
return false;
}
/****************** WindowInit ***********************/
void WindowInit( void )
{
Rect r;
r = screenBits.bounds;
r.top += GetMBarHeight();
gMainWindow = NewCWindow( nil, &r, kEmptyTitle,
kVisible, plainDBox, kMoveToFront,
kNoGoAway, kNilRefCon );
SetPort( gMainWindow );
PaintWindow();
}
/****************** PaintWindow ***********************/
void PaintWindow( void )
{
Rect r;
short row, col, numRows, numCols;
SetPort( gMainWindow );
r = gMainWindow->portRect;
numCols = (r.right - r.left) / kSquareSize;
if ( numCols != numCols/kSquareSize*kSquareSize )
numCols++;
numRows = (r.bottom - r.top) / kSquareSize;
if ( numRows != numRows/kSquareSize*kSquareSize )
numRows++;
GetDateTime( (unsigned long *)(&randSeed) );
gGlobalHue = Random();
for ( row=0; row<numRows; row++ )
for ( col=0; col<numCols; col++ )
{
r.top = row * kSquareSize;
r.bottom = r.top + kSquareSize;
r.left = col * kSquareSize;
r.right = r.left + kSquareSize;
RandomForeColor();
PaintRect( &r );
}
ForeColor( blackColor );
BackColor( whiteColor );
}
/****************** RandomForeColor ***********************/
void RandomForeColor( void )
{
RGBColor color;
HSVColor hsvColor;
if ( gIsRGB )
{
if ( gRandomReds )
color.red = Random();
else
color.red = 0;
if ( gRandomGreens )
color.green = Random();
else
color.green = 0;
if ( gRandomBlues )
color.blue = Random();
else
color.blue = 0;
RGBForeColor( &color );
}
else
{
if ( gRandomHue )
hsvColor.hue = Random();
else
hsvColor.hue = gGlobalHue;
if ( gRandomSaturation )
hsvColor.saturation = Random();
else
hsvColor.saturation = 65535;
if ( gRandomBrightness )
hsvColor.value = Random();
else
hsvColor.value = 65535;
HSV2RGB( &hsvColor, &color );
RGBForeColor( &color );
}
}
/****************** GWorldInit ***********************/
void GWorldInit( void )
{
PicHandlepic;
pic = GetPicture( kForegroundPICT );
if ( pic == nil )
DoError( "\pError loading PICT..." );
// Call HNoPurge() if your PICT is purgeable
gPictWorldRect = (**pic).picFrame;
OffsetRect( &gPictWorldRect, -gPictWorldRect.left,
-gPictWorldRect.top );
gWorldRect = gPictWorldRect;
gWorldRect.bottom += 2;
gWorldRect.right += 2;
gPictWorld = MakeGWorld( &gPictWorldRect );
gSaveWorld = MakeGWorld( &gWorldRect );
gSaveMixWorld = MakeGWorld( &gWorldRect );
gPixMapPict = GetGWorldPixMap( gPictWorld );
gPixMapSave = GetGWorldPixMap( gSaveWorld );
gPixMapSaveMix = GetGWorldPixMap( gSaveMixWorld );
// Lock pixels before you draw or read from PixMap. Unlock when you
are done.
//We're leaving the whole thing locked to avoid the hassle. In a real
app, do it right.
if ( ! LockPixels( gPixMapPict ) )
DoError( "\pLockPixels failed..." );
if ( ! LockPixels( gPixMapSave ) )
DoError( "\pLockPixels failed..." );
if ( ! LockPixels( gPixMapSaveMix ) )
DoError( "\pLockPixels failed..." );
SetGWorld( gPictWorld, kIgnored );
DrawPicture( pic, &gPictWorldRect );
}
/*********************************** MakeGWorld */
GWorldPtr MakeGWorld( Rect *boundsPtr )
{
QDErr err;
GWorldPtrnewGWorld;
err = NewGWorld( &newGWorld, kUseMaxDepth,
boundsPtr, kIgnored, kIgnored, noNewDevice );
// In the real world, call DisposeGWorld() when you are done with the
GWorld...
if ( err != noErr )
DoError( "\pMy call to NewGWorld died! Bye..." );
return( newGWorld );
}
/************************************* EventLoop */
void EventLoop( void )
{
EventRecordevent;
gDone = false;
while ( gDone == false )
{
if ( WaitNextEvent( everyEvent, &event, kSleep, NULL ) )
DoEvent( &event );
MoveFloater();
}
}
/************************************* DoEvent */
void DoEvent( EventRecord *eventPtr )
{
char theChar;
switch ( eventPtr->what )
{
case mouseDown:
HandleMouseDown( eventPtr );
break;
case keyDown:
case autoKey:
theChar = eventPtr->message & charCodeMask;
if ( (eventPtr->modifiers & cmdKey) != 0 )
HandleMenuChoice( MenuKey( theChar ) );
break;
case updateEvt:
BeginUpdate( (WindowPtr)(eventPtr->message) );
EndUpdate( (WindowPtr)(eventPtr->message) );
break;
}
}
/************************************* HandleMouseDown */
void HandleMouseDown( EventRecord *eventPtr )
{
WindowPtrwindow;
short thePart;
long menuChoice;
thePart = FindWindow( eventPtr->where, &window );
switch ( thePart )
{
case inMenuBar:
menuChoice = MenuSelect( eventPtr->where );
HandleMenuChoice( menuChoice );
break;
case inSysWindow :
SystemClick( eventPtr, window );
break;
}
}
/************************************* HandleMenuChoice */
void HandleMenuChoice( long menuChoice )
{
short menu;
short item;
if ( menuChoice != 0 )
{
menu = HiWord( menuChoice );
item = LoWord( menuChoice );
switch ( menu )
{
case mApple:
HandleAppleChoice( item );
break;
case mFile:
HandleFileChoice( item );
break;
case mColors:
HandleColorsChoice( item );
break;
}
HiliteMenu( 0 );
}
}
/************************************* HandleAppleChoice */
void HandleAppleChoice( short item )
{
MenuHandle appleMenu;
Str255 accName;
short accNumber;
switch ( item )
{
case iAbout:
SysBeep( 20 );
break;
default:
appleMenu = GetMHandle( mApple );
GetItem( appleMenu, item, accName );
accNumber = OpenDeskAcc( accName );
break;
}
}
/************************************* HandleFileChoice */
void HandleFileChoice( short item )
{
switch ( item )
{
case iRedraw:
PaintWindow();
DrawFirstFloater();
break;
case iQuit:
gDone = true;
break;
}
}
/************************************* HandleColorsChoice */
void HandleColorsChoice( short item )
{
MenuHandle menu;
menu = GetMenu( mColors );
if ( item == iUseRGB )
{
gIsRGB = true;
SetItem( menu, iRed, "\pRandom Reds" );
SetItem( menu, iGreen, "\pRandom Greens" );
SetItem( menu, iBlue, "\pRandom Blues" );
CheckItem( menu, iUseRGB, true );
CheckItem( menu, iUseHSV, false );
CheckItem( menu, iRed, gRandomReds );
CheckItem( menu, iGreen, gRandomGreens );
CheckItem( menu, iBlue, gRandomBlues );
}
else if ( item == iUseHSV )
{
gIsRGB = false;
SetItem( menu, iHue, "\pRandom Hue" );
SetItem( menu, iSaturation, "\pRandom Saturation" );
SetItem( menu, iBrightness, "\pRandom Brightness" );
CheckItem( menu, iUseRGB, false );
CheckItem( menu, iUseHSV, true );
CheckItem( menu, iHue, gRandomHue );
CheckItem( menu, iSaturation, gRandomSaturation );
CheckItem( menu, iBrightness, gRandomBrightness );
}
else if ( gIsRGB )
{
switch ( item )
{
case iRed:
gRandomReds = !gRandomReds;
CheckItem( menu, iRed, gRandomReds );
break;
case iGreen:
gRandomGreens = ! gRandomGreens;
CheckItem( menu, iGreen, gRandomGreens );
break;
case iBlue:
gRandomBlues = ! gRandomBlues;
CheckItem( menu, iBlue, gRandomBlues );
break;
}
}
else
{
switch ( item )
{
case iHue:
gRandomHue = !gRandomHue;
CheckItem( menu, iHue, gRandomHue );
break;
case iSaturation:
gRandomSaturation = ! gRandomSaturation;
CheckItem( menu, iSaturation, gRandomSaturation );
break;
case iBrightness:
gRandomBrightness = ! gRandomBrightness;
CheckItem( menu, iBrightness, gRandomBrightness );
break;
}
}
PaintWindow();
DrawFirstFloater();
}
/****************** DrawFirstFloater *********************/
void DrawFirstFloater( void )
{
CopyBits( &(gMainWindow->portBits),
(BitMap *)(*gPixMapSave),
&gWorldRect, &gWorldRect, srcCopy, nil );
gSavedFloaterRect = gPictWorldRect;
OffsetRect( &gSavedFloaterRect, 1, 1 );
CopyBits( (BitMap *)(*gPixMapPict),
&(gMainWindow->portBits),
&gPictWorldRect, &gSavedFloaterRect, transparent, nil );
}
/****************** MoveFloater *********************/
void MoveFloater( void )
{
Rect r;
RgnHandlenewRgn, savedRgn, oldClip;
CalcNewFloaterPosition();
CopyBits( (BitMap *)(*gPixMapSave),
(BitMap *)(*gPixMapSaveMix),
&gWorldRect, &gWorldRect, srcCopy, nil );
r = gPictWorldRect;
OffsetRect( &r, gXBump + 1, gYBump + 1 );
CopyBits( (BitMap *)(*gPixMapPict),
(BitMap *)(*gPixMapSaveMix),
&gPictWorldRect, &r, transparent, nil );
r = gSavedFloaterRect;
InsetRect( &r, -1, -1 );
CopyBits( (BitMap *)(*gPixMapSaveMix),
&(gMainWindow->portBits),
&gWorldRect, &r, srcCopy, nil );
OffsetRect( &gSavedFloaterRect, gXBump, gYBump );
r = gSavedFloaterRect;
InsetRect( &r, -1, -1 );
CopyBits( &(gMainWindow->portBits),
(BitMap *)(*gPixMapSaveMix),
&r, &gWorldRect, srcCopy, nil );
r = gWorldRect;
OffsetRect( &r, -gXBump, -gYBump );
CopyBits( (BitMap *)(*gPixMapSave),
(BitMap *)(*gPixMapSaveMix),
&gWorldRect, &r, srcCopy, nil );
CopyBits( (BitMap *)(*gPixMapSaveMix),
(BitMap *)(*gPixMapSave),
&gWorldRect, &gWorldRect, srcCopy, nil );
}
/*********************************** CalcNewFloaterPosition */
void CalcNewFloaterPosition( void )
{
Rect r;
r = gSavedFloaterRect;
OffsetRect( &r, gXBump, gYBump );
if ( (r.left < gMainWindow->portRect.left) ||
( r.right > gMainWindow->portRect.right ) )
gXBump *= -1;
if ( (r.top < gMainWindow->portRect.top) ||
( r.bottom > gMainWindow->portRect.bottom ) )
gYBump *= -1;
}
/***************** DoError ********************/
void DoError( Str255 errorString )
{
ParamText( errorString, kEmptyString, kEmptyString,
kEmptyString );
StopAlert( kErrorAlertID, kNilFilterProc );
ExitToShell();
}
Running PixMapper
As soon as you run PixMapper, the menu bar, featuring the , File, Edit, and Colors menus, will appear. Next, a window will appear, filling the entire main screen. PixMapper will fill the window with colored squares. Finally, PixMapper will animate the PICT, starting in the upper left corner moving towards the lower right corner. Every time the PICT hits the edge of the window, the PICT will bounce off and continue in the opposite direction.
Figure 5. PixMapper in action.
The speed of your PICT will depend on the speed of your machine and the size of the PICT. The important thing to notice is that the PICT animates smoothly with absolutely no flicker. If there is any hesitation, it is most likely due to the system taking time to do some housekeeping chore.
Try selecting Redraw from the File menu. PixMapper will redraw the window and start the animation over again.
Next, go to the Colors menu and play with all the different menu settings. PixMapper lets you play with both the RGB and HSV color models. RGB lets you set a colors red, green, and blue values. If you uncheck Random Reds for example, the screen will be redrawn with a red value of 0 (all greens and blues). If Random Reds is checked, the red component of the color is a random value from 0 to 65535.
If you select HSV Colors, youll be able to work with hue, saturation, and brightness rather than red, green, and blue. Unlike the RGB menu items, if you uncheck Random Hues, a colors hues component is set to 65535 instead of 0. Play around with these settings.
Till Next Month
Next month, well walk through the PixMapper source code. In the meantime, pop open Inside Macintosh and read about GWorlds and PixMaps. See you next month...