![](/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: 07
Column Tag: QuickTime Toolkit
Back In Action
by Tim Monroe
Working with Wired Actions
Introduction
The previous four QuickTime Toolkit articles have focused largely on two topics: how to create and manipulate sprites, and how to attach dynamic, interactive behaviors to sprites. Interactivity is one of QuickTime's greatest assets and perhaps its most significant advantage over competing media architectures. And, no doubt, wired sprites are the cornerstone of QuickTime interactivity. As we've seen, we can do some pretty nifty things with just a few event and action atoms placed in the right locations in a sprite track.
In this article, we're going to take one more look at wired actions. I want to consider a few of the event types and actions recently introduced in QuickTime 5, and I especially want to consider how to use wired actions with other kinds of tracks. So far, all the actions we've encoountered have been triggered by some event involving a sprite (for example, clicking on the sprite) or a sprite track (for example, loading a sprite track key frame). Beginning in QuickTime 4, however, it's also possible to attach wired actions to QuickTime VR nodes and hot spots, to text in text tracks, and to Flash content. Let's take a moment to see what we can do here.
QuickTime VR supports two general kinds of wired actions: (1) actions that apply to a particular node, and (2) actions that apply to a particular hot spot in a node. The node-specific actions can be attached to idle events (so that an action can be triggered periodically) or to frame-loaded events (so that an action can be triggered when the node is first entered). We could, for instance, attach an action to a frame-loaded event to set the initial pan and tilt angles that are displayed when the user enters the node. Similarly, we could attach an action to a frame-loaded event to start a sound track playing when the user enters the node. In addition, we could use idle event actions to dynamically adjust the balance and volume of that sound as the user changes the pan angle of the movie; this provides an easy way to make a sound appear to emanate from a specific location in a node (which is sometimes called directional sound). Or, we could adjust the pan and tilt angles of some other QuickTime VR movie, so that panning and tilting one panorama caused similar panning and tilting in a second.
We can also attach wired actions to hot spots inside of QuickTime VR nodes. We can configure these actions to be triggered by a variety of mouse events, including moving the mouse over a hot spot, moving the mouse out of a hot spot, and clicking the mouse within a hot spot. Once again, the actions that are triggered by these events can be any actions supported by the QuickTime wired actions architecture, such as starting and stopping sounds, enabling and disabling tracks, changing pan and tilt angles, and so forth.
QuickTime allows us to attach wired actions to a text track, giving us wired text. Consider, for instance, the movie shown in Figure 1. This movie contains a text track that's wired to displays the amount of memory currently available in the application's heap. (The text track is updated every two seconds by an idle event action.) We can use this movie to track the memory usage of any QuickTime-savvy application.
![](/file/12652/www.mactech.com.tar/www.mactech.com/articles/mactech/Vol.17/17.07/July01QTToolkit/fig01.gif)
Figure 1: A text track displaying the application's free memory
Or consider the movie shown in Figure 2, which also contains a single text track. This time the text track is wired so that clicking on the word "Apple" launches the user's default web browser and loads the URL
![](/file/12652/www.mactech.com.tar/www.mactech.com/articles/mactech/Vol.17/17.07/July01QTToolkit/fig02.gif)
Figure 2: A text track with hypertext links
Finally, we can attach wired actions to items in a Flash track. Flash is a file format developed by Macromedia, Inc. for displaying vector-based graphics and animations. It's especially popular for web-based content delivery, since it combines compact data size (and hence speedier downloads) with quick rendering on the client, advanced graphics manipulations, and support for mouse-based interactivity with the graphics objects. In QuickTime 4, Apple added a Flash media handler that provides support for Flash graphics, animations, and interactivity inside of QuickTime movies. In addition, QuickTime 4 added the ability to attach wired actions to elements in a Flash track, thereby supplementing the native Flash interactivity. So, for instance, we could use Flash graphic elements (instead of sprites) to provide a user-interface for a QuickTime movie.
Unfortunately, we don't yet know enough about the structure of QuickTime VR movies or Flash tracks to know where to insert event atoms. As a result, we'll have to postpone our hands-on work with wiring VR or Flash content. But we do know how text tracks are put together (see "Word Is Out" in MacTech, November 2000), so here we'll learn how to add some interactivity to our text tracks.
Our sample application this month is called QTWiredActions. The Test menu of QTWiredActions is shown in Figure 3. The first menu item creates the wired text movie shown in Figure 2. The second menu item creates the memory display movie shown in Figure 1.
![](/file/12652/www.mactech.com.tar/www.mactech.com/articles/mactech/Vol.17/17.07/July01QTToolkit/fig03.gif)
Figure 3: The Test menu of QTWiredActions
The next two menu items create two more sprite movies, which use wired actions and operands to make sprites react "physically" with the movie rectangle and with one another.
Text Actions
Let's begin by investigating a few of the ways in which we can use wired actions in text tracks. In general, we attach an event atom to a text media sample by concatenating the atom container that holds the event atom onto the media sample data. (We'll see exactly how this works in a moment.) The event atom can specify any of the kinds of events that we've considered so far, including mouse-click events, mouse-over events, frame-loaded events, and idle events. These wired text samples work exactly as you'd expect; for instance, if a text sample contains a mouse-click event atom, then the associated actions are triggered when the user clicks the mouse button while the cursor is within the bounds of the text track.
Sometimes, however, we'd like to handle user events that involve only part of the text displayed in a text sample. Consider once again the text movie shown in Figure 2. In this case, we want to trigger actions only when the user clicks on specific portions of the text data (namely, the strings "Apple" and "CNN"). To handle this kind of wiring, QuickTime supports a class of atoms called hypertext atoms. The goal here is to provide the sort of hyperlinks that you typically find in web browsers or other HTML-based applications. By clicking on a hypertext link in a text track, the user can launch the default web browser and navigate to a specific URL, by triggering the kActionGoToURL action. But in fact hypertext atoms can contain any kinds of wired action, not just kActionGoToURL actions. So the user's actions can just as easily (for instance) trigger a jump forward or backward in the movie, or cause some changes in an external movie.
As you can see in Figure 2, QuickTime automatically sets the color of hypertext links to the familiar browser-default blue and underlines the hypertext. Both of these provide visual cues that the user can interact with that segment of text. QuickTime also provides wired actions that allow us to change the color of a segment of hypertext, for example when the user rolls the cursor over that segment or after the user has clicked on the link. Figure 4 shows the same hypertext movie (this time on Windows) with the cursor resting over the first hyperlink.
![](/file/12652/www.mactech.com.tar/www.mactech.com/articles/mactech/Vol.17/17.07/July01QTToolkit/fig04.gif)
Figure 4: A selected hypertext link
Adding Actions to a Text Sample
A text media sample consists of a 16-bit length word followed by the text of that sample. Optionally, one or more atoms of additional data (called text atom extensions) may follow the text in the sample. The length word specifies the total number of bytes in the text (not including the 2 bytes occupied by the length field itself or the length of any of the optional text atom extensions). The text atom extensions are organized as "classic" atom structures: a 32-bit length field, followed by a 32-bit type field, followed by the data in the atom. Here, the length field specifies the total length of the atom (that is, 8 plus the number of bytes in the data). All the data in a text extension atom must be in big-endian format.
Whether we use a hypertext atom or any of the standard types of event atoms, we add the event atom to a text media sample in the same way, by attaching the atom container that contains the event and action atoms to the text media sample as a text atom extension. Figure 5 shows the general structure of a text media sample that contains a text action.
![](/file/12652/www.mactech.com.tar/www.mactech.com/articles/mactech/Vol.17/17.07/July01QTToolkit/fig05.gif)
Figure 5: The structure of a wired text media sample
If we're given a text media sample and an event atom container, it's quite easy to wire that atom container to the text. We simply need to determine the lengths of the media sample and the atom container, enlarge the text media sample to hold all of its existing data and the atom container, plus the 8-byte atom header shown in Figure 5. We set up the atom header as appropriate and then copy it and the atom container into the enlarged media sample.
The size of a text atom extension for wired actions, of course, is the size of the atom container plus the size of the atom header (8 bytes). The type of the text atom extension can be one of three values. If the atom container holds a frame-loaded event atom, then the type of the text atom extension should be set to kQTEventFrameLoaded. If the atom container holds any other standard event atom (including an idle event atom), then the type of the text atom extension should be set to kQTEventType. Finally, if the atom container holds a hypertext event atom, then the type of the text atom extension should be set to ‘htxt'. Currently there is no symbolic constant for this value defined in any of the public QuickTime header files, so I've included this definition in the file QTWiredActions.h:
> #define kHyperTextTextAtomType FOUR_CHAR_CODE(‘htxt')
Listing 1 shows our definition of the function QTWired_AddActionsToSample, which adds an event atom container to an existing text media sample. Notice that the function parameters include the text media sample, the event atom container, and the type of atom extension. This type should be one of the three recognized types for text atom extensions that specify wired actions.
Listing 1: Adding wired actions to a text media sample
static OSErr QTWired_AddActionsToSample (Handle theSample, QTAtomContainer theActions, SInt32 theAtomExtType) { Ptr myPtr = NULL; long myHandleLength; long myContainerLength; long myNewLength; OSErr myErr = noErr; if ((theSample == NULL) || (theActions == NULL)) return(paramErr); myHandleLength = GetHandleSize(theSample); myContainerLength = GetHandleSize((Handle)theActions); myNewLength = (long)(sizeof(long) + sizeof(OSType) + myContainerLength); SetHandleSize(theSample, (myHandleLength + myNewLength)); myErr = MemError(); if (myErr != noErr) goto bail; HLock(theSample); // get a pointer to the beginning of the new block of space added to the sample // by the previous call to SetHandleSize; we need to format that space as a text // atom extension myPtr = *theSample + myHandleLength; // set the length of the text atom extension *(long *)myPtr = EndianS32_NtoB((long)(sizeof(long) + sizeof(OSType) + myContainerLength)); myPtr += (sizeof(long)); // set the type of the text atom extension *(OSType *)myPtr = EndianS32_NtoB(theAtomExtType); myPtr += (sizeof(OSType)); // set the data of the text atom extension; // we assume that this data is already in big-endian format HLock((Handle)theActions); BlockMove(*theActions, myPtr, myContainerLength); HUnlock((Handle)theActions); HUnlock(theSample); bail: return(myErr); }
For more information on working with atoms, see "The Atomic Café" in MacTech, September 2000.
Creating Text Actions
As we've just seen, we add a wired action event handler to a text sample by adding a text atom extension of type kQTEventFrameLoaded or kQTEventType to the end of the sample; the data in the text atom extension is the atom container that holds the information about the wired actions triggered by some event. So, our task boils down to this: find the data in a text sample, create an atom container holding information about the desired actions, and then append a text extension atom whose data is that atom container to the end of the text sample data. Then we replace the previous text sample with the new one in the text track.
Listing 2 shows the code that handles the "Make Memory Display Movie..." menu item.
Listing 2: Handling the "Make Memory Display Movie..." menu item
case IDM_MAKE_MEM_DISPLAY_MOVIE: myPrompt = QTUtils_ConvertCToPascalString(kMDSavePrompt); myName = QTUtils_ConvertCToPascalString(kMDSaveFileName); // elicit a file from the user to save the new movie into QTFrame_PutFile(myPrompt, myName, &myFile, &myIsSelected, &myIsReplacing); // create a text movie that displays the amount of memory free in the application heap if (myIsSelected) { myErr = QTWired_CreateMemoryDisplayMovie(&myFile); if (myErr == noErr) QTWired_AddActionsToTextMovie(&myFile, theMenuItem); } myIsHandled = true; break;
We don't need to consider the function QTWired_CreateMemoryDisplayMovie in detail, since it's really just a variant of the QTText_AddTextTrack function we considered in an earlier article. In the present case, we set the initial text in the text track to the single character "0", and we set the font to 48-point Times-Roman (using the constant kFontIDTimes).
The interesting work in Listing 2 is done by the QTWired_AddActionsToTextMovie function, which creates the appropriate wired action atom container and then calls QTWired_AddActionsToSample (defined in Listing 1) to append that container to the first (and only) text media sample in the file created by QTWired_CreateMemoryDisplayMovie. Listing 3 shows our definition of QTWired_AddActionsToTextMovie.
Listing 3: Adding wired actions to a text movie
OSErr QTWired_AddActionsToTextMovie (FSSpec *theFSSpec, UInt16 theMenuItem) { short myResID = 0; short myResRefNum = -1; Movie myMovie = NULL; Track myTrack = NULL; Media myMedia = NULL; TimeValue myTrackOffset; TimeValue myMediaTime; TimeValue mySampleDuration; TimeValue mySelectionDuration; TimeValue myNewMediaTime; TextDescriptionHandle myTextDesc = NULL; Handle mySample = NULL; short mySampleFlags; Fixed myTrackEditRate; QTAtomContainer myActions = NULL; OSErr myErr = noErr; // open the movie file for reading and writing myErr = OpenMovieFile(theFSSpec, &myResRefNum, fsRdWrPerm); if (myErr != noErr) goto bail; myErr = NewMovieFromFile(&myMovie, myResRefNum, &myResID, NULL, newMovieActive, NULL); if (myErr != noErr) goto bail; // find first text track in the movie myTrack = GetMovieIndTrackType(myMovie, kIndexOne, TextMediaType, movieTrackMediaType); if (myTrack == NULL) goto bail; // get first media sample in the text track myMedia = GetTrackMedia(myTrack); if (myMedia == NULL) goto bail; myTrackOffset = GetTrackOffset(myTrack); myMediaTime = TrackTimeToMediaTime(myTrackOffset, myTrack); // allocate some storage to hold the sample description for the text track myTextDesc = (TextDescriptionHandle)NewHandle(4); if (myTextDesc == NULL) goto bail; mySample = NewHandle(0); if (mySample == NULL) goto bail; myErr = GetMediaSample(myMedia, mySample, 0, NULL, myMediaTime, NULL, &mySampleDuration, (SampleDescriptionHandle)myTextDesc, NULL, 1, NULL, &mySampleFlags); if (myErr != noErr) goto bail; // add actions to the first media sample switch (theMenuItem) { case IDM_MAKE_HYPERTEXT_MOVIE: // create an action container for hypertext actions myErr = QTWired_CreateHyperTextActionContainer(&myActions); if (myErr != noErr) goto bail; // add hypertext actions to sample myErr = QTWired_AddActionsToSample(mySample, myActions, kHyperTextTextAtomType); if (myErr != noErr) goto bail; break; case IDM_MAKE_MEM_DISPLAY_MOVIE: // create an action container for wired actions myErr = QTWired_CreateMemoryDisplayActionContainer(&myActions); if (myErr != noErr) goto bail; // add actions to sample myErr = QTWired_AddActionsToSample(mySample, myActions, kQTEventType); if (myErr != noErr) goto bail; break; default: myErr = paramErr; goto bail; } // replace sample in media myTrackEditRate = GetTrackEditRate(myTrack, myTrackOffset); if (GetMoviesError() != noErr) goto bail; GetTrackNextInterestingTime(myTrack, nextTimeMediaSample | nextTimeEdgeOK, myTrackOffset, fixed1, NULL, &mySelectionDuration); if (GetMoviesError() != noErr) goto bail; myErr = DeleteTrackSegment(myTrack, myTrackOffset, mySelectionDuration); if (myErr != noErr) goto bail; myErr = BeginMediaEdits(myMedia); if (myErr != noErr) goto bail; myErr = AddMediaSample( myMedia, mySample, 0, GetHandleSize(mySample), mySampleDuration, (SampleDescriptionHandle)myTextDesc, 1, mySampleFlags, &myNewMediaTime); if (myErr != noErr) goto bail; myErr = EndMediaEdits(myMedia); if (myErr != noErr) goto bail; // add the media to the track myErr = InsertMediaIntoTrack(myTrack, myTrackOffset, myNewMediaTime, mySelectionDuration, myTrackEditRate); if (myErr != noErr) goto bail; // update the movie resource myErr = UpdateMovieResource(myMovie, myResRefNum, myResID, NULL); if (myErr != noErr) goto bail; // close the movie file myErr = CloseMovieFile(myResRefNum); bail: if (myActions != NULL) (void)QTDisposeAtomContainer(myActions); if (mySample != NULL) DisposeHandle(mySample); if (myTextDesc != NULL) DisposeHandle((Handle)myTextDesc); if (myMovie != NULL) DisposeMovie(myMovie); return(myErr); }
Everything in Listing 3 is standard Movie Toolbox stuff that we've seen before, except of course for the application functions QTWired_CreateHyperTextActionContainer (which we'll discuss in the next section) and QTWired_CreateMemoryDisplayActionContainer. This latter function is in fact relatively simple. It builds an atom container that uses the kOperandFreeMemory operand to retrieve the amount of memory that is currently free in the application heap, as well as the kActionTextTrackPasteText wired action to paste the value returned by that operand into the text track. The kActionTextTrackPasteText action, which is new in QuickTime 5, takes three parameters: the text to paste and the beginning and ending locations in the text track to paste the text. Each time we paste a new value into the text track of the memory-display movie, we want to replace all of the existing text, so we'll set the second and third parameters to 0 and 0xffff respectively.
There's one undocumented "gotcha" here: in order for kActionTextTrackPasteText to have any effect, we need to explicitly enable text editing on the target text track. We can do this by executing the kActionTextTrackSetEditable wired action (also new to QuickTime 5). This action takes one parameter, which specifies the kind of editing we want to enable. QuickTime currently supports three settings for the editing state of a text track, which the documentation lists like this:
#define kKeyEntryDisabled 0 #define kKeyEntryDirect 1 #define kKeyEntryScript 2
(Once again, however, these values are not defined in any public header file, so I've added those lines to the file QTWiredActions.h.) The default value kKeyEntryDisabled indicates that no editing is allowed on the text track; key events are passed to the movie controller (which will probably ignore most of them) and any editing actions sent to the track are ignored. The value kKeyEntryDirect indicates that direct editing is to be enabled; this means that key events are passed directly to the text track (for instance, key-down events result in the characters being inserted into the text track). The value kKeyEntryScript indicates that script editing is to be enabled; this means that editing actions sent to the track are interpreted by the text media handler and executed.
For present purposes, we want to enable script editing on the text track. So we'll execute the code in Listing 4 inside of QTWired_CreateMemoryDisplayActionContainer.
Listing 4: Enabling editing on a text track
myErr = QTNewAtomContainer(theActions); if (myErr != noErr) goto bail; // add an event atom that enables text editing myErr = WiredUtils_AddQTEventAndActionAtoms(*theActions, kParentAtomIsContainer, kQTEventIdle, kActionTextTrackSetEditable, &myActionAtom); if (myErr != noErr) goto bail; myEditState = EndianS16_NtoB(kKeyEntryScript); myErr = WiredUtils_AddActionParameterAtom(*theActions, myActionAtom, 1, sizeof(myEditState), &myEditState, NULL);
On every idle event sent to the text track, we set the track to be editable (just before we execute the kActionTextTrackPasteText action). In theory, we could enable editing just once in a frame-loaded event. But the idle event action is simpler to construct and also ensures that we can always paste the current memory value into the text track (since some other action may have disabled editing on that track).
The remainder of QTWired_CreateMemoryDisplayActionContainer is straightforward. It adds an idle event call to the kActionTextTrackPasteText action, using the result of the kOperandFreeMemory operand as the text to be pasted. QuickTime is smart enough to convert the floating-point value returned by kOperandFreeMemory into a string, as expected by kActionTextTrackPasteText. Listing 5 shows the complete definition.
Listing 5: Pasting the amount of free memory into a text track
static OSErr QTWired_CreateMemoryDisplayActionContainer (QTAtomContainer *theActions) { QTAtom myEventAtom = 0; QTAtom myActionAtom = 0; QTAtom myParamAtom = 0; short myEditState; UInt32 myPos; QTAtom myParameterAtom = 0; QTAtom myExpressionAtom = 0; QTAtom myOperandAtom = 0; OSErr myErr = noErr; myErr = QTNewAtomContainer(theActions); if (myErr != noErr) goto bail; // add an event atom that enables text editing myErr = WiredUtils_AddQTEventAndActionAtoms(*theActions, kParentAtomIsContainer, kQTEventIdle, kActionTextTrackSetEditable, &myActionAtom); if (myErr != noErr) goto bail; myEditState = EndianS16_NtoB(kKeyEntryScript); myErr = WiredUtils_AddActionParameterAtom(*theActions, myActionAtom, 1, sizeof(myEditState), &myEditState, NULL); if (myErr != noErr) goto bail; // add an event atom that displays the amount of application memory currently free myErr = WiredUtils_AddQTEventAndActionAtoms(*theActions, kParentAtomIsContainer, kQTEventIdle, kActionTextTrackPasteText, &myActionAtom); if (myErr != noErr) goto bail; // first parameter: the text to be pasted myErr = WiredUtils_AddActionParameterAtom(*theActions, myActionAtom, 1, 0, NULL, &myParameterAtom); if (myErr != noErr) goto bail; myErr = WiredUtils_AddExpressionContainerAtomType( *theActions, myParameterAtom, &myExpressionAtom); if (myErr != noErr) goto bail; myErr = QTInsertChild(*theActions, myExpressionAtom, kOperandAtomType, 1, 1, 0, NULL, &myOperandAtom); if (myErr != noErr) goto bail; myErr = QTInsertChild(*theActions, myOperandAtom, kOperandFreeMemory, 1, 1, 0, NULL, NULL); if (myErr != noErr) goto bail; // second parameter: selection range begin: 0 myPos = EndianU32_NtoB(0); myErr = WiredUtils_AddActionParameterAtom(*theActions, myActionAtom, 2, sizeof(myPos), &myPos, NULL); if (myErr != noErr) goto bail; // third parameter: selection range end: 0xffff myPos = EndianU32_NtoB(0xffff); myErr = WiredUtils_AddActionParameterAtom(*theActions, myActionAtom, 3, sizeof(myPos), &myPos, NULL); bail: return(myErr); }
On classic Macintosh systems (that is, Mac OS 8 and 9), the movie created by all this code will display the amount of free memory in the application heap (or essentially what you could determine by executing the Memory Manager function FreeMem). On Windows systems and on Mac OS X, where there are no application heaps in the classic-Mac sense, the movie will display less useful numbers. In fact, I'm not exactly sure what the numbers displayed on Windows and Mac OS X represent.
Creating Hypertext Actions
Let's consider now how to construct text movies that contain hypertext actions, like the one shown in Figures 2 and 4. The basic ideas are the same as with constructing wired text movies, except that now we add a text atom extension of type kHyperTextTextAtomType to the end of the text sample. Also, the atom container that's inside of the text atom extension should have the structure shown in Figure 6.
![](/file/12652/www.mactech.com.tar/www.mactech.com/articles/mactech/Vol.17/17.07/July01QTToolkit/fig06.gif)
Figure 6: The structure of a hypertext text atom
As you can see, the root atom in a hypertext text atom is of type ‘wtxt'. This atom contains one child of type ‘htxt' for each hypertext link in the text sample. Let's call this child atom a hypertext item atom. A hypertext item atom specifies the information about a single hypertext link in a text sample. We need to specify the starting point and ending point in the sample text for the hypertext link, and we need to specify the event and action atoms associated with that segment of text. So a hypertext item atom needs to contain at least three children, of types ‘strt', ‘end ', and kQTEventType. In our source code, we'll use these constants:
#define kHyperTextTextAtomType FOUR_CHAR_CODE(‘htxt') #define kTextWiredObjectsAtomType FOUR_CHAR_CODE(‘wtxt') #define kHyperTextItemAtomType FOUR_CHAR_CODE(‘htxt') #define kRangeStart FOUR_CHAR_CODE(‘strt') #define kRangeEnd FOUR_CHAR_CODE(‘end ‘)
There's no need to consider the function QTWired_CreateHyperTextActionContainer in detail. It simply builds the atom container shown in Figure 6, using hard-coded values for the parameter data of the kRangeStart and kRangeEnd atoms. Each of our hypertext item atoms contains three event atoms. The first event atom looks for mouse-clicks on the hypertext link and executes a kActionGoToURL action in response. The second and third event atoms watch for the cursor to enter and exit the hypertext link, changing the color of the text appropriately. Listing 6 shows the code for changing the hypertext color to purple when the mouse enters the first hyperlink. The kActionTextTrackSetHyperTextColor action takes two parameters, which are the index of the hypertext link whose color is to change and the desired new color.
Listing 6: Changing the color of a hypertext link
ModifierTrackGraphicsModeRecord myGraphicsModeRecord; myErr = WiredUtils_AddQTEventAndActionAtoms(*theActions, myHyperTextAtom, kQTEventMouseEnter, kActionTextTrackSetHyperTextColor, &myActionAtom); if (myErr != noErr) goto bail; myIndex = EndianS16_NtoB(1); myErr = WiredUtils_AddActionParameterAtom(*theActions, myActionAtom, kIndexOne, sizeof(myIndex), &myIndex, NULL); if (myErr != noErr) goto bail; myGraphicsModeRecord.graphicsMode = EndianS32_NtoB(ditherCopy); myGraphicsModeRecord.opColor.red = EndianS16_NtoB(0x9999); myGraphicsModeRecord.opColor.green = EndianS16_NtoB(0x0000); myGraphicsModeRecord.opColor.blue = EndianS16_NtoB(0xcccc); myErr = WiredUtils_AddActionParameterAtom(*theActions, myActionAtom, kIndexTwo, sizeof(myGraphicsModeRecord), &myGraphicsModeRecord, NULL);
Take a look at the source file QTWiredActions.c for the complete definition of QTWired_CreateHyperTextActionContainer.
Key Events
QuickTime 5 introduced a new type of event, the key event (of type kQTEventKey), which is issued when keys on the keyboard are pressed. Consider, for instance, the movie shown in Figure 7, which displays the ASCII character code of whatever key the user presses. In the case shown here, the user has just pressed the "T" key.
![](/file/12652/www.mactech.com.tar/www.mactech.com/articles/mactech/Vol.17/17.07/July01QTToolkit/fig07.gif)
Figure 7: A movie that displays ASCII character codes of pressed keys.
A key event can be used in event atoms just like any of the other event types we've encountered so far. What's interesting about key events is that we can determine not only that the user has pressed a key, but also which particular key was pressed. We do this by inspecting the event parameters of the key event, using the new operand kOperandEventParameter. This operand itself takes one parameter, which specifies the index of the event parameter we want to inspect. In the case of key events, we can inspect any one of 5 event parameters:
- Parameter index 1 is the horizontal position of the cursor at the time the key event occurred, in coordinates relative to the text track rectangle.
- Parameter index 2 is the vertical position of the cursor at the time the key event occurred, in coordinates relative to the text track rectangle.
- Parameter index 3 encodes any modifier keys that are down when the key event occurs, using these constants from Events.h:
enum { cmdKey = 1 << cmdKeyBit, // 0x0100 shiftKey = 1 << shiftKeyBit, // 0x0200 alphaLock = 1 << alphaLockBit, // 0x0400 optionKey = 1 << optionKeyBit, // 0x0800 controlKey = 1 << controlKeyBit // 0x1000 };
For instance, if the Control and Shift keys are both down when the key event occurs, then the third parameter would be 0x00001200, or 4608.
- Parameter index 4 is the ASCII character code of the key pressed.
- Parameter index 5 is the virtual key code (or scan code) of the key pressed. A virtual key code is a value that represents a specific physical key on a specific model of keyboard. As a result, these values are generally less useful than the ASCII character codes.
We can construct the ASCII-display movie in Figure 7 in exactly the same way we previously constructed the memory-display movie. Instead of issuing the kActionTextTrackPasteText action in response to idle events, we now do so in response to key events. And we get the text to be pasted not from the kOperandFreeMemory operand, but from the kOperandEventParameter operand. Listing 7 shows some of the code we could use to construct the ASCII-display movie. (This code could be substituted for part of Listing 5, for instance.)
Listing 7: Adding a key event
QTAtom myEventParamAtom = 0; short myIndex; // add an event atom that displays the ASCII code of the pressed key myErr = WiredUtils_AddQTEventAndActionAtoms(*theActions, kParentAtomIsContainer, kQTEventKey, kActionTextTrackPasteText, &myActionAtom); if (myErr != noErr) goto bail; // first parameter: the text to be pasted myErr = WiredUtils_AddActionParameterAtom(*theActions, myActionAtom, 1, 0, NULL, &myParameterAtom); if (myErr != noErr) goto bail; myErr = WiredUtils_AddExpressionContainerAtomType( *theActions, myParameterAtom, &myExpressionAtom); if (myErr != noErr) goto bail; myErr = QTInsertChild(*theActions, myExpressionAtom, kOperandAtomType, 1, 1, 0, NULL, &myOperandAtom); if (myErr != noErr) goto bail; myErr = QTInsertChild(*theActions, myOperandAtom, kOperandEventParameter, 1, 1, 0, NULL, &myEventParamAtom); if (myErr != noErr) goto bail; myIndex = EndianS16_NtoB(4); myErr = QTInsertChild(*theActions, myEventParamAtom, kActionParameter, 1, 1, sizeof(myIndex), &myIndex, NULL);
Notice that we add a parameter atom (myEventParamAtom) to the operand atom, to specify the desired index for the event parameter we want to retrieve. In this case, we specify the index 4, to obtain the ASCII character code of the key pressed.
It's also possible to use the kOperandEventParameter operand to retrieve event parameters for mouse-related events, such as mouse-enter and mouse-click events. With mouse events, there are only three available parameters, which are the same as the first three parameters of key events: the horizontal and vertical mouse positions, and the modifier keys currently down. In an earlier article, you may recall, we used the kOperandMouseLocalHLoc and kOperandMouseLocalVLoc operands to get these mouse positions. In QuickTime 5 and later, we can instead use kOperandEventParameter, if we so desire.
Bouncing Sprites
In several of the previous articles, we've seen that we can move a sprite around inside a sprite track by altering its matrix, either directly (by setting the sprite's matrix property) or indirectly (by performing wired actions such as kActionSpriteTranslate). In this section and the next, we'll consider a couple of ways to build on this capability. First, we'll see how to make a sprite bounce around inside of the sprite track's enclosing rectangle; later we'll see how to figure out when two sprites collide with one another. Both of these are ways to give a sprite "physical" properties, so that it appears to react with things around it.
Figure 8 shows (about as well as any static image can, I guess) a sprite bouncing off the track's enclosing rectangle. The sprite is first moving down and to the right; when its bottom edge touches the bottom of the track rectangle, the sprite bounces up and continues to the right. When any other edge of the sprite touches a side of the track rectangle, the sprite does the appropriate thing by moving back away from the side it touched but otherwise continuing in the same direction. (In the event that two of its edges touch two sides of the track rectangle at the same time, the sprite would reverse both its horizontal and vertical directions; this would happen when the sprite moves cleanly into a corner of the track rectangle.)
![](/file/12652/www.mactech.com.tar/www.mactech.com/articles/mactech/Vol.17/17.07/July01QTToolkit/fig08.gif)
Figure 8: A sprite bouncing off the movie edge
Moving the Sprite
The first thing we need to do is get the sprite moving. To do this, we can simply change the horizontal and vertical positions of the sprite during idle events. Since we're going to be changing the direction of movement when the sprite collides with the track rectangle, we'll maintain two sprite track variables whose values are the number of pixels in the horizontal and vertical direction that the sprite is to be offset during the next idle event. We'll use these variables IDs:
#define kXMoveVarID 2000 #define kYMoveVarID 2100
We can set the initial horizontal and vertical offsets by adding a couple of frame-loaded event actions to the sprite, using our utility WiredUtils_AddSpriteTrackSetVariableAction (defined in "Wired" in MacTech, May 2001):
#define kIdleOffset 2 myErr = WiredUtils_AddSpriteTrackSetVariableAction( mySample, kParentAtomIsContainer, kQTEventFrameLoaded, kXMoveVarID, kIdleOffset, 0, NULL, 0); myErr = WiredUtils_AddSpriteTrackSetVariableAction( mySample, kParentAtomIsContainer, kQTEventFrameLoaded, kYMoveVarID, kIdleOffset, 0, NULL, 0);
During idle events, we'll move the sprite horizontally by the current value of the kXMoveVarID variable and vertically by the current value of the kYMoveVarID variable. The speed of the moving sprite is determined both by the values of these variables and by the frequency with which we receive idle events (which, you'll recall, is determined by the sprite track's kSpriteTrackPropertyQTIdleEventsFrequency property). For the bouncing sprite movie, we'll tell the sprite media handler to send us an idle event every tick, and (as you can see) we'll offset the sprite in each direction by two pixels during every idle event. We add actions to move the sprite during an idle event like this:
myErr = WiredUtils_AddQTEventAtom(mySpriteData, kParentAtomIsContainer, kQTEventIdle, &myEventAtom); myErr = QTWired_AddTranslateByVariablesAction(mySpriteData, myEventAtom, kXMoveVarID, kYMoveVarID, NULL);
The QTWired_AddTranslateByVariablesAction function adds to the specified event atom (here, myEventAtom) an action atom that translates the sprite relatively, taking the horizontal and vertical offsets from the sprite track variables whose IDs are specified by the third and fourth parameters (here, kXMoveVarID and kYMoveVarID). Listing 8 shows our definition of the QTWired_AddTranslateByVariablesAction function.
Listing 8: Translating a sprite using variable values
static OSErr QTWired_AddTranslateByVariablesAction ( QTAtomContainer theSprite, QTAtom theParentAtom, QTAtomID theXVariableID, QTAtomID theYVariableID, QTAtom *theActionAtom) { QTAtom myActionAtom = 0; QTAtom myExpressionAtom = 0; QTAtom myParamAtom = 0; QTAtom myOperatorAtom = 0; QTAtom myOperandAtom = 0; QTAtom myOperandTypeAtom = 0; QTAtomID myVariableID; Boolean myBoolean; OSErr myErr = paramErr; if (theSprite == NULL) goto bail; // add a translate action atom to the specified parent atom myErr = WiredUtils_AddActionAtom(theSprite, theParentAtom, kActionSpriteTranslate, &myActionAtom); if (myErr != noErr) goto bail; // first parameter: get value of variable theXVariableID myErr = WiredUtils_AddActionParameterAtom(theSprite, myActionAtom, kFirstParam, 0, NULL, &myParamAtom); if (myErr != noErr) goto bail; myErr = WiredUtils_AddExpressionContainerAtomType (theSprite, myParamAtom, &myExpressionAtom); if (myErr != noErr) goto bail; myErr = QTInsertChild(theSprite, myExpressionAtom, kOperandAtomType, 0, 1, 0, NULL, &myOperandAtom); if (myErr != noErr) goto bail; myErr = QTInsertChild(theSprite, myOperandAtom, kOperandSpriteTrackVariable, 1, 1, 0, NULL, &myOperandTypeAtom); if (myErr != noErr) goto bail; myVariableID = EndianU32_NtoB(theXVariableID); myErr = QTInsertChild(theSprite, myOperandTypeAtom, kActionParameter, 1, 1, sizeof(myVariableID), &myVariableID, NULL); if (myErr != noErr) goto bail; // second parameter: get value of variable theYVariableID myErr = WiredUtils_AddActionParameterAtom(theSprite, myActionAtom, kSecondParam, 0, NULL, &myParamAtom); if (myErr != noErr) goto bail; myErr = WiredUtils_AddExpressionContainerAtomType (theSprite, myParamAtom, &myExpressionAtom); if (myErr != noErr) goto bail; myErr = QTInsertChild(theSprite, myExpressionAtom, kOperandAtomType, 0, 1, 0, NULL, &myOperandAtom); if (myErr != noErr) goto bail; myErr = QTInsertChild(theSprite, myOperandAtom, kOperandSpriteTrackVariable, 1, 1, 0, NULL, &myOperandTypeAtom); if (myErr != noErr) goto bail; myVariableID = EndianU32_NtoB(theYVariableID); myErr = QTInsertChild(theSprite, myOperandTypeAtom, kActionParameter, 1, 1, sizeof(myVariableID), &myVariableID, NULL); if (myErr != noErr) goto bail; // third parameter: false (for relative translation) myBoolean = false; myErr = WiredUtils_AddActionParameterAtom(theSprite, myActionAtom, kThirdParam, sizeof(myBoolean), &myBoolean, NULL); bail: if (theActionAtom != NULL) *theActionAtom = myActionAtom; return(myErr); }
Here we add an action of type kActionSpriteTranslate, which requires three parameters: the desired horizontal translation, the desired vertical translation, and a Boolean that indicates whether the translation is absolute or relative. For the first and second parameters, we add expression container atoms that retrieve the value of the variable with the specified ID. (For more information about expression container atoms, see "Wired" in MacTech, May 2001.)
Detecting Track Rectangle Collisions
So far, so good. But if this were all the wiring we attached to the sprite, we wouldn't get quite the behavior we're looking for. When the movie was first opened, the sprite would begin moving down to the right and would continue moving in that direction forever. We need to add some wiring to figure out when an edge of the sprite hits an edge of the track rectangle and then adjust the direction of movement accordingly. In a nutshell, we want to add some logic that says:
- If the left edge of the sprite is less than the left edge of the track rectangle, then move the sprite back inside the track rectangle and reverse the horizontal direction of travel.
- If the right edge of the sprite is greater than the right edge of the track rectangle, then move the sprite back inside the track rectangle and reverse the horizontal direction of travel.
- If the top edge of the sprite is less than the top edge of the track rectangle, then move the sprite back inside the track rectangle and reverse the vertical direction of travel.
- If the bottom edge of the sprite is greater than the bottom edge of the track rectangle, then move the sprite back inside the track rectangle and reverse the vertical direction of travel.
It's worth pointing out that a more elegant design would use two "if-else" statements here instead of four "if" statements, since the opposite edges of our sprite cannot both be outside the track bounds at the same time. This refinement would, however, require a more complicated wiring. I'll leave that as an exercise for the persnickety reader.
We know the dimensions of the sprite track (since we created it using the constants kIconSpriteTrackHeight and kIconSpriteTrackWidth). So all we really have to learn now is how to figure out the position of the sprite's edges. The sprite media handler supports these four operands, which give us the information we need:
enum { kOperandSpriteBoundsLeft = 3072, kOperandSpriteBoundsTop = 3073, kOperandSpriteBoundsRight = 3074, kOperandSpriteBoundsBottom = 3075 };
The operand kOperandSpriteBoundsLeft, for instance, returns the left side of the sprite's bounding box (the rectangle that encloses the sprite), in the local coordinate system of the sprite track.
In our code, we'll add the side-bounce logic to the sprite by making four calls to a QTWiredActions function QTWired_AddSideBounceToSprite, like this:
QTWired_AddSideBounceToSprite(mySpriteData, kOperandSpriteBoundsLeft, 1, kOperatorLessThan, kXMoveVarID); QTWired_AddSideBounceToSprite(mySpriteData, kOperandSpriteBoundsRight, kIconSpriteTrackWidth, kOperatorGreaterThan, kXMoveVarID); QTWired_AddSideBounceToSprite(mySpriteData, kOperandSpriteBoundsTop, 1, kOperatorLessThan, kYMoveVarID); QTWired_AddSideBounceToSprite(mySpriteData, kOperandSpriteBoundsBottom, kIconSpriteTrackHeight, kOperatorGreaterThan, kYMoveVarID);
The QTWired_AddSideBounceToSprite function is really quite simple, but (as we've grown to expect when building wired actions) a tad lengthy. Since we've built a few wired event handlers already, let's just survey the main points. First of all, we want to install an idle event atom by using our utility function WiredUtils_AddQTEventAndActionAtoms. The action should be a kActionCase action, since we want to ask whether a side of the sprite lies outside the track rectangle. As we saw in the previous article, a kActionCase action has a single parameter, whose data is an atom of type kConditionalAtomType. This conditional atom, in turn, has two children, an expression container atom and an action list atom.
The expression container atom needs to test whether the specified side of the sprite lies outside the sprite track rectangle. As you can see, when we call QTWired_AddSideBounceToSprite, we pass in the operand that we need to use to select the side, along with the track limit and the test to perform (for instance, kOperatorLessThan). We build the expression container atom data like this:
WiredUtils_AddOperatorAtom(theSprite, myExpressionAtom, theTest, &myOperatorAtom); // first operand: the specified side of the sprite QTInsertChild(theSprite, myOperatorAtom, kOperandAtomType, 1, 1, 0, NULL, &myOperandAtom); QTInsertChild(theSprite, myOperandAtom, theSide, 1, 1, 0, NULL, NULL); // second operand: the specified limit WiredUtils_AddOperandAtom(theSprite, myOperatorAtom, kOperandConstant, 2, NULL, theLimit);
The contents of the action list atom are pretty straightforward. Remember that we need to perform two actions: (1) translate the sprite back to the edge of the track rectangle and (2) reverse the direction of travel in the horizontal or vertical dimension. We've already seen how to construct a translate action, so we don't need to repeat that here. We can change the direction of travel simply by negating the value of the variable whose ID is passed to QTWired_AddSideBounceToSprite. We'll do that using another function, QTWired_AddNegateVariableAction, defined in Listing 9.
Listing 9: Negating a sprite track variable
static OSErr QTWired_AddNegateVariableAction ( QTAtomContainer theSprite, QTAtom theParentAtom, QTAtomID theVariableID) { QTAtom myActionAtom = 0; QTAtom myExpressionAtom = 0; QTAtom myParamAtom = 0; QTAtom myOperatorAtom = 0; QTAtom myOperandAtom = 0; QTAtom myOperandTypeAtom = 0; QTAtomID myVariableID; OSErr myErr = paramErr; if ((theSprite == NULL) || (theParentAtom == 0)) goto bail; myErr = WiredUtils_AddActionAtom(theSprite, theParentAtom, kActionSpriteTrackSetVariable, &myActionAtom); if (myErr != noErr) goto bail; // add parameters to the set variable action: variable ID (QTAtomID) and value (float) myVariableID = EndianU32_NtoB(theVariableID); myErr = QTInsertChild(theSprite, myActionAtom, kActionParameter, 0, (short)kFirstParam, sizeof(myVariableID), &myVariableID, NULL); if (myErr != noErr) goto bail; myErr = QTInsertChild(theSprite, myActionAtom, kActionParameter, 0, (short)kSecondParam, 0, NULL, &myParamAtom); if (myErr != noErr) goto bail; myErr = WiredUtils_AddExpressionContainerAtomType( theSprite, myParamAtom, &myExpressionAtom); if (myErr != noErr) goto bail; myErr = QTInsertChild(theSprite, myExpressionAtom, kOperatorAtomType, kOperatorNegate, 1, 0, NULL, &myOperatorAtom); if (myErr != noErr) goto bail; myErr = QTInsertChild(theSprite, myOperatorAtom, kOperandAtomType, 0, 1, 0, NULL, &myOperandAtom); if (myErr != noErr) goto bail; myErr = QTInsertChild(theSprite, myOperandAtom, kOperandSpriteTrackVariable, 1, 1, 0, NULL, &myOperandTypeAtom); if (myErr != noErr) goto bail; myVariableID = EndianU32_NtoB(theVariableID); myErr = QTInsertChild(theSprite, myOperandTypeAtom, kActionParameter, 1, 1, sizeof(myVariableID), &myVariableID, NULL); bail: return(myErr); }
There's nothing too exciting here; this just says: set the value of the variable whose ID is theVariableID to the result of negating the value of the variable whose ID is theVariableID.
Colliding Sprites
So now we've got sprites bouncing off the walls. Let's get them to bounce off one another as well. In specific, let's create a movie with two sprites, both of which have the bouncing logic that we developed in the previous section. Then, let's add some wiring to make them recoil from one another when part of one sprite touches part of the other. Figure 9 shows two sprites about to collide: the sprite with the new QuickTime logo has just bounced off the bottom and is moving up to the right; the other sprite is moving down to the left. When the sprites collide, we'll reverse the horizontal direction of travel of each sprite unless they are traveling in the same horizontal direction when they collide. And ditto for the vertical directions of travel. This logic gives the sprites a nice feel as they bounce around.
![](/file/12652/www.mactech.com.tar/www.mactech.com/articles/mactech/Vol.17/17.07/July01QTToolkit/fig09.gif)
Figure 9: Two sprites colliding
But how do we know when two sprites collide? Well, we know the coordinates of each of the corners of a sprite (using the operands we encountered in the previous section). So a reasonable strategy might be to ask, for each corner of the sprite, whether it lies on top of the other sprite. QuickTime 5 introduced a very useful operand for this, the kOperandSpriteTrackSpriteIDAtPoint operand. This operand takes two parameters, which are the horizontal and vertical coordinates of a point; it returns the ID of the topmost sprite at that point, if any.
In the case where we have just two sprites that can collide, we need to attach the collision logic to only one of the sprites. That sprite (let's call it the collider) can check, on every idle event, whether any of its four corners has come into contact with any part of the other sprite. That is, we'll call kOperandSpriteTrackSpriteIDAtPoint four times, each time passing one of the four corners of the collider. QTWiredActions uses the QTWired_AddCollisionLogicToSprite function (defined in Listing 10) to attach the collision logic to the collider.
Listing 10: Wiring a sprite for collisions
OSErr QTWired_AddCollisionLogicToSprite (QTAtomContainer theSprite) { OSErr myErr = noErr; myErr = QTWired_AddCornerCollisionLogicToSprite(theSprite, kOperandSpriteBoundsLeft, kOperandSpriteBoundsTop); if (myErr != noErr) goto bail; myErr = QTWired_AddCornerCollisionLogicToSprite(theSprite, kOperandSpriteBoundsRight, kOperandSpriteBoundsTop); if (myErr != noErr) goto bail; myErr = QTWired_AddCornerCollisionLogicToSprite(theSprite, kOperandSpriteBoundsLeft, kOperandSpriteBoundsBottom); if (myErr != noErr) goto bail; myErr = QTWired_AddCornerCollisionLogicToSprite(theSprite, kOperandSpriteBoundsRight, kOperandSpriteBoundsBottom); bail: return(myErr); }
We won't bother to dissect QTWired_AddCornerCollisionLogicToSprite, as it mostly covers routine ground while setting a new record for length (almost 300 lines of code and comments). The key step is using kOperandSpriteTrackSpriteIDAtPoint, as described above. You should know that kOperandSpriteTrackSpriteIDAtPoint does its work by hit-testing for sprites in the sprite track (probably using SpriteMediaHitTestAllSprites, which we used in an earlier article). So a sprite ID will be returned as the operand's value only if some non-transparent part of a sprite is situated at the specified point.
Our collision logic as developed so far is pretty good, but alas not perfect. Recall that we are testing each of the four corners of the collider sprite's bounding rectangle, to see if it lies on top of some sprite. If the collider's sprite image at that corner is non-transparent, then the call to kOperandSpriteTrackSpriteIDAtPoint will always return a non-zero value (since the sprite hit test will find that corner, even if the other sprite is nowhere near the collider). We can solve this problem by ensuring that the other sprite has a lower layer property than the collider (so that we get its ID if the other sprite and the collider overlap at the tested point) and by ignoring the collider's ID if kOperandSpriteTrackSpriteIDAtPoint returns it to us.
Another problem arises if the collider's sprite image is transparent at a corner whose coordinates are passed to kOperandSpriteTrackSpriteIDAtPoint. In this case, it's possible for that corner to lie on top of a non-transparent part of the other sprite and hence trigger a hit, even though the sprites do not appear to be touching. Figure 10 illustrates this possibility. In this case, the collider is the top-right sprite, and its lower-left corner is transparent. kOperandSpriteTrackSpriteIDAtPoint will return the ID of the other sprite, because the collider's lower left corner does in fact lie on top of a non-transparent pixel of the other sprite.
![](/file/12652/www.mactech.com.tar/www.mactech.com/articles/mactech/Vol.17/17.07/July01QTToolkit/fig10.gif)
Figure 10: Testing a transparent corner of the collider sprite
I don't see an easy way to solve this problem, but all in all it's a fairly minor one. For most purposes, I suspect, it's good enough to have the sprites recoil from one another using the simple wiring we've developed here. More complex collision algorithms are of course left as exercises for the reader.
Conclusion
In this article, we've seen how to add wiring to text tracks and we've investigated a few of the text-related actions and operands added in QuickTime 5. We've also touched briefly on the new key event (of type kQTEventKey) and seen for the first time the technique for retrieving event parameters. Finally, we've added some wiring to allow sprites to collide with the track rectangle and with one another.
With all of this, we've reached the end of our "mini-series" on sprites and wired actions. We'll revisit them in the future, however, especially when we learn how to work with QuickTime VR movies and Flash tracks. In the next article, though, we'll shift gears and move on to consider some new and exciting QuickTime capabilities.
Credits
Thanks are due to Bill Wright for clarifying some issues with text wiring.
Tim Monroe works in the QuickTime engineering team at Apple Computer, Inc. You can contact 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