TweetFollow Us on Twitter

Jun 02 QT Toolkit

Volume Number: 18 (2002)
Issue Number: 06
Column Tag: QuickTime Toolkit

by Tim Monroe

Virtuosity

Programming with QuickTime VR

Introduction

QuickTime VR (or, more briefly, QTVR) is the part of QuickTime that allows users to interactively explore and examine photorealistic, three-dimensional virtual worlds and objects. A QuickTime VR movie is a collection of one or more nodes; each node is either a panoramic node (also known as a panorama) or an object node (also known as an object). Figure 1 shows a view of a sample panoramic node, and Figure 2 shows a view of an object node. (When a QuickTime VR movie consists of a single node, folks often refer to it as a panorama movie or an object movie, depending on the type of node it contains.)


Figure 1: A QuickTime VR panorama


Figure 2: A QuickTime VR object movie

QuickTime VR movies are managed by the QuickTime VR movie controller, a movie controller component that knows how to interpret user actions in a QuickTime VR movie. The QuickTime VR movie controller also displays a controller bar with buttons that are appropriate to QuickTime VR movies. From left to right, the five buttons allow the user to go back to the previous node, zoom out, zoom in, show the visible hot spots, and translate an object in the movie window. The QuickTime VR movie controller automatically disables any buttons that are not appropriate for the current node type or movie state. For instance, the back button is disabled in Figure 2 because the movie is a single-node movie. Similarly, the translate button is disabled in Figure 1 because the current node is a panoramic node, not an object node.

QuickTime has supported QuickTime VR movie creation and playback since mid-1995. In early 1997, Apple released QuickTime VR version 2.0, which (in addition to numerous other improvements) provided a C programming interface to QuickTime VR. This interface, called the QuickTime VR Manager, provides an extensive set of functions for controlling QuickTime VR movies. In this article, we’ll take a look at the QuickTime VR Manager.

The QuickTime VR movie controller also allows QuickTime VR movies to send and receive wired actions. This allows us, for instance, to use buttons in a Flash track to control a QTVR movie, as illustrated in Figure 3. Here the Flash buttons in the lower-left corner of the movie are configured to send the appropriate QuickTime wired actions to pan, tilt, or zoom the panorama. (We saw how to attach wired actions to Flash track buttons in “The Flash II: Revenge of the Trickster”, MacTech, February 2002.)


Figure 3: A Flash track controlling a QuickTime VR movie

We’ll take a look at the actions that can be targeted at a QuickTime VR movie, and we’ll also see how to attach wired actions to elements in a QuickTime VR movie. We can attach wired actions to a particular node or to a particular hot spot in a node. So, for example, we could wire a hot spot to launch the user’s web browser and navigate to a particular web site when the user clicks that hot spot. Or we can have some actions triggered when the user enters a node. We won’t learn how to build QuickTime VR movies in this article, but we will need to understand some of the structure of these movies in order to learn how to attach wired actions to nodes and hot spots.

The QuickTime VR Manager

The QuickTime VR Manager provides a large number of capabilities that we can use to customize and extend the user’s virtual experience of panoramas and objects. Here we’ll summarize the basic capabilities of the QuickTime VR Manager. Then, in the following sections, we’ll illustrate how to use some of them. The QuickTime VR Manager provides these main capabilities:

  • Positioning. A QuickTime VR movie file contains a scene, which is a collection of one or more nodes. Each node is uniquely identified by its node ID. Within a panoramic node, the user’s view is determined by three factors: the pan angle, the tilt angle, and the vertical field of view (sometimes also called the zoom angle). For objects, the view is also determined by the view center (the position of the center of the object in the movie window). The QuickTime VR Manager provides functions to get and set any of these items. For instance, we can programmatically spin an object around by repeatedly incrementing the current pan angle.
  • Hot spot handling. We can use the QuickTime VR Manager to manage any hot spots in a panorama or object. For instance, we can trigger a hot spot programmatically (that is, simulate a click on the hot spot), enable and disable hot spots, determine whether the cursor is over a hot spot, find all visible hot spots, and so forth. We can also install a callback routine that is called whenever the cursor is over an enabled hot spot.
  • Custom node-entering and -leaving behaviors. The QuickTime VR Manager allows us to perform actions whenever the user enters a new node or leaves the current node. For instance, we might use a node-entering procedure to play a sound when the user enters a particular node. Or, we can use a node-leaving procedure to prevent the user from leaving a node until some task has been accomplished.
  • Getting information. We can use the QuickTime VR Manager to get information about a scene or about a specific node. For instance, we might want to determine the ID and type of the current node. Much of the information about scenes and nodes is stored in atoms in the movie file. To get information about a scene or node that isn’t provided directly by the QuickTime VR Manager, we’ll need to use the QuickTime atom container functions to extract information from those atoms.
  • Intercepting QuickTime VR Manager functions. We can intercept calls to some QuickTime VR Manager functions in order to augment or modify their behavior. For example, to assign behaviors to custom hot spots, we can install an intercept routine that is called whenever a hot spot is triggered. Our intercept routine might check the type of the triggered hot spot and then perform the actions appropriate for that type. Another common use of intercept routines is to intercept positioning functions (changing the pan, tilt, and field of view) and adjust environmental factors accordingly. For instance, we can adjust the balance and volume of a sound as the pan angle changes in a panorama, thereby making it appear that the sound is localized within the panorama.
  • Accessing the prescreen buffer. QuickTime VR maintains an offscreen buffer for each panorama, called the prescreen buffer. The prescreen buffer contains the image that is about to be copied to the screen. We can use QuickTime VR Manager functions to access the prescreen buffer, perhaps to draw a graphic image over the panorama.

This list is not exhaustive. The QuickTime VR Manager provides many other capabilities as well. For a complete description, see the technical documentation cited at the end of this article.

QuickTime VR Movie Playback

Our existing sample applications, such as QTShell, are already able to open and display QuickTime VR movies. The QuickTime VR movie controller handles the basic click-and-drag navigation, keyboard input, and controller bar events. We need to use the QuickTime VR Manager only if we want to exploit some of the capabilities described just above.

Initializing the QuickTime VR Manager

Before we can call the QuickTime VR Manager, however, we need to do a little setting up (over and above what’s required for using QuickTime). First, we need to ensure that the QuickTime VR Manager is available in the current operating environment. There are several Gestalt selectors that we can use to see whether the QuickTime VR Manager is available and what features it has. Listing 1 shows the definition of the QTVRUtils_IsQTVRMgrInstalled function, which indicates whether the QuickTime VR Manager is available in the current operating environment.

Listing 1: Determining whether the QuickTime VR Manager is available

