April 1999

Put a plasma in your form

by Andrea Fasano

With the addition of some new features to BCB3, it's now possible to develop fast, graphic-based applications. In this article, you'll learn how to take advantage of these features, and customize your form with special animated effects. To do so, we'll examine the properties of a TBitmap object, and we'll produce the code that utilizes a simple but amazing graphic effect, the plasma.

Fooling the eyes

Our goal is to successfully trick the eyes into seeing movement. When the application starts, it flashes graphics on the screen at a very rapid rate. Each time, the graphics change just a little, so that the user receives the sensation of motion. The concept is similar to that of watching cartoons. You see a stream of static frames, but because they're flashed so fast on the screen, you see animation. The average film displays approximately 24 frames per second (FPS). So, if your application has a good frame rate, you'll be sure to have smooth animation.

How it works

The way to reach our objective is straightforward: we need a method that calculates and draws some graphics in a back buffer. When the buffer is filled, we'll copy it into the form's background. We'll repeat these operations to achieve a high FPS and, subsequently, good animation. Therefore, you can understand how crucial a factor velocity is, and, how much attention we must pay to speeding up the performance of the application.

Using a back buffer is the first step to optimization; in fact, it's faster to draw all the graphics in a buffer and copy them to the monitor, instead of directly accessing each pixel on the screen. Also, the use of a back buffer avoids the ugly flickering effect (called double-buffering).

Finally, we need only three things to draw a special effect in the form:
bullet A back buffer (to be more precise, a TBitmap object)
bullet A timer, used to invoke the drawing routine
bulletThe code that produces some cool effect

Step 1: Inserting a back buffer

Inserting a back buffer into the application is easy. After you've started a new application, select the form and change its Name property to PlasmaForm. Now, switch to the header file (by pressing [Ctrl][F6] in the Code Editor) and insert this line of code in the public section of the form class:
...
public:
Graphics::TBitmap *backbuffer;
...
We must also allocate some memory, fill some fields for the back buffer when the application starts, and de-allocate the memory used when the program dies. So, switch to the Object Inspector and double-click OnCreate on the Events palette. Then, insert the following code:
void __fastcall 
	TPlasmaForm::FormCreate(TObject *Sender)
{
	backbuffer = new Graphics::TBitmap;
	backbuffer->Height = ClientHeight;
	backbuffer->Width = ClientWidth;
	backbuffer->PixelFormat = pf32bit;
	backbuffer->IgnorePalette = true;
}
The first three lines of code initialize the back buffer and set its size equal to the form's client area. Then we set the PixelFormat property equal to pf32bit. In this way each pixel of the back buffer is formed by four bytes (one byte for the red component, one for the green, and one for the blue; the last one is reserved). We use 32 bits per pixel because the buffer benefits by the maximum number of colors allowed (about 16 million), and because the CPU manages faster 32 bits per pixel instead of 24 bits per pixel (given by the constant pf24bit).

Finally, the bitmap's property, IgnorePalette, is set to true to achieve a faster drawing. The only drawback is a lower picture quality on 256-color video modes. Now we need to de-allocate the memory used by the back buffer when the application ends. So, come back to the Object Inspector and double-click the OnDestroy event. In the method that appears inside the Code Editor, insert the instruction:

void __fastcall 
	TPlasmaForm::FormDestroy(TObject 
	*Sender)
{
	delete backbuffer;
}
And that's all about the back buffer. Now it's ready to be used.

Step 2: Using a timer

As I told you, our application will take advantage of a timer to execute the code that draws the back buffer and copies it inside the form. Of course, there are other ways to perform these actions; for instance, we could use the threads, but they're a lot slower, and a timer is generally simpler to use.

Take a look at the Component Palette and locate the System tab. Click on the watch icon (the first on the left) and place it inside the form. Since it isn't a visual component, you can put it anywhere. Press [F11] and change the Name to PlasmaTimer. Next, insert the value 40 inside the Interval property. This last property determines how much time (in milliseconds) must pass before the OnTimer event occurs. For example, when the timer starts, it waits 40 milliseconds before raising the OnTimer event. Then, the timer waits another 40 milliseconds (ms) and raises the event for the second time and so on, until it gets stopped or the application ends. Setting 40 ms for the Interval property means that in one second, the OnTimer event is raised about 25 times (1000 / 40 = 25).

