Lesson 14: Colors and Transparency
Computers and Color
In order to understand how color is used in a computer we must first take a short journey through the history of computers and color. The very earliest computer screens were nothing more than oscilloscopes that traced a beam of light onto a phosphor coating within an electron tube. The tracing was so fast however that a human's eye could not see it and due to the phosphor coating glowing for a short time afterward coupled with persistence of vision images and text could be drawn onto the tube. Looking back at the very earliest computer screens you'll notice that they are round because of their oscilloscope roots.
These early screens offered a color depth of 1 bit. Since a single bit can only hold two values, 0 and 1, the screen was either off at certain locations (0) or on at other locations (1). As electron tubes progressed over the years their round shape gave way to more rectangular shapes creating the common Cathode Ray Tube (CRT) computer screens from the 1970s to early 2000s. The earliest form of color for these CRT screens was known as monochrome.
Monochrome screens typically displayed white, green, or orange text and graphics. They could only produce one color, the same as the old oscilloscope tubes, so they offered a color depth of 1 bit. They could also address each individual pixel on the screen. To turn on a pixel a programmer would place a 1 at the pixel's address. To turn the pixel off a 0 would be placed at the pixel's address. It was a crude form of color but was very popular for applications that did not need actual colors, such as cash register terminals and word processing stations. By the late 70's computers capable of creating actual colors began to appear. Instead of going over each and every computer at the time we'll focus on the progression of the IBM PC and its use of color.
The first color video graphics cards available were known as CGA (Color Graphics Adapter). They were capable of up to 16 colors in text mode and 4 colors in graphics mode. CGA did this by mixing Red, Green, and Blue (RGB) values together to achieve the desired color. CGA has a color depth of 4 bits per pixel, 1 bit for red, 1 bit for green, 1 bit for blue, and 1 bit for intensity. This can be represented as a binary value of 4 bits:
INTENSITY RED GREEN BLUE
1 1 1 1 = WHITE (15) (by mixing all colors and making intense)
0 1 0 1 = MAGENTA (5) (by mixing red and blue)
1 1 0 0 = LIGHT RED (12) (red bit on with intensity)
0 0 1 1 = CYAN (3) (by mixing blue and green)
The following example program shows what was available when CGA was the only video adapter available for the PC. Save the code as CGADemo.BAS when finished.
Figure 1: The CGA text mode (SCREEN 0)
Figure 2: The CGA graphics mode (SCREEN 1)
Later video adapters followed to include the Enhanced Graphics Adapter (EGA) which was capable of truly being able to display 16 colors at a time in graphics mode for a true color depth of 4 bits. The introduction of the Video Graphics Array (VGA) adapter took this even further being able to display up to 256 colors at a time for a color depth of 8 bits. The following example program displays the VGA 256 default color palette. Save the code as VGADemo.BAS when complete.
Figure 3: VGA 256 default color palette
Later advancements in video technology had their unique names such as SVGA and XGA but they all followed the same RGB ordering of bits as CGA by simply adding more bits available to the red, green, and blue color components. Today 32bit color is the norm offering 8 bits of red, 8 bits of green, 8 bits of blue, and 8 bits for an alpha transparency channel that can all be represented as a 32bit binary string of 0s and 1s.
TRANSPARENCY RED GREEN BLUE
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 = WHITE
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 = MAGENTA
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 = LIGHT RED
1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 = CYAN
There are 256 levels (8 bits) of each color available in 32bit color depth (0 through 255). This is why there are 16.7 million different variations of color available. 256 times 256 times 256 = 16,777,216 or 24 bits = 224 = 16,777,216. The final 8 bits are used for transparency allowing a color to be completely clear (0 or 00000000) to fully opaque (255 or 11111111). There are in fact so many colors in the 32bit color depth palette that they can't all be displayed on a 1920x1080 wide screen monitor. There's only 2,073,600 pixels on the monitor, not nearly enough for 16 million+ unique colors!
32bit colors in QB64 are always returned or set as an unsigned long integer. Remember that unsigned long integers fall within the range of 0 to 4,294,967,295 (close to 4.3 billion) which is the same as 232. By default the transparency bits are always set to 1 meaning that the color black in binary is 11111111000000000000000000000000 or 4,278,190,080 all the way to white 11111111111111111111111111111111 or 4,294,967,295. This is why an unsigned long value needs to be used to store colors. Many times new programmers will mistake the value of 0 for black when in fact 0 is transparent black and wonder why black did not show up where they thought it should.
The _RGB32 and _RGB Statements
The _RGB32 statement gives the programmer an easy way of selecting a color from the 16.7 million available. A value of 0 through 255 needs to be supplied for each of the red, green, and blue components of the color.
MyColor~& = _RGB32(red%, green%, blue%)
Note: _RGB32 has recently gained the ability to work with transparency as well. This will be explained in the _RGBA32 section below.
Colors are achieved by mixing varying degrees of red, green, and blue together to form new colors. You may remember doing this in grade school when you learned that mixing blue and yellow crayons magically created green or mixing red and blue together made purple. Because there are so many colors to choose from many programs offer what's called a color wheel or color picker as seen in Figure 4 below.
Figure 4: Examples of a color picker and wheel
The QB64 IDE will also help you choose colors with its own version of a color picker. When you start typing in the statement _RGB32( a prompt appears allowing you to press SHIFT-ENTER to select a color through the use of slider bars as seen in Figure 5 below.
Figure 5: The IDE RGB color mixer
The _RGB32 statement only sets three of the four values available in a 32bit color, the red, green, and blue values. Other statements exist to manipulate the alpha transparency channel which will be discussed in a bit. Therefore, _RGB32 will always give you a color that is opaque or solid.
The _RGB statement handles colors differently depending on the bit depth of the image or screen it is working with. If an image has a 32bit color depth _RGB will behave exactly the same as _RGB32 and return a true 32bit color value. However, if the image being worked on has an 8bit color depth, or 256 colors, _RGB will return a value from 0 to 255 that as closely approximates the 32bit color as possible. Here is an example program that shows that difference in real time. Save the example code as ColorPick.BAS when finished.
Figure 6: Playing with colors
As the example program shows when in 32bit color mode the colors smoothly change as the values for red, green, and blue are changed. However, when you switch to 8bit color mode the color in the circle somewhat abruptly changes as the values are increased and decreased.
Many locations on the Internet have common HTML color names that are used when creating web sites. Here the w3schools.com website has a nice listing of them. At RapidTables.com is a listing of the colors along with an RGB color picker you can use. However, they list the colors in a rather peculiar way. For instance, the first few colors listed at w3schools.com are:
AliceBlue - #F0F8FF This is AliceBlue
AntiqueWhite - #FAEBD7 This is AntiqueWhite
Aqua - #00FFFF This is Aqua
Aquamarine - #7FFFD4 This is Aquamarine
The curious string of letters and numbers after the pound sign (#) are known as hexadecimal numbers. If you look back at Figure 4 the color picker offers a box in the lower right hand corner to enter a "hex" value. Most color pickers will also show you the R, G, and B values from 0 to 255 when you type in a hexadecimal number in the field. Even the previous example program displays the hexadecimal numbers.
Hexadecimal is another counting system that is used by computers. It's used as a method of shorthand for binary values. Instead of remembering that the value of 65,535 = 1111111111111111 one can simply use the hexadecimal value of FFFF to represent that.
It's not a requirement that you understand how to count in hexadecimal to get a start in game programming. Somewhere down the line though you are going to encounter code that uses it and be at a disadvantage compared to other coders. QB64 can even convert from decimal to hexadecimal and back for you with built in commands. If you are unfamiliar with hexadecimal I highly suggest you view a tutorial on its use. A very good tutorial can be found here at SparkFun's web site.
Hexadecimal is a BASE16 counting system that uses the number 0 through 9 and then A through F. Since there are not 16 different symbols for numbers in the common BASE10 counting system the letters A through F are needed to represent the numbers 10 through 15. Each place holder in the BASE16 counting system is 16 times greater than the one to the right. This is no different than decimal being 10 times greater and binary being 2 times greater. Each hexadecimal digit represents a binary nibble, or 4 bits. Figure 7 below is a conversion chart.
Figure 7: Base numbers conversion chart
Let's take the color BurntOrange for example. When you visit a color picker web site it will list this color as #CC6600. This hexadecimal number equates to the RGB values of (204, 102, 0). If you put those three decimal numbers into _RGB32 you will get the burnt orange color. Here's how the conversion looks.
| 204 | 102 | 0 |decimal RGB
|1 1 0 0|1 1 0 0|0 1 1 0|0 1 1 0|0 0 0 0|0 0 0 0|binary 24bits
| C | C | 6 | 6 | 0 | 0 |hexadecimal
RED GREEN BLUE
Looking at the chart in Figure 7 we see that "C" in hex equates to 1100 in binary. Therefore CC converts to 11001100 and that number in binary equals 204 in decimal. Likewise "6" in hex is 0110 so 66 converts to 01100110 and that number in binary equals 102 in decimal.
All programming languages understand and can use hexadecimal values. In fact, some languages such as Assembler rely heavily on its use. In order to tell QB64 that a number is being presented in hexadecimal form you must precede it with &H (an ampersand and then a capital H). For example, the hexadecimal number:
is the same as presenting the number 255 in decimal. ( FF = 11111111 = 255 ). You can use these hexadecimal values along with the _RGB and _RGB32 statements as well:
BurntOrange~& = _RGB32(&HCC, &H66, &H00) ' the color burnt orange
The Alpha Channel and Transparency
The _LOADIMAGE statement supports PNG images that contain transparent layers. If the PNG image you are loading contains any transparency, or alpha channel, layer information it will be retained when loaded. You can create transparent PNG images by using any graphics program that supports alpha channel features. Transparent, or alpha channel, information is simply a predefined color, or colors, that has been marked as having transparency. This is done by setting the highest 8 bits to something other than 255 or 11111111. The following example program shows this difference. Two bee images are loaded and displayed on the screen. Both bee images have a bright magenta background (255, 0, 255) however the image file tbee0.PNG had the bright magenta color set to be fully transparent by my graphics program. Type the following program in to see the difference. Save the code as AlphaDemo.BAS when finished typing it in.
Figure 8: Standard image left, Transparent image right
The bee on the left, bee0.PNG, has no alpha channel changes to any of the colors so the bright magenta color is seen surrounding the image. The bee on the right, tbee0.PNG, has had the bright magenta alpha channel bits set to 0 making the color completely transparent and allowing the sky image behind to be seen. The bright magenta color is still there in the bee on the right but _PUTIMAGE uses the alpha channel information to allow whatever is underneath to come through.
The _SETALPHA Statement
QB64's _SETALPHA statement can be used to manipulate the alpha channel of any color, or range of colors, within an image to produce transparency. Using the same code let's make the first bee transparent as well by using the _SETALPHA statement. Once you make the changes as seen in line 9 below save the code as SetalphaDemo.BAS.
Figure 9: Both bees now have transparency
The syntax for the _SETALPHA statement is:
_SETALPHA alpha%, color1~& TO color2~&, ImageHandle&
alpha% is the amount of transparency desired from 0 (fully transparent) to 255 (fully opaque or solid). color1~& is the starting color to apply the alpha channel changes to. color2~& is an optional second ending color allowing for a range of colors to have the alpha channel set. ImageHandle& is the image to have the alpha channel changes applied to.
In our example program above _SETALPHA was used to affect a single color but could have been used to affect the entire range of magenta by making this change:
_SETALPHA 0, _RGB32(255, 0, 1) TO _RGB32(255, 0, 255) ' all magenta transparent
The _SETALPHA statement is crazy powerful and takes some playing with to master. If all you need to do is set one color to transparent as we did in the example the _CLEARCOLOR statement is more often used.
The _CLEARCOLOR Statement
The _CLEARCOLOR statement is used to set a specific color to completely transparent. Once again let's modify the example code. Save the code as ClearColorDemo.BAS.
This time instead of using the _SETALPHA statement in line 9 we replaced it with the _CLEARCOLOR statement. The code produces the same output screen as previously seen in Figure 9 above however this time the only supplied parameters needed are the color and image to set.
_CLEARCOLOR color~&, ImageHandle&
_CLEARCOLOR will always set the alpha channel value of color~& to 0 making it completely transparent. To remove the affects of using _CLEARCOLOR on an image the following is used:
_CLEARCOLOR _NONE, ImageHandle&
This effectively removes all transparency previously set with the _CLEARCOLOR statement. The _CLEARCOLOR statement can also be used a function to retrieve the current transparent color of an image.
Transparent~& = _CLEARCOLOR(ImageHandle&) ' get transparent color of image
The _RGBA32 and _RGBA Statements
The _RGBA32 and _RGBA statements operate the same as _RGB32 and _RGB with the addition of being able to set the alpha transparency level at the same time. Instead of three parameters four will now be needed:
ColorValue~& = _RGBA32(red%, green%, blue%, alpha%)
The addition of an alpha% parameter allows the color to be created with a predetermined alpha level set.
One of the reasons I like working with QB64 is because of the consistent active development that is done to it. While writing this lesson it was brought to my attention that the _RGB32 statement gained the ability to optionally work with the alpha channel rendering the need for _RGBA32 obsolete. _RGBA32 is still maintained for compatibility reasons however.
_RGB32 can now accept optional alpha channel information:
MyColor~& = _RGB32(red%, green%, blue%, alpha%)
The POINT Statement
The POINT statement can be used to retrieve the color of an image at any x,y coordinate location. Type the following example code in to see the POINT statement in action. Save the code as PointDemo.BAS when finished.
As you move the mouse pointer around on the screen the color at the mouse pointer is retrieved by the POINT statement in line 15 and saved to an unsigned long integer variable. The POINT statement only needs to know the x,y coordinate of the image's color to retrieve it.
GetColor~& = POINT(x%, y%)
The _RED32, _GREEN32, and _BLUE32 Statements
In the previous example the red, green, and blue components of the color retrieved by POINT were also identified. The _RED32 statement will return a value from 0 to 255 indicating the amount of red in a color. The _GREEN32 statement will return a value between 0 and 255 indicating the amount of green in a color. Finally the _BLUE32 statement will return a value from 0 to 255 indicating the amount of blue in a color.
Red% = _RED32(color~&) ' return just the red component
Green% = _GREEN32(color~&) ' return just the green component
Blue% = _BLUE32(color~&) ' return just the blue component
There are also corresponding _RED, _GREEN, and _BLUE statements for working with 256 color images. These three statements will return the palette index of an 8bit (256) color.
A Colorful Example - Hocus Pocus
It's time to put all this colorful code into action. The following program is saved in your .\tutorial\Lesson14\ directory as HocusPocus.BAS and utilizes all of the statements up to this point. Copy HocusPocus.BAS to your qb64 folder and then load it into your IDE and execute it. Move the mouse around on the screen to make the magic happen.
Figure 10: Hocus Pocus!
This program is an example of particle effects (more on particle effects in lesson 18). The math involved with creating a program like this must be incredible right? Nope, there is no math involved other than resizing a _PUTIMAGE window, incrementing, and decrementing a few variables here and there. Even the gravity effect on the orbs is simulated with simple addition and subtraction.
Each of the colorful blooms created is actually an individual square image that contains an ever decreasing in size transparency layer using _PUTIMAGE's two coordinate placement system. This gives each square the illusion of fading out. The bloom itself is created using the CIRCLE statement contained in a loop to draw ever smaller circles on each square of slightly varying color that eventually leads to bright white. This gives them the appearance of glowing multicolored lights. THERE IS NO CODE KUNG FU going on here, just concepts from this lesson used to create an interesting effect.
Here at the beginning of the code some constants are set up to allow the look and the feel of the program to be changed easily. Lines 16 through 23 can all have their values changed to modify the size of the window, the bloom amounts, sizes, speeds, and to add a bounce to them if desired.
For fun set BOUNCE = TRUE and MAXLIFE = 128. The blooms will bounce a few times.
In lines 25 through 36 a TYPE definition is created that defines a single bloom. This TYPE definition is then used to create a dynamic array in line 38. Each time a new bloom is added to the screen the dynamic array is increased in size to hold it. We now have a construct that can hold many different objects at once that can all be manipulated easily by cycling through this array.
Lines 50 through 62 prepare the program by creating the graphics screen, loading and starting the sound, hiding and then moving the mouse to the middle of the window. The first index in the array is told it has no object (line 61) and can be used to store the first object created.
In the main program loop the mouse is being watched for any movement. If the variables x% or y% have changed, as indicated by their previous values oldx% and oldy%, then a FOR...NEXT loop is initiated in line 69 that counts to BLOOMAMOUNT. The subroutine HOCUS initiates a new bloom object at the current mouse position. Depending on the value of the constant BLOOMAMOUNT this may be done more than once.
The POCUS subroutine in line 76 scans through the array for any active blooms and updates their position and size. It then displays each active bloom to the screen.
At the beginning of the HOCUS subroutine the two variables hx% and hy% are defined as parameters to be passed into the HOCUS subroutine. These values are used as the x,y coordinate location to create a new bloom. The Abra() dynamic array is SHARED to allow access to its contents and finally local variables are declared that will be needed inside of the HOCUS subroutine.
Lines 114 through 140 are used for dynamic array maintenance. Lines 117 through 130 are used to find an available index number for the bloom to be created. Line 117 sets up a loop that will loop as many times as there are indexes in the array. If the variable Abra(Count%).lifespan has reached 0 in line 118 that means the bloom contained at this index has run its course. Lines 119 through 122 mark this index as empty and removes the dead bloom image from memory. Lines 123 through 125 check to see if a free index has already been chosen and if not this newly available index is chosen. In lines 126 through 128 a variable named CleanUp% used as a flag is set to FALSE if any index still has an active bloom contained in it.
Lines 131 through 134 resets the dynamic array since there are no active blooms contained anywhere in the array as indicated by CleanUp% being TRUE. This assures that size of the array does not get too large. When the user stops moving the mouse around and all color blooms have run their course the array is reset freeing the used memory. If however there are active blooms in the array lines 136 through 140 check to see if one of the indexes in the array were free. If line 136 sees that Index% is still holding the value of 0 then a free index was not found and line 137 needs to increase the dynamic array in size by 1. That newly created index value is now going to be used for the new bloom to be created.
Lines 141 through 157 sets up the dynamic array variables with values needed to create a bloom. Line 149 creates the image surface that will hold the bloom. When new images are created by default their color is (0, 0, 0, 0) meaning that they are transparent black. Many times, especially after _NEWIMAGE is used to create a SCREEN the CLS command is used to remove the transparent layer effectively changing the image's color to (255, 0, 0, 0). This will not be done here as we'll use that default transparency to our advantage later on.
Lines 150 through 152 creates the three random color components of the bloom color. Lines 153 through 155 set a color step value that will be used to change the blooms color as it gets smaller. Finally in line 157 an alpha transparency step value is also set used to make the bloom less transparent as it gets smaller.
Lines 158 through 179 is where the actual bloom gets created. First the current destination must be saved in line 158 since we are going to change the destination to the newly created image in line 159. In line 160 a counter is set up with the same size as the bloom. A loop is then set up in line 161 that will continue to loop until the value of Count% becomes 0. Count% is the radius value of each circle to be drawn within the bloom image.
Line 165 draws a circle at the center of the image using the radius value contained in Count%. The color used is defined by the current Red%, Green%, and Blue% component color variables.
Line 170 paints the circle starting at the center of the image using the same color as the previously drawn circle.
Line 172 then sets the alpha transparency of that color based on the value contained in Alpha%.
Lines 173 through 175 then increase the red, green, and blue color components in effect getting ever closer to white as the loop progresses.
Line 176 increases the Alpha% value making each subsequent circle to be drawn less transparent. As the bloom's rings created by the CIRCLE statement get ever smaller they become less transparent until the final circle in white in the center is completely opaque. A glowing orb is formed!
Line 177 decreases the size of the next circle and the loop starts again until finally Count% has reached a radius of 0.
When the loop is complete the original destination is restored in line 179. Figure 11 below shows the progression of the bloom as it is formed.
Figure 11: The creation of a glowing orb
The POCUS subroutine is used to update and display all active blooms within the Abra() dynamic array. Line 197 retrieves the largest index value contained within the array. A loop is then set up which will continue looping until the value of c% becomes 0.
Line 199 checks to see if the bloom is still active. If it is line 200 gets the current size of the bloom.
Line 201 then uses this size to go from a center point outward to that size using _PUTIMAGE. This creates a square region to squeeze the bloom image into. As the lifespan of the bloom progresses this size gets smaller squeezing the image into a smaller and smaller square. This effectively makes the bloom appear to be fading out as time goes on.
In lines 202 through 205 the lifespan of the bloom is decreased, the size of the bloom is decreased, and the x and y location of the bloom is updated.
In lines 206 through 212 then bloom is checked to see if it left the bottom of the screen. If it has and BOUNCE is TRUE then the vertical velocity is simply reversed to give the bloom a bouncing effect. If BOUNCE is set to FALSE then the bloom is simply killed off by setting its .lifespan variable to 0 as it is no longer needed.
Lines 213 and 214 update the x and y speed vectors of the bloom. The horizontal (x) speed is reduced by 10% and the vertical (y) speed is increased which simulates a gravity effect on the bloom.
Line 216 moves onto the next array index to update the bloom located there.
As you can see the effect is rather simple to achieve: create a square image, draw circles with varying color and transparency onto that image, squeeze that square into an ever increasingly smaller area for a set length of time. Rinse and repeat. No difficult math, no code Kung Fu.
Commands and Concepts Learned
New commands introduced in this lesson:
New concepts introduced in this lesson:
persistence of vision
cathode ray tube (CRT)
color graphics adapter (CGA)
enhanced graphics adapter (EGA)
video graphics array (VGA)