Fun With Fractals, Actionscript 3.0
Although I’ve been programming for almost 15 years now, for some reason I was always afraid of fractals. Something about their sheer magnificence told me that I needed to be some sort of theoretical physicist just understand what was going on. The other night, however, I was checking up on the developments in the ds homebrew scene and I stumbled upon this contest for algorithm optimization. Although I don’t plan to enter the contest, it did lead me to some interesting tidbits on fractal generation. Once I got a sample of the algorithm, I decided to have a go at it using Flash and Actionscript 3.0. The image above (click it for a larger view) is what I have come up with after only a few hours.
Complete source code and tutorial after the jump…
In order to follow this tutorial you will need Flash CS3 or a suitable environment to compile AS 3.0 scripts. It wouldn’t be too hard to get this to work on Flash 8 with just a few changes to the syntax. Now open up the IDE and create a blank new document. On the first frame of this document you can begin copy and pasting the following source code.
Lets start by creating a pixel screen buffer the size of our document. In Flash this is achieved using the Bitmap and BitmapData objects. This will allow us to set each pixel on the stage to any color we choose, although there are many other cool things you can do with Bitmap data which I will address in later tutorials.
var imageWidth:int = stage.stageWidth;
var imageHeight:int = stage.stageHeight;
// create screen buffer and container objects
var mandelbrotBitmap:BitmapData = new BitmapData(imageWidth, imageHeight, false, 0xFF666666);
var mandelbrotContainer:Bitmap = new Bitmap(mandelbrotBitmap);
// attach to screen
this.addChild(mandelbrotContainer);
This is a good time to test the movie. If all goes well you should see a light gray screen. Not very exciting, but its always good to compile after any small change while you are laying down the foundation. Otherwise, when something goes wrong, you could spend hours trying to remember when it was the code got borked. Important to note is that we are disabling the alpha channel on the BitmapData and setting the color to a dark gray with an opaque alpha channel as dictated by the 3rd and 4th parameters of the BitmapData constructor as shown in the excerpt below:
I don’t know how many times I’ve been working with bitmaps in Flash and gotten frustrated when nothing appeared on screen. In many cases I spent 10-15 minutes looking through all my code only to realize there was nothing wrong with my drawing routines and that I simply was looking at a completely transparent image. Even if you need alpha support, always start coding without the alpha channel until you can see something solid on the screen. That way it is ruled out as an issue.
In the interest of full disclosure, the source below is not totally original code. It’s basically an Actionscript 3.0 port of the C++ code found in this tutorial. I have, of course, built upon that code foundation and added some Flash-specific color functionality. This is meant simply as a primer for beginner and intermediate flash programmers to see the power of procedural graphics generation using Flash and Actionscript 3.0. It is not intended to explain the mathematics behind the Mandelbrot set. I want to keep these little code snippets as brief as possible, for reasons of clarity and accessibility, so I won’t be getting into complex number theory or anything like that.
Next lets add the static variables for the mandelbrot draw function. These values seed the rendering, so in order to tweak the output you may want to edit any of the first 4 constants. Copy and paste this below the previous code:
var maxIterations:int = 80;
var minReal:Number = 0.45;
var maxReal:Number = 0.47;
var minImaginary:Number = -0.40;
var maxImaginary:Number = minImaginary+(maxReal-minReal)*imageHeight/imageWidth;
var realFactor:Number = (maxReal-minReal)/(imageWidth-1);
var imaginaryFactor:Number = (maxImaginary-minImaginary)/(imageHeight-1);
Like I mentioned before, if you wish to understand how these numbers are derived I suggest you read the original tutorial that I followed. To sum it up simply, the Mandelbrot set emerges through graphing a set of complex numbers (with real and imaginary components) onto a grid of 2D pixels. The values for ‘minReal’ and ‘maxReal’, for example, dictate the scope of the real component of the graph. You may have noticed that I have changed the variable names from the original c++ code to be a bit more descriptive. When I code, I *always* use variable names that make sense. You may think you are saving time by using esoteric short-hand variable names, but its not worth it. Any decrease in compilation time will be marginal, at best, and the code will soon become a mess of cryptic symbols and magic numbers. I am not some stickler for code standards, this is simply for my own benefit. I don’t remember how many old projects of mine I’ve dug up only to stare at the code and be like ” WTF was I thinking!!!”. Don’t let that happen to you. Anyway, onto the real meat and potatoes of this bad-boy.
function drawMandelbrot() {
for (var py:int=0; py<imageHeight; ++py) {
var c_imaginary:Number = maxImaginary - py*imaginaryFactor;
for (var px:int=0; px<imageWidth; ++px) {
var c_real:Number = minReal + px*realFactor;
var Z_real:Number = c_real, Z_imaginary = c_imaginary;
var isInside:Boolean = true;
for (var n:int=0; n<maxIterations; ++n) {
var Z_real2:Number = Z_real*Z_real;
var Z_imaginary2:Number = Z_imaginary*Z_imaginary;
if (Z_real2 + Z_imaginary2 > 4) {
isInside = false;
break;
}
Z_imaginary = 2*Z_real*Z_imaginary + c_imaginary;
Z_real = Z_real2 - Z_imaginary2 + c_real;
}
if (isInside) {
mandelbrotBitmap.setPixel(px, py, 0x000000);
}
}
}
}
This is the main draw routine for this example. What it does is step through each pixel of the screen (noted by ‘px’ and ‘py’) and evaluate whether this pixel lies within the Mandelbrot set or not. To do this it iterates over the equation until it resolves or the ‘maxIterations’ is reached. If it does find that the pixel is within the set, it sets that pixels color to pure black. At this point you should call the draw function by adding this line below:
drawMandelbrot();
Now test the movie at this point, you should see a gray screen with a few specs of black. These little speckles are the Mandelbrot set. In order to see all those trippy colors it is necessary to get a little more information. We don’t only want to know where the set is, but also the proximity of the the neighboring pixels. The proximity is determined by how many times the equation iterates before it terminates. We use this termination value to seed the color. You can do this by changing the last line in the function to also draw for pixels not in the set. This is shown in the updated ‘drawMandelbrot’ function:
function drawMandelbrot() {
for (var py:int=0; py<imageHeight; ++py) {
var c_imaginary:Number = maxImaginary - py*imaginaryFactor;
for (var px:int=0; px<imageWidth; ++px) {
var c_real:Number = minReal + px*realFactor;
var Z_real:Number = c_real, Z_imaginary = c_imaginary;
var isInside:Boolean = true;
var colorScalar:uint = 0;
for (var n:int=0; n<maxIterations; ++n) {
var Z_real2:Number = Z_real*Z_real;
var Z_imaginary2:Number = Z_imaginary*Z_imaginary;
if (Z_real2 + Z_imaginary2 > 4) {
isInside = false;
colorScalar = n;
break;
}
Z_imaginary = 2*Z_real*Z_imaginary + c_imaginary;
Z_real = Z_real2 - Z_imaginary2 + c_real;
}
if (isInside) {
mandelbrotBitmap.setPixel(px, py, 0x000000);
} else {
mandelbrotBitmap.setPixel(px, py, findColor(colorScalar));
}
}
}
}
Replace the existing ‘drawMandelbrot’ function with the one above. Before you compile, you should notice the addition of a new function called ‘findColor’. This is where we calculate the desired color ramp based on the value of the ‘colorScalar’. Please add that function directly below:
function findColor(colorScalar:uint):uint {
var colorRange:int;
var r:uint;
var g:uint;
var b:uint;
var center:Number = maxIterations/2-1;
if (colorScalar < center) {
colorRange = int(255*(colorScalar/center));
r = colorRange;
g = 0x00;
b = 0x00;
} else {
colorRange = int(255*((colorScalar-center)/center));
r = 0xFF;
g = colorRange;
b = 0x00;
}
return r << 16 | g << 8 | b;
}
The ‘findColor’ function takes an unsigned integer value as a parameter and outputs a color as a unsigned integer. The ‘color’Scalar’ variable will be between zero and ‘maxIterations’. If it is at ‘maxIterations’ that usually means it is within the set (and thus black). We are interested in how the color ramps down as the value decreases. The ‘center’ refers to the cutoff between the two different color palettes we will use. We check to see which side of the divide its on, then adjust one color channel at a time. In the first condition we adjust only the red component (leaving other channels black) so we get a nice gradient from black to red. We then bitshift those individual values to get a proper hex color to return.
At this point you should compile and get a better picture of what fractals are all about. Keep in mind that setting the ‘maxIterations’ to a higher value will give smoother results. I used “160″ for the included image. Also try changing the values of ‘minReal’, ‘maxReal’ and ‘minImaginary’ to find different interesting locations. Default values of “-2.0″, “1.0″, and “-1.2″ respectively will give you a good start for tweaking.
You can easily add other colors or mix the color channels in a different fashion. I found this configuration to look pretty good, and as my first experience with fractal programming, I am throughly impressed. I don’t claim to fully understand the algorithm, or the genius of BenoĆ®t Mandelbrot, but I understand enough to know it is a powerful tool for computer graphics.
Listed below is the complete source code for you lazy n00bs:
* Mandelbrot Set *
* www.cybereality.com *
* AS 3.0 port of c++ tutorial found here: *
* http://warp.povusers.org/Mandelbrot/ *
* * * * * * * * * * * * * * * * * * * * * */
//
// stage vars
var imageWidth:int = stage.stageWidth;
var imageHeight:int = stage.stageHeight;
// create screen buffer and container objects
var mandelbrotBitmap:BitmapData = new BitmapData(imageWidth, imageHeight, false, 0xFF666666);
var mandelbrotContainer:Bitmap = new Bitmap(mandelbrotBitmap);
// attach to screen
this.addChild(mandelbrotContainer);
// mandelbrot vars
var maxIterations:int = 160;
var minReal:Number = 0.45;
var maxReal:Number = 0.47;
var minImaginary:Number = -0.40;
var maxImaginary:Number = minImaginary+(maxReal-minReal)*imageHeight/imageWidth;
var realFactor:Number = (maxReal-minReal)/(imageWidth-1);
var imaginaryFactor:Number = (maxImaginary-minImaginary)/(imageHeight-1);
// calculate each pixel
function drawMandelbrot() {
for (var py:int=0; py<imageHeight; ++py) {
var c_imaginary:Number = maxImaginary - py*imaginaryFactor;
for (var px:int=0; px<imageWidth; ++px) {
var c_real:Number = minReal + px*realFactor;
var Z_real:Number = c_real, Z_imaginary = c_imaginary;
var isInside:Boolean = true;
var colorScalar:uint = 0;
for (var n:int=0; n<maxIterations; ++n) {
var Z_real2:Number = Z_real*Z_real;
var Z_imaginary2:Number = Z_imaginary*Z_imaginary;
if (Z_real2 + Z_imaginary2 > 4) {
isInside = false;
colorScalar = n;
break;
}
Z_imaginary = 2*Z_real*Z_imaginary + c_imaginary;
Z_real = Z_real2 - Z_imaginary2 + c_real;
}
if (isInside) {
mandelbrotBitmap.setPixel(px, py, 0x000000);
} else {
mandelbrotBitmap.setPixel(px, py, findColor(colorScalar));
}
}
}
}
// outputs a color ramp
function findColor(colorScalar:uint):uint {
var colorRange:int;
var r:uint;
var g:uint;
var b:uint;
var center:Number = maxIterations/2-1;
if (colorScalar < center) {
colorRange = int(255*(colorScalar/center));
r = colorRange;
g = 0x00;
b = 0x00;
} else {
colorRange = int(255*((colorScalar-center)/center));
r = 0xFF;
g = colorRange;
b = 0x00;
}
return r << 16 | g << 8 | b;
}
// draw the set
drawMandelbrot();
What is so amazing about the Mandelbrot set, or recursive algorithms in general, is how relatively short the code is. The full example is under 75 lines of code (including comments) and could be optimized further. The number of unique images you can produce with variants of this algorithm is staggering for such a compact script. I imagine that all life and nature is coded in a similar respect. One can’t help but see a striking similarity to the imagery found in nature. So thats it right there, folks. The universe is really just a bunch of imaginary numbers! QFT.
I plan on adding some sort of navigation functionality in order to zoom in and out or pan around the image. Currently the only way to alter the rendering is to manually change the constants and recompile. It would be nice to have a gui to handle this as well as picking the color palette to use. That may be saved for a later tutorial or you can code that in yourself as an exercise.
I hope this has been a helpful little intro into fractals in Flash. Surely there are those of you who are familiar with fractal programming, and this may have seemed a bit light on theory. But for many people just learning Flash and Actionscript 3.0 I’m hoping this was a good introduction into the field. If you do end up using this code, please post in the comments and let me know how it worked out for you. Stay tuned for more Flash wizardry to come.
// cybereality
Discussion Area - Leave a Comment