Next comes the most important part. In the Object Inspector, click the OnTimer event, insert the string DrawForm, and press [Enter]. The Code Editor will appear showing this method:

void __fastcall 
	TPlasmaForm::DrawForm(TObject *Sender)
{

}
So, we'll put the code that implements the plasma effect inside this method! In fact, DrawForm will be called 25 times per second, and that's what we want to do initially.

Now, I want to fix a concept, going a little further: at this point in the article, we've just developed a template application. It doesn't matter if you add the code for the plasma effect, to the DrawForm method, or update a label, it only matters that this code is fast enough. If it isn't, the application will suffer a loss in performance.

Okay, but what kind of actions do we have to develop inside the DrawForm? Essentially, the answer is three specific actions:

  1. Accommodate the back buffer dimensions (if the user resizes the form at execution time).
  2. Draw the plasma inside the buffer.
  3. Copy it in the form's background.
Next, we'll analyze these three topics.

Step 3: The tricky section

The plasma is a well-known but ever-beautiful effect. It works in a very simple way. First, we must calculate some values using a nice math function--like sin() or cosin()--and put them in a table. For convenience, we'll call it PlasmaTable (note that this operation is done only one time--at the start of the application). After that, we scan the entire bitmap and we fill each pixel with the values previously stored in PlasmaTable.

Now, the first tip is to animate the plasma, so we access the table using some indexes, and during the scan of the buffer we increment (or decrement) these indexes.

Also, each time the routine is called, we change the starting point of every index by storing the starting point in a position variable (so, each index has its own position variable). Each position variable is incremented (or decremented) every time the routine is called.

If you're a little confused, don't worry. Read this section again carefully and take a look at Listing A. Now, focus your attention on the table; it must be declared somewhere. Of course, the best place is the public section of the TPlasmaForm class:

...
Byte PlasmaTable[256];
...
This table's type is the unsigned char or byte, because the values stored will be used as color intensities. Since we're dealing with RGB colors, and since each component is big as one byte, we have no choice about the table's type. But why does the array hold only 256 elements? That's another little trick. To make the things faster, if you use unsigned char variables as indexes, in order to access the PlasmaTables' values, you don't need to check for the upper and lower bounds of the array. In fact, the increment and the decrement of unsigned variables produces a wrapping around the maximum (or the minimum) value that a variable could hold.

Next, we must discuss the table initialization. Since this step must be done only at the start of the application, a right place is the method FormCreate. To fill the table, we can act in a manner like this one:

for (int x=0; x<256; x++)
    PlasmaTable[x] = 30. * (1. + sin(x * 2. * 3.1415 / 256.));
You can easily try to modify these values to obtain a different effect, or you can try to use other functions instead of the sin().

Step 4: Drawing, copying, and resizing the back buffer

Now that we understand the inner mechanisms of the plasma, we're finally ready to fill the back buffer. First of all, we'll resize the buffer:
buffer->Height = ClientHeight;
buffer->Width = ClientWidth;
Next, we need to find a way to write a single pixel. Thanks to a new TBitmap's feature, we can now access every row of the buffer. The property is called ScanLine, and it returns a void pointer to a given row of the bitmap. Since we know the pixel format used internally by the buffer, we can make a safe type casting. For instance, if you want to plot a white pixel at the coordinates (15, 34), you must produce this code:
unsigned int *LinePtr;
LinePtr = (unsigned int *) backbuffer->ScanLine[34];
LinePtr[15] = 0x00ffffff;
In our case, we use an unsigned int pointer because the back buffer has 32 bits per pixel. Casting the pointer returned by ScanLine to a different type doesn't ensure the right access to the desired pixel. Also, remember that in this pixel format, the first byte represents the blue component, the second byte the green component, while the third one represents the red component; the last byte is unused.

If you look at Listing A, you can see that the final color for each pixel is stored inside PlasmaColor, and is calculated by adding four different values of the table. You can notice the use of a switch statement, which reflects the user's choice during the execution of the application; you can decide the plasma's color by simply clicking a radio group. I put this component inside the form only to clarify the different ways to fill a pixel.

