Bitmap Class, Part 3:
Image Replacement in Radiating Circles
Radiator.swf
In this third instalment of our coverage of Bitmap and BitmapData, we'll look at how these classes can be combined with the Drawing API, through use of the beginBitmapFill() method of the Graphics object.
The project at right presents a sequence of 30 images, using each in succession as the fill for a dynamically drawn circle that begins at the center point of the screen, and expands to fill the entire view. In cinematic terms, this effect is called an iris open, and what we have here is a montage composed of such transitions, repeated in infinite series.
I. Architecture
Once again, we have a single main movie (radiator.fla), with minimal internal assets and linkage to the document class file radiatorDocClass.as. There are 30 external JPEG files, all of identical dimensions (500x500).
(Above, the movie is displayed at 400x400. The version of the code running here also contains an extra feature, an opaque outline circle, which we've eliminated from discussion for sake of simplicity.)
As usual, all code is contained in the document class.
II. radiatorDocClass
Here's the import list:
import flash.display.MovieClip; import flash.events.*; import flash.display.Loader; import flash.net.URLRequest; import flash.display.Bitmap; import flash.display.BitmapData; import flash.display.Shape; import flash.geom.Matrix;
Since we're using the Drawing API, we include flash.display.Shape, which you'll recall we instance in order to create an object for drawing. In previous examples, we've sometimes been able to use the Drawing API directly on the main movie, without a subsidiary object. We can't use that strategy here, because we need two circles in play, and hence must use subsidiaries.
You have not seen flash.geom.Matrix before. This class is required by the beginBitmapFill() method. It can be used to rotate and otherwise modify the image material used in a fill, though we won't be doing any of that here.
Here are the variables, once again grouped for easier discussion:
public var circ_1 = new Shape(); public var circ_2 = new Shape(); public var rad_1:int = 1; public var rad_2:int = 1; public var ldr_1 = new Loader(); public var ldr_2 = new Loader(); public var ready_1:Boolean = false; public var ready_2:Boolean = false; public var theCount = 2; public var drawMode:int = 1; public var which:Shape; public var theRadius:int; public var theLoader:Loader;
There is a pair of instances for the Shape class, which as we said, will allow us to draw our two filled circles. We define variables for the radius value of each of these circles.
The next five variables manage the twin instances of Loader we'll use to bring in our external JPEGs.
The last of this set, theCount, indicates where in the image series we will begin. As you'll see, our images names all end in a number, and the series begins with s3_1.jpg; yet we begin with 2.
We do this to avoid the awkwardness of a blank screen, which might appear if we waited for our first image to load. We've imported a copy of our first image into the main movie timeline. This guarantees a visual starting point. To be sure, this makes our SWF considerably bigger than it would otherwise be, but the expense seems worthwhile.
The number 1 image will come into play eventually, but only on subsequent iterations of our display.
Now we come to the final quartet of variables, all of which are used to control or set conditions for our main loop (to which we are coming). The first, drawMode, tells us which of our two alternating circles we are drawing. The interestingly named which serves as a pointer to one of our two Shapes. Similarly, theRadius will receive the value of either rad_1 or rad_2, depending on drawMode, and theLoader will assume the identity of one of our Loaders.
Basically, these variables make it easier to use one loop structure to drive both circle drawing operations. You'll see more detail presently.
Now for our Constructor method:
function radiatorDocClass()
{
reFill(ldr_1);
reFill(ldr_2);
addEventListener(Event.ENTER_FRAME, animate);
ldr_1.contentLoaderInfo.addEventListener(Event.COMPLETE, img_1Good);
ldr_2.contentLoaderInfo.addEventListener(Event.COMPLETE, img_2Good);
}
The reFill() custom method, which we'll discuss momentarily, sets the fill graphic for each of our circles. We pass one of our Loader objects to each. Next we add an enter-frame listener with a callback to a method called animate(), and finally, we add listeners for load completion on our two Loaders.
Since these load callbacks are very simple, let's get them out of the way first. They look like this:
public function img_1Good(e:Event)
{
ready_1 = true;
}
So these callbacks just set a flag variable to true, indicating that the load-in business is complete. There is a nearly identical twin for img_1Good, as you might expect.
So we come to the first of our main methods, the enter-frame callback animate():
public function animate(e:Event)
{
if(drawMode == 1 && ready_1)
{
drawCirc(circ_1, rad_1);
rad_1 += 30;
}
else if(ready_2)
{
drawCirc(circ_2, rad_2);
rad_2 += 30;
}
}
This method is called on frame entry, or 12 times per second at the default rate (though for some reason, we're using 15 fps for this project). It checks drawMode and ready_1. If the former is 1 and the latter true, we set out to draw the first circle, calling drawCirc() and passing it two arguments, circ_1 and rad_1.
Now, passing a numerical value such as rad_1 (radius) to a method should provoke no surprise; however, this may be the first time we have passed an object -- circ_1, a Shape -- to a method. Have no fear, that's entirely legal, and a very convenient feature of ActionScript.
After we hand off to the drawing method drawCirc(), we increment the radius value by 30 pixels, so that the circle will be that much larger on the next pass.
If our initial test on drawMode and ready_1 comes up negative (and remember, both conditions must be met), we try the second branch of our if/else. Note that we've added a second if test to the else clause. This test looks at the value of ready_2 (which reflects whether or not we've loaded all the data for our second image), and allows us to proceed only if we find true. That being the case, we call our drawing method again, this time configuring it to handle the second circle.
Now it's time to look at that drawing method, drawCirc(), which is where the crucial business of our document class takes place. Because this method is rather long, we've marked it off in four parts, which we'll repeat and discuss in detail:
public function drawCirc(which, theRadius)
{
//PART 1
switch(which)
{
case circ_1:
theLoader = ldr_1;
break;
case circ_2:
theLoader = ldr_2;
break;
}
//PART 2
var matrix = new flash.geom.Matrix();
which.graphics.beginBitmapFill(Bitmap(theLoader.content)
.bitmapData, matrix, true, true);
which.graphics.drawCircle(250,250,theRadius);
which.graphics.endFill();
//PART 3
if(rad_1 > 370) // first circle has filled screen
{
rad_1 = 1;
drawMode = 2;
circ_2.graphics.clear();
reFill(ldr_1);
}
if(rad_2 > 370) // second circle has filled screen
{
rad_2 = 1;
drawMode = 1;
circ_1.graphics.clear();
reFill(ldr_2);
}
//PART 4
addChild(which);
}
The section we're calling Part 1 does a bit of important preliminary business:
switch(which)
{
case circ_1:
theLoader = ldr_1;
break;
case circ_2:
theLoader = ldr_2;
break;
}
Using a switch construction based on which, or the Shape object that has been passed to our method, we assign one of the two Loaders to our holder variable theLoader. Doing this lets us finesse a fine point of syntax in Part 2:
var matrix = new flash.geom.Matrix(); which.graphics.beginBitmapFill(Bitmap(theLoader.content).bitmapData, matrix, true, true); which.graphics.drawCircle(250,250,theRadius); which.graphics.endFill();
The heart of this section is the beginBitmapFill() method, which as we've noted, is the key to this project. We use which.graphics as a prefix for this statement; but the syntax also requires a reference to a Loader, so that we can convert it to Bitmap and extract its bitmapData property. That's why we bothered with the switch business previously. The remaining arguments to beginBitmapFill() include our Matrix object, which is basically just along for the ride in this project, and two Booleans, one controlling repeats (should our circle exceed the native size of our JPEG fill, which it doesn't), and the other controlling pixel smoothing, with which we are not very much concerned here.
With this dense bit of business out of the way, we tell the graphics property (thus the Graphics object) of whichever Shape to draw a circle, beginning at the center of the screen, and extending to the current value of theRadius, which as you'll recall was passed as a parameter when we called this method. (It will be either rad_1 or rad_2.)
Finally, we call the endFill() method. This method must be called in order to display the results of a beginBitmapFill() method; so don't forget that line.
Now we come to Part 3 of our four-part invention:
if(rad_1 > 370) // first circle has filled screen
{
rad_1 = 1;
drawMode = 2;
circ_2.graphics.clear();
reFill(ldr_1);
}
if(rad_2 > 370) // second circle has filled screen
{
rad_2 = 1;
drawMode = 1;
circ_1.graphics.clear();
reFill(ldr_2);
}
Here we have a pair of if tests, examining one or the other of the two radius variables. Each test identifies a point at which one of the circles has expanded to maximum size, filling the screen. When this happens, one circle is covering the other. This allows us to swap out the current fill on the covered circle by calling the reFill() method (to which we are coming, eventually). It also lets us reset the radius back to minimum size (1 pixel), switch over the value of drawMode, and call the clear() method on the Shape, wiping out the previously drawn image.
Again, note that we operate on circ_1 when circ_2 is at maximum expansion, and vice versa. This way, all the costume changes happen offstage, as it were; or at least where no one can see them.
The last part of the drawCirc() method is delightfully brief:
addChild(which);
It simply refreshes the screen.
Now, as promised, let's have a look at the reFill() method, which replaces the JPEG associated with each of the circles, as needed:
public function reFill(which)
{
var theReq = new URLRequest("s3_"+theCount+".jpg");
which.load(theReq);
if(which == ldr_1)
{
ready_1 = false;
ldr_1.contentLoaderInfo.addEventListener(Event.COMPLETE, img_1Good)
}
else
{
ready_2 = false;
ldr_2.contentLoaderInfo.addEventListener(Event.COMPLETE, img_2Good)
}
//increment count
theCount ++;
if(theCount == 31) theCount = 1;
}
As you can probably figure out, the first two lines of this method order up an URLRequest, using the current value of theCount to retrieve the appropriate graphic. (Note that if we hadn't named our JPEGs in a very particular way, using numbers to immediately precede the file extension, all this machinery would not work.)
Next we test on the value of which, the parameter passed to the method, containing either ldr_1 or ldr_2. We set the appropriate ready flag to false, then set up an event listener that will hand off to the appropriate image-good callback when all the data have loaded.
Finally, we increment theCount, and check to see if we've gone past the end of our 30-image range; if so, we go back to image number 1.
And so, once again, we come full circle.
III. Source files
Source files for this project, radiator.fla and radiatorDocClass.as, can be found in the directory BitmapClass/radiator, within MULTIMEDIA in the shared account on student-iat. This directory also contains the 30 JPEG graphics, in a subfolder called images.
|
|