![](/file/12652/www.mactech.com.tar/www.mactech.com/sites/all/themes/custom_front/images/you_are_here_red.gif)
![](/file/12652/www.mactech.com.tar/www.mactech.com/sites/default/files/beta-site.gif)
|
Volume Number: 17 (2001)
Issue Number: 05
Column Tag: QuickTime Toolkit
Wired
by Time Monroe
Using Wired Actions in Sprite Movies
Introduction
In the past two QuickTime Toolkit articles, we've learned how to create movies with sprite tracks. We've also learned several ways to animate the sprites in those tracks, first by adding override samples to the sprite track, and then by adding a video override track or a tween track to the movie. In both cases, we animate the sprites by changing one or more of the sprite properties (image index, matrix, graphics mode, and so forth). Our sprite animations so far have been pretty simple, but in fact we now have the ability to create some fairly complex sprite animations. For instance, we could add several tween tracks to a sprite movie, one that changes a sprite's position (by tweening its matrix) and another that fades the sprite in and out (by tweening its graphics mode). Throw in a background sprite and a video override track and we have all the makings of a reasonably compelling animation.
What we don't have, yet, is any way for the user to interact with the animation (other than simply starting it and stopping it using the controller bar). To be truly compelling, our movies should allow the user to manipulate sprites in the movie, or trigger changes in the movie by clicking on a sprite, or maybe even open a web site in a browser window when the user moves the cursor over a sprite. All of this is possible using one of QuickTime's most powerful features: wired actions. In this article, we're going to see how to attach (or "wire") interactive, dynamic behaviors (that is, "actions") to parts of a sprite movie.
Consider, for example, the movie shown in Figure 1. This looks just like the moving icon movies that we constructed last time (except that now we've used the current QuickTime extension icon and there is no controller bar on the movie window).
Figure 1: An icon sprite movie
But appearances can be deceiving, for lurking within the movie are several new and interesting behaviors. When we move the cursor over the icon sprite, the cursor automatically changes into an open-hand cursor (Figure 2), which might suggest that we can drag the icon around. Indeed we can: if we hold down the mouse button and drag, the sprite is dragged along with the cursor, which changes into the closed-hand cursor (as shown in Figure 3); when we release the mouse button, the icon sprite remains at that new position.
Figure 2:The cursor changing shape inside the sprite
Figure 3:The sprite being dragged around in a sprite movie
Or consider the movie shown in Figure 4. This movie consists of a video track, a sound track, and a sprite track. There are six sprites in the sprite track, and each sprite is wired to duplicate one of the behaviors of the buttons in the standard controller bar. (From left to right, clicking on the sprites performs these actions: go to the beginning of the movie, go backward one frame, start the movie playing, pause the movie, go forward one frame, and go to the end of the movie.) As you can see, the movie has no controller bar, since we can now control the movie using the sprite "buttons" alone.
Figure 4:A sprite track that controls a video track
You might be wondering what's so special about all of this. After all, in our first sprite article ("A Goofy Movie" in MacTech, March 2001), we saw how to move sprites around and respond to mouse button clicks on them. At that time, all we did was change the visibility state of the sprite, but it would have been easy for us instead to repeatedly change the position of the sprite until the mouse button was released (and hence drag it around the movie). The important difference is that there we used a special application (QTSprites) to supply those capabilities, whereas here we shall build a movie file that can be opened by any QuickTime-savvy application and that will have the same behaviors in that application as well. In other words, the dynamic, interactive behaviors are stored in the movie file as part of the media data, not supplied by the playback application.
Our sample application this month is called QTWiredSpritesJr (because it's a stripped-down version of an existing sample code package called QTWiredSprites). Figure 5 shows the Test menu of QTWiredSpritesJr. The two menu items allow us to add the controller sprite track to a QuickTime movie and to create the movie with a draggable sprite shown in Figure 1. This might not seems like a lot, but it will occupy us for quite a while. So sit back and take a deep breath. And get ready to have your QuickTime abilities changed forever.
Figure 5:The Test menu of QTWiredSpritesJr
Events, Parameters, Actions
A wired sprite is a sprite to which one or more event atoms have been attached. An event atom is a QuickTime atom that associates an event with one or more actions that are triggered when that event occurs. Each action can have one or more parameters, which specify any additional information that's necessary for the action to be performed. In addition, an action may have a target, which is the object upon which the action is performed. The structure of a typical event atom is shown in Figure 6. Here, the event atom contains a single child atom, of type kAction. This action atom, in turn, contains two children, one that specifies the type of the action and another that specifies the single parameter to that action. Some actions have more than one parameter, and some have none.
Figure 6:The structure of an event atom
An event atom is attached to a particular sprite by including the atom as a child of the sprite atom (that is, the atom of type kSpriteAtomType) and hence as a sibling of the sprite property atoms. Notice that the event type is the atom ID of the event atom.
Specifying Events
When wired sprites were first introduced (in QuickTime 3.0), there were seven basic types of events that could trigger wired sprite actions, defined by these constants:
enum { kQTEventMouseClick = FOUR_CHAR_CODE('clik'), kQTEventMouseClickEnd = FOUR_CHAR_CODE('cend'), kQTEventMouseClickEndTriggerButton = FOUR_CHAR_CODE('trig'), kQTEventMouseEnter = FOUR_CHAR_CODE('entr'), kQTEventMouseExit = FOUR_CHAR_CODE('exit'), kQTEventFrameLoaded = FOUR_CHAR_CODE('fram'), kQTEventIdle = FOUR_CHAR_CODE('idle') };
Five of these actions concern various states of the cursor and the mouse button. When the cursor moves over any non-transparent part of a sprite's image, the kQTEventMouseEnter event is issued (whether or not the mouse button is down), and when the cursor then moves out of the sprite, the kQTEventMouseExit event is issued. When the mouse button is clicked while the cursor is over a sprite, the kQTEventMouseClick action is issued, and when the mouse button is later released, the kQTEventMouseClickEnd event is issued. Note that the kQTEventMouseClickEnd event is issued whether or not the cursor is still inside of the sprite. If we want to make a wired sprite act like a button, then we can use the kQTEventMouseClickEndTriggerButton event, which is issued only if the mouse button is clicked and released inside of the sprite's image. (This should remind you of the behavior of standard user interface buttons, which are not triggered if the mouse button is released after the cursor is moved outside of the button.)
When I say here that one of these events is issued, I mean that the sprite media handler looks for an event atom of that type inside of the sprite atom; if it finds one, then it interprets and executes the action atoms inside of that event atom. Only the action atoms associated with the affected sprite are executed, however. So, for instance, if two different sprites have actions associated with an event of type kQTEventMouseClick, then only the actions in the event atom of the sprite actually clicked on are executed. If two sprites happen to overlap, the event is sent to the topmost sprite (the one with the lower layer property).
The kQTEventFrameLoaded event is issued when a sprite track key frame is loaded. It is not sent to any particular sprite and hence the event atom that holds the associated actions is not contained inside of any sprite atom. Rather, the event atom is stored in the media sample atom container, as a sibling of the sprite atoms. Frame-loaded events are handy for initializing the state of the sprites in the frame. (More on this later.)
Finally, the kQTEventIdle event is sent to every sprite in a sprite track periodically, after some predefined number of ticks (that is, sixtieths of a second) has elapsed. The frequency with which these idle events are issued is determined by the kSpriteTrackPropertyQTIdleEventsFrequency property of the sprite track. This event is especially useful for triggering actions after some amount of time has elapsed; it will be very useful to us, for instance, when we build our draggable sprite movie.
Subsequent versions of QuickTime have added several more event types, but we will not consider them in this article. The most interesting one, perhaps, is the event type kQTEventKey, which is issued when keys on the keyboard are pressed. In an upcoming article, we'll play with some of these additional event types.
Specifying Actions
Okay, so now we know how to get a sprite to stand up and take notice when certain kinds of events occur: we put an event atom with the appropriate atom ID into the sprite atom (as a sibling of the sprite property atoms that are inside the sprite atom). And we know what happens next: all the action atoms contained inside that event atom are interpreted and executed. So all we really need to learn is what kinds of actions can be triggered by QuickTime events. That is to say, we need to learn what values we can use as the data in an atom of type kWhichAction (see Figure 6 again).
Here comes the really good news: the latest Movies.h file contains over one hundred constants that we can use to specify action types. There are action types for setting a movie's volume, and setting a sprite's image index, and setting a track's graphics mode, and opening a URL in the user's default browser, and on and on. And every one of them can be wired to any one of the available QuickTime events. So we could set up a sprite track in which dragging a certain sprite from right to left changes the balance of the movie's sound track, or moving the mouse over a certain sprite changes the current pan angle of a QuickTime VR node. Really, the sky's the limit.
The available actions are best categorized by the target of the action (that is, the object that is affected by the action). QuickTime currently supports three kinds of targets: movies, tracks, and sprites. There are also some actions that have no target.
A large number of actions perform operations on a movie. By default, the target of these action is the current movie (the one that contains the sprite track), but it's also possible to target other movies. Here's a representative sample of actions that operate on movies:
enum { kActionMovieSetVolume = 1024, kActionMovieSetRate = 1025, kActionMovieSetLoopingFlags = 1026, kActionMovieGoToTime = 1027, kActionMovieGoToTimeByName = 1028, kActionMovieGoToBeginning = 1029, kActionMovieGoToEnd = 1030, kActionMovieStepForward = 1031, kActionMovieStepBackward = 1032, kActionMovieSetSelection = 1033 }
A number of actions operate on tracks within a movie. The default track is the one that contains the sprite that triggers the action. Here are all the currently-defined actions that operate on tracks:
enum { kActionTrackSetVolume = 2048, kActionTrackSetBalance = 2049, kActionTrackSetEnabled = 2050, kActionTrackSetMatrix = 2051, kActionTrackSetLayer = 2052, kActionTrackSetClip = 2053, kActionTrackSetCursor = 2054, kActionTrackSetGraphicsMode = 2055 }
Finally, a fair number of actions have no target at all. A good example here is kActionGoToURL, which opens the user's default web browser and loads a URL.
Specifying Parameters
How does the kActionGoToURL action know which URL to open? The URL is specified in a parameter atom that is a child of the kAction atom. The number of parameters required by each action and the type of data in those parameters is detailed in Apple's technical documentation on wired sprites; it's also listed in Movies.h, for example like this:
kActionGoToURL = 6146, /* (C string urlLink) */
This tells us that the kActionParameter atom in the action atom must contain a NULL-terminated string of characters that specifies the URL to open.
Other actions take more than one parameter. For instance, the kActionMovieSetSelection action requires two parameters, a TimeValue that indicates the beginning time of the selection and another that indicates the ending time of the selection. In this case, the action atom must contain two atoms of type kActionParameter, one with atom index 1 and the other with atom index 2.
Controlling Movies Using Wired Sprites
That's enough theory for the moment. Let's get started wiring some sprites, and let's begin by seeing how to build the sprite track shown in Figure 4, which allows the user to control a movie by clicking on the sprite "buttons". There are two main steps involved here: first we need to build the sprite track (which contains the sprite images, sprites, and initial sprite properties), and then we need to wire those sprites with actions.
Building the Sprite Track
We're already pretty adept at building sprite tracks, so we can proceed quickly here. In the present case, we're going to add a sprite track to an existing movie, so we'll start by getting some information about that movie, including its duration and size:
GetMovieBox(theMovie, &myRect); myWidth = Long2Fix(myRect.right - myRect.left); myHeight = Long2Fix(myRect.bottom - myRect.top); myDuration = GetMovieDuration(theMovie); myTimeScale = GetMovieTimeScale(theMovie);
We'll use these values to set the size (and later the duration) of our new sprite track, like this:
myTrack = NewMovieTrack(theMovie, myWidth, myHeight, kNoVolume); myMedia = NewTrackMedia(myTrack, SpriteMediaType, myTimeScale, NULL, 0);
Now, of course, we need to add some sample data to the new sprite track. As you know, this data includes the sprite images and initial sprite properties. At this point, the QTWired_AddSpriteControllerTrack function calls another QTWiredSpritesJr function,
QTWired_AddControllerButtonSamplesToMedia, to add the appropriate media data:
QTWired_AddControllerButtonSamplesToMedia(myMedia, myRect.right - myRect.left, myRect.bottom - myRect.top, myDuration);
Let's pause and reflect for a moment. We want the sprites in our sprite track to act like buttons, so that clicking on one of the sprites triggers some appropriate action. We also want the sprite image to change while the mouse button is held down over the sprite, in just the same way that a standard button changes its appearance when clicked. Figure 7 shows the movie when the user clicks and on the "Play" sprite and holds the mouse button down.
Figure 7:The button down image for the "Play" sprite
So really we need two images for each sprite, one to be displayed when the sprite is in the normal state and one to be displayed when the sprite is in the "pressed" state. Figure 8 shows the twelve images that we are going to add to the sprite track.
Figure 8:The sprite images
If we store these images in consecutively numbered 'PICT' resources, we can add them all to the sprite key frame sample with these lines of code:
for (myCount = 0; myCount < kNumControllerImages; myCount++) SpriteUtils_AddPICTImageToKeyFrameSample(mySample, kFirstControllerImageID + myCount, &myKeyColor, myCount + 1, NULL, NULL);
Now let's set the initial properties of the six sprites. For the "Go-To-Start" sprite (the first button on the left), we can use the code in Listing 1.
Listing 1: Setting the properties of the "Go-To-Start" sprite
mySixth = theTrackWidth / kNumControllerButtons; myErr = QTNewAtomContainer(&myStartButton); if (myErr != noErr) goto bail; myLocation.h = (kToBeginSpritePosition * mySixth) + ((mySixth - kButtonWidth) / 2); myLocation.v = theTrackHeight - (kButtonHeight + 5); isVisible = true; myIndex = kToBeginUpIndex; myLayer = 1; SpriteUtils_SetSpriteData(myStartButton, &myLocation, &isVisible, &myLayer, &myIndex, NULL, NULL, NULL); SpriteUtils_AddSpriteToSample(mySample, myStartButton, kToBeginSpriteAtomID);
The only mildly tricky thing here is the code we use to determine the horizontal position of the sprite. We first calculate one-sixth of the track width and then move over the desired number of sixths; then we back up just enough that the sprite image is nicely centered in the appropriate sixth of the sprite track.
If we repeat the steps in Listing 1 for the remaining five sprites, add the key frame sample to the media, and add the media to the track, the original movie will then have a new sprite track with the sprites laid out as shown in Figure 4. Of course, clicking on the sprites won't have any effect, since we haven't yet attached any actions to those sprites. Let's see how to do that.
Wiring Actions to Sprites
To wire some actions to a sprite, we need to add one or more atoms of type kQTEventType to the sprite atom and then add one or more action atoms to each of those event atoms. In the case of the "Go-To-Start" sprite, we want to add three event atoms, each of which contains a single action atom:
- When the mouse button is clicked and the cursor is over the sprite, we want to change the sprite's image index property so that the "pressed" image is the current image. That is, we want to change the image to kToBeginDownIndex.
- When the mouse button is released (whether or not the cursor is still within the sprite), we want to change the sprite's image index property so that the normal image is once again the current image. That is, we want to change the image back to kToBeginUpIndex.
- When the mouse button is released and the cursor is still within the sprite, we want to execute the kActionMovieGoToBeginning action.
Listing 2 shows some code that we can use to handle mouse clicks on the "Go-To-Start" sprite.
Listing 2: Handling mouse clicks on the "Go-To-Start" sprite
myErr = QTInsertChild(myStartButton, kParentAtomIsContainer, kQTEventType, kQTEventMouseClick, 1, 0, NULL, &myEventAtom); if (myErr != noErr) goto bail; // add an action atom to the event handler myErr = QTInsertChild(myStartButton, myEventAtom, kAction, 0, 0, 0, NULL, &myActionAtom); if (myErr != noErr) goto bail; myAction = EndianU32_NtoB(kActionSpriteSetImageIndex); myErr = QTInsertChild(myStartButton, myActionAtom, kWhichAction, 1, 1, sizeof(myAction), &myAction, NULL); if (myErr != noErr) goto bail; // add a parameter to the set image index action: image index myIndex = EndianU32_NtoB(kToBeginDownIndex); myErr = QTInsertChild(myStartButton, myActionAtom, kActionParameter, 0, (short)kFirstParam, sizeof(myIndex), &myIndex, NULL);
As you can see, we add an event atom whose atom ID is kQTEventMouseClick to the sprite atom (myStartButton). Then we insert an action atom (of type kAction) into that event atom. The action atom is given two children, one of type kWhichAction whose atom data is kActionSpriteSetImageIndex, and one of type kActionParameter whose atom data is kToBeginDownIndex. In a nutshell, this says: when the user clicks on the "Go-To-Start" sprite, change that sprite's image index to kToBeginDownIndex.
Listing 3 shows some code that we can use to handle mouse-up events for the "Go-To-Start" sprite. The code is entirely analogous to the code in Listing 2; all that has changed is the ID of the event atom and the index of the image.
Listing 3: Handling mouse–up events for the "Go-To-Start" sprite
myErr = QTInsertChild(myStartButton, kParentAtomIsContainer, kQTEventType, kQTEventMouseClickEnd, 1, 0, NULL, &myEventAtom); if (myErr != noErr) goto bail; // add an action atom to the event handler myErr = QTInsertChild(myStartButton, myEventAtom, kAction, 0, 0, 0, NULL, &myActionAtom); if (myErr != noErr) goto bail; myAction = EndianU32_NtoB(kActionSpriteSetImageIndex); myErr = QTInsertChild(myStartButton, myActionAtom, kWhichAction, 1, 1, sizeof(myAction), &myAction, NULL); if (myErr != noErr) goto bail; // add a parameter to the set image index action: image index myIndex = EndianU32_NtoB(kToBeginUpIndex); myErr = QTInsertChild(myStartButton, myActionAtom, kActionParameter, 0, (short)kFirstParam, sizeof(myIndex), &myIndex, NULL);
It's actually even easier to wire the kActionMovieGoToBeginning action to the kQTEventMouseClickEndTriggerButton event, since there are no parameters. Listing 4 shows how we handle this.
Listing 4: Handling button-trigger events for the "Go-To-Start" sprite
myErr = QTInsertChild(myStartButton, kParentAtomIsContainer, kQTEventType, kQTEventMouseClickEndTriggerButton, 1, 0, NULL, &myEventAtom); if (myErr != noErr) goto bail; // add an action atom to the event handler myErr = QTInsertChild(myStartButton, myEventAtom, kAction, 0, 0, 0, NULL, &myActionAtom); if (myErr != noErr) goto bail; myAction = EndianU32_NtoB(kActionMovieGoToBeginning); myErr = QTInsertChild(myStartButton, myActionAtom, kWhichAction, 1, 1, sizeof(myAction), &myAction, NULL);
So adding simple wired actions to sprites really is just another exercise in constructing atom containers and atoms. Figure 9 shows the structure of the "Go-To-Start" sprite atom. (Note that several of the sprite property atoms have been omitted.)
Figure 9: The "Go-To-Start" sprite atom
Setting the Track Properties
We still have a couple of things we need to do before our movie is fully wired. For one thing, we need to indicate that the sprite track has actions in it. We do this by including in the track's media property atom an atom of type kSpriteTrackPropertyHasActions whose atom data is set to true. (For more discussion of media property atoms, see "A Goofy Movie", cited earlier.) Listing 5 shows our definition of QTWired_SetTrackProperties, which is similar to the QTSprites_SetTrackProperties function we used in previous articles.
Listing 5: Setting the sprite track properties
void QTWired_SetTrackProperties (Media theMedia, UInt32 theIdleFrequency) { QTAtomContainer myTrackProperties; RGBColor myBackgroundColor; Boolean hasActions; UInt32 myFrequency; OSErr myErr = noErr; // add a background color to the sprite track myBackgroundColor.red = EndianU16_NtoB(0xffff); myBackgroundColor.green = EndianU16_NtoB(0xffff); myBackgroundColor.blue = EndianU16_NtoB(0xffff); myErr = QTNewAtomContainer(&myTrackProperties); if (myErr == noErr) { QTInsertChild(myTrackProperties, 0, kSpriteTrackPropertyBackgroundColor, 1, 1, sizeof(myBackgroundColor), &myBackgroundColor, NULL); // tell the movie controller that this sprite track has actions hasActions = true; QTInsertChild(myTrackProperties, 0, kSpriteTrackPropertyHasActions, 1, 1, sizeof(hasActions), &hasActions, NULL); // tell the sprite track to generate QTIdleEvents myFrequency = EndianU32_NtoB(theIdleFrequency); QTInsertChild(myTrackProperties, 0, kSpriteTrackPropertyQTIdleEventsFrequency, 1, 1, sizeof(myFrequency), &myFrequency, NULL); SetMediaPropertyAtom(theMedia, myTrackProperties); QTDisposeAtomContainer(myTrackProperties); } }
You'll notice that QTWired_SetTrackProperties also adds an atom of type kSpriteTrackPropertyQTIdleEventsFrequency to the media property atom. As we mentioned earlier, this atom specifies the number of ticks between idle events. Our current sprite track doesn't use idle events, so we pass the parameter kNoQTIdleEvents, which tells the movie controller not to issue idle events at all. Another useful value is 0, which tells the movie controller to issue idle events as fast as possible.
Setting the Track Layer
Each track in a movie has a layer, which determines the order in which the track is drawn into the movie. Tracks with lower layers are drawn after tracks with higher layers, so they will be drawn on top of those higher-layered tracks. When a track is created, its layer is set to 0. In the current case, we want to ensure that our new sprite track is drawn on top of all existing tracks in the movie, since otherwise it might be hidden behind those tracks (and hence be pretty much useless). So we need to get the lowest track layer of those existing tracks and then set the sprite track's layer to some lower value. QTWiredSpritesJr does that with these lines of code:
SetTrackLayer(myTrack, kMaxLayerNumber); SetTrackLayer(myTrack, QTWired_GetLowestLayerInMovie(theMovie) - 1);
A layer is a value of type short, so the largest possible layer value is 0x7fff:
#define kMaxLayerNumber 0x7fff
Listing 6 shows our definition of the QTWired_GetLowestLayerInMovie function.
Listing 6: Finding the lowest track layer in a movie
short QTWired_GetLowestLayerInMovie (Movie theMovie) { long myCount = 0; long myIndex; short myLayer = 0; short myMinLayer = kMaxLayerNumber; myCount = GetMovieTrackCount(theMovie); for (myIndex = 1; myIndex <= myCount; myIndex++) { myLayer = GetTrackLayer(GetMovieIndTrack(theMovie, myIndex)); if (myLayer < myMinLayer) myMinLayer = myLayer; } return(myMinLayer); }
Setting the Movie Controller
One thing that's distinctive about a wired sprite movie is that it must be associated with a movie controller. It's the movie controller's job to determine whether a sprite track contains any event atoms and then to monitor the mouse and keyboard for any relevant events. When any of the designated events occur, the movie controller sends them to the appropriate sprites.
This of course is not a problem for us. All of our sample applications (except for one) use movie controllers to manage the user's interactions with a movie. We've seen, sufficiently I hope, that using movie controllers can save us a tremendous amount of work.
There is, however, one troubling issue that arises here. By default, the standard movie controller displays the controller bar (usually along the bottom edge of the movie). But in some cases we might not want the controller bar to be displayed. This is especially true in the case of the movie that we just built; after all, what's the point of devising our own sprite buttons for controlling a movie if we're saddled with the standard controller bar?
To address this issue, QuickTime 3.0 introduced a new type of movie controller called the no-interface movie controller (or sometimes the none movie controller). The no-interface movie controller operates just like the standard movie controller except that no controller bar is displayed and no keyboard events are passed to it. (Also, we cannot display a badge on a movie that's associated with the no-interface movie controller, but that's sort of a corollary of the fact that no controller bar is displayed.)
The no-interface movie controller is made to order for movies in which we used wired sprites to handle user interaction with the movie or with its elements. But how do we tell QuickTime to use the no-interface movie controller for some particular movie? We do this by attaching a user data item of type kUserDataMovieControllerType to the movie, whose item data is FOUR_CHAR_CODE('none'), like this:
OSType myType = FOUR_CHAR_CODE('none'); myType = EndianU32_NtoB(myType); SetUserDataItem(GetMovieUserData(theMovie), &myType, sizeof(myType), kUserDataMovieControllerType, 1);
(The value FOUR_CHAR_CODE('none') is the component subtype of a movie controller component. Unfortunately, there is no constant defined in any of the public QuickTime header files for this value.)
When an application calls NewMovieController to attach a movie controller to a movie, NewMovieController first looks for a movie user data item of type kUserDataMovieControllerType and, if it finds one, tries to open an instance of a movie controller component having the subtype specified by that item's data. This occurs transparently to the application (which is why existing applications are able to play wired sprite movies using the no-interface movie controller).
Setting the Track Matrix
It's time for a small but important digression. Recall that we determined the size of the new sprite track by calling GetMovieBox, like this:
GetMovieBox(theMovie, &myRect); myWidth = Long2Fix(myRect.right - myRect.left); myHeight = Long2Fix(myRect.bottom - myRect.top);
This method works in almost all cases — I'd venture to guess that it will work for at least 98% of all existing QuickTime movies. But in a few cases, it fails miserably. Figure 10 shows what happens if we try to add our sprite button track to the toy train movie we encountered in an earlier article.
Figure 10: The toy train movie after adding the sprite button track
To understand what's causing this problem, and how we can avoid it, we need to delve a bit deeper into a movie's geometry. When a track's media handler draws some data, it draws it into the rectangle (called the track rectangle) defined by the track's dimensions. The coordinate system in which the point (0, 0) is at the upper-left corner of the track rectangle defines the track coordinate system. When a track is composited into a movie, the track data is transformed using a track matrix, which maps the track coordinate system into the movie coordinate system. But before the movie data is displayed on the output device, that data is transformed once again. QuickTime applies the movie matrix, which maps the movie coordinate system into the display coordinate system (namely, the QuickDraw global coordinate plane).
In most cases, the track matrices and the movie matrix are the identity matrix, so we can safely ignore these various transformations. But a movie can have a non-identity movie matrix. The toy train movie is one such movie, and that's what causes the problem seen in Figure 10. The GetMovieBox function gives us the size and location of the movie in display coordinates, after the track and movie matrices have been applied. Using the movie box to determine the dimensions of the sprite track is guaranteed to produce the wrong results if the movie has a non-identity movie matrix, since the track rectangle is always transformed using that matrix before being drawn to the screen.
A solution to this problem is actually fairly simple. All we need to do is compensate for the movie matrix by calculating the inverse of the movie matrix and installing it as the sprite track's track matrix, like this:
GetMovieMatrix(theMovie, &myMatrix); if (InverseMatrix(&myMatrix, &myInverseMatrix)) SetTrackMatrix(myTrack, &myInverseMatrix);
The InverseMatrix function returns, in the second parameter, the inverse of the matrix specified by the first parameter. If it succeeds in inverting the first matrix, it returns true (and false otherwise). So the sprite track's track matrix and the movie matrix essentially cancel each other out, which is sufficient to place the sprite track in the right place.
One moral of this story is that it's good to test our code on a wide variety of movies to make sure that it's able to handle pretty much anything the user might throw at it. It's perfectly legal — albeit rare — for a movie to have a non-identity movie matrix. So our applications should be smart enough to handle them correctly. A particularly useful collection of "non-standard" movies can be found on the QuickTime 3.0 SDK CD, in the folder titled "Archived Interesting Movies". That collection contains movies with non-identity track matrices, non-identity movie matrices, and a handful of other oddball characteristics. Testing our code on these movies can often reveal some design flaws in our logic.
Putting It All Together
Listing 7 shows our complete definition of the QTWired_AddSpriteControllerTrack function.
Listing 7: Adding a sprite controller track to a movie
OSErr QTWired_AddSpriteControllerTrack (Movie theMovie) { Track myTrack = NULL; Media myMedia = NULL; MatrixRecord myMatrix; RGBColor myKeyColor; Fixed myWidth, myHeight; Rect myRect; TimeValue myDuration = 0L; TimeValue myTimeScale = 0L; OSType myType = FOUR_CHAR_CODE('none'); OSErr myErr = noErr; if (theMovie == NULL) { myErr = paramErr; goto bail; } // get some information about the target movie GetMovieBox(theMovie, &myRect); myWidth = Long2Fix(myRect.right - myRect.left); myHeight = Long2Fix(myRect.bottom - myRect.top); myDuration = GetMovieDuration(theMovie); myTimeScale = GetMovieTimeScale(theMovie); // create a new sprite track in the target movie myTrack = NewMovieTrack(theMovie, myWidth, myHeight, 255); myMedia = NewTrackMedia(myTrack, SpriteMediaType, myTimeScale, NULL, 0); // set the track matrix to compensate for any existing movie matrix GetMovieMatrix(theMovie, &myMatrix); if (InverseMatrix(&myMatrix, &myInverseMatrix)) SetTrackMatrix(myTrack, &myInverseMatrix); myErr = BeginMediaEdits(myMedia); if (myErr != noErr) goto bail; // add sprite images and sprites to the sprite track; add actions to the sprites QTWired_AddControllerButtonSamplesToMedia(myMedia, myRect.right - myRect.left, myRect.bottom - myRect.top, myDuration); myErr = EndMediaEdits(myMedia); if (myErr != noErr) goto bail; // add the media to the track InsertMediaIntoTrack(myTrack, 0, 0, GetMediaDuration(myMedia), fixed1); // set the sprite track properties QTWired_SetTrackProperties(myMedia, kNoQTIdleEvents); myKeyColor.red = myKeyColor.green = myKeyColor.blue = 0xffff; // white MediaSetGraphicsMode(GetMediaHandler(myMedia), transparent, &myKeyColor); // make sure that the sprite track is in the frontmost layer SetTrackLayer(myTrack, kMaxLayerNumber); SetTrackLayer(myTrack, QTWired_GetLowestLayerInMovie(theMovie) - 1); // select the "no-interface" movie controller myType = EndianU32_NtoB(myType); SetUserDataItem(GetMovieUserData(theMovie), &myType, sizeof(myType), kUserDataMovieControllerType, 1); bail: return(myErr); }
Wired Sprite Utilities
You'll notice that I didn't include the complete listing of the QTWired_AddControllerButtonSamplesToMedia function. The reason for this should be clear from a brief glance at Listings 2, 3, and 4: adding simple button behaviors to all of the six sprites would require a total of about 200 lines of code. Obviously, for both readability and maintainability, it would behoove us to encapsulate parts of that function into some reusable utility functions, in just the same way that previously we facilitated our work with sprites by using the functions in SpriteUtilities.c. For the most common operations with wired sprites, we can use the functions in the file WiredSpriteUtilities.c. In this section, we're going to consider a few of those functions.
Adding Event and Action Atoms
In Listings 2, 3, and 4, we begin by adding an event atom of the appropriate type to the existing sprite atom. At that point, since we've just created the sprite atom ourselves, we know that it doesn't already have any event atoms in it, so we can safely call QTInsertChild to add one. More generally, it would be better to look and see whether the sprite atom already contains an event atom of the type we want to add. If it does, then we can just add our new action atoms to that event atom. If it doesn't, then we need to create a new atom of that type. Listing 8 shows the definition of the utility function WiredUtils_AddQTEventAtom, which returns to us either the existing event atom or a new event atom.
Listing 8: Adding an event atom
OSErr WiredUtils_AddQTEventAtom (QTAtomContainer theContainer, QTAtom theActionAtoms, QTAtomID theQTEventType, QTAtom *theNewQTEventAtom) { OSErr myErr = noErr; if ((theContainer == NULL) || (theQTEventType == 0) || (theNewQTEventAtom == NULL)) { myErr = paramErr; goto bail; } if (theQTEventType == kQTEventFrameLoaded) { *theNewQTEventAtom = QTFindChildByID(theContainer, theActionAtoms, kQTEventFrameLoaded, 1, NULL); if (*theNewQTEventAtom == 0) myErr = QTInsertChild(theContainer, theActionAtoms, kQTEventFrameLoaded, 1, 1, 0, NULL, theNewQTEventAtom); } else { *theNewQTEventAtom = QTFindChildByID(theContainer, theActionAtoms, kQTEventType, theQTEventType, NULL); if (*theNewQTEventAtom == 0) myErr = QTInsertChild(theContainer, theActionAtoms, kQTEventType, theQTEventType, 1, 0, NULL, theNewQTEventAtom); } bail: return(myErr); }
Notice that frame-loaded events are treated differently here. The atom type of a frame-loaded event is kQTEventFrameLoaded (not kQTEventType, as with other events) and the ID should be 1. Also, for frame-loaded events, we expect the caller to set the theActionAtoms parameter to kParentAtomIsContainer, so that the event is inserted as a child of the container atom.
Once we have an event atom, we want to add one or more action atoms to it. As we know, an action atom is a parent atom that always includes a child of type kWhichAction. We can use the WiredUtils_AddActionAtom function, defined in Listing 9, to add an action atom to an event atom.
Listing 9: Adding an action atom to an event atom
OSErr WiredUtils_AddActionAtom (QTAtomContainer theContainer, QTAtom theEventAtom, long theActionConstant, QTAtom *theNewActionAtom) { QTAtom myActionAtom = 0; OSErr myErr = noErr; if ((theContainer == NULL) || (theActionConstant == 0)) { myErr = paramErr; goto bail; } myErr = QTInsertChild(theContainer, theEventAtom, kAction, 0, 0, 0, NULL, &myActionAtom); if (myErr != noErr) goto bail; theActionConstant = EndianU32_NtoB(theActionConstant); myErr = QTInsertChild(theContainer, myActionAtom, kWhichAction, 1, 1, sizeof(theActionConstant), &theActionConstant, NULL); bail: if (theNewActionAtom != NULL) { if (myErr != noErr) *theNewActionAtom = 0; else *theNewActionAtom = myActionAtom; } return(myErr); }
Actually, the value we pass for theEventAtom can be kParentAtomIsContainer, in which case the action atom is added as a child of theContainer. This will be useful later when we want to create complex wired actions involving conditional execution of actions.
WiredSpriteUtilities.c defines the function WiredUtils_AddQTEventAndActionAtoms, which just calls WiredUtils_AddQTEventAtom and then WiredUtils_AddActionAtom.
Listing 10: Adding event and action atoms
OSErr WiredUtils_AddQTEventAndActionAtoms (QTAtomContainer theContainer, QTAtom theAtom, long theEvent, long theAction, QTAtom *theActionAtom) { QTAtom myEventAtom = 0; OSErr myErr = noErr; myEventAtom = theAtom; if (theEvent != 0) { myErr = WiredUtils_AddQTEventAtom(theContainer, theAtom, theEvent, &myEventAtom); if (myErr != noErr) goto bail; } myErr = WiredUtils_AddActionAtom(theContainer, myEventAtom, theAction, theActionAtom); bail: return(myErr); }
To add a parameter to an action, we add a child atom of type kActionParameter. WiredSpriteUtilities.c defines the function WiredUtils_AddActionParameterAtom, which just calls QTInsertChild. Bear in mind that the data passed in the theParamData parameter must be in big-endian format, since WiredUtils_AddActionParameterAtom does not perform any byte swapping.
Listing 11: Adding an action parameter atom
OSErr WiredUtils_AddActionParameterAtom (QTAtomContainer theContainer, QTAtom theActionAtom, long theParameterIndex, long theParamDataSize, void *theParamData, QTAtom *theNewParamAtom) { return(QTInsertChild(theContainer, theActionAtom, kActionParameter, 0, (short)theParameterIndex, theParamDataSize, theParamData, theNewParamAtom)); }
Setting a Sprite Image Index
Let's put these utilities to work for us. Listing 12 defines the WiredUtils_AddSpriteSetImageIndexAction function, which adds to the sprite atom theAtom the children necessary to change the sprite's image index in response to the event specified by the theEvent parameter. (For the moment, we'll ignore the call to WiredUtils_AddTrackAndSpriteTargetAtoms, since our sprite actions in this article all use the default targets.)
Listing 12: Adding a sprite index setting action atom
OSErr WiredUtils_AddSpriteSetImageIndexAction (QTAtomContainer theContainer, QTAtom theAtom, long theEvent, long theTrackTargetType, void *theTrackTarget, long theTrackTypeIndex, long theSpriteTargetType, void *theSpriteTarget, short theImageIndex, QTAtom *theActionAtom) { QTAtom myActionAtom = 0; OSErr myErr = noErr; myErr = WiredUtils_AddQTEventAndActionAtoms(theContainer, theAtom, theEvent, kActionSpriteSetImageIndex, &myActionAtom); if (myErr != noErr) goto bail; theImageIndex = EndianS16_NtoB(theImageIndex); myErr = WiredUtils_AddActionParameterAtom(theContainer, myActionAtom, kFirstParam, sizeof(theImageIndex), &theImageIndex, NULL); if (myErr != noErr) goto bail; myErr = WiredUtils_AddTrackAndSpriteTargetAtoms (theContainer, myActionAtom, theTrackTargetType, theTrackTarget, theTrackTypeIndex, theSpriteTargetType, theSpriteTarget); if (theActionAtom != NULL) *theActionAtom = myActionAtom; bail: return(myErr); }
Now we can go back and rework some of our earlier code. In particular, all the code in Listings 2, 3, and 4 can be replaced by these lines:
WiredUtils_AddSpriteSetImageIndexAction(myStartButton, kParentAtomIsContainer, kQTEventMouseClick, 0, NULL, 0, 0, NULL, kToBeginDownIndex, NULL); WiredUtils_AddSpriteSetImageIndexAction(myStartButton, kParentAtomIsContainer, kQTEventMouseClickEnd, 0, NULL, 0, 0, NULL, kToBeginUpIndex, NULL); WiredUtils_AddMovieGoToBeginningAction(myStartButton, kParentAtomIsContainer, kQTEventMouseClickEndTriggerButton);
Variables and Conditionals
In addition to the basic event-and-action interactivity that we've witnessed so far, QuickTime also supports what we might call programmable interactivity. That is to say, our wiring can make use of standard programming concepts like variables, function calls, and flow-control logic. In our wired actions, we can set and get the values of variables, read movie and system characteristics, and control the execution of actions using "if-then" and "while" constructs. As you can imagine, this opens the door for some sophisticated and truly amazing wired actions. In practice, variables, environment checking, and flow control are always used in combination; so let's roll out the theory in this section and defer actual examples of their use until the next section.
Setting Variables
Each sprite track can have a set of variables — called sprite track variables — whose values can be set and read by wired actions. Variables are particularly useful for maintaining state information, such as whether a particular button is down or whether a sprite has reached a certain location in the movie. In QuickTime 3, the values of sprite track variables were always floating-point values; beginning in QuickTime 4, these values can be either floating-point values or NULL-terminated strings.
We refer to a sprite track variable using a variable ID, which is of type QTAtomID. (A QTAtomID is declared as a signed long integer, of type long.) To set the value of a sprite track variable, we execute either the kActionSpriteTrackSetVariable or the kActionSpriteTrackSetVariableToString action. Both these actions require two parameters, the ID of the variable to be set and the desired value of the variable (which is a floating-point value or a string, respectively). Listing 13 defines a function that we can use to set the value of a variable to a specified floating-point value.
Listing 13: Setting a sprite track variable
OSErr WiredUtils_AddSpriteTrackSetVariableAction (QTAtomContainer theContainer, QTAtom theAtom, long theEvent, QTAtomID theVariableID, float theValue, long theTrackTargetType, void *theTrackTarget, long theTrackTypeIndex) { QTAtom myActionAtom = 0; OSErr myErr = noErr; myErr = WiredUtils_AddQTEventAndActionAtoms(theContainer, theAtom, theEvent, kActionSpriteTrackSetVariable, &myActionAtom); if (myErr != noErr) goto bail; theVariableID = EndianU32_NtoB(theVariableID); myErr = WiredUtils_AddActionParameterAtom(theContainer, myActionAtom, kFirstParam, sizeof(theVariableID), &theVariableID, NULL); if (myErr != noErr) goto bail; EndianUtils_Float_NtoB(&theValue); myErr = WiredUtils_AddActionParameterAtom(theContainer, myActionAtom, kSecondParam, sizeof(theValue), &theValue, NULL); myErr = WiredUtils_AddTrackTargetAtom(theContainer, myActionAtom, theTrackTargetType, theTrackTarget, theTrackTypeIndex); bail: return(myErr); }
We can perform numeric operations on the values of floating-point variables by creating expression atoms (which we'll consider shortly). Also, we can concatenate the values of two string variables by executing the kActionSpriteTrackConcatVariables action; this action requires three parameters, the variable IDs of the two string variables to be concatenated and the variable ID of the variable to which the result is to be assigned.
Controlling Action Processing
QuickTime supports two basic mechanisms for controlling the flow of wired action execution, the kActionCase action and the kActionWhile action. The kActionWhile action takes a single parameter, of type kConditionalAtomType. This atom is a parent atom that contains two children, one of type kExpressionContainerAtomType and another of type kActionListAtomType. The expression container atom defines an expression; as long as the expression evaluates to a non-zero value, the action list is executed. (An action list is simply a list of one or more action atoms.) Figure 11 shows the general structure of a kActionWhile atom.
Figure 11: The structure of a while atom
A case atom is slightly more complex than a while atom. In a case atom, there is exactly one parameter atom, which contains an arbitrary number of kConditionalAtomType atoms (which may have any unique atom IDs). When a case atom is executed, the conditional atoms are evaluated (starting with the atom with index 1); when the expression in one of the expression atoms evaluates to a non-zero value, the associated action atom list is executed. Figure 12 shows the general structure of a kActionCase atom.
Figure 12: The structure of a case atom
A case atom is analogous to an "if" statement followed by some number of "else if" statements. We can therefore emulate an "if-then" construct by creating a case atom that contains just one conditional atom.
Using Expressions and Operands
Now we need to understand how to construct an expression container atom. An expression container atom is a parent atom that contains either an operator atom or an operand atom. Operator atoms provide a means of combining operand atoms (or indeed other operator atoms) using numerical and logical operations. For instance, Figure 13 shows the structure of an expression container atom that adds two operands together. Notice that the atom ID of an operator atom specifies the kind of operation to perform. Notice also that the IDs of the child operand atoms can be any unique IDs. For operations in which the order of the operands is important, they are ordered by the atom index.
Figure 13: A sample expression container atom
If an operator atom for a binary operation contains more than two children, the operation is applied to the first two children; then the operation is applied once more to that result and the third child, and so on until all the children have been used. This allows us to perform multiple operations without an undue amount of atom nesting.
So ultimately it all boils down to operands, which are the "raw materials" for operations and expressions. There are two basic kinds of operands (aside from expressions, which can also serve as operands): constant operands and function operands. A constant operand is a leaf atom of type kOperandConstant whose atom data is a floating-point value; a constant operand atom is contained in an atom of type kOperandAtomType.
A function operand returns information about some object, most often the current setting of some property of the operand's target. For instance, the kOperandMovieVolume operand returns the current volume of the target movie (which by default is the movie that contains the sprite track). And the kOperandSpriteTrackVariable operand returns the current value of a specified sprite track variable. And the kOperandMouseLocalHLoc operand returns the current horizontal position of the cursor. Most function operands take no parameters; when a parameter is required, it's added as a child of the operand atom (in just the same way that parameters are added to action atoms).
The file Movies.h defines constants for over a hundred function operands. We can use them to get information about the user's internet connection speed, the current day of the week, the current operating system, the movie rate, a track's width and height, a sprite's current image index, and so forth.
Draggable Sprites
Let's consider a real-life example that uses programmable actions, the draggable sprite movie shown in Figures 1, 2, and 3. The basic idea is very simple: we want to set the position of the sprite to the position of the cursor for as long as the mouse button is held down over the sprite. There is (as of QuickTime 5.0) no operand that returns the current state of the mouse button, so we'll have to keep track of that state ourselves, using a sprite track variable. We define the ID of that variable like this:
#define kMouseStateVariableID 2
Then we need to install three event atoms in the icon sprite atom:
- When the mouse button is clicked within the sprite image, we want to set the value of the mouse-state variable to 1. So we'll add a kActionSpriteTrackSetVariable action to the sprite's kQTEventMouseClick event atom.
- When the mouse button is released, we want to set the value of the mouse-state variable to 0. So we'll add a kActionSpriteTrackSetVariable action to the sprite's kQTEventMouseClickEnd event atom.
- Whenever we receive an idle event, we'll check to see whether the mouse button is down or up; if it's down, we set the position of the sprite to the current position of the cursor. So we'll add a conditional action to the sprite's kQTEventIdle event atom.
These first two items are easy to do, especially now that we have our wired sprite utilities at hand:
WiredUtils_AddSpriteTrackSetVariableAction(theContainer, mySpriteAtom, kQTEventMouseClick, kMouseStateVariableID, 1, 0, NULL, 0); WiredUtils_AddSpriteTrackSetVariableAction(theContainer, mySpriteAtom, kQTEventMouseClickEnd, kMouseStateVariableID, 0, 0, NULL, 0);
The third item is a bit trickier, however, since it involves constructing a conditional action and using operands to read the current mouse position. Let's step though this process carefully. For the remainder of this section, we'll dispense with our standard error checking (solely to save some space). In addition, for the moment we'll also dispense with the wired sprite utilities, so that we see how to do this using just the basic QuickTime APIs.
Creating a Condition
Suppose that myEventAtom is the idle event atom inside the icon sprite atom. We want to add an "if-then" decision to this atom, so we begin by inserting an action atom of type kActionCase together with a parameter atom that will serve as the parent of the conditional atom, which in turn is the parent of the expression container and atom list atoms. (See Figure 12 again.)
QTInsertChild(theContainer, myEventAtom, kAction, 0, 0, 0, NULL, &myActionAtom); myAction = EndianU32_NtoB(kActionCase); QTInsertChild(theContainer, myActionAtom, kWhichAction, 1, 1, sizeof(myAction), &myAction, NULL); QTInsertChild(theContainer, myActionAtom, kActionParameter, 1, kFirstParam, 0, NULL, &myParamAtom); QTInsertChild(theContainer, myParamAtom, kConditionalAtomType, 0, 1, 0, NULL, &myConditionalAtom);
Specifying the Conditional Expression
The conditional atom contains an expression container atom that indicates the condition under which the action list contained in the conditional atom is to be executed:
QTInsertChild(theContainer, myConditionalAtom, kExpressionContainerAtomType, 1, 1, 0, NULL, &myExpressionAtom);
The expression container atom, in turn, contains an expression. In the present case, we want the expression to be: "if the value of the variable with ID kMouseStateVariableID is equal to 1". This gets decomposed into an operator atom of type kOperatorEqualTo that has two child atoms (one for each of the two operands):
QTInsertChild(theContainer, myExpressionAtom, kOperatorAtomType, kOperatorEqualTo, 1, 0, NULL, &myOperatorAtom)
The first child of the operator atom contains the constant value 1. So we need to insert an operand atom into the operator atom:
QTInsertChild(theContainer, myOperatorAtom, kOperandAtomType, 0, 1, 0, NULL, &myOperandAtom);
Then we need to insert an operand type atom of type kOperandConstant; in this case, we can insert the constant value as the atom data, like this:
myConstantValue = 1; QTInsertChild(theContainer, myOperandAtom, kOperandConstant, 1, 1, 0, NULL, &myOperandTypeAtom); EndianUtils_Float_NtoB(&myConstantValue); QTSetAtomData(theContainer, myOperandTypeAtom, sizeof(myConstantValue), &myConstantValue);
Note that we need to make sure that the atom data (of type float) is in big-endian format.
Finally, we need to insert a second operand atom into the operator atom. This time, however, the operand is not a constant value; rather, it's the value of our sprite track variable. So we insert into the operand atom an operand type atom of type kOperandSpriteTrackVariable; this atom then holds a parameter atom that specifies which sprite track variable to use.
QTInsertChild(theContainer, myOperatorAtom, kOperandAtomType, 0, 2, 0, NULL, &myOperandAtom); QTInsertChild(theContainer, myOperandAtom, kOperandSpriteTrackVariable, 1, 1, 0, NULL, &myOperandTypeAtom); myVariableID = EndianU32_NtoB(kMouseStateVariableID); QTInsertChild(theContainer, myOperandTypeAtom, kActionParameter, 1, 1, sizeof(myVariableID), &myVariableID, NULL);
And so we are finished building the expression container atom.
Specifying the Conditional Actions
Now we need to build the action list atom that will be executed if the expression container atom evaluates to a non-zero value. We begin by inserting an action list atom into the conditional atom; this atom will contain a single child atom, which is an action atom of type kActionSpriteTranslate.
QTInsertChild(theContainer, myConditionalAtom, kActionListAtomType, 1, 1, 0, NULL, &myActionListAtom); QTInsertChild(theContainer, myActionListAtom, kAction, 0, 0, 0, NULL, &myActionAtom); myAction = EndianU32_NtoB(kActionSpriteTranslate); QTInsertChild(theContainer, myActionAtom, kWhichAction, 1, 1, sizeof(myAction), &myAction, NULL);
The kActionSpriteTranslate action requires three parameters, which are the horizontal and vertical positions to translate to, and a Boolean value that indicates whether those values specify an absolute or relative translation. We add the first parameter like this:
QTInsertChild(theContainer, myActionAtom, kActionParameter, 0, (short)kFirstParam, 0, NULL, &myParameterAtom); QTInsertChild(theContainer, myParameterAtom, kExpressionContainerAtomType, 1, 1, 0, NULL, &myExpressionAtom); QTInsertChild(theContainer, myExpressionAtom, kOperandAtomType, 0, 1, 0, NULL, &myOperandAtom); QTInsertChild(theContainer, myOperandAtom, kOperandMouseLocalHLoc, 1, 1, 0, NULL, NULL);
The first parameter is an expression container atom that contains a function operand, kOperandMouseLocalHLoc, which returns the current horizontal position of the mouse. The second parameter retrieves the current vertical position of the mouse in exactly the same way:
QTInsertChild(theContainer, myActionAtom, kActionParameter, 0, (short) kSecondParam, 0, NULL, &myParameterAtom); QTInsertChild(theContainer, myParameterAtom, kExpressionContainerAtomType, 1, 1, 0, NULL, &myExpressionAtom); QTInsertChild(theContainer, myExpressionAtom, kOperandAtomType, 0, 1, 0, NULL, &myOperandAtom); QTInsertChild(theContainer, myOperandAtom, kOperandMouseLocalVLoc, 1, 1, 0, NULL, NULL);
The operands kOperandMouseLocalHLoc and kOperandMouseLocalVLoc, like all non-string operands, return a floating-point value (of type float). But the kActionSpriteTranslate action expects its first two parameters to be of type Fixed. It's important to know that QuickTime automatically converts the floating-point values into Fixed values before passing them to kActionSpriteTranslate. So we don't need to worry about converting the data types here.
Finally, we add the third parameter like this:
myIsAbsolute = true; QTInsertChild(theContainer, myActionAtom, kActionParameter, 0, (short)kThirdParam, sizeof(myIsAbsolute), &myIsAbsolute, NULL);
So, we are finished building the action list atom.
Putting It All Together Again
Listing 14 shows a version of the QTWired_MakeSpriteDraggable function. Once again I've omitted most of the error-checking to enhance the readability of the code. And this version uses some wired sprite utilities to create the expression container and atom list atoms. The file QTWiredSpritesJr.c contains a more complete version that has better error checking; in addition, that version contains both wired utility calls and basic QuickTime API calls. So you can see how it's done both ways.
Listing 14: Making a sprite draggable
OSErr QTWired_MakeSpriteDraggable (QTAtomContainer theContainer, QTAtomID theID) { QTAtom mySpriteAtom = 0; QTAtom myEventAtom = 0; QTAtom myActionAtom = 0; QTAtom myParamAtom = 0; QTAtom myConditionalAtom, myExpressionAtom, myOperatorAtom, myActionListAtom, myParameterAtom; short myOperandIndex; QTAtomID myVariableID; float myConstantValue; Boolean myIsAbsolute; OSErr myErr = noErr; // find the sprite atom with the specified ID in the specified container mySpriteAtom = QTFindChildByID(theContainer, kParentAtomIsContainer, kSpriteAtomType, theID, NULL); if (mySpriteAtom == 0) { myErr = paramErr; goto bail; } // add a mouse click event handler WiredUtils_AddSpriteTrackSetVariableAction(theContainer, mySpriteAtom, kQTEventMouseClick, kMouseStateVariableID, 1, 0, NULL, 0); // add a mouse click end event handler WiredUtils_AddSpriteTrackSetVariableAction(theContainer, mySpriteAtom, kQTEventMouseClickEnd, kMouseStateVariableID, 0, 0, NULL, 0); // add an idle event handler WiredUtils_AddQTEventAndActionAtoms(theContainer, mySpriteAtom, kQTEventIdle, kActionCase, &myActionAtom); // add a parameter atom to the kActionCase action atom; this will serve as a parent to // hold the expression and action atoms WiredUtils_AddActionParameterAtom(theContainer, myActionAtom, kFirstParam, 0, NULL, &myParamAtom); WiredUtils_AddConditionalAtom(theContainer, myParamAtom, 1, &myConditionalAtom); WiredUtils_AddExpressionContainerAtomType(theContainer, myConditionalAtom, &myExpressionAtom); WiredUtils_AddOperatorAtom(theContainer, myExpressionAtom, kOperatorEqualTo, &myOperatorAtom); myOperandIndex = 1; myConstantValue = 1; WiredUtils_AddOperandAtom(theContainer, myOperatorAtom, kOperandConstant, myOperandIndex, NULL, myConstantValue); myOperandIndex = 2; myVariableID = kMouseStateVariableID; WiredUtils_AddVariableOperandAtom(theContainer, myOperatorAtom, myOperandIndex, 0, NULL, 0, myVariableID); WiredUtils_AddActionListAtom(theContainer, myConditionalAtom, &myActionListAtom); WiredUtils_AddActionAtom(theContainer, myActionListAtom, kActionSpriteTranslate, &myActionAtom); // first parameter: get current mouse position x WiredUtils_AddActionParameterAtom(theContainer, myActionAtom, kFirstParam, 0, NULL, &myParameterAtom); WiredUtils_AddExpressionContainerAtomType(theContainer, myParameterAtom, &myExpressionAtom); WiredUtils_AddOperandAtom(theContainer, myExpressionAtom, kOperandMouseLocalHLoc, 1, NULL, 0); // second parameter: get current mouse position y WiredUtils_AddActionParameterAtom(theContainer, myActionAtom, kSecondParam, 0, NULL, &myParameterAtom); WiredUtils_AddExpressionContainerAtomType(theContainer, myParameterAtom, &myExpressionAtom); WiredUtils_AddOperandAtom(theContainer, myExpressionAtom, kOperandMouseLocalVLoc, 1, NULL, 0); // third parameter: true myIsAbsolute = true; WiredUtils_AddActionParameterAtom(theContainer, myActionAtom, kThirdParam, sizeof(myIsAbsolute), &myIsAbsolute, NULL); bail: return(myErr); }
Before we move on, I should mention that QTWired_MakeSpriteDraggable isn't entirely satisfactory. When you click the cursor on the icon sprite, the sprite is immediately moved so that its registration point lies under the hot spot of the cursor. This is because the kActionSpriteTranslate action moves the registration point of the sprite to the specified location (or offsets it by the specified amount, if the translation is relative). Ideally, we should be able to "grab" the sprite at any point in its image and move it so that the relative positions of the sprite image and cursor remaining constant. This is an easy refinement, but one that I'll leave as an exercise for the reader.
Sprite Button Behaviors
We've managed to make the icon sprite draggable, but we haven't yet written any code to change the cursor while the icon sprite is being dragged around. A little browsing in Movies.h will reveal the kActionTrackSetCursor action, which takes one parameter (of type QTAtomID) that specifies the ID of the desired cursor. IDs less than or equal to 1000 are reserved for use by QuickTime. If the ID is 0, then the default system cursor is used. If the ID is greater than 1000, then QuickTime looks in the target track's media property atom for an atom of type 'crsr' with the specified ID; if it finds that atom, it uses the atom's data (which is assumed to be structured just like a Macintosh 'crsr' resource) as the cursor. (On Windows, QuickTime always uses the black-and-white versions of the specified cursors.) Movies.h also defines these constants to allow us to access some built-in cursors (shown in Figure 14):
enum { kQTCursorOpenHand = -19183, kQTCursorClosedHand = -19182, kQTCursorPointingHand = -19181, kQTCursorRightArrow = -19180, kQTCursorLeftArrow = -19179, kQTCursorDownArrow = -19178, kQTCursorUpArrow = -19177, kQTCursorIBeam = -19176 };
Figure 14: QuickTime's built-in cursors
So we do in fact know how to add the finishing touches to the icon movie: just add an action atom of type kActionTrackSetCursor to the appropriate event atoms.
QuickTime 4 introduced a much simpler and more efficient way to attach button-like characteristics to a sprite, using sprite button behaviors. We attach these behaviors to a sprite by including an atom of type kSpriteBehaviorsAtomType in the sprite atom. This atom, in turn, contains one to three child atoms that indicate the desired changes that are to be triggered by the state of the mouse button and the location of the cursor:
- To change the sprite's image on cursor and mouse events, we add a child of type kSpriteImageBehaviorAtomType to the sprite behaviors atom.
- To change the appearance of the cursor on cursor and mouse events, we add a child of type kSpriteCursorBehaviorAtomType to the sprite behaviors atom.
- To change the string that's displayed in the status area of a web browser window, we add a child of type kSpriteStatusStringsBehaviorAtomType to the sprite behaviors atom.
The atom data in all three of these cases is a structure of type QTSpriteButtonBehaviorStruct, defined like this:
struct QTSpriteButtonBehaviorStruct { QTAtomID notOverNotPressedStateID; QTAtomID overNotPressedStateID; QTAtomID overPressedStateID; QTAtomID notOverPressedStateID; };
In a sprite image behavior atom, these fields specify the sprite image indices to use for the specified state. In a sprite cursor behavior atom, these fields specify the cursor IDs to use. In a sprite status strings behavior atom, these fields specify the IDs of sprite string variables. If we don't want the current image or cursor or status string to change when one of these four states is entered, we set the appropriate field to –1.
Listing 15 shows our definition of the QTWired_AddCursorChangeToSprite function, which we use to set the custom mouse-over and mouse-down cursors for the icon sprite.
Listing 15: Setting a custom cursor for a sprite
OSErr QTWired_AddCursorChangeToSprite (QTAtomContainer theContainer, QTAtomID theID) { QTAtom mySpriteAtom = 0; QTAtom myBehaviorAtom = 0; QTSpriteButtonBehaviorStruct myBehaviorRec; OSErr myErr = paramErr; // find the sprite atom with the specified ID in the specified container mySpriteAtom = QTFindChildByID(theContainer, kParentAtomIsContainer, kSpriteAtomType, theID, NULL); if (mySpriteAtom == 0) goto bail; // insert a new sprite behaviors atom into the sprite atom myErr = QTInsertChild(theContainer, mySpriteAtom, kSpriteBehaviorsAtomType, 1, 1, 0, NULL, &myBehaviorAtom); if (myErr != noErr) goto bail; // set the sprite cursor behavior myBehaviorRec.notOverNotPressedStateID = EndianS32_NtoB(-1); myBehaviorRec.overNotPressedStateID = EndianS32_NtoB(kQTCursorOpenHand); myBehaviorRec.overPressedStateID = EndianS32_NtoB(kQTCursorClosedHand); myBehaviorRec.notOverPressedStateID = EndianS32_NtoB(-1); myErr = QTInsertChild(theContainer, myBehaviorAtom, kSpriteCursorBehaviorAtomType, 1, 1, sizeof(QTSpriteButtonBehaviorStruct), &myBehaviorRec, NULL); bail: return(myErr); }
The actions described in a sprite behaviors atom are inserted at the front of the list of actions associated with a particular event; this allows those behaviors to be overridden by other actions contained in the sprite's event atoms.
I'll leave it as an exercise for the reader to rework the sprite controller track code to use sprite behaviors instead of kActionSpriteSetImageIndex actions.
Conclusion
In this article, we've seen how to use wired actions to construct sprite "buttons" that control a linear QuickTime movie; we've also seen how to make a sprite draggable. Both of these are pretty simple examples, but they do give us a hint of the incredible power waiting to be harnessed. We've used only a handful of sprite actions, and only two function operands. So we've got plenty of room to expand our wiring repertoire. In the next two articles, we'll continue investigating wired actions.
Credits
Most of the utilities in the file WiredSpriteUtilities.c were originally written by Sean Allen; once again, 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.
Tim Monroe is still trying to figure out how to add wired actions to his lizards. They just sit and bask all day. You can send your ideas to him at monroe@apple.com.
![](/file/12652/www.mactech.com.tar/www.mactech.com/sites/all/themes/custom_front/img/search_text.gif)
- SPREAD THE WORD:
- Slashdot
- Digg
- Del.icio.us
- Newsvine