So far we have delt with simple scenes consisting of a few lines and shapes. In most situations we will want to deal with much more complex scenes consisting of many objects. One way to draw multiple objects would be to simply repeat code. But for large numbers of objects it can be easier to declare a record that describes a type of object's properties and then use an array of that record to store and draw multiple objects.
Excercise 3-15. We're going to write a program that fills the screen with flower-like polygons that appear to bloom.
Create a record that defines the properties of a single flower. These properties should be Position, PetalCount, and Color. Then create a dynamic array variable of this record.
Position is declared as a TVector2D since we want X and Y coordinates. PetalCount is an integer because we will never have a fraction of a petal. Color is an integer because colors are always 32-bit integers.
Now define a procedure that draws a flower given an instance of TFlower. Don't implement the procedure yet, just define the header (i.e. procedure DrawFlower(...)). This procedure will take 2 arguments: Flower and Radius (the latter will be used for the "bloom" effect).
We declare Flower with the const directive to pass Flower by reference. We do this because we don't want the entire record copied in memory when we call DrawFlower. We only want the reference to its address in memory. Radius is simply declared as a real variable.
Call DrawFlower (even though we haven't implemented it yet) inside of OnEnterFrame. You'll need to create an instance of TFlower locally in order to do so. Initialize this instance to be positioned at the origin, to have 5 petals, and to be any color you like. The radius passed to DrawFlower should be about 5. The reason we are doing this is so that we can easily debug DrawFlower as we are writing it.
Now we will implement DrawFlower. As a start, write code inside DrawFlower that would draw an octagon (8 sides) using MoveTo and LineTo statements. Hint: Think circles. You'll probably want to use the following code:
function PolarToXY(Theta,Radius : Double) : TVector2D;
begin
Result.X := cos(Theta*PI/180)*Radius;
Result.Y := sin(Theta*PI/180)*Radius;
end;
A circle is basically a regular polygon with the number of sides
approaching infinity. When we draw a circle, we don't have to draw an infinite number of sides of course because as
becomes quite large the circle's edges appear smooth. Previously we drew a circle by stepping 1 degree at a time. This basically means that we created a regular polygon with 360 sides. If we want to draw a polygon with 8 sides we just have to step by 360/8 = 45 degrees and connect the dots.
Modify the code to draw any regular polygon (use N as variable that represents the number of sides). Start with N = 5, but try other values to make sure your code works.
We have to modify the code at only a few locations. First we declare N as an Integer and initialize it to 5. Then we replace 8 with N since this is the total number of sides. 45 was the step size in degrees and this was calculated from 360/8 so we simply replace 45 with 360/N.
Instead of using MoveTo-LineTo statements. Let's draw a filled regular polygon with one call to FillPolygon(Points,LineColor,FillColor). In order to do this we need an array of points. We can get such an array by declaring a dynamic array Points and then using SetLength(ArrayName,Size) to set the size of the array where size is the number of vertices. We then use a similar loop in which we define the coordinates of each vertex using Points[K].
We use SetLength(Points,N) after N has been initialized. This time we loop from 0 to N-1. The reason we do this is important. Before, we started the pen at 0 degrees with a single call to MoveTo. Then we looped from 1 to N calling LineTo. Going all the way to N draws a line that closes the polygon. FillPolygon automatically closes the polygon so we only have to specify up to N-1 points.
Now we have code that draws a polygon but this doesn't really look like a flower. Assuming a flower has a star-shaped look to it, we can modify the code so that every other vertex is at half the radius. To do this, we recommend using an N of 10. Each point should then be either PolarToXY(0,0,360/N*K,Radius) or PolarToXY(0,0,360/N*K,Radius/2), but how do we alternate between the two? This is where the mod operator is helpful. If we do modular division by 2 on K then the result is 0 when K is even and 1 when K is odd. So we can use K mod 2 = 0 in an if statement to determine when K is even. Implement code that alternates between Radius and Radius/2 using this advice.
Now we have a star shape. To make the petals of the flower appear as though they are separate, add a second for loop after FillPolygon with MoveTo-LineTo statements that draws a line from the center of the flower to each of the Radius/2 vericies (i.e. when K mod 2 = 1). Then place a small circle at the center.
There are two ways to do this. The first is shown below using an if K mod 2 = 1 statement.
The second way is to access only the elements you want by using 2*K+1. This takes a little thought. If you start with K = 0 then 2*K+1 gives 1. At K = 1, 2*K+1 gives 3, etc. This means that you will also have to modify the N-1 part of the for loop to be (N div 2)-1. In other words, if N is 10, then (N div 2)-1 is 4, so at K = 4, 2*K+1 gives 9 which is the last odd number before 10 (where we want to stop). Just to make the code more efficient, we should use a with statement so that the program doesn't have to compute 2*K+1 twice (once for Points[2*K+1].X and once for Points[2*K+1].Y). The first way is obviously easier and either technique produces the same result, but the second way is actually more efficient. Here efficiency doesn't matter because this isn't a performance critical part of code (i.e., it's not executed a billion times per second), but it's important to learn techniques that improve efficiency.
We now have a basic flower drawing. Go through the code and add in the properties of Flower (Position, PetalCount, Color) where appropriate so that DrawFlower works for any flower.
For Position, you should replace every instance of (0,0) with (Flower.Position.X,FlowerPosition.Y). You can simply assign FlowerCount*2 to N. Finally, Color is specified only once in FillPolygon.
Let's create a function called GenerateRandomFlower that will fill an instance of TFlower with random initialization values. GenerateRandomFlower should take 1 argument (Flower) passed by reference. Remember that we want to modify the values of Flower inside the procedure so we cannot use the const directive to pass it by reference (what should we use?). You can use the Random(UpperBound) procedure which generates a random integer from 0 to UpperBound-1. The position should be somewhere within the screen bounds (i.e., between -14 and 14 on X and -10 and 10 on Y). Remember that to position between two bounds such as -14 and 14 you can do something like Random(28)-14. If you want coordinates such as 12.3 to be included you can do (Random(280)-140)/10. The flowers can have any number of petals that you want as long as it is at least 3. The color should be chosen with HSI2Color where Hue is Random(255). Use full saturation and full intensity.
The var directive should be used to pass Flower by reference. To position the flower between -14 and 14 on the X use (Random(280)-140)/10. For -10 and 10 on Y use (Random(200)-100)/10. For PetalCount we chose a range of 3 to 12: 3+Random(10). A random color at full saturation and intensity is HSI2Color(Random(255),255,255).
Let's generate 50 flowers randomly on the screen. We'll initialize all the flowers in Initialize. First, you should declare a global constant (it will be used by other OnEnterFrame so it must be global) called FlowerCount set to 50. Inside of Initialize, set the length of the dynamic array to FlowerCount. Then simply loop trhough every entry in Flowers (0 to FlowerCount-1) and call GenerateRandomFlower. You will then need to go to OnEnterFrame and again loop through every entry in Flowers and call DrawFlower(Flowers[K],FrameCount/100). Don't forget to call Randomize inside of Initialize in order to reset the random number generator.