QTVRUtils_IsQTVRMgrInstalled
Boolean QTVRUtils_IsQTVRMgrInstalled (void)
{
   Boolean         myQTVRAvail = false;
   long            myAttrs;
   OSErr           myErr = noErr;
   myErr = Gestalt(gestaltQTVRMgrAttr, &myAttrs);
   if (myErr == noErr)
      if (myAttrs & (1L << gestaltQTVRMgrPresent))
         myQTVRAvail = true;
   return(myQTVRAvail);
}

For simplicity, we’ll introduce a global variable to keep track of whether the QuickTime VR Manager is available:

gQTVRMgrIsPresent = QTVRUtils_IsQTVRMgrInstalled();
On Windows operating systems, we need to call the InitializeQTVR function to initialize the QuickTime 
VR Manager, like this:

#if TARGET_OS_WIN32
   InitializeQTVR();
#endif

We also need to close our connection to the QuickTime VR Manager before our application terminates:

#if TARGET_OS_WIN32
   TerminateQTVR();
#endif

Calling any other QuickTime VR Manager functions before calling InitializeQTVR will result in an error on Windows.

Getting the QTVR Instance

The QuickTime VR Manager keeps track of QuickTime VR movies using an identifier called a QTVR instance (of data type QTVRInstance). Virtually all QuickTime VR Manager functions operate on QTVR instances. You can think of an instance as representing a scene — that is, a collection of nodes — or sometimes just the current node. We obtain a QTVR instance by calling the QTVRGetQTVRInstance function. QTVRGetQTVRInstance takes a reference to a QTVR track, which we can obtain by calling QTVRGetQTVRTrack. Listing 2 shows our definition of QTApp_SetupWindowObject, which we call for every movie we open.

Listing 2: Getting a QTVR instance

QTApp_SetupWindowObject
void QTApp_SetupWindowObject (WindowObject theWindowObject)
{
   Track                        myQTVRTrack = NULL;
   Movie                        myMovie = NULL;
   MovieController              myMC = NULL;
   QTVRInstance                 myInstance = NULL;
   if (theWindowObject == NULL)
      return;
   // make sure we can safely call the QTVR API
   if (!gQTVRMgrIsPresent)
      return;
   // find the QTVR track, if there is one
   myMC = (**theWindowObject).fController;
   myMovie = (**theWindowObject).fMovie;
   myQTVRTrack = QTVRGetQTVRTrack(myMovie, 1);
   QTVRGetQTVRInstance(&myInstance, myQTVRTrack, myMC);
   (**theWindowObject).fInstance = myInstance;
   // do any QTVR window configuration
   if (myInstance != NULL) {
      // set unit to radians
      QTVRSetAngularUnits(myInstance, kQTVRRadians);
   }
}

Notice that we keep track of the QTVR instance by storing it in the fInstance field of the window object associated with the movie (here, theWindowObject). This gives us an easy way to determine whether a given movie window contains a QuickTime VR movie. Notice also that we call the QTVRSetAngularUnits function to set our preferred angular units to radians. The QuickTime VR Manager can work with either degrees or radians when specifying angular measurements (for instance, when we call QTVRGetPanAngle). The default angular unit type is degrees. Internally, the QuickTime VR Manager always uses radians, and in some situations it gives us measurements in radians no matter what the current angular unit. In general, therefore, I find it easier to work in radians most of the time, so I’ve reset the angular unit type to radians. (Your preference may vary.) We can define some simple macros to allow us to convert between degrees and radians:

#define kVRPi                      ((float)3.1415926535898)
#define kVR2Pi                      ((float)(2.0 * 3.1415926535898))
#define QTVRUtils_DegreesToRadians(x)   \
                                       ((float)((x) * kVRPi / 180.0))
#define QTVRUtils_RadiansToDegrees(x)   \
                                       ((float)((x) * 180.0 / kVRPi))

We don’t need to explicitly release or dispose of a QTVR instance; the value we obtain by calling QTVRGetQTVRInstance remains valid until we dispose of the associated movie controller.

Controlling View Angles

Finally we’re ready to use the QuickTime VR Manager to do some real work. The most basic way to use the API is to control the view angles of a node — the pan, tilt, and field of view angles. Listing 3 defines a function that gradually increments the pan angle through 360 degrees. With panoramas, this has the effect of making the user seem to spin a full circle (as if the user were spinning on a rotating stool). With objects, this has the effect of making the object spin around a full circle (as if the object were spinning on a turntable).

Listing 3: Spinning a node around once

SpinAroundOnce
void SpinAroundOnce (QTVRInstance theInstance)
{
   float      myOrigPanAngle, myCurrPanAngle;
   myOrigPanAngle = QTVRGetPanAngle(theInstance);
   for (myCurrPanAngle = myOrigPanAngle; 
         myCurrPanAngle <= myOrigPanAngle + kVR2Pi; 
         myCurrPanAngle += QTVRUtils_DegreesToRadians(10.0)) {
      QTVRSetPanAngle(theInstance, myCurrPanAngle);
      QTVRUpdate(theInstance, kQTVRCurrentMode);
   }
}

The idea here is simple: get the starting pan angle (by calling QTVRGetPanAngle) and then repeatedly increment the pan angle by a certain amount (here, 10 degrees) until a full circle has been traversed. Note that we need to call the QTVRUpdate function after we set a new pan angle to make sure the updated view is displayed on the screen.

Drawing on a Panorama

Suppose we want to draw a logo or other graphic element on top of a panorama (as seems to be in vogue on broadcast television channels these days). As we learned earlier, we can draw into a panorama’s prescreen buffer before that buffer is copied to the screen. (Object nodes don’t have prescreen buffers, so this technique won’t work for those kinds of nodes.) We exploit this capability by installing a prescreen buffer imaging completion procedure, which is called by the QuickTime VR Manager each time the prescreen buffer is about to be copied to the screen. We install our procedure using the QTVRSetPrescreenImagingCompleteProc function:

ImagingCompleteUPP      myImagingProc;
myImagingProc = NewImagingCompleteProc(MyPrescreenRoutine);
QTVRSetPrescreenImagingCompleteProc(myInstance, 
            myImagingProc, (SInt32)theWindowObject, 0);

The QTVRSetPrescreenImagingCompleteProc function takes four parameters, which are the QTVR instance, a universal procedure pointer to the imaging complete procedure, a four-byte reference constant, and four-byte flags parameter. In this case, we pass the window object reference as the third parameter so that the imaging complete procedure can access any data associated with the window.

