TweetFollow Us on Twitter

Mar 01 QTToolkit Volume Number: 17 (2001)
Issue Number: 3
Column Tag: QuickTime Toolkit

A Goofy Movie

By Tim Monroe

Working with Sprites in QuickTime Movies

Introduction

The major new technology introduced in QuickTime version 2.1 (released in 1995) was support for sprites and sprite animation. A sprite is a graphical object that has a number of properties, including its current image, location, size, layer, graphics mode, and visibility state. These properties determine the appearance of the sprite at any instant in time. By varying one or more of these properties over time, we can animate the sprite. For instance, Figure 1 shows the first frame of a QuickTime movie that contains one sprite, whose image is the original icon for the QuickTime system extension.


Figure 1. A movie with a sprite

We can move the icon to the right by gradually changing the horizontal location of the sprite. In addition, we can change the image associated with the sprite at any time. In this case, we'll change the icon from the old QuickTime extension icon to the new QuickTime extension icon when the sprite gets to the halfway point, as shown in Figure 2. The new icon then continues moving at the same rate until it reaches the right side of the movie box.


Figure 2. The sprite with a new image

Figure 3 shows another sprite movie. In this case, there is only one image associated with the sprite for the entire duration of the movie, and the sprite location remains constant. We'll animate this sprite by changing the graphics mode, from totally transparent to totally opaque. What we've done here is recreate, using sprite animation, the appearing-penguin movie that was the very first QuickTime movie we built in this series of articles (see "Making Movies" in MacTech, June 2000).


Figure 3. The penguin movie using sprite animation

Recall how we went about creating the first version of our penguin movie: we opened a picture resource and drew that picture into an offscreen graphics world; we compressed the data in that graphics world and added it as a frame to the movie file. Then we repeated the drawing and compressing 99 times, each time with gradually more opacity, to create 99 additional movie frames. The resulting movie file contained 100 compressed images, for a total size of about 472 kilobytes.

