Bitmap Class, Part 2:
Image Swapping in Sequential Blocks

SeqBlockSwap.swf

In this posting, we'll extend our use of Bitmap and BitmapData with a project that involves two images. Using the getPixels() and setPixels() methods of BitmapData, we'll copy a rectangular block from one image and replace the corresponding block in another. This is (basically) a massively scaled-up version of what happens whenver you load a new image source into a Loader object; but here we'll place the action under fine script control.

Eventually, we'll modify the project shown at right to do random block replacement. However, that trick involves some fairly complicated maneuvers, added to a basic procedure that is also not without complexity. So let's start with the simpler proposition: painting one image, blockwise, into (or over) another.

I. Architecture

Again, we've kept the architecture of this project as simple as possible: a single main movie (SeqBlockSwap.fla), with no internal assets except a linkage to the document class file SBSDocClass.as. We're using two external JPEG files, replace.jpg (from the sequential-eraser project) and image_B.jpg. Both are identical in dimensions, 400x400.

All the code action takes place in the document class, to which we turn.

II. SBSDocClass

The import list for this project is extensive:

  import flash.display.MovieClip;
  import flash.display.Bitmap;
  import flash.display.BitmapData;
  import flash.display.Loader;
  import flash.net.URLRequest;
  import flash.events.Event;
  import flash.events.TimerEvent;
  import flash.utils.Timer;
  import flash.geom.Rectangle;
  import flash.utils.ByteArray;

The first eight items on this list should be familiar. The last two may not be. The flash.geom.Rectangle class lets us work with rectangles. Don't confuse it with the drawRect() method in the Drawing API, which does rather similar things, but as a method of a class, not a distinct class in itself. The class flash.utils.byteArray gives us a way to work with a two-dimensional set of numbers. Again, don't confuse this class with the more general Array class, which is meant for more general purposes.

You'll see how both these new classes come into play as we go.

Here are the variables, separated into logical groups for easier discussion:

  public var imageA:Loader;
  public var imageB:Loader;	
  public var reqA:URLRequest = new URLRequest("replace.jpg");
  public var reqB:URLRequest = new URLRequest("img_B.jpg")
  public var reqX:URLRequest;
		
  var myBMD:BitmapData;
		
  public var lineTimer:Timer;
		
  public var Yco:int;
  public var Xco:int;

The first five variables handle image loading, and probably require little comment, except for the last one (reqX). This is a blank URLRequest variable, which we need in order to swap the values of the two initial URLRequests, reqA and reqB, at the end of each replacement cycle. As before, we've simplified the addresses of the JPEGs, as you'll see them in the downloadable source. The code given here will work if you place the image files in the same directory as your SWF.

There is a variable for a BitmapData object, since we'll be modifiying the internal structure of a Bitmap. Note that there's no Bitmap object -- in this project, we've streamlined the process by converting the Loader object into a Bitmap on the fly: we'll explain that piece of business later.

There's a Timer here, becuase we'll be using the same Timer-drive loop strategy we used in the sequential eraser.

Finally, there are variables to maintain a count for both X and Y coordinates, as you might expect in a project that works with a Cartesian grid.

Thus we come to the Constructor, which is about as simple as can be:

  function SBSDocClass()
  {
    initialize();
  }

Our Constructor immediately hands off to a custom method called initialize(). Doing this lets us re-run our setup instructions at the end of each replacement cycle. Remember, a Constructor is only executed once, when a class is instantiated; so if you want a class to recycle, or re-run itself indefinitely, you'll want to move most or all the business of the Constructor to a repeatable method.

So here's what the initializer looks like:

  public function initialize()
  {
    Yco = 0;
    Xco = 0;
	
    lineTimer = new Timer(75,100);
			
    imageA = new Loader();
    imageB = new Loader();
			
    //LOAD FIRST IMAGE AND ADD LISTENER FOR 'COMPLETE' EVENT
    imageA.contentLoaderInfo.addEventListener(Event.COMPLETE, image_A_Good);
    imageA.load(reqA);
  }

First we set our two counter variables to zero, so that action always starts in the upper left corner of the screen.

Second, we instantiate a Timer with a 75-millisecond delay and 100 iterations. Note that this Timer hasn't started to run, yet.

Next, we instantiate our two Loader classes, one for each of the images in our system. And thus we come to the somewhat complicated (but actually kind of graceful) business of image loading callbacks.

In the brave new world of ActionScript 3.0 and Loader classes, images don't just pop into memory. Actually, they didn't in the old regime of ActionScript 2.0, either; but the cruder forms of coding that were common in that day often didn't take account of this fact, leading to awkward and unpredictable behaviors.

In our project, we don't proceed to the main business of image swapping until both images are completely loaded into memory. This could entail a delay, even if you intend to load the images locally (as in our code listing, though our live example actually loads from elsewhere on our server). Remember, no one can really predict how a bit will go once it takes to the Internet.

So, we handle image loading in two stages. First we add a listener for the COMPLETE event on imageA, the first of our Loaders. The callback for this listener is the custom method image_A_Good(). With this arrangement in place, we load the image.

Thus to the first callback:

  function image_A_Good(event:Event)
  {
    addChild(imageA);
    if(numChildren > 1) removeChildAt(0);
			
    //LOAD SECOND IMAGE AND ADD LISTENER FOR 'COMPLETE' EVENT
    imageB.contentLoaderInfo.addEventListener(Event.COMPLETE, image_B_Good);
    imageB.load(reqB);
  }

As you can see, the last two lines of this method do the same thing for imageB that we earlier did for imageA, handing off to a callback by the name of image_B_Good(). So successful completion of one image load sends us into a virtual waiting room for the next.

