TweetFollow Us on Twitter

QuickTime - Wired

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.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Tokkun Studio unveils alpha trailer for...
We are back on the MMORPG news train, and this time it comes from the sort of international developers Tokkun Studio. They are based in France and Japan, so it counts. Anyway, semantics aside, they have released an alpha trailer for the upcoming... | Read more »
Win a host of exclusive in-game Honor of...
To celebrate its latest Jujutsu Kaisen crossover event, Honor of Kings is offering a bounty of login and achievement rewards kicking off the holiday season early. [Read more] | Read more »
Miraibo GO comes out swinging hard as it...
Having just launched what feels like yesterday, Dreamcube Studio is wasting no time adding events to their open-world survival Miraibo GO. Abyssal Souls arrives relatively in time for the spooky season and brings with it horrifying new partners to... | Read more »
Ditch the heavy binders and high price t...
As fun as the real-world equivalent and the very old Game Boy version are, the Pokemon Trading Card games have historically been received poorly on mobile. It is a very strange and confusing trend, but one that The Pokemon Company is determined to... | Read more »
Peace amongst mobile gamers is now shatt...
Some of the crazy folk tales from gaming have undoubtedly come from the EVE universe. Stories of spying, betrayal, and epic battles have entered history, and now the franchise expands as CCP Games launches EVE Galaxy Conquest, a free-to-play 4x... | Read more »
Lord of Nazarick, the turn-based RPG bas...
Crunchyroll and A PLUS JAPAN have just confirmed that Lord of Nazarick, their turn-based RPG based on the popular OVERLORD anime, is now available for iOS and Android. Starting today at 2PM CET, fans can download the game from Google Play and the... | Read more »
Digital Extremes' recent Devstream...
If you are anything like me you are impatiently waiting for Warframe: 1999 whilst simultaneously cursing the fact Excalibur Prime is permanently Vault locked. To keep us fed during our wait, Digital Extremes hosted a Double Devstream to dish out a... | Read more »
The Frozen Canvas adds a splash of colou...
It is time to grab your gloves and layer up, as Torchlight: Infinite is diving into the frozen tundra in its sixth season. The Frozen Canvas is a colourful new update that brings a stylish flair to the Netherrealm and puts creativity in the... | Read more »
Back When AOL WAS the Internet – The Tou...
In Episode 606 of The TouchArcade Show we kick things off talking about my plans for this weekend, which has resulted in this week’s show being a bit shorter than normal. We also go over some more updates on our Patreon situation, which has been... | Read more »
Creative Assembly's latest mobile p...
The Total War series has been slowly trickling onto mobile, which is a fantastic thing because most, if not all, of them are incredibly great fun. Creative Assembly's latest to get the Feral Interactive treatment into portable form is Total War:... | Read more »

Price Scanner via MacPrices.net

Early Black Friday Deal: Apple’s newly upgrad...
Amazon has Apple 13″ MacBook Airs with M2 CPUs and 16GB of RAM on early Black Friday sale for $200 off MSRP, only $799. Their prices are the lowest currently available for these newly upgraded 13″ M2... Read more
13-inch 8GB M2 MacBook Airs for $749, $250 of...
Best Buy has Apple 13″ MacBook Airs with M2 CPUs and 8GB of RAM in stock and on sale on their online store for $250 off MSRP. Prices start at $749. Their prices are the lowest currently available for... Read more
Amazon is offering an early Black Friday $100...
Amazon is offering early Black Friday discounts on Apple’s new 2024 WiFi iPad minis ranging up to $100 off MSRP, each with free shipping. These are the lowest prices available for new minis anywhere... Read more
Price Drop! Clearance 14-inch M3 MacBook Pros...
Best Buy is offering a $500 discount on clearance 14″ M3 MacBook Pros on their online store this week with prices available starting at only $1099. Prices valid for online orders only, in-store... Read more
Apple AirPods Pro with USB-C on early Black F...
A couple of Apple retailers are offering $70 (28%) discounts on Apple’s AirPods Pro with USB-C (and hearing aid capabilities) this weekend. These are early AirPods Black Friday discounts if you’re... Read more
Price drop! 13-inch M3 MacBook Airs now avail...
With yesterday’s across-the-board MacBook Air upgrade to 16GB of RAM standard, Apple has dropped prices on clearance 13″ 8GB M3 MacBook Airs, Certified Refurbished, to a new low starting at only $829... Read more
Price drop! Apple 15-inch M3 MacBook Airs now...
With yesterday’s release of 15-inch M3 MacBook Airs with 16GB of RAM standard, Apple has dropped prices on clearance Certified Refurbished 15″ 8GB M3 MacBook Airs to a new low starting at only $999.... Read more
Apple has clearance 15-inch M2 MacBook Airs a...
Apple has clearance, Certified Refurbished, 15″ M2 MacBook Airs now available starting at $929 and ranging up to $410 off original MSRP. These are the cheapest 15″ MacBook Airs for sale today at... Read more
Apple drops prices on 13-inch M2 MacBook Airs...
Apple has dropped prices on 13″ M2 MacBook Airs to a new low of only $749 in their Certified Refurbished store. These are the cheapest M2-powered MacBooks for sale at Apple. Apple’s one-year warranty... Read more
Clearance 13-inch M1 MacBook Airs available a...
Apple has clearance 13″ M1 MacBook Airs, Certified Refurbished, now available for $679 for 8-Core CPU/7-Core GPU/256GB models. Apple’s one-year warranty is included, shipping is free, and each... Read more

Jobs Board

Seasonal Cashier - *Apple* Blossom Mall - J...
Seasonal Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Seasonal Fine Jewelry Commission Associate -...
…Fine Jewelry Commission Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) Read more
Seasonal Operations Associate - *Apple* Blo...
Seasonal Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Read more
Hair Stylist - *Apple* Blossom Mall - JCPen...
Hair Stylist - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.