Our prescreen buffer imaging completion procedure is called after QuickTime VR has finished drawing into the prescreen buffer. When it’s called, the current graphics port is set to the prescreen buffer. All we need to do is draw a picture at the appropriate spot, as shown in Listing 4.

Listing 4: Drawing a picture on top of a panorama

MyPrescreenRoutine
pascal OSErr MyPrescreenRoutine 
   (QTVRInstance theInstance, WindowObject theWindowObject)
{
#pragma unused(theInstance)
   ApplicationDataHdl      myAppData;
   Rect                           myMovieRect;
   Rect                           myPictRect;
   // get the application-specific data associated with the window
   myAppData = (ApplicationDataHdl)
                     GetAppDataFromWindowObject(theWindowObject);
   if (myAppData == NULL)
      return(paramErr);
   // if there is no picture to display, just return
   if ((**myAppData).fPicture == NULL)
      return(noErr);
   // get the current size of the movie
   GetMovieBox((**theWindowObject).fMovie, &myMovieRect);
   // set the size and position of the overlay rectangle
   MacSetRect(&myPictRect, 0, 0, 32, 32);
   MacOffsetRect(&myPictRect, 
                  myMovieRect.right - (myPictRect.right + 5), 
                  myMovieRect.bottom - (myPictRect.bottom + 5));
   // draw the picture
   DrawPicture((**myAppData).fPicture, &myPictRect);
   return(noErr);
}

There’s nothing very complicated in this prescreen buffer imaging completion procedure. Essentially, it just figures out where in the buffer to draw the picture and then draws it. We assume that a handle to the picture data is stored in the fPicture field of the application data record.

Intercepting QuickTime VR Manager Functions

Suppose we want to play a sound every time the user clicks on (that is, triggers) a hot spot. The easiest way to do this is to install an intercept procedure that is called each time a hot spot is triggered. The intercept procedure simply plays the sound and then returns, whereupon QuickTime VR processes the hot spot click as usual. Listing 5 shows a simple hot spot triggering intercept procedure.

Listing 5: Playing a sound on hot spot clicks

MyInterceptRoutine
pascal void MyInterceptRoutine (
                              QTVRInstance theInstance, 
                              QTVRInterceptPtr theMsg, 
                              WindowObject theWindowObject, 
                              Boolean *cancel)
{
#pragma unused(theInstance, theWindowObject)
   Boolean         myCancelInterceptedProc = false;
   switch (theMsg->selector) {
      case kQTVRTriggerHotSpotSelector:
         MyPlaySound();
         break;
   }
   *cancel = myCancelInterceptedProc;
}

An intercept routine is executed whenever the intercepted routine is called, either programmatically or by a user action. On entry, the QuickTime VR Manager provides three pieces of information: the relevant QTVR instance, a pointer to an intercept record, and an application-defined reference constant, which we use here to pass in the window object. The intercept record (pointed to by the theMsg parameter) has this structure:

struct QTVRInterceptRecord {
   SInt32            reserved1;
   SInt32            selector;
   SInt32            reserved2;
   SInt32            reserved3;
   SInt32            paramCount;
   void              *parameter[6];
}

For present purposes, we need to inspect only the selector field, which contains a value that indicates which intercepted routine is being called. As you can see in Listing 5, we look for any calls to QTVRTriggerHotSpot and call the application-defined function MyPlaySound when we get one.

We install an intercept procedure by calling the QTVRInstallInterceptProc function, as shown in Listing 6.

Listing 6: Installing an intercept routine

MyInstallInterceptRoutine
void MyInstallInterceptRoutine (
         QTVRInstance theInstance, WindowObject theWindowObject)
{
   QTVRInterceptUPP      myInterceptProc;
   myInterceptProc = 
            NewQTVRInterceptProc(MyInterceptRoutine);
   QTVRInstallInterceptProc(theInstance, 
            kQTVRTriggerHotSpotSelector, myInterceptProc, 
            (SInt32)theWindowObject, 0);
}

The QuickTime VR File Format

Unlike movies containing other interactive media types (such as sprite or Flash) where the media data can be stored in a single track, a QuickTime VR movie always contains several tracks. A panorama movie, for instance, contains a panorama image track (which holds the image data for the panorama), a panorama track (which contains information about the panoramic node), and a QTVR track (which maintains general information about the movie, such as the default imaging properties). Similarly, an object movie contains an object image track (which holds the image data for the object), an object track (which contains information about the object node), and a QTVR track. For multi-node movies, the QTVR track also contains a list of the nodes in the movie and an indication of which node is the default node. Movies with hot spots also contain a hot spot image track (a video track where the hot spots are designated by colored regions).

Usually this structure is important to us only when we want to create a QuickTime VR movie. But it’s also useful when we want to alter an existing QuickTime VR movie or to extract information not provided by the available QuickTime VR Manager functions.

Working with Node Information

A QTVR track maintains general information about a QuickTime VR movie. Each individual sample in the QTVR track’s media is an atom container called a node information atom container. This atom container holds a node header atom, which contains information about a single node, such as the node’s type, ID, and name. The node information atom container can also hold a hot spot parent atom if the node has any hot spots in it. The QuickTime VR Manager provides the QTVRGetNodeInfo function that we can use to get a copy of a particular node information atom container or of any of its children. Listing 7 defines a function that we can use to get a copy of a node header atom for a specified node ID.

Listing 7: Finding the node header atom data

QTVRUtils_GetNodeHeaderAtomData
OSErr QTVRUtils_GetNodeHeaderAtomData 
            (QTVRInstance theInstance, UInt32 theNodeID, 
            QTVRNodeHeaderAtomPtr theNodeHdrPtr)
{
   QTAtomContainer            myNodeInfo;
   QTAtom                     myAtom;
   OSErr                      myErr = noErr;
   // get the node information atom container for the specified node
   myErr = QTVRGetNodeInfo(theInstance, theNodeID, 
            &myNodeInfo);
   if (myErr != noErr)
      return(myErr);
   // get the single node header atom in the node information atom container
   myAtom = QTFindChildByID(myNodeInfo, 
            kParentAtomIsContainer, kQTVRNodeHeaderAtomType, 1, 
            NULL);
   if (myAtom != 0)
      myErr = QTCopyAtomDataToPtr(myNodeInfo, myAtom, false, 
               sizeof(QTVRNodeHeaderAtom), theNodeHdrPtr, NULL);
   else 
      myErr = cannotFindAtomErr;
   QTDisposeAtomContainer(myNodeInfo);
   return(myErr);
}

As you can see, we call QTVRGetNodeInfo to get the node information atom container for the specified node ID; then we call QTFindChildByID to find the single node header atom inside that container. If we find that atom, we call QTCopyAtomDataToPtr to make a copy of its data. A node header atom has this structure:

struct QTVRNodeHeaderAtom {
   UInt16                     majorVersion;
   UInt16                     minorVersion;
   OSType                     nodeType;
   QTAtomID                   nodeID;
   QTAtomID                   nameAtomID;
   QTAtomID                   commentAtomID;
   UInt32                     reserved1;
   UInt32                     reserved2;
};

Listing 8 defines the function QTVRUtils_GetNodeType, which reads the nodeType field of a node header atom to determine the node type. (In fact, the QuickTime VR Manager provides the QTVRGetNodeType function to get a node’s type; we present QTVRUtils_GetNodeType simply to show another way of getting that information.)

Listing 8: Getting a node type

QTVRUtils_GetNodeType
OSErr QTVRUtils_GetNodeType (QTVRInstance theInstance, 
            UInt32 theNodeID, OSType *theNodeType)
{
   QTVRNodeHeaderAtom      myNodeHeader;
   OSErr                        myErr = noErr;
   // make sure we always return some meaningful value
   *theNodeType = kQTVRUnknownType;
   // get the node header atom data
   myErr = QTVRUtils_GetNodeHeaderAtomData(theInstance, 
            theNodeID, &myNodeHeader);
   if (myErr == noErr)
      *theNodeType = EndianU32_BtoN(myNodeHeader.nodeType);
   return(myErr);
}

There is no need to deallocate the block of data returned by QTVRUtils_GetNodeHeaderAtomData because it is allocated on the stack in a local variable.

Working with a VR World

All samples in a QTVR track use a single sample description. The data field of that sample description holds a VR world atom container, which holds general information about the scene contained in the QuickTime VR movie, including the name of the entire scene, the default node ID, and the default imaging properties. We can use the QTVRGetVRWorld function to retrieve a copy of a movie’s VR world atom container. Listing 9 illustrates how to use this function.

Listing 9: Finding the VR world atom data

QTVRUtils_GetVRWorldHeaderAtomData
OSErr QTVRUtils_GetVRWorldHeaderAtomData 
            (QTVRInstance theInstance, 
             QTVRWorldHeaderAtomPtr theVRWorldHdrAtomPtr)
{
   QTAtomContainer            myVRWorld;
   QTAtom                     myAtom;
   OSErr                      myErr = noErr;
   // get the VR world
   myErr = QTVRGetVRWorld(theInstance, &myVRWorld);
   if (myErr != noErr)
      return(myErr);
   // get the single VR world header atom in the VR world
   myAtom = QTFindChildByIndex(myVRWorld, 
         kParentAtomIsContainer, kQTVRWorldHeaderAtomType, 1, 
            NULL);
   if (myAtom != 0)
      myErr = QTCopyAtomDataToPtr(myVRWorld, myAtom, false, 
            sizeof(QTVRWorldHeaderAtom), theVRWorldHdrAtomPtr, 
            NULL);
   else
      myErr = cannotFindAtomErr;
   QTDisposeAtomContainer(myVRWorld);
   return(myErr);
}

A VR world atom container contains (perhaps among other things) a single VR world header atom, whose structure is defined by the QTVRWorldHeaderAtom data type:

struct QTVRWorldHeaderAtom {
   UInt16                     majorVersion;
   UInt16                     minorVersion;
   QTAtomID                   nameAtomID;
   UInt32                     defaultNodeID;
   UInt32                     vrWorldFlags;
   UInt32                     reserved1;
   UInt32                     reserved2;
};

We can use this information to determine the node ID of a scene’s default node, as shown in Listing 10.

Listing 10: Finding a scene’s default node

QTVRUtils_GetDefaultNodeID
UInt32 QTVRUtils_GetDefaultNodeID (QTVRInstance theInstance)
{
   QTVRWorldHeaderAtom           myVRWorldHeader;
   UInt32                        myNodeID = kQTVRCurrentNode;
   OSErr                         myErr = noErr;
   myErr = QTVRUtils_GetVRWorldHeaderAtomData(theInstance, 
            &myVRWorldHeader);
   if (myErr == noErr)
      myNodeID = EndianU32_BtoN(myVRWorldHeader.defaultNodeID);
   return(myNodeID);
}

QTVRUtils_GetDefaultNodeID can be useful if we need to know the ID of a movie’s default node, since there is no QuickTime VR Manager function that returns this information directly.

Wired Actions and QuickTime VR

In a handful of recent articles, we’ve seen how to work with QuickTime wired actions in conjunction with sprite tracks, text tracks, and Flash tracks. We use wired actions to attach dynamic, interactive behaviors to elements in a QuickTime movie and to allow different elements in those movies (and indeed in different movies) to communicate with one another. In this section, we’ll investigate how to work with wired actions and QuickTime VR movies.

Sending Actions to QuickTime VR Movies

Let’s begin by taking a look at the wired actions that can be targeted at a QuickTime VR movie. When action wiring was first introduced, in QuickTime 3, these five wired actions were supported:

enum {
   kActionQTVRSetPanAngle             = 4096,
   kActionQTVRSetTiltAngle            = 4097,
   kActionQTVRSetFieldOfView          = 4098,
   kActionQTVRShowDefaultView         = 4099,
   kActionQTVRGoToNodeID              = 4100
};

The first three actions allow us to set a new pan angle, tilt angle, or field of view in a QuickTime VR movie. Each of these actions takes a single parameter, a value of type float that specifies the desired new angle. This value should be specified in degrees (not radians) and is by default an absolute angle to pan, tilt, or zoom to. It’s often useful to specify a relative value instead; we can indicate that the parameter value is relative by inserting into the action atom an atom of type kActionFlags whose atom data is a long integer with (at least) the kActionFlagActionIsDelta flag set. Listing 11 shows how we can build an atom container holding a wired atom that pans the target QuickTime VR movie one degree to the left each time it gets an idle event. (We’ll see later how to make sure the movie is sent idle events.)

Listing 11: Panning a QuickTime VR movie during idle events

