Hack 36 Panoramic Images

figs/expert.gif figs/hack36.gif

Use a little ingenuity to create 3D panoramas for playback in Flash, simulating immersion in an environment.

Panoramic imaging is a rendering technique in which the viewer appears to be standing in the center of a 3D-like environment. The image can be rotated, and the technique uses a texture image with distortions to simulate the depth of the surroundings. This technique was popularized with technologies like QuickTime VR (http://www.apple.com/quicktime/qtvr/authoringstudio) from Apple.

Panoramic content abounds, especially on travel and tourist web sites. However, most solutions require Java or a third-party plugin, reducing the likelihood users will view them, and some require licenses for development or deployment tools. Although several techniques are used for panoramic imaging?spherical, cubic, and so on?cylindrical panoramas, in which the texture is projected on the walls of a round "room," are the most common. Cylindrical panoramas are both the easiest to create and fastest to render, allowing the Flash Player to display them with adequate performance.

Although the Flash Player lacks the features and speed of some panoramic viewing tools, it doesn't require the user to install more software beyond the Flash Player (which is more ubiquitous than Java or any other third-party plugin). It also allows panoramic views to be controlled from inside a Flash movie where you can add interactivity or integrate it with other content. Moreover, there's no licensing fee for development or distribution.

Creating Panoramic Images

A panoramic image (a.k.a. a panorama, or simply pano) is a long horizontal image representing a 360-degree view of the surroundings, as shown in Figure 5-5.

Figure 5-5. A panoramic image
figs/flhk_0505.gif


This type of image is usually created by taking multiple shots from a given point with a rotating camera mounted on a tripod. Multiple images are often stitched together to create a flat cylindrical view. Various solutions can create and edit panoramas, from cameras that automatically create panoramic photos to stitching software designed to work with various image types.

Although creating panoramic images is beyond the scope of this book, it's simpler than it sounds. Sites such as Panoguide.com (http://www.panoguide.com) cover this topic in detail; you can find a guide to panorama editing software (http://www.panoguide.com/software/recommendations.html) and a gallery of downloadable panos (http://www.panoguide.com/gallery) on the same site.

To create our panorama-like view in Flash, we'll start with the pano.jpg panoramic image downloadable from the book's web site (the final Flash movie is also downloadable as pano.fla).

Using Flash for Image Manipulation

To emulate a 3D panorama, we'll cut our flat panoramic image into multiple strips, as shown in Figure 5-6 [Hack #35]

Figure 5-6. Vertical slices of a panorama
figs/flhk_0506.gif


Although only the area indicated by the green outline in Figure 5-6 is shown at runtime, each image strip is seamlessly scaled to match the arrangement shown in the figure. When the images are cropped at the top and bottom, and the area outside the viewer's field of vision is hidden, the impression of depth is complete. The scaling provides the illusion of a 3D view?enlarging the strips on the periphery relative to those in the center approximates mapping the panoramic image on the inside surface of a cylindrical wall, with the viewpoint being at the center of the circle.

JPEG format is the typical image format used for panoramas. Once you've created a pano (or downloaded the sample pano.jpg from this book's web site or a sample from Panoguide.com's gallery), import the file into Flash (using FileImportImport to Library).

Let's start the code by setting some simple data that will be used by our image movie clip:

var viewWidth:Number = 450; 

var viewHeight:Number = 400; 

var precision:Number = 8; 

var viewFOV:Number = 60;

where:


viewWidth

The width of the source panorama image.


viewHeight

The height of the source panorama image.


precision

The precision specifies the width of each strip of image. Setting this to 1 ensures maximum fidelity but requires more processing power than Flash can muster. The optimal value is determined manually by testing, but starting with a reasonably high quality value (lower number), such as 8, is recommended. You should increase the width of the strips only if the effect seems too slow. You can also get a better impression of how the effect works if you set this value high (such as to 50), because then the strips in the effect become obvious.


viewFOV

The field of vision (a.k.a. field of view) in degrees. It controls how much the image will appear distorted (i.e., it controls the curvature effect of the distortion caused by scaling the strips as you get further away from the center of the image). This value depends directly on the size and aspect ratio of the image and that's why it must be set manually. Typical useful values are 60 to 80 degrees. A value of 1 yields a flat image (no 3D effect) in which the image is effectively mapped onto a plane rather than a curved surface. A value of 180 yields an abnormally high curvature (i.e., a "fish-eye" effect).

After our values have been set, we need to cut the image into strips. First, we'll need a function to create strips to be used as masks:

this.createBox = function (name:String, x:Number, y:Number, 

     w:Number, h:Number, target:MovieClip):MovieClip {

  // This function creates the rectangles that are used as masks

  if (target == undefined) {

    target = this;

  }

  var box:MovieClip = target.createEmptyMovieClip(name, 

                       this.currentDepth++);

  box._x = x;

  box._y = y;

  // Use the Drawing API to draw a rectangle.

  box.lineStyle(undefined);

  box.moveTo (0, 0);

  box.beginFill(0x000000, 30);

  box.lineTo (w, 0);

  box.lineTo (w, h);

  box.lineTo (0, h);

  box.lineTo (0, 0);

  box.endFill( );

  return (box);

};

Then we can create the new images, each in its position, according to the view settings (viewWidth, viewHeight, precision, and viewFOV). We duplicate the original image (presumed to be previously imported into the Library) and create a strip mask for it. The pano.fla file on this book's web site contains a fully commented version of this code (reduced here for brevity):

var xpos:Number = 0; 

var currentDepth:Number = 100; 

var photoList:Array = []; 

while (viewWidth%precision != 0) {

  viewWidth++;

}

var boxCount:Number = 0;

var stripMask:MovieClip;

var stripPhoto:MovieClip;

var posX:Number;

var ang:Number;

var h:Number;

var viewTotal:Number = (viewHeight * 180) / viewFOV;

for (var i = 0; i < viewWidth; i += precision) {

  // Find the correct height and scale for this strip

  posX = ((viewWidth / 2) - ( i + (precision / 2)));

  ang = Math.asin(Math.abs(posX / (viewTotal / 2)));

  h = (Math.cos(ang) * (viewTotal / 2) - viewTotal / 2) * -1;

  // Create mask box

  stripMask = this.createBox("box_" + boxCount, 

                             i, s, precision, viewHeight);

  // Duplicate photo

  stripPhoto = this.photo.duplicateMovieClip("photo_" + boxCount,

                                             1000 + boxCount);

  stripPhoto._y = -h;

  stripPhoto._xscale = ((viewHeight + h * 2) / photo._height) * 100;

  stripPhoto._yscale = stripPhoto._xscale;

  stripPhoto.setMask(stripMask);

  photoList.push({photo:stripPhoto, mask:stripMask, 

                  scale:stripPhoto._xscale / 100});

  boxCount++;

}

photo._visible = false;

Now, our strips are done; each strip is masked by a mask clip and the strips are correctly scaled. The Stage thus contains a large number of individual photo clips, each of which can be seen through the "slot" of the mask clip as it moves from left to right (i.e., each mask clip stays still as the image that it masks moves).

References to each clip have been added to the photoList array to make them easily accessible. We now need code to place all images at their correct positions to form the arrangement seen in Figure 5-6:

this.redrawStrips = function( ) {

  // Redraw (reposition) all photos. 

  // Masks remain where they are.

  var tpos:Number;

  var cpos:Number = 0;

  // Create local variables to handle the  

  // properties of each strip, photoList[i]:

  //   mx: mask clip _x location

  //   mw: mask clip _width property

  //   pw: photo clip _width property

  //   s:  strip scaling factor

  var mx:Number; 

  var mw:Number;

  var pw:Number;

  var s:Number;

  for (var i = 0; i < this.photoList.length; i++) {

    mx = photoList[i].mask._x;

    mw = photoList[i].mask._width;

    pw = photoList[i].photo._width;

    s = photoList[i].scale;

    tpos = mx - ((cpos + xpos) * s);

    // Update the photo x scroll position, 

    // looping it back to the start if required.

    while (tpos > mx + mw) {

      tpos -= pw;

    }

    while (tpos + pw < mx) {

      tpos += pw;

    }

    // Fill in the gap between the start and end  

    // of the pano to make it appear continuous.

    if ( (tpos > mx) && (tpos < mx + mw) ) {

      // Duplicate for filling, left

      var alt:MovieClip = photoList[i].photo.duplicateMovieClip(

                         "alternatePhoto", 998);

      alt._x = tpos - pw;

      var altM:MovieClip = photoList[i].mask.duplicateMovieClip(

                          "alternateMask", 997);

      alt.setMask(altM);

    } else if ( (tpos + pw > mx) && (tpos + pw < mx + mw) ) {

      // Duplicate for filling, right

      var alt:MovieClip = photoList[i].photo.duplicateMovieClip(

                         "alternatePhoto", 998);

      alt._x = tpos+pw;

      var altM:MovieClip = photoList[i].mask.duplicateMovieClip(

                          "alternateMask", 997);

      alt.setMask(altM);

    }

    // Move the current photo clip

    photoList[i].photo._x = tpos;

    cpos += mw / s;

  }

};

This code takes a position offset variable, xpos, and moves all the images up or down based on the value of each strip's scale. At the end of this process, each strip is moved up or down so that the strips take up the vertical positions shown in Figure 5-6.

Finally, we set our initial position and make sure the images are moved to their correct places:

this.xpos = 0;

this.redrawStrips( );

The code so far renders a static panoramic view in Flash, but we need code to let the user rotate the view so she feels the sensation of depth and can explore the panorama. Several possible user interfaces can give the viewer the ability to scroll the panorama. Allowing the user to click and drag to scroll the image is a good choice (another option would be using buttons for left and right scrolling). In this example, we will scroll the panorama based on the mouse cursor position. If the cursor is to the left of the screen's center and the user clicks and holds the mouse down, the panorama scrolls to the left. Scrolling to the right operates similarly. The scrolling speed is controlled by how far to the left or right of the image center the mouse pointer is. The final FLA also replaces the mouse pointer with a simple arrow clip, arrow, which shows the scrolling direction.

The preceding code set our offset variable xpos before calling our redrawStrips( ) method (which controls the position of each photo under the strip, and therefore the scrolling). Since our replacement cursor, the arrow movie clip, is already in the Flash movie, we just need to add the following code to make it respond to mouse movement:

arrow.onMouseMove = function( ) {

  this.isInside = this._parent._xmouse > 0 && 

                  this._parent._xmouse < this._parent.viewWidth && 

                  this._parent._ymouse > 0 && 

                  this._parent._ymouse < this._parent.viewHeight;

  if (this.isInside) {

    // Arrow is over the pano, so show the custom  

    // arrow cursor instead of the standard mouse pointer.

    if (!this._visible) {

      Mouse.hide( );

      this._visible = true;

    }

    if (this._visible) {

      // Show the left arrow or right arrow frame depending on whether 

      // it is to the left or right, and make the custom cursor mouse.

      this.gotoAndStop((this._parent._xmouse < this._parent.viewWidth / 2) ?

               "left" : "right");

    }

  } else {

    // Arrow is not over the pano, so show the standard mouse

    // pointer instead of our custom cursor.

    if (this._visible) {

      Mouse.show( );

      this._visible = false;

    }

  }

};

arrow.onMouseDown = function( ) {

  // If mouse is down, change xpos to create the scroll effect

  // when redrawStrips( ) is called.

  this.onEnterFrame = function( ) {

    if (this.isInside) {

      this._parent.xpos -= ((this._parent.viewWidth / 2) - this._x) / 10;

      // Max moving speed.

      this._parent.redrawStrips( );

    }

  };

};

arrow.onMouseUp = function( ) {

  this.onEnterFrame = undefined;

};

While this movement code creates only a left-right scrolling panorama (as opposed to being able to look up and down as well), it's reasonably simple code that can be easily modified.

For those feeling adventurous, you might try to simulate a panorama mapped onto the inside of a sphere rather than a cylinder. This would allow the user to look up and down as well as left and right. (Hint: you can mask the image a second time with vertical strips to create the vertical warping or use a series of square masks that get larger as you get further from the center of the image.)

Final Thoughts

While Flash is no match for other panorama viewer software in quality or speed, having in-movie panoramas that can be controlled by ActionScript is a big plus. Basic panoramic rendering is just the beginning; you could add hotspots or links using ActionScript. Many Flash sites use sliding images as a substitute for panoramic viewing. This cylindrical pano-rendering hack offers a more immersive experience without requiring the user to download other plugins.

?Zeh Fernando