Okay, after we finished filling the buffer, we must copy it inside the form. The simplest (and most efficient) way is given by the use of the Draw method. You only need to specify the destination coordinates and what you want to copy

Canvas->Draw (0, 0, backbuffer);
That's all! Try to compile the source code and watch what happens. Here's one last trick. To speed up the execution of a method, you should avoid the use of many (and large) local variables. In particular, declare all variables as static: each time the method is called, it won't lose precious time in allocating memory for the local variables. At last, our method performs very few and simple tasks: it resizes the back buffer, scans the entire bitmap, fills each pixel with some color, and copies the buffer to the screen.

Conclusion

In this article, we showed you how to develop a simple graphic effect, and some tricks to improve the application's performance. You can use this knowledge to customize your form (or whatever has a Canvas!) in order to produce, for example, cool splash screens. It's a great exercise to modify the source code, and to draw new and astounding graphics. So, good luck!

Listing A: Code for achieving smooth animation

//---------------------------------------------------------------------------
 void __fastcall TPlasmaForm::DrawForm(TObject *Sender)
{
 /*** These variables holds the plasma direction and aspect. ***/
 static Byte PlasmaDir1,
                      PlasmaDir2,
                      PlasmaDir3,
                      PlasmaDir4;
 /*** These variables holds the old values of the plasma position. ***/
 static Byte PlasmaPos1 = 0,
                      PlasmaPos2 = 0,
                      PlasmaPos3 = 0,
                      PlasmaPos4 = 0;
 static unsigned int  PlasmaColor,   /*** Final plasma color ***/
                      *LinePtr;      /*** Pointer to a buffer's row of 
					  	pixels ***/
 static int           x,
                      y;

 /*** If the user resizes the windows, we must accomodate the size of the
      back buffer. ***/
 backbuffer->Height = ClientHeight;
 backbuffer->Width = ClientWidth;

 PlasmaDir1 = PlasmaPos1;
 PlasmaDir2 = PlasmaPos2;

 /*** We scan the entire bitmap, row by row ***/
 for (y=0; y<backbuffer->Height; y++)
    {
      /*** We now obtain a pointer to the start of the current row ***/
      LinePtr = (unsigned int *) backbuffer->ScanLine[y];

      PlasmaDir3 = PlasmaPos3;
      PlasmaDir4 = PlasmaPos4;

      for(x=0; x<backbuffer->Width; x++)
      {
        /*** Using the Plasma Table, we obtain the color for the given
             pixel of the row. Try to change this line of code. ***/
        PlasmaColor = PlasmaTable[PlasmaDir1] + PlasmaTable[PlasmaDir2] +
                      PlasmaTable[PlasmaDir3] + PlasmaTable[PlasmaDir4];

        /*** Check for the color choosen in the radio group. ***/
        switch (rgPlasmaColor->ItemIndex)
         {
          case cRed:
                /*** Only the red component. (green = blue = 0) ***/
                LinePtr [x] = (PlasmaColor<<16);
                break;
          case cGreen:
                /*** Only the green component. (red = blue = 0) ***/
                LinePtr [x] = (PlasmaColor<<8);
                break;
          case cBlue:
                /*** Only the blue component. (red = green = 0) ***/
                LinePtr [x] = (PlasmaColor);
                break;
          case cMix:
          default:
                /*** This is a mix of colours. Try to changes the values,
                     and remember that each color component is big as 1
                     Byte. ***/
                LinePtr [x] = (((255-PlasmaColor)<<16) |  //Red
                                (PlasmaColor<<8)       |  //Green
                                (128+(PlasmaColor>>1)));  //Blue
                break;
         }

        PlasmaDir3 += (Byte) 1;
        PlasmaDir4 += (Byte) 2;
      }

      PlasmaDir1 += (Byte) 2;
      PlasmaDir2 += (Byte) 1;
    }

    /*** This section controls the plasma speed. Try to change these
         values as you like. ***/
    PlasmaPos1 += (Byte) 2;
    PlasmaPos2 -= (Byte) 4;
    PlasmaPos3 += (Byte) 6;
    PlasmaPos4 -= (Byte) 8;

    /*** Finally, the back buffer is copied into the Canvas's Form ***/
    Canvas->Draw (0, 0, backbuffer);
}
//---------------------------------------------------------------------------