Color Animation
Volume Number: 9
Issue Number: 9
Column Tag: C Workshop
Related Info: Color Quickdraw Palette Manager Vert. Retrace Mgr
Toolbox Utilities Picture Utilities Standard File Resource Manager
Gestalt Manager Memory Manager Color Manager
Simple Real-time Color Frame Animation
Using GWorlds and the Pallette Manager
By Scott B. Steinman, O.D., Phd., St. Louis, Missouri
Note: Source code files accompanying article are located on MacTech CD-ROM or
source code disks.
About the author
Dr. Scott Steinman is an assistant professor at Washington University and has
designed and developed their new computer-based Infant Vision Clinic. A software
developer since 1982, Dr. Steinman is completing a textbook on the Prograph iconic
programming language coauthored by Kevin Carver. He now programs in C++,
Prograph and LabView. AppleLink: SBSTEINMAN. AOL: SBSTEINMAN
Introduction
It’s commonly assumed by users of other computers that the Macintosh computer
can't be used for real-time color animation. The Macintosh lacks dedicated graphics
co-processors, such as the Amiga computer's Blitter chip, that speed up critical
drawing operations. On the Amiga and IBM-compatible computers, a sequence of
animated images may be both created and displayed in real time on a frame-by-frame
basis by using video memory page-flipping techniques. The lack of dedicated graphics
coprocessors and dual video memory banks in the Macintosh places the burden of
generating images directly upon the CPU; it must execute QuickDraw graphics routines
on the fly. As we all know, QuickDraw routines can be far from what their name
implies. While graphics accelerator boards such as Apple’s 8•24GC do speed up some
drawing routines, QuickDraw operations still can't be executed quickly enough for both
real-time generation and display of animation frames.
The overhead of executing QuickDraw drawing routines may be circumvented by
generating the individual frames of the animation sequence in advance and storing them
in an array of off-screen pixel maps, stored in memory but not displayed. When the
animation sequence is to be shown to the program user, each frame is quickly displayed
by transferring the frame’s offscreen pixel map contents to the active graphics
device’s memory by calling CopyBits. By reducing the graphics operations required
during each animation frame interval to a single copying procedure, the animation may
be carried out in real time.
Information about color animation on the Mac has only recently become available
with the release of Inside Macintosh, volume VI (and the new Inside Macintosh:Devices
volume). The program code explained in this article makes use of several Toolbox
Managers to facilitate the presentation and storage of animated color images in real
time. I should first clarify what I mean by “real time”. Unlike the QuickTime
multimedia manager’s definition of “real time” as 30 frames per second, the
programs I use in my laboratory require presentation of animated images at the rate of
67 frames per second -- one frame per vertical retrace of the AppleColor RGB
monitor. This article will describe simple techniques for the generation of animation
sequences, saving animation frames to disk for later retrieval, and displaying
animation sequences in real-time. The frame images are written to disk files as a
collection of PICT2 color picture resources along with a resource to preserve
pertinent settings for playback of the animation sequence. The code is written in Think
C 5.0.4 and Resorcerer 1.1.1. and makes use of several features of System 7.0, as well
as highlights of the Vertical Retrace Manager, 32-bit QuickDraw, the Picture Utilities
Package, the Standard File Package and the Memory Manager. The same techniques
presented here may be used for monochrome (1-bit) images under System 7.0.
The GWorld Off-Screen Graphics Environment
Until recently, programmers have had to make major modifications to
black-and-white programs to take advantage of the color environment of Macintosh
computers. Programs written for monochrome Macs used simple BitMap
representations of graphics to be shown on the monitor screen, where each bit in
memory storage corresponded to one pixel on the screen. With the introduction of
color, programmers had to keep track of complicated inter-relationships between
color pixel maps, color tables, inverse color tables, graphics device records and color
graphics ports. Color graphics programming proved to be difficult enough that many
programmers just kept using black-and-white graphics.
Apple has now provided simpler ways of accessing the new color environment.
One of these is the GWorld off-screen graphics environment that is part of 32-bit
QuickDraw (Inside Macintosh, Volume VI). The other is the Palette Manager, which
simplifies the specification and modification of color (Inside Macintosh, Volumes V and
VI). Before delving into the workings of the real-time animation code listings, a brief
introduction to some aspects of GWorlds, the Palette Manager and the Resource
Manager is required. Later in this article, the Standard File Package and Picture
Utilities Package, which are used to store the animation sequence in a disk file, are
also discussed. The reader is referred to the excellent paper by Rensick for a
discussion of the Vertical Retrace Manager, used here to accurately time the playback
of the animation sequence.
Real-time animation has always been somewhat difficult on the Mac, even with
just black and white (1-bit) graphics. To display an animated image, a program had to
attempt to draw each animation frame within a single screen refresh interval (15
msec on the AppleColor monitor). If the drawing time exceeded this interval, the image
would appear to flash or shear, or even worse, the animation would slow down to a
crawl. QuickDraw drawing routines are still relatively slow, pr eventing direct
drawing of an animation frame’s contents directly to the screen in real time,
e specially if the image is complex. These speed constraints can be bypassed with
off-screen BitMaps or PixMaps. Drawing to off-screen memory is faster than drawing
to the screen, and the contents of this memory may then be copied quickly to the
screen. Similarly, animation can be accelerated by drawing each frame’s image in
advance to an element of an array of off-screen BitMaps (or PixMaps), then quickly
copying the contents of each BitMap to the screen in turn using the ToolBox CopyBits
routine. Each call to CopyBits can be executed within one screen refresh interval (see
the Discussion section for performance tests).
The GWorld graphics world construct, new to 32-bit QuickDraw, is an enhanced
equivalent of the PixMap record. It contains not only a map of an image’s pixels and
their colors, but also color table (CTable) and graphics device (GDevice) records.
GWorlds encapsulate and insulate the inner workings of these records from the user.
The programmer needs only to create a GWorld graphics environment with a call to the
NewGWorld routine and later dispose of it with a call to DisposeGWorld. In the case of
color frame animation, an array of GWorlds may be created where each GWorld is
assigned to hold one animation frame’s image. Rather than attempting to draw each
frame directly on the screen, we instead draw them in advance into an array of
off-screen GWorld records. When the animation sequence is to be displayed, all we
need do is copy the GWorld contents to the screen one-by-one in real time.
As a bonus, the GWorld record does more than just store our animated images; it
also accelerates the animation display itself. When off-screen images are copied from
memory to the screen, the speed of the transfer is affected not only by image size and
number of colors, but also by the position of the image in memory. In other words, on
what bit (within a byte) or byte (within a long word) of memory does the leftmost
pixel of the image fall? If there is a mismatch in the position in memory between the
image in the PixMap and the desired copy of the image on the screen, CopyBits must
account for it, and therefore works slower. Images whose PixMaps are aligned to long
word boundaries are transferred by CopyBits faster than those that are not. GWorlds
allow some control over the speed of image transfers by correcting for these factors. If
the programmer converts the desired GWorld bounding rectangle to global QuickDraw
coordinates before passing it to NewGWorld (see Inside Macintosh, Volume VI), the
NewGWorld routine will automatically optimize the graphics image position within the
GWorld pixel map so that CopyBits will work as quickly as possible. This optimization
doesn’t affect where the graphics will appear on the screen, but only the internal
representation of the graphics image within the GWorld PixMap.
GWorlds and the Palette Manager
Display monitors are represented in the Macintosh operating system by a
GDevice data record that contains, among other things, the color environment for that
monitor. This ensures that each monitor of a multiple-monitor system preserves its
own individual color environment. Offscreen graphics are also represented using a
GDevice (graphics device) record, but in this case the off-screen device does not exist
in a physical sense. When a GWorld is created with NewGWorld, the programmer may
choose to create a new off-screen graphics device to hold the GWorld’s image, or to use
a pre-existing GDevice. This option allows an array of GWorlds to share a single
graphics device rather than create one off-screen device for each GWorld. The
programmer must also provide a CTable record for the GWorld’s graphics device,
either explicitly by passing a color table record to NewGWorld or implicitly by using
the CTable color table of the physical graphics device on which the GWorld’s image will
ultimately be displayed.
Because GWorlds contain graphics device information, the Palette Manager
routines used for graphics devices are made available to them. In the frame animation
program, parallel updates in color information of the animation display window and of
each animation frame are accomplished easily using the same Palette Manager calls.
This means that the color environment of the display window and the animation frames
is always kept synchronized. The frame’s image therefore blends seamlessly into the
window background.
Pictures and Resource Files
Macintosh graphics may be specified in two ways: either as a PixMap, in which
the graphics are represented as arrays of pixels on the screen, or as a Picture,
collections of encoded drawing instructions which may be used at any time to
reconstruct the pixel form of the graphics image. Single color graphics images are
typically stored as Pictures in the PICT file format. The equivalent file format for
animation sequences is the PICS file, which is essentially a collection of Pictures with
added information such as animation playback speed. These standard file formats differ
in that a PICT graphics file stores the images in the file's data fork, while the PICS
animation file stores the individual frame images in the file's resource fork. The color
information for the images is stored in each case within the PICT data or resources
themselves -- no separate color table (‘clut’) or palette (‘pltt’) resources are
provided. If a programmer wishes to extract the color information in a standard
data-fork-based PICT file to set the palette of the window that will display the file’s
picture, the image must be picked apart piecemeal with complicated and fairly cryptic
custom QuickDraw bottleneck routines supplied by the programmer (Sheets, 1988).
Special spooling techniques must be used just to read and write the Picture data of PICT
data files (see Inside Macintosh, Volumes V and VI).
A more straightforward way of saving and restoring the individual graphics
images and the color environment is to save the images as PICT resources. Their color
information can be easily retrieved from the PICTs themselves using the new System
7.0 Picture Utilities Package. Using PICT resources may reduce to some extent direct
compatibility with commercial paint packages that use data fork PICT storage, but the
programmer may more easily restore the color environment before playing an
animation sequence from disk. A resource editor such as ResEdit or Resorcerer may be
used to transfer graphics created by commercial paint programs to PICT resources
format via the clipboard. In addition, commercial animation packages store
information in the PICS format.
The GetPictInfo function is used to obtain the color table of the animation
sequence file’s PICT resources so that we may reset the color environment of our
off-screen GWorlds and display window after reading in an animation file. This allows
the program code that will display an animated visual stimulus previously stored in a
disk file to easily set the window background color equal to that of the animation frame.
This is very important when the animated image is to be displayed on a background
without a visible border between the animated image and the background. By retrieving
the CTable from the PICT resources, the proper background color is maintained every
time a stored animated image is presented.
Resources also provide an elegant way to store relevant playback parameters in
the animation file. A PICS animation information (‘INFO’) resource may be created to
store a structure containing the animation frame presentation rate, use of color, frame
size, etc. This resource is read from the file before the animation is to be presented to
the user, which serves to ensure the proper presentation of the animation. An added
benefit of storing the playback parameters as resources is that this data may be easily
examined and modified later in the animation file using ResEdit or Resorcerer to create
new permutations of the animated image. Were the playback parameters stored in the
data fork of the animation file, modifications (with a hexadecimal file editor) would be
difficult at best.
The Color Frame Animation Program
Using the techniques outlined above, 100 frames of animation containing a 200
by 200 pixel frame area in 8-bit (256 color) gray-scale have been animated in
real-time1 (67 frames/second) on a Mac IIfx computer. As discussed below, it is
possible to animate even larger images in real-time. The program code listed here is
part of a program for visual motion experiments that is composed of two
interdependent parts. The first part calculates and draws the animation frames for a
moving stimulus, which are then stored to a disk file. The second part presents the
moving stimuli to the user and tallies responses (Steinman and Nawrot, 1992).
Several crucial variables are shown here as global variables rather than function
parameters in order to simplify the function declarations, but the use of global
variables should be limited in practice. In my research applications, I encapsulate the
animation and animation file access code into a C++ class called CAnimation that can be
subclassed to produce different animated images.
Main.c is the file that contains the program’s main function. This listing opens
with a series of variable declarations that deal with the construction of an animation
sequence The most important variable is an array of pointers to GWorlds called
gFrames that holds the animation sequence frame images in PixMap form. These
PixMaps are transferred to the screen when the animation is to be shown to the user.
By the way, the extern declaration (gFramePICTs) is for an array of handles to
Pictures for storage of the animation frames to disk. Next are gPalette, a handle to a
palette, and gCTable, a handle to a color table, which both will specify gray levels used
in the display window and animation frames. The animation code was written for vision
experiments to present gray-scale stimuli whose contrast levels are set individually
by the experimenter. The animation settings are stored in a custom Settings structure,
and a custom Flags structure holds general program execution flags.
CheckEnvironment takes advantage of the Gestalt Manager to check whether or not
we have the proper environment for running this program. In this case, it means
ensuring that System 7, the Slot Manager, the Palette Manager, 32-bit QuickDraw and
a floating-point chip are present. The SetMonitors function calls the HasDepth Toolbox
routine to check if we can set the monitor color depth to a desired depth of 4 bits (16
colors), then SetDepth actually does so.
The InitParams function initializes major animation parameters and nulls the
handles and pointers used by later memory allocation. Since animation sequences
require large amounts of memory, the program depends heavily upon the Memory
Manager to make the most efficient use of available memory. Most data structures are
allocated dynamically and moved to high memory whenever possible to allow the
Memory Manager access to as much contiguous RAM as possible. The MaximizeHeap
function further assures that free space is available for storing animation sequences.
It checks how much heap space may be freed up by purging memory blocks and
compacting the heap, then actually performs the purging and compaction to make as
much contiguous heap space available to our program as possible. The function CleanUp
frees the memory allocated for the animation sequences in PixMap and Picture
formats, the CTable and the Palette. This function is called upon program exit, either
when the experimenter has completed the construction of stimuli or running an
experiment, or if a serious error such as insufficient memory occurs during
run-time. The function ErrorHandler presents error messages to the user by
retrieving error message strings from a STR# resource.
The program header file, FrameAnim.h, provides general definitions such as
resource ID number constants for the frame images, color environment and
parameters that will be saved in a disk file (see listing Files.c). The resource ID
number kGrayCTableID identifies one color table resource stored in the Macintosh
ROM. The ROM contains several color table resources containing gray-scale “ramps”
whose ID number is simply the color depth plus 32. In the present program, a depth of
4 bits (16 gray levels) is used, and so the ROM gray-scale resource ID number is 4
plus 32, or 36. Also included are the user interface resource IDs, a series of
color-specific definitions, including palette indices for the gray levels to be defined by
the program. The first entry of a palette (entry 0) is defined by Apple Computer to be
white, while the last entry (in the 16 color palette, entry number 15) must be black.
The remaining palette entries (1-14) may be used by the animation program,
although only two entries (entries kPalBkgnd and kPalTarget, used for the background
and target colors, respectively) are used in the present code examples. Finally,
animation-specific constants are defined. kMaxFrames is the maximum allowable
number of animation frames. Although this has been defined here to be 60 frames, the
animation sequence may be composed of any number of frames, limited only by the
amount of available application heap memory and disk space. The speed of the animation
is limited only by the color depth and image size, not by the number of animation
frames.
The header file also contains important data type definitions including the
specification of a structure to store the animation playback parameters as a resource
in the animation file. The reader is directed to the article by Sheets for further
information on the PICS animation format. Although some of the fields of the structure
are not used by the present program, they are included for compatibility with
commercial PICS animation programs. The PICSInfoRec structure contains information
on the animation sequence’s use of color, frame size and frame playback delay. The
Settings structure includes key information about the animated targets, such as the
target gray level, the number of frames in the animation sequence and the delay
between the presentation of each frame, as well as which monitor to display the target
on in a multi-monitor system.
Some variables, such as the frame delay, must also be kept in a structure that
can be accessed easily by the vertical blank interrupt code. The time-critical video
interrupt routines (listed in VBlank.c) need to access this information quickly. The
XVBLTask structure encapsulates the VBLTask record passed to Vertical Retrace
Manager routines, the state of the CPU’s A5 register, and copies of the most critical
animation playback variables. This information must all be passed as a unit to the
interrupt task service routine, because interrupt routines take no arguments and may
only receive information by means of global variables retrieved from the processor’s
A0 register during the interrupt task. We’ll return to these issues when we discuss
the VBlank.c file.
The color environment is manipulated by the code in Setup.c. InitPalette sets up a
gray-scale palette and color table to be shared by the display window (gMainWindow)
and the animation frame GWorlds (gFrames). A grayscale ramp color table is obtained
from the Macintosh system ROM with a call to GetCTable and saved in gCTable. The
NewPalette routine then creates a gray-scale palette from gCTable. Two flags,
pmTolerant and pmExplicit, tell the Palette Manager how the palette will be used (see
van Brink, 1990 and Inside Macintosh, Volume V, for a full discussion of Palette
Manager routines). They dictate how a color that is requested from the palette for a
drawing operation relates to the colors actually present in the display device’s color
look-up table. The pmTolerant flag states that a given palette entry has a “close
match” present in the look-up table (precisely how close it must be is given in the
last parameter passed to NewPalette). The pmExplicit flag states that a requested
palette entry has a corresponding color in the device color look-up table at the same
entry number in the table. These two flags (and the last NewPalette parameter) in a
sense describe the degree of “noise” that’s accepted in the colors the system will
actually provide when the programmer requests a specific color. Although it is not
intuitive, a combination of the pmTolerant and pmExplicit flags is required for proper
reconstruction of the color environment from our animation file. This flag combination
ensures that a color requested from the palette exactly matches its corresponding
device look-up table color entry; in other words, the color asked for is precisely the
color obtained. The call to NSetPalette assigns the new gPalette palette to a specific
window, in this case, the gMainWindow window in which the animation will be
displayed. Finally, the palette is activated for use in the display window.
The UpdateWindowColors function resets the color palette if the user requests a
new background or target color. It directly replaces specific color entries in the
palette with new color values. SetEntryColor places the new color value (in RGBColor
form) at a given entry position (kPalTarget or kPalBkgnd) in the gPalette palette. The
changes to the palette are then mirrored in the gCTable color table that is used to
create animation frame GWorlds via the Palette2CTab routine. Finally, the updated
palette is activated for use. The corresponding changes to the GWorlds’ color
environment are handled by the UpdateGWorldColors function in the Animation.c file.
By the way, the GetNumMonitors function simply uses the GetMainDevice and
GetNextDevice Toolbox routines to check if there are one or two monitors connected to
our Macintosh. If there are two, our program will give us the option of displaying the
animation on the main monitor or the second monitor.
Listing Animation.c is the heart of the animation program. It begins with a Rect
declaration that defines the drawing area for the animation frame images. sDrawArea is
the bounds of an animation frame image referenced to the coordinates of the display
window in which the animation will be presented. gBounds defines the extent of the
frame image within the PixMap of each GWorld, expressed in the GWorld’s local
QuickDraw coordinates. These rectangles that define the bounds of the image drawing
and storage areas are set in the SetDrawCoords and SetOrigin functions.
The PrepareFilm function allocates and initializes the off-screen GWorlds that
hold the animation frames in pixel map form for later display. First, the display
window palette is updated to ensure that GWorlds are created using the most recent
color values. Next, the SetDrawCoords function is called to define the extent of the
frame image stored in the GWorlds. This value, gBounds, is defined in local QuickDraw
coordinates. As discussed above in the section about GWorld off-screen graphics
environments, several optimizations are performed upon the GWorld image if the
bounding rectangle is converted to global QuickDraw coordinates before passing it to
NewGWorld. Therefore, the bounding rectangle is converted to global coordinates and
stored in the gGlobBounds variable. An array of new GWorlds is created using the
NewGWorld routine (after destroying any previously used GWorlds and compacting the
heap). The first GWorld in the array will by default be assigned an off-screen graphics
device by passing no flags to NewGWorld. By passing no color table handle to
NewGWorld and specifying a color depth of zero, NewGWorld is forced to use the color
table and depth of the graphics device on which the animation will be later displayed
(see van Ortiz, 1990, for a description of NewGWorld input parameters). The
GWorlds will therefore be set to a depth of 4 (for 16 colors) and the default color table
of a 16-color device since the monitor has already been set by SetMonitorDepth in
Main.c to a depth of 4. The NoPurgePixels routine ensures that the pixel map memory
of the GWorld won’t be destroyed without our knowledge if the Memory Manager must
free up memory during program execution. After creating the first GWorld in the
sequence, we can find out the amount of memory needed for a GWorld and its associated
PixMap via GetPtrSize and GetHandleSize, respectively, then use this information to
free a memory block large enough to hold the remaining GWorlds by passing it to
MaximizeHeap. The remaining GWorlds in the gAnimFrames array are then created
using the GDevice of the first GWorld, obtained with the GetGWorldDevice routine. In
this manner, all of the GWorlds share the same graphics device, saving memory.
The DoFilm function creates the individual animation frame images. The
PrepareFilm function is first called to prepare GWorlds to house the animation
frames. The current drawing port and device are saved by a call to GetGWorld. To draw
directly into the GWorld pixel maps, the memory handle that points to the pixel map
must be locked using the LockPixels routine, just as other memory handles in
Macintosh programs must be locked with the HLock routine. The current drawing port
and device are then set to those of the GWorld we wish to draw in with SetGWorld. Next,
the PlaceTargets function, which does the actual drawing, is called. Finally, the
previous drawing port and device are restored and the GWorld pixel map memory
handle is unlocked. The drawing pen colors are reset to black and white for proper
operation of CopyBits (CopyBits will alter the colors of the image it transfers if the
pen colors are not set to these values).
PlaceTargets carries out the actual drawing of the animation frame images into
both the GWorld pixel maps (for display of the animation sequence) and the Picture
records (to be saved in the animation file on disk). First, the size of the target, a
simple rectangle in this example program, is calculated for this frame. The target will
grow and shrink in the animation. Then the image to be stored in the GWorld is drawn.
The drawing pen size is set, and the GWorld pixel map contents are erased using the
background color. The pen color is then set to the target color and the target is drawn.
In order to save the animation frame in Picture format in a resource file, a
Picture of the frame must be created. Unfortunately, Picture instructions do not
understand Palette Manager calls, and the older Color Manager equivalents must be
used. To do so, the target and background colors must first be converted to the
RGBColor form understood by the Color Manager. The GetEntryColor routine converts
palette entries into RGBColors. A Picture record is then opened for filling with
drawing instructions using the OpenPicture call. To copy the frame’s contents into the
Picture, we repeat the drawing instructions that create the animated target for that
frame. Since a Picture record has been opened, the image is re created in the Picture.
Simple, isn’t it? The frame drawing instructions are the same as those used to draw
into the GWorld except that they use the Color Manager routine RGBForeColor instead
of PmForeColor. The Picture sequence is finally closed via ClosePicture.
The PlayFilm function is called when the animation sequence is to be presented to
the user. It first hides the mouse pointer from view, then locks the GWorld PixMaps so
that we may access them. The SetUpVBlankAnimation function is called to initiate the
animation. Among other things, it initializes the frameIndex frame counter in the
XVBLTask structure to the first frame of the animation sequence. The animation display
is enabled by setting the vblCount field of the XVBLTask structure to a non-zero value.
A loop is now entered that copies each frame’s image from its GWorld to the display
window until the frame counter frameIndex has reached the final frame number; that
is, when the entire animation sequence has been shown. Within the loop, the
showFrame flag in the XVBLTask structure is set to false to prevent the next frame
from being copied to the screen until the next vertical retrace interrupt has occurred.
Our VBlank interrupt routine (in file VBlank.c) sets this flag to true whenever a
VBlank interrupt occurs, allowing a new frame to be displayed. When the display loop
exits, the ShutDownVBlankAnimation function is called to pr eventing further
animation. The background is then erased by calling PaintRect, and the mouse pointer
is revealed again.
UpdateGWorldColors is used to change each animation frame GWorld’s color
information whenever the display window’s color environment is modified. This keeps
the color environment of the frame and the window in sync. UpdateGWorldColors treats
each GWorld as a color port with its own palette. The same Palette Manager calls are
used for each GWorld as for a window.
Finally, the DisposeFrames function frees the memory used to store the
animation sequence frame images in the GWorlds.
Listing VBlank.c contains the video interrupt-based animation timing routines.
We’ll concentrate on the portions of this code that are specific to presenting color
frame animation. For a full discussion of millisecond and vertical retrace timing,
consult Rensick’s article. SetUpVBlankAnimation sets up the video interrupt task in
which we will do the actual animation. A custom XVBLTaskRec structure is defined in
the header file FrameAnim.h to store both the VBL task record, the contents of the
CPU’s A5 register, and additional animation playback parameters that we’ll need to
access during the interrupt task. Why have I made such a fuss about storing the
contents of the A5 register in this structure? The A5 register holds a pointer to the
beginning of a memory block that holds our application’s global variables. If you wish
to access any global variable’s values during an interrupt service routine, you must
ensure that the value in the A5 register is valid. Unfortunately, when an interrupt
occurs, the operating system takes over, and its A5 setting may not be the same as that
of your program. Therefore, you must save the contents in the A5 register before the
interrupt occurs, then use it to access your program’s global variable space.
Why global variables? Interrupt task routines do not take any arguments! The
only way to pass information to them is by way of a global variable. More what is more
important, we only get to pass one global variable (through its address) which is
passed onto the A0 register of the CPU. We have defined a trap, GetVBLRec, in the
header file to push the contents of the A0 register onto the stack (and therefore into a
local variable within our VideoProc interrupt service routine, described below).
We must now fill the fields of the XVBLTaskRec structure with their required
values. SetUpVBlankAnimation first finds the slot number of the NuBus slot holding
our Macintosh’s graphics board (or the slot number assigned to the internal video
output). The task procedure address is set to that of the VideoProc procedure that
regulates our animation. The vblCount field of the video task record, which determines
how often the VideoProc will be executed, will eventually be given a value equal to
frameDelay, the number of screen refreshes between animation frames. In other
words, once every frameDelay vertical blanks, a new animation frame will be
displayed. The value of vblCount therefore determines in part the velocity of a moving
target shown in an animation sequence. However, for now we set this field to 0, which
will inhibit the display of the animation until we are ready. The current setting of the
A5 register is saved in the a5 field. The frames field is set to the number of frames in
the animation sequence and the index to the currently displayed animation frame in the
frame array (frameIndex) is initialized to -1 (it will be set to 0 on the first video
interrupt). The showFrame flag is set to false to ensure that when the display loop in
file Animation.c is executed, a new frame won’t be displayed until a video interrupt has
in fact occurred.
The VideoProc function is the core of the real-time animation technique. This
function is called once every frameDelay vertical blanks. It retrieves a pointer to the
XVBLTaskRec, stored by the operating system’s VBL interrupt handler, from the
processor register A0. VideoProc can then access the XVBLTaskRec structure’s
contents. If we haven’t yet shown the last frame of the animation sequence, we
increment the frame index, and set the showFrame flag to true and vblCount to the
frame delay. If the entire sequence has been shown, we inhibit showing any more
animation by setting showFrame to false and vblCount to 0. When the VideoProc
function exits, the program returns control to the frame display loop in PlayFilm.
With the showFrame flag set to true, CopyBits can be called and the GWorld image is
transferred to screen memory. The video interrupt task is finally removed from the
interrupt queue by a call to ShutDownVBlankAnimation, which removes the video
interrupt task from the interrupt queue, restoring the interrupt queue to its original
state.
The last major code fragment presented here, listing Files.c, shows how the
newly created animation sequence and playback parameters are stored to disk and
retrieved later when the animation sequence is to be displayed to the user. This code
makes use of System 7.0’s new Standard File Package, which simplifies the
presentation of dialogs to users for selecting parent folders and file names for files to
be saved or opened. Recent changes to the File Manager mirror the simplification of the
Standard File Package to new routines for the creation, opening, reading and writing of
files. All of the new functions take advantage of a new StandardFileReply structure,
which combines the old fName, parID and volID variables that describe the location of a
file on a storage volume (along with other information), into one simple to use
structure.
SaveFilm is responsible for setting up the saving of the animation information to
a disk file. It calls the Standard File Package’s StandardPutFile routine to present the
typical Save File dialog. Unless the user cancels the selection of a file name for the new
file, FSpCreateResFile is called to create a resource file (a file with a resource fork)
to hold our animation information. The creator of this file is ‘FRMA’, our animation
program’s signature. The file type is set to ‘PICS’, the standard animation file format.
The WriteFilm function is then called.
WriteFilm performs the actual storage of the information into the file. Some of
the code within this function is required due to an easily overlooked feature of the
Resource Manager. When a resource file is opened, a table of the resources in the file
called a “resource map” is created, which effectively links each resource in the file to
a handle to a data record in memory that holds the contents of the resource. WriteFilm
creates a resource file and uses AddResource to add handles to a playback parameter
structure and each animation frame Picture to the resource map of the file. When
CloseResFile is later called, this information is automatically stored as resources in
the disk file. However, CloseResFile also destroys the resource map, which has the
side-effect of freeing the memory handles in the resource map. These handles still
point to the array of animation frame Pictures and the animation playback structure,
so these structures are now destroyed. While this is not a problem for data structures
that are not used later, you must avoid destruction of any data that might be referenced
at some later point in your program. This could have adverse effects on program
execution (in other words, the dreaded “bomb”). Therefore, you should use a copy of
critical information for storage in a resource file. Only the copy would be destroyed
when CloseResFile frees the resource map, leaving the original data intact. Luckily in
our frame animation program, the Pictures and playback information can be disposed
of once the animation sequence is saved to disk.
WriteFilm first allocates storage for a PICSInfoRec structure (sPicInfo) to hold
values of the global playback parameter variables declared in Main.c. These variables
must be encased in a structure to be saved in one block to a resource. The resource fork
of the file selected and created in SaveImage is opened for use with a call to
FSpOpenResFile. The sPicInfo structure is then added to the resource file map via
AddResource. Each frame of the animation sequence, stored in Picture format in the
framePICT array, is then transferred to a separate resource by a call to AddResource.
AddResource is passed a handle to an animation frame Picture, a definition of the type
of resource to be saved (in this case, a ‘PICT’ Picture resource), a unique ID number
for the resource as defined in the header file shown in FrameAnim.h, and a optional
name for the resource that helps the programmer when viewing or modifying these
resources with ResEdit or Resorcerer. UpdateResFile automatically forces the
resources in our new resource file to be updated (physically written to the file).
CloseResFile releases the file’s resource map and closes the resource file.
Unfortunately, when the handles in the resource map are freed, the handles are not
reset to “null” (the value of an unused handle or pointer). If a memory-freeing
function were called at any time after saving the resource file and these handles were
not set to “null”, the function would attempt to free the memory locations contained in
the handles, which the Memory Manager could by now have assigned to hold other
information! The WriteImage function therefore explicitly sets these handles to “null”
to prevent a potential catastrophe.
Reading a previously-saved animation sequence file involves similar steps as
those carried out in SaveFilm and WriteFilm. The OpenFilm function calls
StandardGetFile to present the standard Get File dialog, then ReadFilm to read the
contents of the animation resource file. The ReadFilm function starts by freeing
memory that has been previously allocated for animation frames, the palette and the
color table, so that these may be replaced by new ones created from the animation file.
The resource file selected in OpenFilm is then opened, and a PICSInfoRec structure
created by reading the corresponding resource from the animation file. Because the
playback parameter structure (sPicInfo) is stored in a custom resource, there is no
dedicated ToolBox routine for reading the contents of this resource. Instead, the generic
Get1Resource routine is used to create and initialize the memory that will store the
contents of this custom ‘INFO’ resource. The memory block returned by this routine is
typecast to a PICSInfoRec structure. The global variable frameDelay is set with the
speed member of the PICSInfoRec structure and the refresh rate. A new set of GWorld
frames is then created as well. The frame images, stored in Picture form in ‘PICT‘
resources in the animation file, are read with a loop containing calls to GetPicture. The
number of iterations through this loop is set by counting the number of ‘PICT’
resources in the file; that is, the number of frames in the animation sequence.
DetachResource allows the Picture structures (now stored in the gFramePICTs array)
to remain in memory and not be destroyed after the resource file is closed and the
file’s resource map is freed.
A new palette and color table are created from these Pictures by using the new
Picture Utilities Package’s GetPictInfo routine, which can read the colors presaent in a
‘PICT’ resource into either a Color Table or a Palette. By passing the returnColorTable
flag, we choose to retrieve the colors of the first frame of the animation sequence into a
Color Table. We can either get a table of all the exact colors or the best-fitting colors
to those in the Picture. In our case, we select the exact colors with the systemMethod
flag and reset bkgndGray and targetGray with these color values. The call to
NewColorsFromPICT function does the equivalent of the UpdateWindowColors function
to reset the color table and palette for our animation sequence. We also read the
Picture’s size into the Settings structure’s frameSize field. The frame images in the
Pictures are then drawn into the animation sequence GWorlds. Finally, the resource
file is closed.
The DrawPICTToGWorld function transfers the animation frame images from the
framePICT array into an array of GWorlds (gFrames). For each animation frame in
turn, the pixel map of the GWorld is drawn into simply by calling the DrawPicture
routine, which converts the Picture instructions into PixMap images. The
DrawPicture call is enclosed by the appropriate memory locking and port-setting
procedures required when accessing GWorld pixel maps.
Files.c concludes with the DisposFramePicts function to free the memory
allocated for the framePICT array. The individual frames of the sequence are
deallocated via the KillPicture routine, which specifically destroys Picture structures
specifically.
The final few listings, which won’t be discussed here, contain the dialog box
functions (Dialogs.c), the event-handling functions (Events.c) and the Rez-format
resource listings (FrameAnim.r).
Performance
Real-time color animation may be accomplished by methods other than frame
animation. Baro and Hughes describe a color-cycling animation technique using the
Palette Manager on single images. However, there are many types of animated stimuli
that cannot easily be created by palette modifications alone, the most important being
targets that move along paths or change shape. Frame animation is better suited for
these types of targets. By precomputing the graphics images of each frame, either
programmatically or with a commercial paint package, we remove the need to perform
time-consuming drawing operations when presenting animation. At playback, only one
fast CopyBits operation is required, allowing larger, more complicated images to be
used. By using a single transfer routine call to perform the animation, the Vertical
Retrace Manager may be used for accurate timing of the animation presentation,
ensuring precise frame timing and target velocities.
Unfortunately, frame animation does introduce some limitations upon the
complexity of the stimulus. The allowable physical size and color depth of the
animation frames constrain the overall size of the target and the number of colors that
can be displayed at once (see Macintosh Technical Notes # 277). These limitations
arise from the upper envelope of how much graphics memory may be transferred by
the CopyBits routine during one vertical retrace interval. The larger the image and the
higher its color depth, the longer it takes to transfer the image from off-screen
memory to the screen. If the transfer time is not kept brief, the transfer may take
more than one retrace period, slowing down the animation and introducing inaccuracies
in visual motion velocity. When testing the animation code presented here on a
Macintosh IIfx computer, it was found that when using 4-bit color (16 color)
displays, real-time animation was possible only on images limited to 350 by 350
pixels in size. When 8-bit color (256 color) displays were used, the largest possible
image size for real-time animation was reduced to 230 by 230 pixels. If only simple
targets are to be presented, 2-bit color allows the display of full-screen (640 by 480
pixels) real-time stimuli in which the target color may still be manipulated
somewhat. The amount of available memory also places an upper limit on the number
of frames and the size and depth of the frames that may be displayed in an animation
sequence.
Although the current program has been tested on the Macintosh IIfx, IIci, IIsi,
and II, both with and without the Apple 8•24GC QuickDraw accelerator graphics board,
there is no guarantee that color Macintoshes with slower CPUs, such as the Macintosh
LC, will be able to transfer large multi-colored images quickly enough. Unfortunately,
I was not able to test my code on such machines. On these models, smaller images of
fewer colors may be required, or the programmer may have to restrict animation to
palette animation techniques alone. The installation of a QuickDraw accelerator board
will not significantly increase the speed of the animation display because only a single
CopyBits call is executed within the vertical retrace interrupt task. However, it would
speed up the initial drawing of the images to the off-screen GWorlds.
For those owners of monochrome Macintoshes, the code I’ve presented will still
work. The latest version of 32-bit QuickDraw included in System 7.0 (see Inside
Macintosh, volume VI) will automatically create a black and white BitMap within
GWorld records created with NewGWorld when run on a monochrome Mac. Simply
remove the palette and color table routines from my program and join the fun!
Despite the current restrictions on the use of color frame animation, the
techniques described here allow the creation of complex moving stimuli in real-time.
The code listings are intended only to provide a framework with which an animation
program may be written. Several improvements could be made on it, including the
simultaneous use of frame animation and palette animation, which I’ve done in my
laboratory for vision experiments. The reader is referred to the recent article by Rich
Collyer for tips on how to use Palette animation together with GWorlds.
Footnotes
1 The duration of each animation frame was confirmed using
millisecond-resolution timing routines (Rensick, 1990). An animation sequence was
generated and displayed, with the timer started at the onset of the animation display
and stopped at the end of the sequence. The total elapsed time was then divided by the
number of frames to get the single frame time. A single frame was found to take 15
msec, the duration of a vertical retrace on the AppleColor RGB monitor.
Bibliography
Anstis, Stuart and Paradiso, Michael. “Programs for visual psychophysics on the
Amiga: a tutorial,” Behavior Research Methods, Instruments & Computers,
1989, v.21, p.548ff.
Baro, John A. and Hughes, Howard C. “The display and animation of full-color images
in real time on the Macintosh computer,” Behavior Research Methods,
Instruments & Computers, 1991, v.23, p. 537ff.
Collyer, Rich. “Palette Manager animation,” Develop, Winter 1991, p.78ff.
Inside Macintosh: Volumes I-VI.
Macintosh Technical Notes # 276, “Gimme depth or gimme death,” June 1990.
Macintosh Technical Notes # 277, “Of time and space and CopyBits,” June 1990.
Ortiz, Guillermo. “Braving offscreen worlds,” Develop, January 1990, p.28ff.
Rensick, Ronald A. “Toolbox-based routines for Macintosh timing and display,”
Behavior Research Methods, Instruments & Computers, 1990, v.22, p.105ff.
Sheets, Steven. “Color screen dump FKEY,” MacTutor, February 1988, p.52ff.
Sheets, Steven. “Animating PICS,” MacTutor, March 1991, p.64ff.
Steinman, Scott B. and Nawrot, Mark. “Real-time color frame animation for visual
psychophysics on the Macintosh computer,” Behavior Research Methods,
Instrumentation and Computers, 1992, v.24, p.439ff.
Steinman, Scott B. and Nawrot, Mark. “Software for the generation and display of
random-dot cinematograms on the Macintosh computer,” Behavior Re search
Methods, Instrumentation and Computers, 1992, v.24, p.573ff.
“Think Reference,” version 2.0, Symantec Corporation.
van Brink, David. “All about the Palette Manager,” Develop, January 1990, p.22ff.
Wenderoth, Peter. “Software-based visual psychophysics using the Commodore
Amiga with Deluxe Paint III,” Behavior Research Methods, Instruments &
Computers, 1990, v.22, p.383ff.
Program Listings
As with all articles in MacTech Magazine, only the most relevant code is shown
here. For complete listings, see the MacTech Magazine Source Code disk or our online
areas. For more information, see page 2 of this issue.
Excerpts from Listing: Main.c
// •----------- Set Monitor Color Depth ------
// • Changes monitor depth to match
// • desired depth of animation frames.
static void SetMonitorDepth( void )
{
PixMapHandle pmh = NullHandle;
gDevice = GetMainDevice();
if (gSettings.numMonitors == 2 &&
gSettings.displayMonitor == kSecondary)
gDevice = GetNextDevice( gDevice );
HLock( (Handle) gDevice );
pmh = (**gDevice).gdPMap;
HLock( (Handle) pmh );
gSettings.currDepth = (**pmh).pixelSize;
HUnlock( (Handle) pmh );
HUnlock( (Handle) gDevice );
if (HasDepth( gDevice, kDepth, 1, 1 ) != 0)
SetDepth( gDevice, kDepth, 1, 1 );
else
ErrorHandler( kNoDepthMsg,
(char *) NilString, (char *) NilString,
(char *) NilString );
}
// • ---------- Free As Much Heap As Possible
// • Get more contiguous heap space.
void MaximizeHeap( const long bytesNeeded )
{
long totalBytes, contigBytes;
// • If enough contiguous heap space, exit.
if (MaxBlock() >= bytesNeeded)
return;
// • See how much space is possible.
PurgeSpace( &totalBytes, &contigBytes );
// • Free all purgeable, unlocked blocks and compact memory.
PurgeMem( totalBytes );
CompactMem( totalBytes );
}
Listing: Animation.c
// •••••••••••••••••••••••••••••••••••••••••••
// • Program: FrameAnim File: Animation.c
// • © 1993 by Dr. Scott Steinman. All Rights Reserved.
// •••••••••••••••••••••••••••••••••••••••••••
#include "FrameAnim.h
// • ---------- External Globals -------------
// • From Main.c file
extern CWindowPtr gMainWindow;
extern GWorldPtr gFrames[];
extern PaletteHandle gPalette;
extern CTabHandle gCTable;
extern Settings gSettings;
// • From Files.c file
extern PicHandle gFramePICTs[];
// • From VBlanks.c file
extern XVBLTask gXVBLTask;
// • ---------- Globals ----------------------
// • GWorld bounds
Rect gGlobBounds, // • Global coords
gBounds; // • Local coords
// •----------- Static Variables -------------
static Rect sDrawArea, // • Frame bounds
sTarget; // • Target square
static Point sOrigin, // • Window center
sCenter; // • Frame center
static short sRectSize; // • Target width
static Boolean sIncrSize; // • T=grow,F=shrink
// • ---------- Static Functions -------------
static void PlaceTargets( const short );
static void SetDrawCoords( void );
// • ---------- Find Drawing Origin ----------
// • Find center of display window.
void FindOrigin( void )
{
short v, h; // • Size of display window
h = gMainWindow->portRect.right -
gMainWindow->portRect.left;
v = gMainWindow->portRect.bottom -
gMainWindow->portRect.top;
SetPt( &sOrigin, h / 2, v / 2 );
}
// • ---------- Set Drawing Coordinates ------
// • Set up drawing bounds for GWorlds and display window
// • in local and global coordinates.
static void SetDrawCoords( void )
{
short radius;
Point topLeft, botRight;
radius = gSettings.frameSize / 2; // • Half-width of frame
// • Set up drawing area and center point inGWorlds
SetRect( &gBounds, 0, 0, gSettings.frameSize,
gSettings.frameSize );
SetPt( &sCenter, gSettings.frameSize / 2,
gSettings.frameSize / 2 );
// • Set local coordinates in window for GWorld copying
SetRect( &sDrawArea, sOrigin.h - radius, sOrigin.v - radius,
sOrigin.h + radius, sOrigin.v + radius );
// • Convert GWorld drawing area to global screen
// • coordinates (Used in NewGWorld for faster
// • CopyBits operations).
SetPt( &topLeft, sDrawArea.left, sDrawArea.top );
LocalToGlobal( &topLeft );
SetPt( &botRight, sDrawArea.right, sDrawArea.bottom );
LocalToGlobal( &botRight );
SetRect( &gGlobBounds, topLeft.h, topLeft.v,
botRight.h, botRight.v );
}
// • ---------- Allocate Memory for Frames ---
// • Inits blocks of memory for the GWorlds to be stored.
void PrepareFilm( void )
{
GDHandle currentDev = NullHandle;
CGrafPtr currentPort = NullPointer;
QDErr qderror;
short i;
Str255 errStr;
// • Save cur port & device
GetGWorld( & currentPort, & currentDev );
UpdateWindowColors(); // • Ensure palette is current
// • Free any previously-allocated animation frame GWorlds.
DisposeFrames();
// • Set bounds for drawing into GWorlds and window.
SetDrawCoords();
// • Allocate space on heap for new GWorlds.
qderror = NewGWorld( &gFrames[ 0 ], 0, &gGlobBounds,
NullHandle, NullHandle, (GWorldFlags) NoFlags );
if (qderror != noErr) {
ErrNumToPstr( qderror, (char *) errStr );
ErrorHandler( kNoGWorldMsg, (char *) "\pErrorCode = ",
(char *) errStr, (char *) NilString );
}
if (gFrames[ 0 ] == NullPointer)
ErrorHandler( kNoMemoryMsg, (char *) NilString,
(char *) NilString, (char *) NilString );
// • Move GWorld PixMaps to top of heap
// • to maximize contiguous memory.
MoveHHi( (Handle) GetGWorldPixMap( gFrames[ 0 ] ) );
// • Prevent freeing of PixMap memory until we're ready.
NoPurgePixels( GetGWorldPixMap( gFrames[ 0 ] ) );
// • Get as much contiguous heap space
// • for the GWorlds as possible.
MaximizeHeap( (GetPtrSize( gFrames[ 0 ] ) +
GetHandleSize( GetGWorldPixMap( gFrames[ 0 ] ) ) )
* kMaxFrames );
for (i = 1; i < gSettings.numFrames; i++) {
qderror = NewGWorld( &gFrames[ i ], 0,
&gGlobBounds, NullHandle,
GetGWorldDevice( gFrames[ 0 ] ), noNewDevice );
if (qderror != noErr) {
ErrNumToPstr( qderror, (char *) errStr );
ErrorHandler( kNoGWorldMsg, (char *)
"\pErrorCode = ", (char *) errStr,
(char *) NilString );
}
if (gFrames[ i ] == NullPointer)
ErrorHandler( kNoMemoryMsg, (char *) NilString,
(char *) NilString, (char *) NilString );
MoveHHi( (Handle) GetGWorldPixMap( gFrames[ i ] ) );
NoPurgePixels( GetGWorldPixMap( gFrames[ i ] ) );
}
SetGWorld( currentPort, currentDev );
// • Restore port & device
UpdateGWorldColors();
}
// • ---------- Plot Targets in Frames -------
// • Draws rectangle target inside frame
// • for demonstrating animation.
static void PlaceTargets( const short frm )
{
RGBColor rgbTarget, rgbBkgnd;
if (sIncrSize) {
sRectSize += gSettings.sizeDiff;
if (sRectSize >=
gSettings.frameSize - 10) {
sRectSize -= gSettings.sizeDiff * 2;
sIncrSize = false;
}
}
else {
sRectSize -= gSettings.sizeDiff;
if (sRectSize <= 0) {
sRectSize += gSettings.sizeDiff * 2;
sIncrSize = true;
}
}
SetRect( &sTarget, sCenter.h -
(sRectSize / 2), sCenter.v -
(sRectSize / 2), sCenter.h +
(sRectSize / 2), sCenter.v +
(sRectSize / 2) );
PenSize( 2, 2 ); // • Set outline width of target rectangle.
PmForeColor( kPalBkgnd ); // • Clear bkgnd
PaintRect( &gBounds );
PmForeColor( kPalTarget ); // • Draw outline
FrameRect( &sTarget );
ResetForeAndBackColors();
// • Get background and target colors
GetEntryColor( gPalette, kPalBkgnd, &rgbBkgnd );
GetEntryColor( gPalette, kPalTarget, &rgbTarget );
// • Repeat drawing into Pict for saving to file. Use
// • Color Mgr routines instead of Palette Mgr routines.
gFramePICTs[ frm ] = OpenPicture( &gBounds );
PenSize( 2, 2 );
RGBForeColor( &rgbBkgnd );
PaintRect( &gBounds );
RGBForeColor( &rgbTarget );
PaintRect( &sTarget );
ClosePicture();
ResetForeAndBackColors();
}
// • ---------- Record Frames in Film Sequence
// • Records the frames which are later shown with PlayFilm.
void DoFilm( void )
{
GDHandle currentDev = NullHandle;
CGrafPtr currentPort = NullPointer;
short i;
sRectSize = 0;
sIncrSize = true;
PrepareFilm(); // • Allocate film storage.
GetGWorld( & currentPort, & currentDev );
// • Save current port & device
for (i = 0; i < gSettings.numFrames; i++) {
// • Draw frame area directly into GWorld
LockPixels( GetGWorldPixMap( gFrames[ i ] ) );
SetGWorld( gFrames[ i ], NullPointer );
PlaceTargets( i );
SetGWorld( currentPort, currentDev );
// • Reset port & device
UnlockPixels( GetGWorldPixMap( gFrames[ i ] ) );
ResetForeAndBackColors();
}
SysBeep( 1 );
}
// • ------------ Show Recorded Sequence -----
// • Play a pre- recorded animation sequence.
void PlayFilm( void )
{
long finalTicks;
short i;
ResetForeAndBackColors();
HideCursor(); // • Get cursor out of the way
for (i = 0; i < gSettings.numFrames; i++)
LockPixels( GetGWorldPixMap( gFrames[ i ] ) );
// • Insert VBlank task into interrupt queue.
SetUpVBlankAnimation();
// • Allow VBlank task to execute.
gXVBLTask.vbl.vblCount = 1;
// • On each VBlank copy frame to the screen.
while (gXVBLTask.frameIndex < gSettings.numFrames) {
if (gXVBLTask.showFrame == true) {
gXVBLTask.showFrame = false;
CopyBits( &gFrames[ gXVBLTask.frameIndex ]->portPixMap,
&gMainWindow->portPixMap, &gBounds,
&sDrawArea, srcCopy, NullHandle );
}
}
// • Remove VBlank task from interrupt queue.
ShutDownVBlankAnimation();
for (i = 0; i < gSettings.numFrames; i++)
UnlockPixels( GetGWorldPixMap( gFrames[ i ] ) );
// • Clear the screen when finished.
Delay( 5, &finalTicks );
PmForeColor( kPalBkgnd );
PaintRect( &(gMainWindow->portRect) ); // • Clear win bkgd
ResetForeAndBackColors();
ShowCursor(); // • Put the cursor back out
}
// • ---------- Update Frame GWorld Palettes
// • Sets gray levels of GWorlds.
void UpdateGWorldColors( void )
{
GDHandle currentDev = NullHandle;
CGrafPtr currentPort = NullPointer;
short i;
GetGWorld( & currentPort, & currentDev );
// • Save current port & device
for (i = 0; i < gSettings.numFrames; i++) {
if (gFrames[ i ] != NullPointer) {
SetGWorld( gFrames[ i ], NullPointer );
NSetPalette( (WindowPtr) gFrames[ i ],
gPalette, true );
ActivatePalette( (WindowPtr) gFrames[ i ] );
}
}
SetGWorld( currentPort, currentDev );
// • Restore port & device
}
// • ---------- Dispose of the Frame GWorlds
// • Dispose of the animation frame GWorlds.
void DisposeFrames( void )
{
short i;
for (i = 0; i < kMaxFrames; i++) {
if (gFrames[ i ] != NullPointer) {
DisposeGWorld( gFrames[ i ] );
gFrames[ i ] = NullPointer;
}
}
}
// • ---------- Reset Foregnd & Bkgnd Colors
void ResetForeAndBackColors( void )
{
PmForeColor( kPalBlack );
PmBackColor( kPalWhite );
}
// • ---------- Round Up a Number ------------
// • Rounds up a floating-point number
// • into next highest whole integer.
short RoundUp( const double inputNumber )
{
short wholeNumber, roundNumber;
wholeNumber = (short) inputNumber;
if (inputNumber - wholeNumber == 0.0)
return( wholeNumber );
else
return( wholeNumber + 1 );
}
excerpts from Listing: VBlank.c
// • ---------- Increment Frame Number -------
// • ---------- (VBlank Task Routine) --------
#pragma options(assign_registers,redundant_loads)
// • Increment frame number of pre- recorded animation sequence
// • in sync with vertical blank interrupt.
void VideoProc( void )
{
long currA5;
XVBLTaskPtr theVBLTask;
// • Get our task record from A0 register
theVBLTask = (XVBLTaskPtr) GetVBLRec();
currA5 = SetA5( theVBLTask->a5 );
if (theVBLTask->frameIndex < theVBLTask->frames) {
theVBLTask->frameIndex++; // • Set next frame #
theVBLTask->showFrame = true; // • Show this frame
// • Reset VBL countdown
theVBLTask->vbl.vblCount = theVBLTask->frameDelay;
}
else {
theVBLTask->showFrame = false; // • Don't display frame
theVBLTask->vbl.vblCount = 0; // • Inhibit VBL task
}
currA5 = SetA5( currA5 );
}
Listing: Files.c
// •••••••••••••••••••••••••••••••••••••••••••
// • Program: FrameAnim File: Files.c
// • © 1993 by Dr. Scott Steinman. All Rights Reserved.
// •••••••••••••••••••••••••••••••••••••••••••
// • NOTES: Animation files are saved in a PICS
// • file format that includes:
// • (a) One 'INFO' resource for playback information.
// • (b) One 'PICT' (PICT2) resource for each stored image.
// •••••••••••••••••••••••••••••••••••••••••••
#include "FrameAnim.h
#include
#include
#include
// • ---------- External Globals -------------
// • From Main.c file
extern GWorldPtr gFrames[];
extern PaletteHandle gPalette;
extern CTabHandle gCTable;
extern Handle gMenuBar;
extern Settings gSettings;
extern Flags gFlags;
// • From Animation.c file
extern Rect gBounds;
// • ---------- Internal Globals -------------
PicHandle gFramePICTs[ kMaxFrames ]; // • Frame Pictures
// •----------- Static Variables -------------
static PICSInfoHandle sPicInfo; // • Animation parameters
// • ---------- Static Functions -------------
static void DrawPICTToGWorld( const short );
static void NewColorsFromPICT( void );
static void ReadFilm( const StandardFileReply * );
static void WriteFilm( const StandardFileReply * );
// • ---------- Save Frame Images in File ----
// • Save an animation sequence to animation file on disk.
void SaveFilm( void )
{
StandardFileReply theReply;
Str255 thePrompt, theDefaultName;
strcpy( (char *) thePrompt,
(char *) "\pSave Animation File as..." );
strcpy( (char *) theDefaultName, (char *) "\p" );
StandardPutFile( &thePrompt, &theDefaultName, &theReply );
if (theReply.sfGood) {
FSpCreateResFile( &theReply.sfFile, 'FRMA',
'PICS', theReply.sfScript );
WriteFilm( &theReply );
}
}
// • --------- Write the Frames to File ------
// • Writes entire animation sequence
// • to animation file on disk.
static void WriteFilm( const StandardFileReply *theReply )
{
OSErr resultCode;
short resFile, i;
Str255 frameText;
// • Transfer animation information to PicsInfo record
sPicInfo = (PICSInfoHandle)
NewHandle( sizeof( PICSInfoRec ) );
if (sPicInfo == NullHandle)
ErrorHandler( kNoMemoryMsg, (char *) NilString,
(char *) NilString, (char *) NilString );
HLock( (Handle) sPicInfo );
(**sPicInfo).speed = RoundUp(
kRefreshRate / gSettings.frameDelay );
(**sPicInfo).depth = kDepth;
(**sPicInfo).version = 0;
(**sPicInfo).creator = 'FRMA';
(**sPicInfo).largest = 0; // • Not used.
(**sPicInfo).bwColor = 1;
HUnlock( (Handle) sPicInfo );
// • Write the animation info and frames
// • to resource fork of file
resFile = FSpOpenResFile( &theReply->sfFile, fsWrPerm );
AddResource( sPicInfo, 'INFO', kPICSInfoID,
"\pAnimation Parameters" );
for (i = 0; i < gSettings.numFrames; i++) {
PtoCstr( (char *) frameText );
sprintf( (char *) frameText,
"Animation Frame %d of %ld", i + 1,
gSettings.numFrames );
CtoPstr( (char *) frameText );
AddResource( gFramePICTs[ i ], 'PICT',
kFirstPictID + i, frameText );
UpdateResFile( resFile );
// • Force write of resource to file.
}
CloseResFile( resFile );
// • Null handles to resources that
// • have been freed by CloseResFile
sPicInfo = NullHandle;
for (i = 0; i < gSettings.numFrames; i++)
gFramePICTs[ i ] = NullHandle;
DisableItem( GetMHandle( kFileID ), kSaveFilmItem ); }
// • ---------- Open Animation Sequence File
// • Retrieve entire animation sequence
// • from animation file on disk.
void OpenFilm( void )
{
SFTypeList typeList;
StandardFileReply theReply;
short numTypes;
numTypes = 1; // • Only one type of file
typeList[ 0 ] = 'PICS'; // • Only Anim files
StandardGetFile( NullPointer, numTypes,
&typeList, &theReply );
if (theReply.sfGood)
ReadFilm( &theReply );
else
gFlags.cancel = true;
}
// • ---------- Read the Frames from File ----
// • This does the opposite of WriteFilm.
static void ReadFilm( const StandardFileReply *theReply )
{
PictInfo pictInfo;
OSErr err;
short resFile, i;
// • Free previously-allocated color records
if (gCTable != NullHandle) {
DisposCTable( gCTable );
gCTable = NullHandle;
}
if (gPalette != NullHandle) {
DisposePalette( gPalette );
gPalette = NullHandle;
}
// • Read the animation information resource.
resFile = FSpOpenResFile( &theReply->sfFile, fsRdPerm );
sPicInfo = (PICSInfoHandle) Get1Resource(
'INFO', kPICSInfoID );
// • Pull out frame delay from PICS resource
// • (other fields ignored here)
HLock( (Handle) sPicInfo );
gSettings.frameDelay = RoundUp(
kRefreshRate / (**sPicInfo).speed );
HUnlock( (Handle) sPicInfo );
// • Prepare new frame GWorlds.
PrepareFilm();
// • Find the # frame image PICT resources.
gSettings.numFrames = Count1Resources( 'PICT' );
// • Copy contents of first PICT rsrc to first GWorld.
DisposeFramePICTs();
gFramePICTs[ 0 ] = GetPicture( kFirstPictID );
DetachResource( gFramePICTs[ 0 ] );
// • Get frame size info from first PICT.
err = GetPictInfo( gFramePICTs[ 0 ], &pictInfo,
returnColorTable+suppressBlackAndWhite,
kNumColors, systemMethod, 0 );
gSettings.frameSize = pictInfo.sourceRect.right -
pictInfo.sourceRect.left;
// • Get color info from PICT to reset
// • color table and palette.
HLock( (Handle) pictInfo.theColorTable );
gSettings.bkgndGray = (**pictInfo.theColorTable).ctTable[ 0
].rgb.red / 256;
gSettings. targetGray = (**pictInfo.theColorTable).ctTable[ 1
].rgb.red / 256;
HUnlock( (Handle) pictInfo.theColorTable );
DisposCTable( pictInfo.theColorTable );
NewColorsFromPICT(); // • Create & init color table/palette.
UpdateWindowColors();
ResetForeAndBackColors();
ResetWindBkgnd();
DrawPICTToGWorld( 0 ); // • Transfer frame image to GWorld.
for (i = 1; i < gSettings.numFrames; i++) {
gFramePICTs[ i ]= GetPicture( kFirstPictID + i );
DetachResource( gFramePICTs[ i ] );
DrawPICTToGWorld( i );
}
ResetForeAndBackColors();
CloseResFile( resFile );
sPicInfo = NullHandle;
for (i = 1; i < gSettings.numFrames; i++)
gFramePICTs[ i ] = NullHandle;
SysBeep( 1 );
}
// • ---------- Create & Init CTable From PICT
// • Create and initialize color table and palette using
// • background and target colors found in animation file.
static void NewColorsFromPICT( void )
{
RGBColor c;
short i;
// • Create new CTable and palette.
gCTable = GetCTable( kGrayCTableID );
DetachResource( gCTable );
(*gCTable)->ctFlags |= 0x4000; // • Speeds up later CopyBits
gPalette = NewPalette( kNumColors, gCTable,
pmTolerant+pmExplicit, 0 );
// • Initialize palette entries.
c.red = c.green = c.blue = 0;
for (i = 0; i < kNumColors; i++) {
SetEntryColor( gPalette, i, &c );
SetEntryUsage( gPalette, i, pmTolerant+pmExplicit, 0 );
}
c.red = c.green = c.blue = 65535;
SetEntryColor( gPalette, kPalWhite, &c );
// • Force palette color entries into CTable.
Palette2CTab( gPalette, gCTable );
}
// • ---------- Draw PICT into GWorld --------
// • Draw the contents of an animation file into the GWorlds.
static void DrawPICTToGWorld( const short frm )
{
GDHandle currentDev = NullHandle;
CGrafPtr currentPort = NullPointer;
GetGWorld( & currentPort, & currentDev );
// • Save current port & device
if (gFramePICTs[ frm ] != NullHandle) {
HLock( (Handle) gFramePICTs[ frm ]);
LockPixels( GetGWorldPixMap( gFrames[ frm ] ) );
SetGWorld( gFrames[ frm ], NullPointer );
DrawPicture( gFramePICTs[ frm ], &gBounds );
SetGWorld( currentPort, currentDev );
// • Reset port & device
UnlockPixels( GetGWorldPixMap( gFrames[ frm ] ) );
HUnlock( (Handle) gFramePICTs[ frm ]);
}
}
// • --------- Dispose of the Frame PICT -----
// • Dispose of the animation Picture record.
void DisposeFramePICTs( void )
{
short i;
for (i = 0; i < kMaxFrames; i++) {
if (gFramePICTs[ i ]!= NullHandle) {
KillPicture( gFramePICTs[ i ]);
gFramePICTs[ i ]= NullHandle;
}
}
}