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.
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:
A back buffer (to be more precise, a TBitmap object)
| A timer, used to invoke the drawing routine
| The code that produces some cool effect
| |
... 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.
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:
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().
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.
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);
}
//---------------------------------------------------------------------------