|
Flash CS4 Resources |
Example: Animated spinning moonThe Animated spinning moon example demonstrates techniques for working with Bitmap objects and bitmap image data (BitmapData objects). The example creates an animation of a spinning, spherical moon using a flat image of the moon’s surface as the raw image data. The following techniques are demonstrated:
To get the application files for this sample, see www.adobe.com/go/learn_programmingAS3samples_flash. The Animated spinning moon application files can be found in the Samples/SpinningMoon folder. The application consists of the following files:
Loading an external image as bitmap dataThe first main task this sample performs is loading an external image file, which is a photograph of the moon’s surface. The loading operation is handled by two methods in the MoonSphere class: the MoonSphere() constructor, where the loading process is initiated, and the imageLoadComplete() method, which is called when the external image is completely loaded. Loading an external image is similar to loading an external SWF; both use an instance of the flash.display.Loader class to perform the loading operation. The actual code in the MoonSphere() method that starts loading the image is as follows: var imageLoader:Loader = new Loader();
imageLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, imageLoadComplete);
imageLoader.load(new URLRequest("moonMap.png"));
The first line declares the Loader instance named imageLoader. The third line actually starts the loading process by calling the Loader object’s load() method, passing a URLRequest instance representing the URL of the image to load. The second line sets up the event listener that will be triggered when the image has completely loaded. Notice that the addEventListener() method is not called on the Loader instance itself; instead, it’s called on the Loader object’s contentLoaderInfo property. The Loader instance itself doesn’t dispatch events relating to the content being loaded. Its contentLoaderInfo property, however, contains a reference to the LoaderInfo object that’s associated with the content being loaded into the Loader object (the external image in this case). That LoaderInfo object does provide several events relating to the progress and completion of loading the external content, including the complete event (Event.COMPLETE) that will trigger a call to the imageLoadComplete() method when the image has completely loaded. While starting the external image loading is an important part of the process, it’s equally important to know what to do when it finishes loading. As shown in the code above, the imageLoadComplete() function is called when the image is loaded. That function does several things with the loaded image data, described in subsequent sections. However, to use the image data, it needs to access that data. When a Loader object is used to load an external image, the loaded image becomes a Bitmap instance, which is attached as a child display object of the Loader object. In this case, the Loader instance is available to the event listener method as part of the event object that’s passed to the method as a parameter. The first lines of the imageLoadComplete() method are as follows: private function imageLoadComplete(event:Event):void
{
textureMap = event.target.content.bitmapData;
...
}
Notice that the event object parameter is named event, and it’s an instance of the Event class. Every instance of the Event class has a target property, which refers to the object triggering the event (in this case, the LoaderInfo instance on which the addEventListener() method was called, as described previously). The LoaderInfo object, in turn, has a content property that (once the loading process is complete) contains the Bitmap instance with the loaded bitmap image. If you want to display the image directly on the screen, you can attach this Bitmap instance (event.target.content) to a display object container. (You could also attach the Loader object to a display object container). However, in this sample, the loaded content is used as a source of raw image data rather than being displayed on the screen. Consequently, the first line of the imageLoadComplete() method reads the bitmapData property of the loaded Bitmap instance (event.target.content.bitmapData) and stores it in the instance variable named textureMap, which, as described in the following section, is used as a source of the image data to create the animation of the rotating moon. Creating animation by copying pixelsA basic definition of animation is the illusion of motion, or change, created by changing an image over time. In this sample, the goal is to create the illusion of a spherical moon rotating around its vertical axis. However, for the purposes of the animation, you can ignore the spherical distortion aspect of the sample. Consider the actual image that’s loaded and used as the source of the moon image data: As you can see, the image is not one or several spheres; it’s a rectangular photograph of the surface of the moon. Because the photo was taken exactly at the moon’s equator, the parts of the image that are closer to the top and bottom of the image are stretched and distorted. To remove the distortion from the image and make it appear spherical, we will use a displacement map filter, as described later. However, because this source image is a rectangle, to create the illusion that the sphere is rotating, the code simply needs to slide the moon surface photo horizontally, as described in the following paragraphs. Notice that the image actually contains two copies of the moon surface photograph next to each other. This image is the source image from which image data is copied repeatedly to create the appearance of motion. By having two copies of the image next to each other, a continuous, uninterrupted scrolling effect can more easily be created. Let’s walk through the process of the animation step-by-step to see how this works. The process actually involves two separate ActionScript objects. First, there is the loaded source image, which in the code is represented by the BitmapData instance named textureMap. As described previously, textureMap is populated with image data as soon as the external image loads, using this code: textureMap = event.target.content.bitmapData; The content of textureMap is the image shown previously. In addition, to create the animated rotation, the sample uses a Bitmap instance named sphere, which is the actual display object that shows the moon image onscreen. Like textureMap, the sphere object is created and populated with its initial image data in the imageLoadComplete() method, using the following code: sphere = new Bitmap();
sphere.bitmapData = new BitmapData(textureMap.width / 2, textureMap.height);
sphere.bitmapData.copyPixels(textureMap,
new Rectangle(0, 0, sphere.width, sphere.height),
new Point(0, 0));
As the code shows, sphere is instantiated. Its bitmapData property (the raw image data that is displayed by sphere) is created with the same height and half the width of textureMap. In other words, the content of sphere will be the size of one moon photo (since the textureMap image contains two moon photos side-by-side). Next the bitmapData property is filled with image data using its copyPixels() method. The parameters in the copyPixels() method call indicate several things:
Represented visually, the code copies the pixels from textureMap outlined in the following image and pastes them onto sphere. In other words, the BitmapData content of sphere is the portion of textureMap highlighted here: Remember, however, that this is just the initial state of sphere—the first image content that’s copied onto sphere. With the source image loaded and sphere created, the final task performed by the imageLoadComplete() method is to set up the animation. The animation is driven by a Timer instance named rotationTimer, which is created and started by the following code: var rotationTimer:Timer = new Timer(15); rotationTimer.addEventListener(TimerEvent.TIMER, rotateMoon); rotationTimer.start(); The code first creates the Timer instance named rotationTimer; the parameter passed to the Timer() constructor indicates that rotationTimer should trigger its timer event every 15 milliseconds. Next, the addEventListener() method is called, specifying that when the timer event (TimerEvent.TIMER) occurs, the method rotateMoon() is called. Finally, the timer is actually started by calling its start() method. Because of the way rotationTimer is defined, approximately every 15 milliseconds Flash Player calls the rotateMoon() method in the MoonSphere class, which is where the animation of the moon happens. The source code of the rotateMoon() method is as follows: private function rotateMoon(event:TimerEvent):void
{
sourceX += 1;
if (sourceX > textureMap.width / 2)
{
sourceX = 0;
}
sphere.bitmapData.copyPixels(textureMap,
new Rectangle(sourceX, 0, sphere.width, sphere.height),
new Point(0, 0));
event.updateAfterEvent();
}
The code does three things:
Remember that this code is called repeatedly, every 15 milliseconds. As the source rectangle’s location is continuously shifted, and the pixels are copied onto sphere, the appearance on the screen is that the moon photo image represented by sphere continuously slides. In other words, the moon appears to rotate continuously. Creating the spherical appearanceThe moon, of course, is a sphere and not a rectangle. Consequently, the sample needs to take the rectangular moon surface photo, as it continuously animates, and convert it into a sphere. This involves two separate steps: a mask is used to hide all the content except for a circular region of the moon surface photo, and a displacement map filter is used to distort the appearance of the moon photo to make it look three-dimensional. First, a circle-shaped mask is used to hide all the content of the MoonSphere object except for the sphere created by the filter. The following code creates the mask as a Shape instance and applies it as the mask of the MoonSphere instance: moonMask = new Shape(); moonMask.graphics.beginFill(0); moonMask.graphics.drawCircle(0, 0, radius); this.addChild(moonMask); this.mask = moonMask; Note that since MoonSphere is a display object (it is based on the Sprite class), the mask can be applied directly to the MoonSphere instance using its inherited mask property. ![]() Simply hiding parts of the photo using a circle-shaped mask isn’t enough to create a realistic-looking rotating-sphere effect. Because of the way the photo of the moon’s surface was taken, its dimensions aren’t proportional; the portions of the image that are more toward the top or bottom of the image are more distorted and stretched compared to the portions in the equator. To distort the appearance of the moon photo to make it look three-dimensional, we’ll use a displacement map filter. A displacement map filter is a type of filter that is used to distort an image. In this case, the moon photo will be “distorted” to make it look more realistic, by squeezing the top and bottom of the image horizontally, while leaving the middle unchanged. Assuming the filter operates on a square-shaped portion of the photo, squeezing the top and bottom but not the middle will turn the square into a circle. A side effect of animating this distorted image is that the middle of the image seems to move farther in actual pixel distance than the areas close to the top and bottom, which creates the illusion that the circle is actually a three-dimensional object (a sphere). The following code is used to create the displacement map filter, named displaceFilter: var displaceFilter:DisplacementMapFilter;
displaceFilter = new DisplacementMapFilter(fisheyeLens,
new Point(radius, 0),
BitmapDataChannel.RED,
BitmapDataChannel.GREEN,
radius, 0);
The first parameter, fisheyeLens, is known as the map image; in this case it is a BitmapData object that is created programmatically. The creation of that image is described below in the section Creating a bitmap image by setting pixel values. The other parameters describe the position in the filtered image at which the filter should be applied, which color channels will be used to control the displacement effect, and to what extent they will affect the displacement. Once the displacement map filter is created, it is applied to sphere, still within the imageLoadComplete() method: sphere.filters = [displaceFilter]; The final image, with mask and displacement map filter applied, looks like this: ![]() With every cycle of the rotating moon animation, the BitmapData content of sphere is overwritten by a new snapshot of the source image data. However, the filter does not need to be re-applied each time. This is because the filter is applied to the Bitmap instance (the display object) rather than to the bitmap data (the raw pixel information). Remember, the Bitmap instance is not the actual bitmap data; it is a display object that displays the bitmap data on the screen. To use an analogy, a Bitmap instance is like the slide projector that is used to display photographic slides on a screen, and a BitmapData object is like the actual photographic slide that can be presented through a slide projecter. A filter can be applied directly to a BitmapData object, which would be comparable to drawing directly onto a photographic slide to alter the image. A filter can also be applied to any display object, including a Bitmap instance; this would be like placing a filter in front of the slide projector’s lens to distort the output shown on the screen (without altering the original slide at all). Because the raw bitmap data is accessible through a Bitmap instance’s bitmapData property, the filter could have been applied directly to the raw bitmap data. However, in this case, it makes sense to apply the filter to the Bitmap display object rather than to the bitmap data. For detailed information about using the displacement map filter in ActionScript, see Filtering display objects. Creating a bitmap image by setting pixel valuesOne important aspect of a displacement map filter is that it actually involves two images. One image, the source image, is the image that is actually altered by the filter. In this sample, the source image is the Bitmap instance named sphere. The other image used by the filter is known as the map image. The map image is not actually displayed on the screen. Instead, the color of each of its pixels is used as an input to the displacement function—the color of the pixel at a certain x, y coordinate in the map image determines how much displacement (physical shift in position) is applied to the pixel at that x, y coordinate in the source image. Consequently, to use the displacement map filter to create a sphere effect, the sample needs the appropriate map image—one that has a gray background and a circle that’s filled with a gradient of a single color (red) going horizontally from dark to light, as shown here: ![]() Because only one map image and filter are used in this sample, the map image is only created once, in the imageLoadComplete() method (in other words, when the external image finishes loading). The map image, named fisheyeLens, is created by calling the MoonSphere class’s createFisheyeMap() method: var fisheyeLens:BitmapData = createFisheyeMap(radius); Inside the createFisheyeMap() method, the map image is actually drawn one pixel at a time using the BitmapData class’s setPixel() method. The complete code for the createFisheyeMap() method is listed here, followed by a step-by-step discussion of how it works: private function createFisheyeMap(radius:int):BitmapData
{
var diameter:int = 2 * radius;
var result:BitmapData = new BitmapData(diameter,
diameter,
false,
0x808080);
// Loop through the pixels in the image one by one
for (var i:int = 0; i < diameter; i++)
{
for (var j:int = 0; j < diameter; j++)
{
// Calculate the x and y distances of this pixel from
// the center of the circle (as a percentage of the radius).
var pctX:Number = (i - radius) / radius;
var pctY:Number = (j - radius) / radius;
// Calculate the linear distance of this pixel from
// the center of the circle (as a percentage of the radius).
var pctDistance:Number = Math.sqrt(pctX * pctX + pctY * pctY);
// If the current pixel is inside the circle,
// set its color.
if (pctDistance < 1)
{
// Calculate the appropriate color depending on the
// distance of this pixel from the center of the circle.
var red:int;
var green:int;
var blue:int;
var rgb:uint;
red = 128 * (1 + 0.75 * pctX * pctX * pctX / (1 - pctY * pctY));
green = 0;
blue = 0;
rgb = (red << 16 | green << 8 | blue);
// Set the pixel to the calculated color.
result.setPixel(i, j, rgb);
}
}
}
return result;
}
First, when the method is called it receives a parameter, radius, indicating the radius of the circle-shaped image to create. Next, the code creates the BitmapData object on which the circle will be drawn. That object, named result, is eventually passed back as the return value of the method. As shown in the following code snippet, the result BitmapData instance is created with a width and height as big as the diameter of the circle, without transparency (false for the third parameter), and pre-filled with the color 0x808080 (middle gray): var result:BitmapData = new BitmapData(diameter,
diameter,
false,
0x808080);
Next, the code uses two loops to iterate over each pixel of the image. The outer loop goes through each column of the image from left to right (using the variable i to represent the horizontal position of the pixel currently being manipulated), while the inner loop goes through each pixel of the current column from top to bottom (with the variable j representing the vertical position of the current pixel). The code for the loops (with the inner loop’s contents omitted) is shown here: for (var i:int = 0; i < diameter; i++)
{
for (var j:int = 0; j < diameter; j++)
{
...
}
}
As the loops cycle through the pixels one by one, at each pixel a value (the color value of that pixel in the map image) is calculated. This process involves four steps:
|