AddVRAct_CreateIdleActionContainer
static OSErr AddVRAct_CreateIdleActionContainer 
            (QTAtomContainer *theActions)
{
   QTAtom         myEventAtom = 0;
   QTAtom         myActionAtom = 0;
   long            myAction;
   float         myPanAngle = 1.0;
   UInt32         myFlags;
   OSErr         myErr = noErr;
   myErr = QTNewAtomContainer(theActions);
   if (myErr != noErr)
      goto bail;
   myErr = QTInsertChild(*theActions, kParentAtomIsContainer, 
            kQTEventIdle, 1, 1, 0, NULL, &myEventAtom);
   if (myErr != noErr)
      goto bail;
   myErr = QTInsertChild(*theActions, myEventAtom, kAction, 
            1, 1, 0, NULL, &myActionAtom);
   if (myErr != noErr)
      goto bail;
   myAction = EndianS32_NtoB(kActionQTVRSetPanAngle);
   myErr = QTInsertChild(*theActions, myActionAtom, 
            kWhichAction, 1, 1, sizeof(long), &myAction, NULL);
   if (myErr != noErr)
      goto bail;
   AddVRAct_ConvertFloatToBigEndian(&myPanAngle);
   myErr = QTInsertChild(*theActions, myActionAtom, 
            kActionParameter, 1, 1, sizeof(float), &myPanAngle, 
            NULL);
   if (myErr != noErr)
      goto bail;
   myFlags = EndianU32_NtoB(kActionFlagActionIsDelta | 
            kActionFlagParameterWrapsAround);
   myErr = QTInsertChild(*theActions, myActionAtom, 
            kActionFlags, 1, 1, sizeof(UInt32), &myFlags, NULL);
bail:
   return(myErr);
}

The action kActionQTVRShowDefaultView sets the current node to its default view (that is, the view that is displayed when the node is first entered). The kActionQTVRGoToNodeID action takes a single parameter that specifies a node ID; when the action is executed, the node with that ID becomes the current node. QuickTime 3 also introduced four wired action operands, which we can use to get information about the current state of a QuickTime VR movie:

enum {
   kOperandQTVRPanAngle                = 4096,
   kOperandQTVRTiltAngle               = 4097,
   kOperandQTVRFieldOfView             = 4098,
   kOperandQTVRNodeID                  = 4099
};

QuickTime 5 added three more actions that we can send to a QuickTime VR movie:

enum {
   kActionQTVREnableHotSpot           = 4101,
   kActionQTVRShowHotSpots            = 4102,
   kActionQTVRTranslateObject         = 4103
};

The kActionQTVREnableHotSpot action enables or disables a hot spot. This action requires two parameters, a long integer that specifies a hot spot ID and a Boolean value that specifies whether to enable (true) or disable (false) the hot spot. The kActionQTVRShowHotSpots action shows or hides all hot spots in a node, depending on the Boolean value in the parameter atom. The kActionQTVRTranslateObject action sets the view center of an object node to the values specified in the action’s two parameters. To allow us to retrieve the current hot spot visibility state and the current view center, QuickTime 5 introduced three additional operands:

enum {
   kOperandQTVRHotSpotsVisible        = 4100,
   kOperandQTVRViewCenterH            = 4101,
   kOperandQTVRViewCenterV            = 4102
};

There is currently no operand that will allow us to determine whether a particular hot spot is enabled.

Adding Actions to QuickTime VR Movies

We can add two kinds of wired actions to QuickTime VR movies: (1) actions that are associated with a particular node and (2) actions that are associated with a particular hot spot in a node. Examples of node-specific actions are setting the pan and tilt angles when the user first enters the node and performing some actions periodically when the movie gets an idle event. An example of a hot-spot-specific action might be playing a sound when the cursor is moved over a hot spot.

All QuickTime VR wired actions are attached to a particular node, so the atom containers holding the actions are placed in the node information atom container that is contained in the media sample for that node in the QTVR track. So, our job here boils down to finding a media sample in the QTVR track, constructing some atom containers for our desired actions, placing those action containers into the appropriate places in the media sample, and then writing the modified media sample back into the QTVR track. We’ll also need to put an atom into the media property atom container of the QTVR track to enable wired action and idle event processing.

Adding Actions to a Hot Spot

Let’s begin by seeing how to attach some wired actions to a particular hot spot in a node. Let’s suppose that we know both the node ID and the hot spot ID, and that we have already constructed the atom container that holds the wired actions. Recall that a QTVR track contains one media sample for each node in the movie and that that media sample is a node information atom container. For simplicity, we’ll assume that we want to wire a hot spot in a single-node QuickTime VR movie. As a result, we can get the media sample by calling GetMediaSample, like this:

GetMediaSample(myMedia, mySample, 0, NULL, myMediaTime, NULL, 
         &mySampleDuration, (SampleDescriptionHandle)myQTVRDesc, 
         NULL, 1, NULL, &mySampleFlags);

If GetMediaSample returns successfully, then mySample will be the atom container that holds the atoms we want to modify.

At this point, we’ll call an application function AddVRAct_SetWiredActionsToHotSpot to add our wired actions to the specified hot spot:

AddVRAct_SetWiredActionsToHotSpot(mySample, myHotSpotID, 
            myActions);

The first thing we need to do in AddVRAct_SetWiredActionsToHotSpot is find the hot spot parent atom inside the node information atom container:

myHotSpotParentAtom = QTFindChildByIndex(theSample, 
            kParentAtomIsContainer, kQTVRHotSpotParentAtomType, 
            1, NULL);

A hot spot parent atom contains a hot spot atom (of type kQTVRHotSpotAtomType) for each hot spot in the node. The ID of the hot spot atom is the same as the ID of the hot spot, so we can find the appropriate hot spot atom like this:

myHotSpotAtom = QTFindChildByID(theSample,
            myHotSpotParentAtom, kQTVRHotSpotAtomType, 
            theHotSpotID, NULL);

We add wired actions to a hot spot by inserting an event atom (that is, an atom of type kQTEventType) into the hot spot atom:

QTInsertChildren(theSample, myHotSpotAtom, theActions);

Listing 12 shows our complete definition of AddVRAct_SetWiredActionsToHotSpot.

Listing 12: Adding wired actions to a hot spot

AddVRAct_SetWiredActionsToHotSpot
static OSErr AddVRAct_SetWiredActionsToHotSpot 
            (Handle theSample, long theHotSpotID, 
             QTAtomContainer theActions)
{
   QTAtom         myHotSpotParentAtom = 0;
   QTAtom         myHotSpotAtom = 0;
   short         myCount, myIndex;
   OSErr         myErr = paramErr;
   myHotSpotParentAtom = QTFindChildByIndex(theSample, 
            kParentAtomIsContainer, kQTVRHotSpotParentAtomType, 
            1, NULL);
   if (myHotSpotParentAtom == NULL)
      goto bail;
   myHotSpotAtom = QTFindChildByID(theSample, 
            myHotSpotParentAtom, kQTVRHotSpotAtomType, 
            theHotSpotID, NULL);
   if (myHotSpotAtom == NULL)
      goto bail;
   // see how many events are already associated with the specified hot spot
   myCount = QTCountChildrenOfType(theSample, myHotSpotAtom, 
            kQTEventType);
   for (myIndex = myCount; myIndex > 0; myIndex--) {
      QTAtom         myTargetAtom = 0;
      // remove all the existing events
      myTargetAtom = QTFindChildByIndex(theSample, 
            myHotSpotAtom, kQTEventType, myIndex, NULL);
      if (myTargetAtom != 0) {
         myErr = QTRemoveAtom(theSample, myTargetAtom);
         if (myErr != noErr)
            goto bail;
      }
   }
   if (theActions) {
      myErr = QTInsertChildren(theSample, myHotSpotAtom, 
            theActions);
      if (myErr != noErr)
         goto bail;
   }
bail:
   return(myErr);
}

