Hack 36 Panoramic Images 
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.

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]

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
File Import Import 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
|