Before we get there, however, we need to explain the first two lines of image_A_Good(). We need to use the addChild() method to place our baseline image on the screen. However, this project is designed to recycle indefinitely, so after adding the base image, we need to check to see if it's been added over a previous base image, which is indeed what happens if this is not the first iteration of our code. If we find an earlier image in memory, we erase it, using removeChildAt(0). (This method, like addChild() is executed at the root, since this is a document class.) If we don't add this precaution, our system will create an unlimited stack of children. Practically speaking, this is relatively harmless; but if you left this movie running for many hours, you could run out of memory. It's always important to clean up after yourself when using addChild().

With all this out of the way, we're ready for the next stage of preparation. The second-stage callback is much simpler:

  function image_B_Good(event:Event)
  {
    lineTimer.addEventListener(TimerEvent.TIMER, blockOut);
    lineTimer.start();
  }

Notice that we don't add imageB to the Display List. It's in memory, but it never needs to be made fully visible. We only need one square chunk of this image at a time.

Since both our graphics are ready, we can turn to our Timer class, and get it started by adding an event listener and calling the start() method.

Now we're off to the races. The blockOut() method does the major business of our project:

  function lineOut(e:TimerEvent):void
  {
    var rect:Rectangle = new Rectangle(Xco, Yco, 40,40);
    var bytes:ByteArray = Bitmap(imageB.content).bitmapData.getPixels(rect);
    bytes.position = 0;
    Bitmap(imageA.content).bitmapData.setPixels(rect, bytes);
			
    Xco +=40
    if(Xco == 400) 
    {
      Xco = 0;
      Yco +=40;
    }
    addChild(imageA);
			
    if(Yco == 400)
    {
      //Shuffle the image URLs (need three variables for this)
      reqX = reqB
      reqB = reqA;
      reqA = reqX;	
      initialize();
    }		
  }

First, we instantiate a Rectangle class. (We could have declared rect at the top of our class file, but it's harmless enough to declare it locally, and it MUST be instantiated locally.) This Rectangle defines an area (a 40x40-pixel square) which is in effect laid over both our images. We'll swap pixels that fall within this square from one image to the other.

Doing this requires an instance of the ByteArray class, which we here call bytes. We populate this ByteArray with data from imageB, using rect as a template. Since the expression is quite complicated, let's look at it again:

    var bytes:ByteArray = Bitmap(imageB.content).bitmapData.getPixels(rect);

Back when we were reviewing our variable declarations, we noted that we do not declare any variable of type Bitmap. That's because we instead use a conversion method to turn imageB.content into an instance of the Bitmap class. This transaction uses the same principle we see in our old friend MovieClip(root). This time, however, we're converting to a Bitmap, and we're operating on the content property of a Loader, imageB. Once it has loaded something, every Loader has a content property, which refers to its raw data. This property can be converted to a Bitmap.

So, once we've performed that transformation with Bitmap(imageB.content), we proceed with our dot notation, tacking on the property bitmapData. That is, once we've turned the raw data from our Loader into a Bitmap, we invoke its bitmapData property (which comes free with every Bitmap), in order to use the getPixels() method of the BitmapData object. As an argument to this method, we pass rect.

Yes, the bitmapData property refers to a BitmapData object. If this does not drive you mad, it will make you strong, happy, and free.

Now, this procdure was not what you might call simple, or indeed easy. We've had to deal with classes within classes, method upon method, and a very dense piece of dot notation. Odds are, you will not immediately grasp what this bit of code is doing. Study it at length. Write it on the backs of your hands. Make it a mantra. Maybe that will help.

Anyway, the result of this little mind-bender is a 40x40 chunk of our second, or incoming image, now held in memory by the Flash Player. The next two lines bring that chunk into play. Again, they're worth repeating:

    bytes.position = 0;
    Bitmap(imageA.content).bitmapData.setPixels(rect, bytes);

Setting the position property of our ByteArray (bytes) to zero signals that we are going to transfer our selected chunk starting from the upper left corner. Do not leave out this instruction, as it will cause your script to crash.

Finally, we use a slightly less complex version of our previous, convoluted operation to convert the content of imageA to Bitmap, so we can use the setPixels() method of BitmapData to write in the previously selected chunk. This method takes two arguments, both rect and bytes, the latter being our ByteArray.

As Neal Stephenson says, "after this, it's all a chase scene." Having swapped one 40x40 block in our base image, we increment Xco in order to move on to the next block to the right. If our X counter has reached 400, we've finished a row, and so set Xco back to zero, then increment Yco to begin the next row.

When we're done with this record-keeping, we refresh the screen.

The last bit of blockOut() requiring comment is the end-of-cycle condition, which kicks in when Yco reaches 400, indicating that we have replaced the very last block in our image. This being the case, we call initialize() to restart the action; but first, we need to swap our two image sources, or URLRequests, so that the previous A image (base) becomes the new B image (incoming), and vice versa.

To accomplish this switch, we use that blank URLRequest instance, reqX, to hold our B address. Then we copy the A address into the B address. Finally, we copy the old B address (now the X address) into the A address. A is now where B was, and B where A.

And so we begin again, ad infinitum.

III. Source files

Source files for this project, SeqBlackSwap.fla and SBSDocClass.as, can be found in the directory BitmapClass/swap, within MULTIMEDIA in the shared account on student-iat. This directory also contains the JPEG graphics.



University of Baltimore Logo

Last updated: 04/26/08 15:50:31
Copyright © 2008 School of Information Arts and Technologies