You’ll notice that we look to see whether the hot spot atom already contains any event atoms; if so, we remove them from the hot spot atom. This ensures that the event atom we pass to AddVRAct_SetWiredActionsToHotSpot is the only one in the hot spot atom.

Adding Actions to a Node

We add wired actions to a node by inserting children into the node information atom container for that node. The type of a child atom for a wired action should be the same as the event type, and the ID should be 1. Listing 13 defines the AddVRAct_SetWiredActionsToNode function, which we use to add a wired atom to a particular node. The first parameter is assumed to be the node information atom container.

Listing 13: Adding wired actions to a node

AddVRAct_SetWiredActionsToNode
static OSErr AddVRAct_SetWiredActionsToNode 
            (Handle theSample, QTAtomContainer theActions, 
            UInt32 theActionType)
{
   QTAtom         myEventAtom = 0;
   QTAtom         myTargetAtom = 0;
   OSErr         myErr = noErr;
   // look for an event atom in the specified actions atom container
   if (theActions != NULL)
      myEventAtom = QTFindChildByID(theActions, 
            kParentAtomIsContainer, theActionType, 1, NULL);
   // look for an event atom in the node information atom container
   myTargetAtom = QTFindChildByID(theSample, 
            kParentAtomIsContainer, theActionType, 1, NULL);
   if (myTargetAtom != 0) {
      // if there is already an event atom in the node information atom container,
      // then either replace it with the one we were passed or remove it
      if (theActions != NULL)
         myErr = QTReplaceAtom(theSample, myTargetAtom, 
            theActions, myEventAtom);
      else
         myErr = QTRemoveAtom(theSample, myTargetAtom);
   } else {
      // there is no event atom in the node information atom container,
      // so add in the one we were passed
      if (theActions != NULL)
         myErr = QTInsertChildren(theSample, 
            kParentAtomIsContainer, theActions);
   }
   return(myErr);
}

We can add an idle event handler to a node like this:

AddVRAct_SetWiredActionsToNode(mySample, myActions, 
            kQTEventIdle);

And we can add a frame-loaded event handler to a node like this:

AddVRAct_SetWiredActionsToNode(mySample, myActions, 
            kQTEventFrameLoaded);

Other event types (such as kQTEventMouseClick or kQTEventKey) might not make sense for a node-based wired action.

Updating the Media Property Atom

When we added some wiring to a sprite track, we needed to include in the track’s media property atom an atom of type kSpriteTrackPropertyHasActions whose atom data is set to true. (See “Wired”, in MacTech, May 2001.) This atom tells the movie controller that the sprite track has wiring associated with it. If, in addition, any of the wired sprites employs the kQTEventIdle event, we also need to add an atom of type kSpriteTrackPropertyQTIdleEventsFrequency whose atom data indicates the desired idle event frequency, in ticks. We need to add these same atoms to the media property atom when we wire a QuickTime VR movie. Listing 14 defines the function AddVRAct_WriteMediaPropertyAtom, which we use to add the appropriate atoms.

Listing 14: Adding atoms to the media property atom

AddVRAct_WriteMediaPropertyAtom
static OSErr AddVRAct_WriteMediaPropertyAtom (Media theMedia, 
            long thePropertyID, long thePropertySize, 
            void *theProperty)
{
   QTAtomContainer      myPropertyAtom = NULL;
   QTAtom                  myAtom = 0;
   OSErr                  myErr = noErr;
   // get the current media property atom
   myErr = GetMediaPropertyAtom(theMedia, &myPropertyAtom);
   if (myErr != noErr)
      goto bail;
   // if there isn’t one yet, then create one
   if (myPropertyAtom == NULL) {
      myErr = QTNewAtomContainer(&myPropertyAtom);
      if (myErr != noErr)
         goto bail;
   }
   // see if there is an existing atom of the specified type; if not, then create one
   myAtom = QTFindChildByID(myPropertyAtom, 
            kParentAtomIsContainer, thePropertyID, 1, NULL);
   if (myAtom == NULL) {
      myErr = QTInsertChild(myPropertyAtom, 
         kParentAtomIsContainer, thePropertyID, 1, 0, 0, NULL, 
            &myAtom);
      if ((myErr != noErr) || (myAtom == NULL))
         goto bail;
   }
   // set the data of the specified atom to the data passed in
   myErr = QTSetAtomData(myPropertyAtom, myAtom, 
            thePropertySize, (Ptr)theProperty);
   if (myErr != noErr)
      goto bail;
   // write the new atom data out to the media property atom
   myErr = SetMediaPropertyAtom(theMedia, myPropertyAtom);
bail:
   if (myPropertyAtom != NULL)
      myErr = QTDisposeAtomContainer(myPropertyAtom);
   return(myErr);
}

To indicate that the QuickTime VR movie has wired actions embedded in it, we can call AddVRAct_WriteMediaPropertyAtom like this:

myHasActions = true;
AddVRAct_WriteMediaPropertyAtom(myMedia, 
            kSpriteTrackPropertyHasActions, 
            sizeof(Boolean), &myHasActions);

And we can set the idle frequency like this:

myFrequency = EndianU32_NtoB(30);
AddVRAct_WriteMediaPropertyAtom(myMedia, 
            kSpriteTrackPropertyQTIdleEventsFrequency, 
            sizeof(UInt32), &myFrequency);

Saving the Modified Media Data

So far, we’ve added some wired atoms to a node information atom container or to a hot spot atom inside of a node information atom container, and we’ve updated the media property atom of the QTVR track. To save these changes, we need to replace the appropriate sample in the QTVR track media and then update the movie atom. Listing 15 shows the complete definition of the AddVRAct_AddWiredActionsToQTVRMovie function, which we use to wire a QuickTime VR movie.

Listing 15: Adding wired actions to a QuickTime VR movie