Using sprite animation, we can reduce that size dramatically. Our new penguin movie file contains only a single compressed image and 100 sets of "instructions" that indicate the desired level of opacity in each movie frame. The 100 movie frames are generated at playback time by the sprite media handler from that image and those 100 sets of instructions. The size of the sprite version of the penguin movie is only about 36 kilobytes. (No, that's no typo; the exact same visual output can be achieved with a file less than one tenth the size of our original movie file.)

Already you can see that sprite animation differs significantly from what's often called cel animation, where each frame of the animation is a fully-rendered picture of some characters superimposed on a background image. If cel animation is like a recorded symphony, then sprite animation is more like a set of sounds together with instructions for playing back those sounds in the right order and at the right time. If the instructions in a sprite movie aren't too complicated, they can be executed at runtime just as smoothly as decompressing and playing back the fully-rendered version of the movie. And, as we've seen, the images and instructions can take up a lot less space.

QuickTime 3.0 extended the capabilities of the sprite media handler by adding support for wired sprites, or sprites that react to mouse events (and other kinds of events) and that have various actions attached (or "wired") to them. For instance, we can use a wired sprite to control the properties of the sprites in the movie (so that clicking on one sprite causes another sprite to disappear or to change location). Or, we can control various aspects of movie playback, such as the volume and balance of a sound track, or the graphics mode of a video track. By using wired sprites in a movie, we can add a level of interactivity previously unavailable in QuickTime.

In this article, we're going to learn how to create movies that contain sprite tracks. We'll see how to create both the icon movie shown in Figure 1 and the penguin movie shown in Figure 3. Our sample application this month is called QTSprites, and its Test menu is shown in Figure 4.


Figure 4. The Test menu of QTSprites

(The "space movie" is a more complex movie that does sprite animation using location changes, layer changes, and image changes. We won't learn how to build it in this article, but the code for doing so is contained in the file QTSprites.c.)

We'll begin by looking in more detail at sprites and their properties. Then we'll take a look at the structure of sprite tracks and see how to build the icon and penguin sprite movies. Toward the end, we'll see how to add some simple interactivity to our sprite movies without using wired sprites. We'll postpone our investigation of wired sprites to an upcoming QuickTime Toolkit article.

Sprite Properties

In a QuickTime movie, a sprite is a graphical object that belongs to a sprite track (of type SpriteMediaType). In the simplest case, the basic appearance of a sprite is set by selecting one out of an array of images associated with the sprite. The current sprite image index is one of the five main sprite properties, defined using these constants:

enum {
   kSpritePropertyMatrix                           = 1,
   kSpritePropertyVisible                        = 4,
   kSpritePropertyLayer                           = 5,
   kSpritePropertyGraphicsMode                  = 6,
   kSpritePropertyImageIndex                     = 100
};

The sprite matrix is a 3-by-3 matrix that controls the location, size, and rotation of the sprite image within the sprite track. (A sprite's matrix is added to the track matrix of the sprite track.) The sprite visibility state is an integer value that controls whether the sprite is currently visible. (This value is interpreted as a Boolean value but is stored in the movie file as a 16-bit short integer.) The sprite layer is an integer value that determines, when two or more sprites have locations that overlap, which sprite is drawn on top of the other sprite(s). Sprites with lower layer values are drawn on top of sprites with higher layer values; if we want to ensure that some sprite is drawn behind all other overlapping sprites, we can set its layer to the special value kBackgroundSpriteLayerNum (appropriately defined in the file Movies.h as 32767). Finally, the sprite graphics mode determines how the sprite is drawn into the sprite track. We specify a sprite graphics mode using a structure of type ModifierTrackGraphicsModeRecord, defined like this:

struct ModifierTrackGraphicsModeRecord {
   long                         graphicsMode;
   RGBColor                   opColor;
};

This structure contains the QuickDraw graphics mode and the color used by some of those graphics modes. For instance, if graphicsMode is blend, then opColor specifies the weight color (which determines the amount of blending of the source and destination pixels).

A sprite's current image does not have to come from a sprite image array. Instead, the sprite media handler can use a video track in the same QuickTime movie as the source for the sprite's image. This allows us to create even more intricate animations than are possible by simply varying the five basic sprite properties. As a very simple example, we could create a sprite track containing two sprites, one whose image looks like a television set and a superimposed sprite whose images are derived from a video track. The net effect would be a sprite track containing a television playing the video.

Sprite Tracks

A sprite track consists of one or more sprite media samples. There are two basic kinds of sprite media samples: (1) those that define the sprite image array and set the initial properties of a frame, and (2) those that animate the sprites in the track by specifying changes to the sprites' properties. The sprite media handler relies on the distinction between key frames and difference frames, which we encountered in the previous article ("Honey, I Shrunk the Kids" in MacTech, February 2001). The image arrays and initial properties are stored in key frames, and the sprite property changes are stored in difference frames. The only departure here is purely terminological: when we're working with sprite data, the difference frames are called override frames (because the data in those frames overrides the data in the key frames).

The Format of Key Frame Samples

The data in both key frames and override frames is stored in atom containers. (Indeed, atom containers were introduced in QuickTime version 2.1 primarily for the purpose of organizing sprite media data.) A key frame atom container contains a child atom of type kSpriteAtomType for each sprite in the key frame. This atom contains leaf atoms that define the initial properties of the sprite. The atom IDs of the sprite atoms are numbered sequentially, starting at 1; these atom IDs are also called sprite IDs. Figure 5 shows the basic structure of a key frame sample.


Figure 5. The structure of a key frame sample

A sprite atom contains child atoms that define the initial properties of the sprite. It can contain a child for each of the five basic sprite properties, as well as an atom (of type kSpriteNameAtomType) that defines the sprite's name. Figure 6 shows the structure of a sprite atom.


Figure 6. The structure of a sprite atom

As Figure 5 indicates, a key frame atom container also contains a single child atom of type kSpriteSharedDataAtomType (with atom ID 1) which contains the image data for all the sprites. This atom contains one sprite images container atom, of type kSpriteImagesContainerAtomType (also with atom ID 1). This atom, in turn, contains one atom of type kSpriteImageAtomType for each individual image in the key frame. Note that all the images for all the sprites are contained in the single images container atom. Figure 7 shows the structure of a sprite shared data atom.


Figure 7. The structure of the shared sprite data

The Format of Override Samples

The structure of an override sample is somewhat simpler than that of a key frame sample, largely because an override sample does not contain any image data. An override sample is an atom container that contains a sprite atom (of type kSpriteAtomType) for each sprite that is being animated by that override sample. The sprite atoms contain child atoms for each of the properties that are changing from the previous key frame or override sample. Figure 8 shows the structure of a typical override sample.


Figure 8. The structure of an override sample

The ID of a sprite atom in the override sample should be the same as the ID of the sprite atom in the key frame atom whose data that the override atom is overriding.

Creating Sprite Tracks

As we've just learned, a sprite track consists of key frame samples that contain the images for the sprites in a track and the initial properties of those sprites, and override samples that change one or more of the properties of those sprites. In both cases, the sample data is contained in an atom container. Building a sprite track is therefore largely a matter of creating the appropriate atom containers and inserting them at the desired times in the sprite track media.

Creating Sprite Tracks and Media

When the user selects an item in the Test menu, QTSprites calls the QTApp_HandleMenu function, which is shown in Listing 1.

Listing 1: Handling items in the Test menu

QTApp_HandleMenu
Boolean QTApp_HandleMenu (UInt16 theMenuItem)
{
   Boolean            myIsHandled = false;

   switch (theMenuItem) {

      case IDM_MAKE_ICONS_MOVIE:
      case IDM_MAKE_PENGUIN_MOVIE:
      case IDM_MAKE_SPACE_MOVIE:
         QTSprites_CreateSpritesMovie(theMenuItem);
         myIsHandled = true;
         break;

      case IDM_USE_BACKGROUND_IMAGE:
         gUseBackgroundPicture = !gUseBackgroundPicture;
         myIsHandled = true;
         break;

   } // switch (theMenuItem)

   return(myIsHandled);
}

As you can see, we call the function QTSprites_CreateSpritesMovie to create each of the three sample movies, passing in the menu item so that we know which movie to create. QTSprites_CreateSpritesMovie is defined in Listing 2.

Listing 2: Creating a sprite movie

QTSprites_CreateSpritesMovie
OSErr QTSprites_CreateSpritesMovie (UInt16 theMenuItem)
{
   Movie               myMovie = NULL;
   Track               myTrack = NULL;
   Media               myMedia = NULL;
   FSSpec               myFile;
   Boolean            myIsSelected = false;
   Boolean            myIsReplacing = false;
   Fixed               myHeight = 0;
   Fixed               myWidth = 0;
   StringPtr          myPrompt = 
      QTUtils_ConvertCToPascalString(kSpriteSavePrompt);
   StringPtr          myFileName = 
      QTUtils_ConvertCToPascalString(kSpriteSaveMovieFileName);
   long                  myFlags = createMovieFileDeleteCurFile | 
                                    createMovieFileDontCreateResFile;
   short               myResRefNum = 0;
   short               myResID = movieInDataForkResID;
   OSErr               myErr = noErr;

   // prompt the user for the destination file name
   QTFrame_PutFile(myPrompt, myFileName, &myFile, 
                                 &myIsSelected, &myIsReplacing);
   myErr = myIsSelected ? noErr : userCanceledErr;
   if (!myIsSelected)
      goto bail;

   // create a movie file for the destination movie
   myErr = CreateMovieFile(&myFile, FOUR_CHAR_CODE('TVOD'), 
               smSystemScript, myFlags, &myResRefNum, &myMovie);
   if (myErr != noErr)
      goto bail;

   // create the sprite track and media
   QTSprites_GetMovieSize(theMenuItem, &myHeight, &myWidth);

   myTrack = NewMovieTrack(myMovie, myWidth, myHeight,
                                       kNoVolume);
   myMedia = NewTrackMedia(myTrack, SpriteMediaType, 
                                       kSpriteMediaTimeScale, NULL, 0);

   BeginMediaEdits(myMedia);

   // add the appropriate samples to the sprite media
   switch (theMenuItem) {
      case IDM_MAKE_ICONS_MOVIE:
         QTSprites_AddIconMovieSamplesToMedia(myMedia);
         break;
      case IDM_MAKE_PENGUIN_MOVIE:
         QTSprites_AddPenguinMovieSamplesToMedia(myMedia);
         break;
      case IDM_MAKE_SPACE_MOVIE:
         QTSprites_AddSpaceMovieSamplesToMedia(myMedia);
         break;
      default:
         goto bail;
   }

   EndMediaEdits(myMedia);

   // add the media to the track
   InsertMediaIntoTrack(myTrack, 0, 0, 
                           GetMediaDuration(myMedia), fixed1);

   // set the sprite track properties
   QTSprites_SetTrackProperties(myMedia, theMenuItem);

   // add the movie resource to the movie file
   myErr = AddMovieResource(myMovie, myResRefNum, &myResID, 
                     myFile.name);

bail:
   if (myResRefNum != 0)
      CloseMovieFile(myResRefNum);

   if (myMovie != NULL)
      DisposeMovie(myMovie);

   free(myPrompt);
   free(myFileName);

   return(myErr);
}

The QTSprites_CreateSpritesMovie function is remarkably similar to each of the other movie-creation functions we've used earlier in this series of articles (compare, for instance, the QTMM_CreateVideoMovie function in "Making Movies" in MacTech, June 2000). There are only three main additions for QTSprites. First, since we want to be able to create any one of three different sprite movies, we call the function QTSprites_GetMovieSize to get the desired size for each of those movies. QTSprites_GetMovieSize is defined in Listing 3.

Listing 3: Getting the size of a sprite movie

QTSprites_GetMovieSize
void QTSprites_GetMovieSize (UInt16 theMenuItem, 
                                    Fixed *theHeight, Fixed *theWidth)
{
   if ((theHeight == NULL) || (theWidth == NULL))
      return;

   switch (theMenuItem) {
      case IDM_MAKE_ICONS_MOVIE:
         *theWidth = (long)kIconSpriteTrackWidth << 16;
         *theHeight = (long)kIconSpriteTrackHeight << 16;
         break;
      case IDM_MAKE_PENGUIN_MOVIE:
         *theWidth = (long)kPenguinSpriteTrackWidth << 16;
         *theHeight = (long)kPenguinSpriteTrackHeight << 16;
         break;
      case IDM_MAKE_SPACE_MOVIE:
         *theWidth = (long)kSpaceSpriteTrackWidth << 16;
         *theHeight = (long)kSpaceSpriteTrackHeight << 16;
         break;
   }
}

This is pretty simple stuff; we just convert some long integer constants to the Fixed data type and return them to the caller.

The second difference between QTSprites_CreateSpritesMovie and our earlier movie-creation functions is that we use the menu item number passed in to select the appropriate function for adding samples to the sprite media. And, third, once we've added those samples to the media, we call the QTSprites_SetTrackProperties function to set some sprite track properties. We'll consider sprite track properties in more detail.

Setting Sprite Properties

A key frame sample is an atom container that contains an atom of type kSpriteAtomType for each sprite in the frame (which contains the initial properties of the sprite) and an atom of type kSpriteSharedDataAtomType (which contains atoms that hold the sprite images). So the first thing we need to do is create an atom container, like this:

myErr = QTNewAtomContainer(&mySample);

Let's begin by adding the sprite atoms to the key frame sample. A sprite atom is itself an atom container, because it contains child atoms for each of the initial sprite properties we want to assign it. (Any properties we don't explicitly define in a key frame sample are set to default values.) So we need to create another atom container, like so:

myErr = QTNewAtomContainer(&mySpriteData);

Now we want to add one or more property atoms to the sprite atom. For the icon movie, we want to set the initial location, visibility state, layer, and image index. We can set the image index, for instance, like this:

short      myIndex = 1;

myIndex = EndianS16_NtoB(myIndex);
myErr = QTInsertChild(mySpriteData, kParentAtomIsContainer, 
               kSpritePropertyImageIndex, 1, 0, sizeof(short), 
               &myIndex, NULL);

This code inserts a child of type kSpritePropertyImageIndex into the sprite atom, making sure that the atom data (in this case, a short integer whose value is 1) is in big-endian format. Similarly, we can set the initial visibility state of the icon sprite using these lines of code:

short      isVisible = true;

isVisible = EndianS16_NtoB(isVisible);
myErr = QTInsertChild(mySpriteData, kParentAtomIsContainer, 
               kSpritePropertyVisible, 1, 0, sizeof(short), 
               &isVisible, NULL);

(You might have thought that the default value for the visibility state of a sprite would be true, but sadly that's not so. So we need to explicitly configure our sprites to be visible or they won't be drawn.)

To increase the readability of our code, we'll define a utility function called SpriteUtils_SetSpriteData that allows us to set the main sprite properties in one fell swoop. Then we can define the initial state of the icon sprite like this:

myLocation.h = 32;
myLocation.v = 32;
isVisible = true;
myLayer = -1;
myIndex = 1;

SpriteUtils_SetSpriteData(mySpriteData, &myLocation, 
               &isVisible, &myLayer, &myIndex, NULL, NULL, NULL);

The SpriteUtils_SetSpriteData function is defined in the file SpriteUtilities.c; its definition is shown in Listing 4.

Listing 4: Setting properties of a sprite

SpriteUtils_SetSpriteData
OSErr SpriteUtils_SetSpriteData (
         QTAtomContainer theSprite, 
         Point *theLocation,
         short *theVisible,
         short *theLayer, 
         short *theImageIndex,
         ModifierTrackGraphicsModeRecord *theGraphicsMode, 
         StringPtr theSpriteName, 
         QTAtomContainer theActionAtoms)
{
   QTAtom                  myPropertyAtom;
   OSErr                  myErr = noErr;

   // set the sprite location data
   if (theLocation != NULL) {
      MatrixRecord      myMatrix;

      SetIdentityMatrix(&myMatrix);
      myMatrix.matrix[2][0] = ((long)theLocation->h << 16);
      myMatrix.matrix[2][1] = ((long)theLocation->v << 16);
      EndianUtils_MatrixRecord_NtoB(&myMatrix);

      myPropertyAtom = QTFindChildByIndex(theSprite,                                       kParentAtomIsContainer, 
                                 kSpritePropertyMatrix, 1, NULL);
      if (myPropertyAtom == 0)
         myErr = QTInsertChild(theSprite, 
                              kParentAtomIsContainer, 
                              kSpritePropertyMatrix, 1, 0, 
                              sizeof(MatrixRecord), &myMatrix, NULL);
      else
         myErr = QTSetAtomData(theSprite, myPropertyAtom, 
                              sizeof(MatrixRecord), &myMatrix);

      if (myErr != noErr)
         goto bail;
   }

   // set the sprite visibility state
   if (theVisible != NULL) {
      short          myVisible = *theVisible;

      myVisible = EndianS16_NtoB(myVisible);

      myPropertyAtom = QTFindChildByIndex(theSprite, 
                              kParentAtomIsContainer, 
                              kSpritePropertyVisible, 1, NULL);
      if (myPropertyAtom == 0)
         myErr = QTInsertChild(theSprite, 
                              kParentAtomIsContainer, 
                              kSpritePropertyVisible, 1, 0, 
                              sizeof(short), &myVisible, NULL);
      else
         myErr = QTSetAtomData(theSprite, myPropertyAtom, 
                              sizeof(short), &myVisible);

      if (myErr != noErr)
         goto bail;
   }

   // set the sprite layer
   if (theLayer != NULL) {
      short          myLayer = *theLayer;

      myLayer = EndianS16_NtoB(myLayer);

      myPropertyAtom = QTFindChildByIndex(theSprite, 
                              kParentAtomIsContainer, 
                              kSpritePropertyLayer, 1, NULL);
      if (myPropertyAtom == 0)
         myErr = QTInsertChild(theSprite, 
                              kParentAtomIsContainer, 
                              kSpritePropertyLayer, 1, 0, 
                              sizeof(short), &myLayer, NULL);
      else
         myErr = QTSetAtomData(theSprite, myPropertyAtom, 
                              sizeof(short), &myLayer);

      if (myErr != noErr)
         goto bail;
   }

   // set the sprite image index
   if (theImageIndex != NULL) {
      short          myImageIndex = *theImageIndex;

      myImageIndex = EndianS16_NtoB(myImageIndex);

      myPropertyAtom = QTFindChildByIndex(theSprite, 
                              kParentAtomIsContainer, 
                              kSpritePropertyImageIndex, 1, NULL);
      if (myPropertyAtom == 0)
         myErr = QTInsertChild(theSprite, 
                              kParentAtomIsContainer, 
                              kSpritePropertyImageIndex, 1, 0, 
                              sizeof(short), &myImageIndex, NULL);
      else
         myErr = QTSetAtomData(theSprite, myPropertyAtom, 
                              sizeof(short), &myImageIndex);

      if (myErr != noErr)
         goto bail;
   }

   // set the sprite graphics mode
   if (theGraphicsMode != NULL) {
      ModifierTrackGraphicsModeRecord      myGraphicsMode;

      myGraphicsMode.graphicsMode = 
                  EndianU32_NtoB(theGraphicsMode->graphicsMode);
      myGraphicsMode.opColor.red = 
                  EndianU16_NtoB(theGraphicsMode->opColor.red);
      myGraphicsMode.opColor.green = 
                  EndianU16_NtoB(theGraphicsMode->opColor.green);
      myGraphicsMode.opColor.blue = 
                  EndianU16_NtoB(theGraphicsMode->opColor.blue);

      myPropertyAtom = QTFindChildByIndex(theSprite, 
                           kParentAtomIsContainer, 
                           kSpritePropertyGraphicsMode, 1, NULL);
      if (myPropertyAtom == 0)
         myErr = QTInsertChild(theSprite, 
                           kParentAtomIsContainer, 
                           kSpritePropertyGraphicsMode, 1, 0, 
                           sizeof(myGraphicsMode), &myGraphicsMode, 
                           NULL);
      else
         myErr = QTSetAtomData(theSprite, myPropertyAtom, 
                           sizeof(myGraphicsMode), &myGraphicsMode);

      if (myErr != noErr)
         goto bail;
   }

   // set the sprite name
   if (theSpriteName != NULL) {
      QTAtom       mySpriteNameAtom;

      mySpriteNameAtom = QTFindChildByIndex(theSprite, 
                                 kParentAtomIsContainer, 
                                 kSpriteNameAtomType, 1, NULL);
      if (mySpriteNameAtom == 0)
         myErr = QTInsertChild(theSprite, 
                                 kParentAtomIsContainer, 
                                 kSpriteNameAtomType, 1, 0, 
                                 theSpriteName[0] + 1, theSpriteName, 
                                 NULL);
      else
         myErr = QTSetAtomData(theSprite, mySpriteNameAtom, 
                                 theSpriteName[0] + 1, theSpriteName);

      if (myErr != noErr)
         goto bail;
   }

   // set the action atoms
   if (theActionAtoms != NULL)
      myErr = QTInsertChildren(theSprite, 
                              kParentAtomIsContainer, theActionAtoms);

bail:
   if ((myErr != noErr) && (theSprite != NULL))
      QTRemoveChildren(theSprite, 0);

   return(myErr);
}

For each parameter that is not NULL, SpriteUtils_SetSpriteData looks to see whether the sprite atom container already contains an atom of that the corresponding type (by calling QTFindChildByIndex). If it does contain such an atom, then SpriteUtils_SetSpriteData calls QTSetAtomData to reset the data in that atom; otherwise, it calls QTInsertChild to add an atom of that type. In all cases, the data passed in is converted to big-endian format before being inserted into an atom.

Now that the mySpriteData atom container holds a child atom for each initial property we want to set, we need to add it to the key frame sample atom container, mySample. Once again, we'll define a utility function to help us out:

SpriteUtils_AddSpriteToSample(mySample, mySpriteData, 
kQTIconSpriteAtomID);

SpriteUtils_AddSpriteToSample is defined in Listing 5.
Listing 5: Adding a sprite data atom to a sample container
SpriteUtils_AddSpriteToSample
OSErr SpriteUtils_AddSpriteToSample 
      (QTAtomContainer theSample, QTAtomContainer theSprite, 
         QTAtomID theSpriteID)
{
   QTAtom            mySpriteAtom = 0;
   OSErr            myErr = paramErr;

   // see if the sample already contains a sprite atom of the specified ID
   mySpriteAtom = QTFindChildByID(theSample, 
                           kParentAtomIsContainer, 
                           kSpriteAtomType, theSpriteID, NULL);
   if (mySpriteAtom != 0)
      goto bail;

   // here, the index 0 means to append the sprite to the sample
   myErr = QTInsertChild(theSample, kParentAtomIsContainer, 
                           kSpriteAtomType, theSpriteID, 0, 0, NULL, 
                           &mySpriteAtom);
   if (myErr != noErr)
      goto bail;

   myErr = QTInsertChildren(theSample, mySpriteAtom, 
                           theSprite);

bail:
   return(myErr);
}

As you can see, SpriteUtils_AddSpriteToSample first calls QTFindChildByID to determine whether the specified atom container already contains a sprite atom with the specified sprite ID. If there is an atom of that ID already in the sample, SpriteUtils_AddSpriteToSample returns an error to the caller. Otherwise, it calls QTInsertChild to create a sprite atom of that ID in the sample atom container. Then it calls QTInsertChildren to copy the children from the sprite atom container passed as a parameter into the newly inserted atom in the sample.

Setting Sprite Images

So far, then, we've created a sprite atom that contains the desired initial properties of the icon sprite and added it to the key frame atom container mySample. Now we need to create an atom of type kSpriteSharedDataAtomType and add the required subatoms to it that contain the sprite image data. In the case of the icon sprite movie, we need to add two images to that atom, one for the old QuickTime extension icon and one for the new icon. The function QTSprites_AddIconMovieSamplesToMedia does this by executing these lines of code:

myKeyColor.red = myKeyColor.green = myKeyColor.blue = 0xffff;

SpriteUtils_AddPICTImageToKeyFrameSample(mySample, 
                     kOldQTIconID, &myKeyColor, 1, NULL, NULL);
SpriteUtils_AddPICTImageToKeyFrameSample(mySample, 
                     kNewQTIconID, &myKeyColor, 2, NULL, NULL);

Once again, we're relying on a function defined in the file SpriteUtilities.c to hide the nitty-gritty details of building the required atom from our main application. SpriteUtils_AddPICTImageToKeyFrameSample (defined in Listing 6) reads a picture from our application's resource fork, recompresses the picture with the specified color as a transparency color, and then adds it to the key frame sample atom container.

Listing 6: Adding compressed image data to a key frame sample

SpriteUtils_AddPICTImageToKeyFrameSample
OSErr SpriteUtils_AddPICTImageToKeyFrameSample 
         (QTAtomContainer theKeySample, short thePictID, 
            RGBColor *theKeyColor, QTAtomID theID, 
            FixedPoint *theRegistrationPoint, 
            StringPtr theImageName)
{
   PicHandle            myPicture = NULL;
   Handle                  myCompressedPicture = NULL;
   ImageDescriptionHandle   myImageDesc = NULL;
   OSErr                  myErr = noErr;
   
   // get picture from resource
   myPicture = (PicHandle)GetPicture(thePictID);
   if (myPicture == NULL)
      myErr = resNotFound;

   if (myErr != noErr)
      goto bail;
   
   DetachResource((Handle)myPicture);
   
   // convert it to image data compressed by the animation compressor
   myErr = ICUtils_RecompressPictureWithTransparency
               (myPicture, theKeyColor, NULL, &myImageDesc, 
               &myCompressedPicture);
   if (myErr != noErr)
      goto bail;

   // add it to the key sample
   HLock(myCompressedPicture);
   myErr = SpriteUtils_AddCompressedImageToKeyFrameSample
               (theKeySample, myImageDesc, 
               GetHandleSize(myCompressedPicture), 
               *myCompressedPicture, theID, theRegistrationPoint, 
               theImageName);

bail:
   if (myPicture != NULL)
      KillPicture(myPicture);

   if (myCompressedPicture != NULL)
      DisposeHandle(myCompressedPicture);

   if (myImageDesc != NULL)
      DisposeHandle((Handle)myImageDesc);

   return(myErr);
}

SpriteUtils_AddPICTImageToKeyFrameSample does most of its work by calling two other utility functions, ICUtils_RecompressPictureWithTransparency and SpriteUtils_AddCompressedImageToKeyFrameSample. We won't dissect either of these functions in detail here, as that would take us too far afield. SpriteUtils_AddCompressedImageToKeyFrameSample really just does what you'd imagine to build the atom container illustrated in Figure 7.

Adding the Key Frame Sample to the Sprite Media

So, we've managed to build the key frame sample. Now we just need to add it to the sprite media. To do this, we first need to create a handle to a sprite sample description, which we'll pass to AddMediaSample. A sprite sample description is defined by the SpriteDescription data type, declared like this:

struct SpriteDescription {
   long                      descSize;
   long                      dataFormat;
   long                      resvd1;
   short                      resvd2;
   short                      dataRefIndex;
   long                      version;
   OSType                   decompressorType;
   long                      sampleFlags;
};

We allocate a handle to a sprite sample description by calling NewHandleClear:

mySampleDesc = (SampleDescriptionHandle)
                           NewHandleClear(sizeof(SpriteDescription));

The decompressorType field of the sprite sample description specifies the type of compressor used to compress the sample data (or 0 if no compression is used). The sprite media handler supports compressed sample data; the only restriction is that the compressor used to compress the data must be lossless. (A compressor is lossless if the result of compressing some data and then decompressing it yields the original data unchanged; otherwise, the compressor is lossy.) For the moment, we'll just use the uncompressed data that we've created. So we can just go right ahead and call AddMediaSample:

myErr = AddMediaSample(theMedia, (Handle)mySample, 0, 
         GetHandleSize(mySample), kSpriteMediaFrameDurationIcon, 
         mySampleDesc, 1, 0, NULL);

We can use yet another utility function defined in SpriteUtilities.c:

SpriteUtils_AddSpriteSampleToMedia(theMedia, mySample, 
                        kSpriteMediaFrameDurationIcon, true, NULL);

Listing 7 shows our definition of SpriteUtils_AddSpriteSampleToMedia.

Listing 7: Adding a sprite sample to the sprite media

SpriteUtils_AddSpriteSampleToMedia
OSErr SpriteUtils_AddSpriteSampleToMedia (Media theMedia, 
               QTAtomContainer theSample, TimeValue theDuration, 
               Boolean isKeyFrame, TimeValue *theSampleTime)
{
   SampleDescriptionHandle    mySampleDesc = NULL;
   OSErr                              myErr = noErr;
   
   mySampleDesc = (SampleDescriptionHandle)
                        NewHandleClear(sizeof(SpriteDescription));
   if (mySampleDesc == NULL) {
      myErr = MemError();
      goto bail;
   }

   myErr = AddMediaSample(theMedia,
                     (Handle)theSample,
                     0,
                     GetHandleSize(theSample),
                     theDuration,
                     mySampleDesc,
                     1,
                     (short)(isKeyFrame ? 0 : mediaSampleNotSync),
                     theSampleTime);

bail:
   if (mySampleDesc != NULL)
      DisposeHandle((Handle) mySampleDesc);
   return(myErr);
}

Note that SpriteUtils_AddSpriteSampleToMedia adds the specified sample data as a key frame sample or an override frame sample, depending on the value of the isKeyFrame parameter.

Creating Override Samples

With these sprite utility functions at our disposal, it's now quite easy to create some override samples and add them to the sprite media. For the icon movie, we want to create 99 override samples. Each override sample will move the icon 2 pixels to the right; furthermore, when the icon reaches the halfway point, we'll change the image index from 1 to 2. Listing 8 shows the code we use to add those override samples to the sprite media.

Listing 8: Adding some override samples

QTSprites_AddIconMovieSamplesToMedia
for (myCount = 1; myCount <= kNumOverrideSamples; myCount++) {
   QTRemoveChildren(mySample, kParentAtomIsContainer);
   QTRemoveChildren(mySpriteData, kParentAtomIsContainer);

   // every frame, bump the icon's location
   myLocation.h += 2;

   // change icon half way thru
   if (myCount == kNumOverrideSamples / 2)
      myIndex = 2;

   SpriteUtils_SetSpriteData(mySpriteData, &myLocation, NULL, 
                        NULL, &myIndex, NULL, NULL, NULL);
   SpriteUtils_AddSpriteToSample(mySample, mySpriteData, 
                        kQTIconSpriteAtomID);
   SpriteUtils_AddSpriteSampleToMedia(theMedia, mySample, 
                     kSpriteMediaFrameDurationIcon, false, NULL);   
}

Setting Sprite Track Properties

We saw earlier that we can use a sprite image as the background of a sprite track by setting the layer of the image to kBackgroundSpriteLayerNum. But what if we want to have a solid background color for the entire sprite track? We could of course create a sprite image of the proper size and color and then put it into the first key frame sample of the sprite track. The sprite media handler, however, provides a better method to set a solid background color by allowing us to set the sprite track's background color property. The background color property is one of several sprite track properties that control global aspects of the sprite track. The currently defined sprite track properties are accessed using these constants:

enum {
   kSpriteTrackPropertyBackgroundColor         = 101,
   kSpriteTrackPropertyOffscreenBitDepth      = 102,
   kSpriteTrackPropertySampleFormat               = 103,
   kSpriteTrackPropertyScaleSpritesToScaleWorld
                                                            = 104,
   kSpriteTrackPropertyHasActions                  = 105,
   kSpriteTrackPropertyVisible                     = 106,
   kSpriteTrackPropertyQTIdleEventsFrequency   = 107
};

To set one or more sprite track properties, we need to create a media property atom, an atom container that contains a child atom for each of the properties we want to set. The atom type of the child atom should be set to one of these sprite property constants, and the atom ID should be set to 1. The type of the atom data varies from property to property. For the background color sprite track property, the atom data is an RGBColor structure. Once we've constructed the media property atom, we attach it to the sprite track by calling the SetMediaPropertyAtom function. Listing 9 shows how we set a solid white background for the penguin sprite movie. (If we don't specify a background sprite image and we don't set the background color property, then we'll get the default sprite track background color, which is black.)

Listing 9: Setting the background color of a sprite track

QTSprites_SetTrackProperties
void QTSprites_SetTrackProperties (Media theMedia, 
                                                   UInt16 theMenuItem)
{
   QTAtomContainer         myTrackProperties;
   RGBColor                  myBackgroundColor;
   OSErr                     myErr = noErr;

   if (!gUseBackgroundPicture) {
      // add a background color to the sprite track
      QTSprites_GetBackgroundColor(theMenuItem, 
                              &myBackgroundColor);

      myErr = QTNewAtomContainer(&myTrackProperties);
      if (myErr == noErr) {
         QTInsertChild(myTrackProperties, 0, 
                        kSpriteTrackPropertyBackgroundColor, 1, 1, 
                        sizeof(RGBColor), &myBackgroundColor, NULL);

         SetMediaPropertyAtom(theMedia, myTrackProperties);

         QTDisposeAtomContainer(myTrackProperties);
      }
   }
}

The kSpriteTrackPropertyOffscreenBitDepth property specifies the desired bit depth of the offscreen graphics world where the sprite data is drawn before it is copied to the screen. The atom data is of type short, and the default value is 0 (which means to use the bit depth of the deepest monitor that intersects the onscreen sprite window). Setting this property can save memory if your sprite graphics are drawn at a lower bit depth than the user's monitor. The kSpriteTrackPropertySampleFormat property specifies the override sample interpretation mode, which indicates how the sprite media handler interprets override samples. Currently there are two possible modes:

enum {
   kKeyFrameAndSingleOverride         = 1L << 1,
   kKeyFrameAndAllOverrides            = 1L << 2
};

If the override sample interpretation mode is kKeyFrameAndSingleOverride, then the sprite media handler generates the sprite data for a particular override sample by applying the changes in that override sample directly to the key frame sample, ignoring any previous override samples. (This is the default mode.) On the other hand, if the mode is kKeyFrameAndAllOverrides, then the sprite media handler generates sprite data by applying the changes in a particular override sample to the data generated by applying the changes in all previous override samples to the key frame sample data. In other words, kKeyFrameAndSingleOverride specifies that a particular override frame contains absolute changes to key frame data, while kKeyFrameAndAllOverrides specifies relative changes.

The kSpriteTrackPropertyScaleSpritesToScaleWorld property specifies whether the sprites in the sprite track are rescaled whenever the sprite track is resized. This is useful mostly when the sprite images have been compressed using a resolution-independent codec (for example, the Curve codec). The atom data is of type Boolean, and the default value is false.

The kSpriteTrackPropertyVisible property specifies whether the sprite track is visible. The atom data is of type Boolean, and the default value is true. It might occasionally be useful to set the sprite track to be invisible if there are other tracks in the movie and you want to allow the user to click on items in those tracks (by putting an invisible sprite track in front that intercepts those clicks).

The remaining two sprite track properties apply only to sprite tracks that contain wired actions. The kSpriteTrackPropertyHasActions property specifies whether the sprite track contains any wired actions; the atom data is of type Boolean and the default value is false. The kSpriteTrackPropertyQTIdleEventsFrequency property indicates the desired frequency at which the sprite media handler should send idle events (events of type kQTIdleEvent) to the sprite track. In this case, the atom data is of type UInt32 and the default value is kNoQTIdleEvents (which means not to issue any idle events). We'll consider these two sprite track properties in more detail in our article on wired sprites.

Putting it All Together

Let's see what the two functions QTSprites_AddIconMovieSamplesToMedia and QTSprites_AddPenguinMovieSamplesToMedia look like in their entirety. Listing 10 shows the definition of QTSprites_AddIconMovieSamplesToMedia.

Listing 10: Adding samples to the icon sprite movie

QTSprites_AddIconMovieSamplesToMedia
void QTSprites_AddIconMovieSamplesToMedia (Media theMedia)
{
   QTAtomContainer         mySample = NULL;
   QTAtomContainer         mySpriteData = NULL;
   RGBColor                  myKeyColor;
   Point                     myLocation;
   short                     isVisible;
   short                     myLayer, myIndex, myCount;
   OSErr                     myErr = noErr;

   // create a new, empty key frame sample
   myErr = QTNewAtomContainer(&mySample);
   if (myErr != noErr)
      goto bail;

   myKeyColor.red = myKeyColor.green = myKeyColor.blue = 
                                          0xffff;      // white

   // add images to the key frame sample
   SpriteUtils_AddPICTImageToKeyFrameSample(mySample, 
                     kOldQTIconID, &myKeyColor, 1, NULL, NULL);
   SpriteUtils_AddPICTImageToKeyFrameSample(mySample, 
                     kNewQTIconID, &myKeyColor, 2, NULL, NULL);

   // add the initial sprite properties to the key frame sample
   myErr = QTNewAtomContainer(&mySpriteData);
   if (myErr != noErr)
      goto bail;

   // the QT icon sprite
   myLocation.h   = 32;
   myLocation.v   = 32;
   isVisible      = true;
   myLayer         = -1;
   myIndex         = 1;
   
   SpriteUtils_SetSpriteData(mySpriteData, &myLocation, 
            &isVisible, &myLayer, &myIndex, NULL, NULL, NULL);
   SpriteUtils_AddSpriteToSample(mySample, mySpriteData, 
            kQTIconSpriteAtomID);
   SpriteUtils_AddSpriteSampleToMedia(theMedia, mySample, 
            kSpriteMediaFrameDurationIcon, true, NULL);

   // add a few override samples to change the icon's location and image
   for (myCount = 1; myCount <= kNumOverrideSamples; 
                        myCount++) {
      QTRemoveChildren(mySample, kParentAtomIsContainer);
      QTRemoveChildren(mySpriteData, kParentAtomIsContainer);

      // every frame, bump the icon's location
      myLocation.h += 2;

      // change icon half way thru
      if (myCount == kNumOverrideSamples / 2)
         myIndex = 2;

      SpriteUtils_SetSpriteData(mySpriteData, &myLocation, 
                     NULL, NULL, &myIndex, NULL, NULL, NULL);
      SpriteUtils_AddSpriteToSample(mySample, mySpriteData, 
                     kQTIconSpriteAtomID);
      SpriteUtils_AddSpriteSampleToMedia(theMedia, mySample, 
                     kSpriteMediaFrameDurationIcon, false, NULL);
   }

bail:
   if (mySample != NULL)
      QTDisposeAtomContainer(mySample);

   if (mySpriteData != NULL)
      QTDisposeAtomContainer(mySpriteData);   
}

Listing 11 shows our complete definition of QTSprites_AddPenguinMovieSamplesToMedia.

Listing 11: Adding samples to the penguin sprite movie

QTSprites_AddPenguinMovieSamplesToMedia
void QTSprites_AddPenguinMovieSamplesToMedia (Media theMedia)
{
   QTAtomContainer         mySample = NULL;
   QTAtomContainer         mySpriteData = NULL;
   RGBColor                  myKeyColor;
   Point                     myLocation;
   short                     isVisible;
   short                     myLayer, myIndex, myCount;
   ModifierTrackGraphicsModeRecord 
                              myGraphicsMode;
   OSErr                     myErr = noErr;

   // create a new, empty key frame sample
   myErr = QTNewAtomContainer(&mySample);
   if (myErr != noErr)
      goto bail;

   myKeyColor.red = myKeyColor.green = myKeyColor.blue = 
                                          0xffff;      // white

   // add images to the key frame sample
   SpriteUtils_AddPICTImageToKeyFrameSample(mySample, 
                     kPenguinPictID, &myKeyColor, 
                     kPenguinImageIndex, NULL, NULL);

   // add the initial sprite properties to the key frame sample
   myErr = QTNewAtomContainer(&mySpriteData);
   if (myErr != noErr)
      goto bail;

   // the penguin sprite
   myLocation.h   = 0;
   myLocation.v   = 0;
   isVisible      = true;
   myLayer         = -1;
   myIndex         = 1;

   // set the initial blend amount (0 = fully transparent; 0xffff = fully opaque)
   myGraphicsMode.graphicsMode = blend;
   myGraphicsMode.opColor.red = 0;
   myGraphicsMode.opColor.green = 0;
   myGraphicsMode.opColor.blue = 0;
   
   SpriteUtils_SetSpriteData(mySpriteData, &myLocation, 
            &isVisible, &myLayer, &myIndex, &myGraphicsMode, 
            NULL, NULL);
   SpriteUtils_AddSpriteToSample(mySample, mySpriteData, 
            kPenguinSpriteAtomID);
   SpriteUtils_AddSpriteSampleToMedia(theMedia, mySample, 
            kSpriteMediaFrameDurationPenguin, true, NULL);

   // add a few override samples to change the penguin's opacity
   for (myCount = 1; myCount <= kNumOverrideSamples; 
                              myCount++) {
      QTRemoveChildren(mySample, kParentAtomIsContainer);
      QTRemoveChildren(mySpriteData, kParentAtomIsContainer);

      // every frame, bump the penguin's opacity
      myGraphicsMode.graphicsMode = blend;
      myGraphicsMode.opColor.red = (myCount - 1) * 
                                 (0xffff / kNumOverrideSamples - 1);
      myGraphicsMode.opColor.green = (myCount - 1) * 
                                 (0xffff / kNumOverrideSamples - 1);
      myGraphicsMode.opColor.blue = (myCount - 1) * 
                                 (0xffff / kNumOverrideSamples - 1);

      SpriteUtils_SetSpriteData(mySpriteData, NULL, NULL, NULL, 
                  NULL, &myGraphicsMode, NULL, NULL);
      SpriteUtils_AddSpriteToSample(mySample, mySpriteData, 
                  kPenguinSpriteAtomID);
      SpriteUtils_AddSpriteSampleToMedia(theMedia, mySample, 
                  kSpriteMediaFrameDurationPenguin, false, NULL);
   }

bail:   
   if (mySample != NULL)
      QTDisposeAtomContainer(mySample);

   if (mySpriteData != NULL)
      QTDisposeAtomContainer(mySpriteData);
}

For the definition of QTSprites_AddSpaceMovieSamplesToMedia, see the file QTSprites.h. The basic strategy is the same, but the increased complexity of the space movie gives a considerably longer function.

Hit Testing

As I mentioned earlier, QuickTime sprites can be interactive. That is to say, we can create movies with sprite tracks whose sprites respond to user actions like mouse movements and mouse button clicks. When we consider wired sprites in a future article, we'll investigate the full power of this interactivity. In the meantime, let's take a look at a more limited form of interactivity supported by non-wired sprites, the ability to determine whether the user has clicked on a sprite (also called hit testing).

Programmatically finding clicks on a sprite is a two-stage process. First, we need to determine when the user has clicked the mouse button inside of the movie rectangle. Then we need to determine whether that mouse click was on top of a sprite. For the first task, we can add a case to the switch statement in our movie controller action filter function QTApp_MCActionFilterProc, looking for the mcActionMouseDown movie controller action (as shown in Listing 12).

Listing 12: Detecting clicks in the movie window

QTApp_MCActionFilterProc
switch (theAction) {

   // handle window resizing
   case mcActionControllerSizeChanged:
      QTFrame_SizeWindowToMovie(myWindowObject);
      break;

   // handle idle events
   case mcActionIdle:
      QTApp_Idle((**myWindowObject).fWindow);
      break;

   // handle mouse-down events
   case mcActionMouseDown:
      isHandled = QTSprites_HitTestSprites(myWindowObject, 
                                          (EventRecord *)theParams);
      break;

   default:
      break;
}

The movie controller issues an mcActionMouseDown action whenever it receives a mouse-down event that's inside the movie rectangle. The parameter data passed to our action filter function (in theParams) is a pointer to the event record for that mouse-down event. As you can see, our filter function simply calls the application-defined function QTSprites_HitTestSprites to see whether the click is on a sprite and, if so, to react appropriately. (For more information about movie controller action filter functions, see "QuickTime 101" in MacTech, January 2000.)

The sprite media handler supplies the SpriteMediaHitTestOneSprite and SpriteMediaHitTestAllSprites functions, which we can use to determine whether the user has clicked on a sprite. For present purposes, we'll use SpriteMediaHitTestAllSprites, which looks at each one of the sprites in a sprite track to see whether it is currently at a specified location. SpriteMediaHitTestAllSprites is declared essentially like this:

ComponentResult SpriteMediaHitTestAllSprites 
               (MediaHandler    mh, long flags, Point loc, 
                     QTAtomID *spriteHitID);

If a sprite is situated at the specified location in the sprite track associated with the specified sprite media handler, then SpriteMediaHitTestAllSprites returns the ID of that sprite in the spriteHitID parameter. If more than one sprite is situated at that location, then spriteHitID is set to the ID of the frontmost sprite (that is, the sprite with the lowest layer number). If no sprite is situated at that location, then spriteHitID is set to 0.

By default, the loc parameter should be the location of the mouse click, in coordinates local to the sprite track. However, the event record passed to our movie controller action filter function contains the location of the mouse click in global coordinates. Rather than bother with converting the global location to a local position, we can add spriteHitTestLocInDisplayCoordinates to the flags parameter, to indicate that the loc parameter is in global coordinates. These flags are understood by SpriteMediaHitTestAllSprites:

enum {
   spriteHitTestBounds                              = 1L << 0,
   spriteHitTestImage                              = 1L << 1,
   spriteHitTestInvisibleSprites               = 1L << 2,
   spriteHitTestIsClick                              = 1L << 3,
   spriteHitTestLocInDisplayCoordinates      = 1L << 4
};

If you want to accept clicks anywhere within the rectangular bounding box of a sprite, add in the spriteHitTestBounds flag. If, conversely, you want to accept clicks only on a non-transparent part of a sprite, add in the spriteHitTestImage flag. You must specify one or the other of these two flags, or else no hit-testing will occur. Setting both of these flags is tantamount to setting only spriteHitTestImage. (Earlier versions of QuickTime required you to set both flags if you wanted image testing, but current versions do not.)

By default, SpriteMediaHitTestAllSprites and SpriteMediaHitTestOneSprite test only visible sprites for hits. If you want to test invisible sprites as well, set the spriteHitTestInvisibleSprites flag. If you want to pass the mouse click onto the codec that is rendering the sprite image, then specify the spriteHitTestIsClick flag; this is currently useful only with the Ripple codec.

In QTSprites_HitTestSprites, we want to test both visible and invisible sprites for hits, and we want to test only within the non-transparent portions of a sprite image. So we'll specify our flags parameter like this:

myFlags = spriteHitTestImage | 
                     spriteHitTestLocInDisplayCoordinates | 
                     spriteHitTestInvisibleSprites;

Now, what will we do when we detect a click on a sprite? In theory, we can do anything we want. We've got the entire QuickTime API at our disposal, so we could do some neat things like launch the user's web browser and navigate to a specific page, or download a file from a remote location, or even build a QuickTime movie. Even just within the context of our sprite track we could do some rather interesting stuff, like moving the sprite to a new location in the sprite track, changing its image index, and so forth. For simplicity, we'll limit ourselves to changing the sprite's visibility state: if the sprite is currently visible, we'll make it invisible (and vice versa).

SpriteMediaGetSpriteProperty(myHandler, myAtomID, 
                        kSpritePropertyVisible, (void *)&isVisible);
SpriteMediaSetSpriteProperty(myHandler, myAtomID, 
                        kSpritePropertyVisible, (void *)!isVisible);

Listing 13 shows our complete definition of QTSprites_HitTestSprites.

Listing 13: Hit testing sprites

QTSprites_HitTestSprites
Boolean QTSprites_HitTestSprites 
            (WindowObject theWindowObject, EventRecord *theEvent)
{
   ApplicationDataHdl      myAppData = NULL;
   MediaHandler               myHandler = NULL;
   Boolean                     isHandled = false;
   long                           myFlags = 0L;
   QTAtomID                     myAtomID = 0;
   Point                        myPoint;
   ComponentResult            myErr = noErr;

   myAppData = (ApplicationDataHdl)
         QTFrame_GetAppDataFromWindowObject(theWindowObject);
   if (myAppData == NULL)
      goto bail;

   if (theEvent == NULL)
      goto bail;

   // make sure that the click is in our window
   if ((**theWindowObject).fWindow != 
                     QTFrame_GetFrontMovieWindow())
      goto bail;

   myHandler = (**myAppData).fSpriteHandler;
   myFlags = spriteHitTestImage | 
                     spriteHitTestLocInDisplayCoordinates | 
                     spriteHitTestInvisibleSprites;
   myPoint = theEvent->where;
   
   myErr = SpriteMediaHitTestAllSprites(myHandler, myFlags, 
                                       myPoint, &myAtomID);
   if ((myErr == noErr) && (myAtomID != 0)) {
      Boolean            isVisible;

      // the user has clicked on a sprite;
      // for now, we'll just toggle the visibility state of the sprite
      SpriteMediaGetSpriteProperty(myHandler, myAtomID, 
                        kSpritePropertyVisible, (void *)&isVisible);
      SpriteMediaSetSpriteProperty(myHandler, myAtomID, 
                        kSpritePropertyVisible, (void *)!isVisible);

      isHandled = true;
   }

bail:
   return(isHandled);
}

You'll notice that we need to make sure that the window associated with the window object passed to QTSprites_HitTestSprites is the frontmost movie window, by executing these lines of code:

if ((**theWindowObject).fWindow != 
                  QTFrame_GetFrontMovieWindow())
   goto bail;

This is because, when we are handling an event on Macintosh computers, we call MCIsPlayerEvent for each open movie controller, until we find one that handles the event. It's possible to have two or more overlapping sprite movies such that a mouse click does not hit a sprite in the frontmost movie window but does hit one in an overlapped window. We can avoid unexpected behaviors by limiting our hit testing to the frontmost movie window. (On Windows, this additional check is unnecessary but harmless.)

Conclusion

In this article, we've seen how to create and work with sprite tracks in QuickTime movies. Sprite tracks are remarkably easy to create. It's really just an exercise in creating atom containers and adding them as samples to a sprite media. Keep in mind that sprite media data isn't just a collection of pixels that are copied from the movie file onto the screen. Rather, sprites are distinct objects, with properties that can be changed dynamically to animate the sprites. This is what accounts for the drastic size differences between sprite and non-sprite versions of a movie. This is also what accounts for our ability to interact with individual sprites (for instance, our ability to hit test mouse clicks on sprites).

In the next article, we'll continue our investigation of sprites. Among other things, we'll see how to use a video track as the source of a sprite's image data, and we'll see how to use modifier tracks to animate sprites. Believe it or not, using modifier tracks instead of override samples will result in even further size reductions for our sprite movie files. Stay tuned!

Acknowledgements and References

The utilities in the file SpriteUtilities.c were originally written by Sean Allen; I have taken the liberty of reworking them to bring the general programming style into conformance with the rest of the sample code we've encountered in this series of articles. The code for building the space movie is based on an existing sample code package, MakeSpriteMovie.c, also by Sean Allen.

The definitive API reference for QuickTime sprites and wired actions is the book QuickTime Wired Movies And Sprite Animation by Tom Maremaa (downloadable from http://developer.apple.com/techpubs/quicktime/qtdevdocs/RM/pdfframe.htm).


Tim Monroe is amazed at how fast his new lizard Libra is growing. Maybe he'll try feeding insects to his own children to see if they grow any faster. In the meantime, you can contact him at monroe@apple.com.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Summon your guild and prepare for war in...
Netmarble is making some pretty big moves with their latest update for Seven Knights Idle Adventure, with a bunch of interesting additions. Two new heroes enter the battle, there are events and bosses abound, and perhaps most interesting, a huge... | Read more »
Make the passage of time your plaything...
While some of us are still waiting for a chance to get our hands on Ash Prime - yes, don’t remind me I could currently buy him this month I’m barely hanging on - Digital Extremes has announced its next anticipated Prime Form for Warframe. Starting... | Read more »
If you can find it and fit through the d...
The holy trinity of amazing company names have come together, to release their equally amazing and adorable mobile game, Hamster Inn. Published by HyperBeard Games, and co-developed by Mum Not Proud and Little Sasquatch Studios, it's time to... | Read more »
Amikin Survival opens for pre-orders on...
Join me on the wonderful trip down the inspiration rabbit hole; much as Palworld seemingly “borrowed” many aspects from the hit Pokemon franchise, it is time for the heavily armed animal survival to also spawn some illegitimate children as Helio... | Read more »
PUBG Mobile teams up with global phenome...
Since launching in 2019, SpyxFamily has exploded to damn near catastrophic popularity, so it was only a matter of time before a mobile game snapped up a collaboration. Enter PUBG Mobile. Until May 12th, players will be able to collect a host of... | Read more »
Embark into the frozen tundra of certain...
Chucklefish, developers of hit action-adventure sandbox game Starbound and owner of one of the cutest logos in gaming, has released their roguelike deck-builder Wildfrost. Created alongside developers Gaziter and Deadpan Games, Wildfrost will... | Read more »
MoreFun Studios has announced Season 4,...
Tension has escalated in the ever-volatile world of Arena Breakout, as your old pal Randall Fisher and bosses Fred and Perrero continue to lob insults and explosives at each other, bringing us to a new phase of warfare. Season 4, Into The Fog of... | Read more »
Top Mobile Game Discounts
Every day, we pick out a curated list of the best mobile discounts on the App Store and post them here. This list won't be comprehensive, but it every game on it is recommended. Feel free to check out the coverage we did on them in the links below... | Read more »
Marvel Future Fight celebrates nine year...
Announced alongside an advertising image I can only assume was aimed squarely at myself with the prominent Deadpool and Odin featured on it, Netmarble has revealed their celebrations for the 9th anniversary of Marvel Future Fight. The Countdown... | Read more »
HoYoFair 2024 prepares to showcase over...
To say Genshin Impact took the world by storm when it was released would be an understatement. However, I think the most surprising part of the launch was just how much further it went than gaming. There have been concerts, art shows, massive... | Read more »

Price Scanner via MacPrices.net

Apple Watch Ultra 2 now available at Apple fo...
Apple has, for the first time, begun offering Certified Refurbished Apple Watch Ultra 2 models in their online store for $679, or $120 off MSRP. Each Watch includes Apple’s standard one-year warranty... Read more
AT&T has the iPhone 14 on sale for only $...
AT&T has the 128GB Apple iPhone 14 available for only $5.99 per month for new and existing customers when you activate unlimited service and use AT&T’s 36 month installment plan. The fine... Read more
Amazon is offering a $100 discount on every M...
Amazon is offering a $100 instant discount on each configuration of Apple’s new 13″ M3 MacBook Air, in Midnight, this weekend. These are the lowest prices currently available for new 13″ M3 MacBook... Read more
You can save $300-$480 on a 14-inch M3 Pro/Ma...
Apple has 14″ M3 Pro and M3 Max MacBook Pros in stock today and available, Certified Refurbished, starting at $1699 and ranging up to $480 off MSRP. Each model features a new outer case, shipping is... Read more
24-inch M1 iMacs available at Apple starting...
Apple has clearance M1 iMacs available in their Certified Refurbished store starting at $1049 and ranging up to $300 off original MSRP. Each iMac is in like-new condition and comes with Apple’s... Read more
Walmart continues to offer $699 13-inch M1 Ma...
Walmart continues to offer new Apple 13″ M1 MacBook Airs (8GB RAM, 256GB SSD) online for $699, $300 off original MSRP, in Space Gray, Silver, and Gold colors. These are new MacBook for sale by... Read more
B&H has 13-inch M2 MacBook Airs with 16GB...
B&H Photo has 13″ MacBook Airs with M2 CPUs, 16GB of memory, and 256GB of storage in stock and on sale for $1099, $100 off Apple’s MSRP for this configuration. Free 1-2 day delivery is available... Read more
14-inch M3 MacBook Pro with 16GB of RAM avail...
Apple has the 14″ M3 MacBook Pro with 16GB of RAM and 1TB of storage, Certified Refurbished, available for $300 off MSRP. Each MacBook Pro features a new outer case, shipping is free, and an Apple 1-... Read more
Apple M2 Mac minis on sale for up to $150 off...
Amazon has Apple’s M2-powered Mac minis in stock and on sale for $100-$150 off MSRP, each including free delivery: – Mac mini M2/256GB SSD: $499, save $100 – Mac mini M2/512GB SSD: $699, save $100 –... Read more
Amazon is offering a $200 discount on 14-inch...
Amazon has 14-inch M3 MacBook Pros in stock and on sale for $200 off MSRP. Shipping is free. Note that Amazon’s stock tends to come and go: – 14″ M3 MacBook Pro (8GB RAM/512GB SSD): $1399.99, $200... Read more

Jobs Board

*Apple* Systems Administrator - JAMF - Syste...
Title: Apple Systems Administrator - JAMF ALTA is supporting a direct hire opportunity. This position is 100% Onsite for initial 3-6 months and then remote 1-2 Read more
Relationship Banker - *Apple* Valley Financ...
Relationship Banker - Apple Valley Financial Center APPLE VALLEY, Minnesota **Job Description:** At Bank of America, we are guided by a common purpose to help Read more
IN6728 Optometrist- *Apple* Valley, CA- Tar...
Date: Apr 9, 2024 Brand: Target Optical Location: Apple Valley, CA, US, 92308 **Requisition ID:** 824398 At Target Optical, we help people see and look great - and Read more
Medical Assistant - Orthopedics *Apple* Hil...
Medical Assistant - Orthopedics Apple Hill York Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Now Read more
*Apple* Systems Administrator - JAMF - Activ...
…**Public Trust/Other Required:** None **Job Family:** Systems Administration **Skills:** Apple Platforms,Computer Servers,Jamf Pro **Experience:** 3 + years of Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.