AddVRAct_AddWiredActionsToQTVRMovie
static void AddVRAct_AddWiredActionsToQTVRMovie 
            (FSSpec *theFSSpec)
{   
   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;
   QTVRSampleDescriptionHandle
                              myQTVRDesc = NULL;
   Handle                     mySample = NULL;
   short                     mySampleFlags;
   Fixed                      myTrackEditRate;
   QTAtomContainer         myActions = NULL;
   Boolean                  myHasActions;
   long                        myHotSpotID = 0L;
   UInt32                     myFrequency;
   OSErr                     myErr = noErr;
   // open the movie file and get the QTVR track from the movie
   // 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 the first QTVR track in the movie;
   myTrack = GetMovieIndTrackType(myMovie, 1, kQTVRQTVRType, 
            movieTrackMediaType);
   if (myTrack == NULL)
      goto bail;
   // get the first media sample in the QTVR 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 QTVR track
   myQTVRDesc = (QTVRSampleDescriptionHandle)NewHandle(4);
   if (myQTVRDesc == NULL)
      goto bail;
   mySample = NewHandle(0);
   if (mySample == NULL)
      goto bail;
   myErr = GetMediaSample(myMedia, mySample, 0, NULL, 
            myMediaTime, NULL, &mySampleDuration, 
            (SampleDescriptionHandle)myQTVRDesc, NULL, 1, NULL, 
            &mySampleFlags);
   if (myErr != noErr)
      goto bail;
   // add idle actions
   // create an action container for idle actions
   myErr = AddVRAct_CreateIdleActionContainer(&myActions);
   if (myErr != noErr)
      goto bail;
   // add idle actions to sample
   myErr = AddVRAct_SetWiredActionsToNode(mySample, myActions, 
            kQTEventIdle);
   if (myErr != noErr)
      goto bail;
   myErr = QTDisposeAtomContainer(myActions);
   if (myErr != noErr)
      goto bail;
   // add frame-loaded actions
   // create an action container for frame-loaded actions
   myErr = AddVRAct_CreateFrameLoadedActionContainer
            (&myActions);
   if (myErr != noErr)
      goto bail;
   // add frame-loaded actions to sample
   myErr = AddVRAct_SetWiredActionsToNode(mySample, myActions, 
            kQTEventFrameLoaded);
   if (myErr != noErr)
      goto bail;
   myErr = QTDisposeAtomContainer(myActions);
   if (myErr != noErr)
      goto bail;
   // add hot-spot actions
   // find the first hot spot in the selected node; don’t bail if there are no hot spots
   myErr = AddVRAct_GetFirstHotSpot(mySample, &myHotSpotID);
   if ((myErr == noErr) && (myHotSpotID != 0)) {
      // create an action container for hot-spot actions
      myErr = AddVRAct_CreateHotSpotActionContainer
            (&myActions);
      if (myErr != noErr)
         goto bail;
      // add hot-spot actions to sample 
      myErr = AddVRAct_SetWiredActionsToHotSpot(mySample, 
            myHotSpotID, myActions);
      if (myErr != noErr)
         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)myQTVRDesc, 
                     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;
   // set the media property atom to enable wired action and idle-time processing
   myHasActions = true;
   myErr = AddVRAct_WriteMediaPropertyAtom(myMedia, 
            kSpriteTrackPropertyHasActions, sizeof(Boolean), 
            &myHasActions);
   if (myErr != noErr)
      goto bail;
   myFrequency = EndianU32_NtoB(1);
   myErr = AddVRAct_WriteMediaPropertyAtom(myMedia, 
            kSpriteTrackPropertyQTIdleEventsFrequency, 
            sizeof(UInt32), &myFrequency);
   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)
      QTDisposeAtomContainer(myActions);
   if (mySample != NULL)
      DisposeHandle(mySample);
   if (myQTVRDesc != NULL)
      DisposeHandle((Handle)myQTVRDesc);
   if (myMovie != NULL)
      DisposeMovie(myMovie);
}

Conclusion

In this article, we’ve learned how to work with the QuickTime VR Manager to control the operation of QuickTime VR movies programmatically. We’ve seen how to adjust pan, tilt, and zoom angles, how to alter the displayed image by drawing into a panorama’s prescreen buffer, and how to intercept some QuickTime VR Manager functions. As usual, these few examples of using the VR APIs are just the tip of the iceberg; with just a little bit more time and energy, we can develop some even more impressive interactive applications using QuickTime VR.

We’ve also taken a look at QuickTime VR and wired actions, first reviewing how to send actions to VR movies and then (more importantly) learning how to embed wired actions into QuickTime VR movies. We haven’t yet learned how to actually create QuickTime VR movies from scratch, but we do have a preliminary idea of how they are put together (at least in part). Perhaps in a future article we’ll learn how to build QuickTime VR movies.

Credits and References

Thanks to Bryce Wolfson for reviewing an earlier version of this article and for providing some helpful comments. The code for adding wired actions to QuickTime VR movies is based on some code by Bill Wright. For complete information on the QuickTime VR Manager, see the book Virtual Reality Programming With QuickTime VR 2.1 by Apple Computer, Inc.


Tim Monroe is a member of the QuickTime engineering team. You can contact him at monroe@apple.com. The views expressed here are not necessarily shared by his employer.

 
AAPL
$119.00
Apple Inc.
+1.40
MSFT
$47.75
Microsoft Corpora
+0.28
GOOG
$540.37
Google Inc.
-0.71

MacTech Search:
Community Search:

Software Updates via MacUpdate

Skype 7.2.0.412 - Voice-over-internet ph...
Skype allows you to talk to friends, family and co-workers across the Internet without the inconvenience of long distance telephone charges. Using peer-to-peer data transmission technology, Skype... Read more
HoudahSpot 3.9.6 - Advanced file search...
HoudahSpot is a powerful file search tool built upon MacOS X Spotlight. Spotlight unleashed Create detailed queries to locate the exact file you need Narrow down searches. Zero in on files Save... Read more
RapidWeaver 6.0.3 - Create template-base...
RapidWeaver is a next-generation Web design application to help you easily create professional-looking Web sites in minutes. No knowledge of complex code is required, RapidWeaver will take care of... Read more
iPhoto Library Manager 4.1.10 - Manage m...
iPhoto Library Manager lets you organize your photos into multiple iPhoto libraries. Separate your high school and college photos from your latest summer vacation pictures. Or keep some photo... Read more
iExplorer 3.5.1.9 - View and transfer al...
iExplorer is an iPhone browser for Mac lets you view the files on your iOS device. By using a drag and drop interface, you can quickly copy files and folders between your Mac and your iPhone or... Read more
MacUpdate Desktop 6.0.3 - Discover and i...
MacUpdate Desktop 6 brings seamless 1-click installs and version updates to your Mac. With a free MacUpdate account and MacUpdate Desktop 6, Mac users can now install almost any Mac app on macupdate.... Read more
SteerMouse 4.2.2 - Powerful third-party...
SteerMouse is an advanced driver for USB and Bluetooth mice. It also supports Apple Mighty Mouse very well. SteerMouse can assign various functions to buttons that Apple's software does not allow,... Read more
iMazing 1.1 - Complete iOS device manage...
iMazing (was DiskAid) is the ultimate iOS device manager with capabilities far beyond what iTunes offers. With iMazing and your iOS device (iPhone, iPad, or iPod), you can: Copy music to and from... Read more
PopChar X 7.0 - Floating window shows av...
PopChar X helps you get the most out of your font collection. With its crystal-clear interface, PopChar X provides a frustration-free way to access any font's special characters. Expanded... Read more
OneNote 15.4 - Free digital notebook fro...
OneNote is your very own digital notebook. With OneNote, you can capture that flash of genius, that moment of inspiration, or that list of errands that's too important to forget. Whether you're at... Read more

Latest Forum Discussions

See All

Lucha Amigos (Games)
Lucha Amigos 1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0 (iTunes) Description: Forget Ninja Turtles, and meet Wrestlers Turtles! Crazier, Spicier and…Bouncier! Sling carapaces of 7 Luchadores to knock all... | Read more »
Raby (Games)
Raby 1.0.3 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0.3 (iTunes) Description: ***WARNING - Raby runs on: iPhone 5, iPhone 5C, iPhone 5S, iPhone 6, iPhone 6 Plus, iPad Mini Retina, iPad Mini 3, iPad 4, iPad Air,... | Read more »
Oddworld: Stranger's Wrath (Games)
Oddworld: Stranger's Wrath 1.0 Device: iOS Universal Category: Games Price: $5.99, Version: 1.0 (iTunes) Description: ** PLEASE NOTE: Oddworld Stranger's Wrath requires at least an iPhone 4S, iPad 2, iPad Mini or iPod Touch 5th gen... | Read more »
Bounce On Back (Games)
Bounce On Back 1.0 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0 (iTunes) Description: | Read more »
Dwelp (Games)
Dwelp 1.0 Device: iOS Universal Category: Games Price: $.99, Version: 1.0 (iTunes) Description: === 50% off for a limited time, to celebrate release === Dwelp is an elegant little puzzler with a brand new game mechanic. To complete a... | Read more »
Make Way for Fat Chicken, from the Maker...
Make Way for Fat Chicken, from the Makers of Scrap Squad Posted by Jessica Fisher on November 26th, 2014 [ permalink ] Relevant Games has announced they will be releasing their reverse tower defense game, | Read more »
Tripnary Review
Tripnary Review By Jennifer Allen on November 26th, 2014 Our Rating: :: TRAVEL BUCKET LISTiPhone App - Designed for the iPhone, compatible with the iPad Want to create a travel bucket list? Tripnary is a fun way to do exactly that... | Read more »
Ossian Studios’ RPG, The Shadow Sun, is...
Ossian Studios’ RPG, The Shadow Sun, is Now Available for $4.99 Posted by Jessica Fisher on November 26th, 2014 [ permalink ] Universal App - Designed for iPhone and iPad | Read more »
Mmmm, Tasty – Having the Angry Birds for...
The very first Angry Birds debuted on iOS back in 2009. When you sit back and tally up the number of Angry Birds games out there and the impact they’ve had on pop culture as a whole, you just need to ask yourself: “How would the birds taste... | Read more »
Rescue Quest Review
Rescue Quest Review By Jennifer Allen on November 26th, 2014 Our Rating: :: PATH BASED MATCH-3Universal App - Designed for iPhone and iPad Guide a wizard to safety by matching gems. Rescue Quest might not be an entirely original... | Read more »

Price Scanner via MacPrices.net

Apple Store Black Friday sale for 2014: $100...
BLACK FRIDAY The Apple Store has posted their Black Friday deals for 2014. Receive a $100 PRODUCT(RED) branded iTunes gift card with the purchase of select Macs, $50 with iPads, and $25 with iPods,... Read more
Black Friday: 15% off iTunes Gift Cards
Staples is offering 15% off $50 and $100 iTunes Gift Cards on their online store as part of their Black Friday sale. Click here for more information. Shipping is free. Best Buy is offering $100... Read more
BEVL Releases Dock Tailored for iPhone 6 and...
Seattle based BEVL has released their first product: an iPhone dock that is divergent in build quality, rock-solid function and visual simplicity to complement the iPhone. BEVL is now accepting... Read more
Black Friday: $150 off 13-inch Retina MacBook...
 Best Buy has 13-inch 2.6GHz Retina MacBook Pros on sale for $150 off MSRP on their online store as part of their Black Friday sale. Choose free shipping or free local store pickup (if available).... Read more
Black Friday: $300 off 15-inch Retina MacBook...
 B&H Photo has the new 2014 15″ Retina MacBook Pros on sale for $300 off MSRP as part of their Black Friday sale. Shipping is free, and B&H charges NY sales tax only: - 15″ 2.2GHz Retina... Read more
Black Friday: Up to $140 off MacBook Airs, fr...
 B&H Photo has 2014 MacBook Airs on sale for up to $140 off MSRP as part of their Black Friday sale. Shipping is free, and B&H charges NY sales tax only: - 11″ 128GB MacBook Air: $799 $100... Read more
Black Friday: 13-inch 2.5GHz MacBook Pro on s...
 Best Buy has the 13″ 2.5GHz MacBook Pro on sale for $899.99 on their online store as part of their Black Friday sale. Choose free shipping or free instant local store pickup (if available). Their... Read more
Black Friday: 21-inch 1.4GHz iMac on sale for...
 Best Buy has the 21″ 1.4GHz iMac on sale for $899.99 on their online store as part of their Black Friday sale. Their price is $200 off MSRP. Choose free shipping or free local store pick up. Price... Read more
Black Friday iPad Air 2 sale prices, $100 off...
 Best Buy has iPad Air 2s on sale for $100 off MSRP on their online store for Black Friday. Choose free shipping or free local store pickup (if available). Sale prices available for online orders... Read more
2014 1.4GHz Mac mini on sale for $449, save $...
 B&H Photo has the new 1.4GHz Mac mini on sale for $449.99 including free shipping plus NY tax only. Their price is $50 off MSRP, and it’s the lowest price available for this new model. Adorama... Read more

Jobs Board

*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
Senior Event Manager, *Apple* Retail Market...
…This senior level position is responsible for leading and imagining the Apple Retail Team's global event strategy. Delivering an overarching brand story; in-store, Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.