Lesson 5: Graphics
BASIC originally came with a primitive set of graphics commands to create dots, lines, boxes, circles and to allow painting of screen areas. Even though the commands are very simple a creative programmer could do very interesting things with them. In this lesson we'll cover these primitive commands and in a later lesson step it up with new fancy graphics commands offered by QB64.
A quick note about new QB64 commands first
In this lesson commands are going to be introduced that were not native to the BASIC language but instead introduced by QB64. These new commands enhance the BASIC language to take advantage of today's more powerful computers and features that were not available many years ago. Most new commands introduced by QB64 start with an underscore ( _ ) character. This was done for compatibility reasons since QB64 was written to allow BASIC source code from the 1970's and later to compile and run.
One of the new commands you'll learn about is _NEWIMAGE(). Let's say someone wrote a program back in 1992 and created a variable in their code called NewImage%. If QB64 did not precede these new commands with an underscore that variable created way back then would be interpreted as a command in QB64. By adding the underscore to new commands old source code has a better than 99% probability of compiling and running just fine in modern versions of QB64.
A programmer can also create custom subroutines and functions and name them anything they like (more on subroutines and functions in a later lesson). If a programmer made a function called NewImage() then again QB64 would interpret this as a command rather than a custom function written many years ago.
There are literally hundreds of thousands, if not millions, of BASIC source code files scattered around the Internet. Because the developers of QB64 took compatibility into account you can be fairly certain that code from 1989 you found on the Internet will compile and run in QB64 today.
Colors
Before we get into graphics commands we need to learn about color use in QB64 first. Most graphics commands have a color option attached to them allowing for color customization. While there are options in QB64 to work with 256 color screens this course is only going to work with 32bit color (16 million) screens. This is just going to be a quick introduction to colors in QB64. We will delve into this topic in much greater detail in the advanced graphics lesson.
The _RGB32() statement is the most common way to set or create colors in QB64. There are a wide variety of color commands available in QB64 but this will be the one we use in this lesson. _RGB32() returns an unsigned long integer data type. Unsigned long integers have a range of 0 through 4,294,967,295 which is also the maximum value of a 32bit number (2^32 = 4,294,967,296). The _RGB32() statement creates colors by mixing red, green, and blue values together.
Color = _RGB32(red, green, blue)
There are 256 levels of each color available (0 through 255). By varying the levels for red, green, and blue you can make up to 16 million different color combinations (256 * 256 * 256 = 16,777,216). There is also a fourth optional parameter for changing the alpha layer, or transparency of a color, using the same 256 levels. This is why we need to use an unsigned long integer variable type to store color information (red(256) * green(256) * blue(256) * alpha(256) = 4,294,967,295). Unsigned long integers can be created with the use of ~& (the tilde character means "unsigned" and as we learned before the & character means long integer). Some examples are:
BrightRed~& = _RGB32(255, 0, 0) ' red set to full intensity
Red~& = _RGB32(127, 0, 0) ' red set to half intensity
BrightGreen~& = _RGB32(0, 255, 0) ' green set to full intensity
BrightBlue~& = _RGB32(0, 0, 255) ' blue set to full intensity
White~& = _RGB32(255, 255, 255) ' all three colors set to full intensity
Gray~& = _RGB32(127, 127, 127) ' all three colors set to half intensity
Many times you'll see programs that have constants set with common colors used throughout the source code:
CONST WHITE = _RGB32(255, 255, 255)
CONST GRAY = _RGB32(127, 127, 127)
CONST DARKGRAY = _RGB32(63, 63, 63)
CONST PURPLE = _RGB32(255, 0, 255) ' red and blue full intensity
CONST YELLOW = _RGB32(255, 255, 0) ' red and green full intensity
We'll get much deeper into the workings of color and the other color statements available in a later advanced graphics lesson.
The Graphics Screen
Up to this point we have been doing everything in a text only screen where graphics are not permitted. The first thing we need to do is get QB64 into a graphics screen mode. The SCREEN statement is used to do this. SCREEN 0 is the text mode we have been working with up to this point and is considered the default SCREEN. Since QB64 was developed to support GWBASIC and QuickBasic source code from the 80's and 90's, SCREEN 1 through SCREEN 13 modes are available. However, these legacy SCREEN modes are very limited in both size and colors available when working with today's graphics cards. If you are interested in learning about the legacy SCREEN modes you can click here to read about them on the QB64 Wiki.
In this course we are not going to bother with legacy SCREEN modes but instead use the new SCREEN options that QB64 has introduced for today's modern graphics cards.
Points of Light
Using the SCREEN statement along with QB64's _NEWIMAGE() statement you can create a graphics screen of any size with up to a 32bit color depth. Think of a graphics screen as a blank sheet of graph paper with each individual little square at your command. Each one of those little squares is known as a pixel which is the smallest point that can be referenced on a graphics screen. A pixel is literally a single point of colored light that you can control. Your computer's monitor screen is made of millions of pixels (2,073,600 for a 1920x1080 monitor). You can reference each and every one of those pixels using an X,Y coordinate system built into QB64. Type in the following program that illustrates how a graphics screen is laid out as shown in Figure 1.
Save the program as ScreenDemo.BAS when you have finished typing it in.
Figure 1: A 640x480 graphics screen
A graphics screen of 640 pixels wide by 480 pixels high was created using this line of code:
SCREEN _NEWIMAGE(640, 480, 32) ' switch into a 640x480 graphics screen
The SCREEN statement is used to change the screen's mode. Back in the early days, when QuickBasic ruled, that could be a text only mode, SCREEN 0, or a few other options such as SCREEN 13 which set the screen to 320 pixels wide by 200 pixels high with a maximum of 256 colors. Screen modes also had fixed dimensions of 320x200, 640x200, 640x350, and 640x480. That's very small given today's very high resolution monitors. The early limitations had to do with limited video RAM on video cards and CRT monitors having fixed frequencies and dimensions. These limitations no longer exist today so those early legacy modes are of no use any longer.
QB64 introduced the _NEWIMAGE() statement to allow the programmer to create new images (a blank canvas if you will) in video RAM. By pairing the SCREEN statement with QB64's _NEWIMAGE() statement you can build a new screen image of any size you like. The _NEWIMAGE() statement requires three parameters:
_NEWIMAGE(width, height, colors)
Width is how many pixels wide, height is how many pixels high, and colors is the color depth desired, 256 for 256 colors and 32 for 32bit color (16 million colors). The _NEWIMAGE() statement is used for much more than just creating a screen but we'll cover that in another lesson on advanced graphics.
It's important to remember that to a computer zero is a number like any other and most operations start with zero instead of one. In the example program we created a 640 by 480 screen but Figure 1 points out that the coordinate system is actually referenced as 0-639 for width and 0-479 for height. That's because of the computer starting with zero. If you count zero as a number then 0 through 639 is actually 640 pixels and 0 through 479 is 480 pixels. That's also why in Figure 1 the center point is referenced as 319, 239 instead of 320, 240. Because the computer starts at zero everything seems "off" by one pixel. You'll need to remember this when creating graphics using QB64. Note that this is not something QB64 alone does, all programming languages use zero as a starting point for their graphics screen.
The PSET() statement is used to turn a single pixel on with a given color. PSET() requires the following parameters:
PSET(x, y), color
The x and y coordinate pair refer to the location on the screen where the pixel is to be manipulated. Color is provided by the _RGB32() statement which will be explained shortly. In the example program PSET() was used to turn the upper left (0, 0), middle (319, 239), and lower right pixel (639, 479) on with a color of WHITE.
Line Up Everyone
The LINE statement is used to draw lines, boxes, and filled boxes. The LINE statement takes the following parameters:
LINE(x1, y1) - (x2, y2), color, BF, style
The LINE statement needs a start coordinate (x1, y1), an end coordinate (x2, y2), a color, an optional B to create a box or an optional BF to create a filled box. An optional style can also be defined giving the line a pattern. Type the following program into your IDE to see all of the different ways LINE can be utilized. Save the program as LineDemo.BAS when you are finished.
As the above example code illustrates the B and BF parameters are optional. (x1, y1) is also optional. If you omit the start coordinates (x1, y1) from the LINE statement it uses the last known position as a starting point. Type in the next example to see how this works. Save the code as LineDemo2.BAS when finished.
You can even add a little style to the lines you draw. Type in the next example program and save it as LineDemo3.BAS when finished.
Figure 2: A line with style
Setting up line styles is done by creating a value that contains a 16 bit pattern. For example, if you were to create a line using the number 43690 you would get a line with every other pixel turned on. Using the value of 65280 you would produce a dashed line. Type in the following code and execute it to see different style values being used and how they affect the line's output. Save the code as LineStyle.BAS when finished.
Decimal numbers ranging in value from 0 to 65535 translate to binary numbers containing 16 places (16 bits). The decimal number 43690 can be converted to the binary number 1010101010101010 creating a pattern that can be used in the line's style. Where ever a 1 resides the line's pixel is turned on and a 0 means the pixel is turned off. This pattern is repeated over and over throughout the length of the line. Likewise, 65280 converts to 1111111100000000 in binary so you get a dashed line, 8 pixels on, 8 pixels off, 8 pixels on, 8 pixels off, and so on.
We're not going to get into decimal to binary conversion here. You can simply use a calculator that supports programmer functions, such as the Windows calculator, to type in the binary pattern you wish and then have the calculator convert that pattern to decimal for you.
In the hands of a creative programmer the LINE statement can do some pretty wild things with very little code. Type the next program in and save as Illusion.BAS to see the LINE statement on digital drugs.
Figure 3: An animated illusion made with lines
And here is the code with straight lines. After typing in the code save this example as Illusion2.BAS.
Warning: This code creates an effect that may induce an epileptic seizure. If you are prone to such seizures do not execute this code.
Figure 4: Makes the eyes go doink!
Going in CIRCLEs
The CIRCLE statement is used to draw full or partial circles and ellipses onto a graphics screen. Let's start with a simple circle. The CIRCLE statement requires a minimum of three parameters:
CIRCLE (x, y), radius, color
The (x, y) pair are coordinates located on the graphics screen at the center of the circle. Radius is the distance from the center point to draw the circle's outer boundary. Remember that the radius of a circle is half the diameter. Color is optional but will probably always be supplied as the default white can get boring. Type the following code into your IDE and save it as Circle.BAS when completed.
Figure 5: A yellow circle
By default the CIRCLE statement draws a full 360 degree arc with an aspect ratio of one. You can override these values to draw only part of a circle, or an arc, by specifying the start and stop radian values of the arc. You can also supply an aspect ratio to change the circle into an ellipse.
CIRCLE (x, y), radius, color, start_radian, stop_radian, aspect
To better illustrate this type in the following program and execute it. Save the code as Radians.BAS when completed.
Note: Don't be intimidated by the length of the code. Type in one line at a time until you're finished. If you are unwilling or highly hesitant to type this tiny bit of code in you may want to rethink your programmer ambitions.
Figure 6: Radian demonstration
Radians fall in the range of 0 to 6.2831852 or 2 times Pi. Supplying the CIRCLE statement with positive radian values produces an arc from start_radian to stop_radian. Supplying negative radian values to CIRCLE produces the same arc but this time with lines drawn from the radian endpoints back to the center of the arc. This creates a pie looking structure as seen in Figure 6 above. Arcs are always drawn in a counter-clockwise direction. If you were to designate Pi as the start radian value and half Pi as the end radian value the end result would not be an arc drawn between those two points in the upper left quadrant. The arc would include the entire circle except for this quadrant because of the arc being drawn counter-clockwise.
This Wikipedia article on radians does a very good job of explaining the relationship between a circle's radius and circumference.
The example program also illustrates how you can affect the CIRCLE statement's aspect ratio. The default aspect ratio for the CIRCLE statement is 1 resulting in a perfectly round circle. Smaller aspect values squash the ellipse while larger values make the ellipse taller. If you simply want to change the aspect ratio of a circle without having to give radians you can simply skip over them like so:
CIRCLE (319, 239), 100, _RGB32(255, 255, 0), , , 2 ' create a tall yellow ellipse
By supplying no values for the start and end radian the CIRCLE statement uses the default values that create a closed arc, also known as a circle.
Here is a program that shows the power of the CIRCLE statement and how creative programming can use statements in ways that seem to defy their purpose. This example uses the CIRCLE statement to draw straight lines creating the hands of a clock and the tick marks going around the clock face. Save the program as CircleClock.BAS when finished.
Figure 7: A clock made with CIRCLEs
Paint by Numbers
While the LINE statement offers a way to create a filled box the CIRCLE statement does not offer a method of creating a filled circle. There also needs to be a mechanism for painting irregular shaped objects created with multiple LINE statements. This is where the PAINT statement comes in. Type in the following program to see the PAINT statement work its magic.
Figure 8: A PAINTing
The PAINT statement is used to fill in an area with a color until another color is encountered. In the example program this line:
PAINT (319, 239), BRIGHTGREEN, GREEN
is instructing the computer to fill in a color at coordinate (319, 239) (the center of the circle) with the color bright green, _RGB32(0, 255, 0), until the color green, _RGB32(0, 127, 0) is encountered. The green color of the circle creates a border that the PAINT statement stays within. Since the portion of the red box inside the circle is not green the PAINT statement will paint right over top of it.
Because of the way the PAINT statement operates it's important to remember the order in which objects are drawn. Let's take the previous example and simply swap the LINE and CIRCLE statements so the circle is drawn first to see what happens.
Figure 9: Well that didn't go as planned?
Because the box was drawn over top of the circle the PAINT statement was able to spread out over the entire screen. Only where it saw green pixels did it stop leaving the partial circle as seen in Figure 9 above. The same result would happen if the box were not filled by the LINE statement:
Figure 10: Holy holes Batman!
Because the box is red instead of green it will create holes in the circle that the PAINT statement will exploit to allow paint to leak out and fill the screen as seen in Figure 10 above. Only by planning the order of graphics statements before-hand will you get the desired results with PAINT that you are looking for. Type in the following program and save it as CircleBounce.BAS when completed.
Figure 11: Bouncing PAINTed CIRCLEs
We'll get to the new commands seen in this program in later lessons. For now focus on the three constants contained in the code, CIRCLES, SCREENWIDTH, and SCREENHEIGHT. By changing the values of these constants you can change the entire look and feel of the program. The PAINT command takes quite a bit of horsepower from the CPU. You can see this by changing the constant CIRCLES to a large number. The larger the number the slower the code. By toggling the PAINT statement on and off you can see the difference in speed for yourself. Keep this in mind when designing graphics based programs using PAINT.
The _DISPLAY Statement
In a few of the program examples in this lesson you may have noticed the use of the _DISPLAY statement. The _DISPLAY statement is used to synchronize your program's output screen with the refresh rate of the monitor. A monitor needs to update, or refresh, the screen every so often. This is done at a precisely timed interval and is known as the refresh rate. However, if your program is not synchronized with this interval you'll get flashing and flickering effects appearing on the screen as graphics are drawn. This is not a problem if your program simply needs to draw an image and then wait for an input from the user for instance. But, if your program needs to constantly update an image or its position, such as with animation, this flickering effect becomes very noticeable.
The _DISPLAY statement forces all graphics updates drawn to the screen into a queue. When the monitor begins a new refresh period the _DISPLAY statement dumps all of the graphics to the screen at once. This ensures that the drawing of graphics is perfectly synchronized with the monitor screen refresh periods eliminating the flickering and flashing. Most of the time the _DISPLAY statement is used within a loop where graphics needs to be constantly updated:
DO ' Begin animation loop
CLS ' Clear the screen of the last frame's drawn graphics
'
' Your code here that redraws the screen's new graphics positions and/or appearances
'
_DISPLAY ' update the screen with graphics changes (dump the queue)
LOOP ' go back and draw the next frame
Without the _DISPLAY statement in the loop the graphics will flicker and flash due to being out of sync with the monitor. Go back to a few of the previous program examples and remove the _DISPLAY statement from the loop to see the huge difference that using _DISPLAY has.
The _AUTODISPLAY Statement
The _AUTODISPLAY statement is used to turn the _DISPLAY statement off. Once the _DISPLAY statement has been used no updates to the screen will be done until another _DISPLAY statement is encountered and explicitly used. For instance, take the next bit of code for example:
DO ' Begin animation loop
CLS ' Clear the screen of the last frame's drawn graphics
Frame = Frame + 1 ' increment frame counter
'
' Your code here that redraws the screen's graphics
'
_DISPLAY ' update the screen with graphics changes (dump the queue)
LOOP UNTIL Frame = 100 ' leave loop after 100 frames have been drawn
_AUTODISPLAY ' turn _DISPLAY off
PRINT "Did you like that animation? (Y/N)"
INPUT Answer$
If the _AUTODISPLAY statement were not present after the loop the user would never see "Did you like that animation? (Y/N)" or the flashing cursor waiting for user input. By using the _DISPLAY statement you told QB64 that you as the programmer will handle when updates to the screen should happen. To give control back to QB64 use the _AUTODISPLAY statement.
The _LIMIT Statement
Another statement you may have noticed in the previous examples is the _LIMIT statement. The _LIMIT statement restricts a loop to a fixed number of loops per second, or in gaming terms, a fixed number of frames per second (FPS). The CPU cycles saved by using this statement are then free to be used for other processes. The statement is very easy to use:
_LIMIT FramesPerSecond%
The reasons you would want to use _LIMIT:
To keep from overheating your CPU by keeping the usage low.
To slow down graphics movements and animations.
Give more CPU time to the other processes running within your code.
Code like this would really tax your CPU:
DO
LOOP UNTIL INKEY$ <> "" ' leave loop when user presses a key
This code will sit and wait for the user to press any key. However, that loop is running at full speed utilizing the entire power available by the CPU. This takes CPU power away from other tasks running in your computer and heats the CPU. A better approach would be:
DO
_LIMIT 10 ' run the loop at ten times per second
LOOP UNTIL INKEY$ <> "" ' leave loop when user presses a key
This loop will perform the exact same function but will only check for user input 10 times per second, more than enough for a simple task such as this. Best of all your CPU stays nice and cool.
Remove the _LIMIT statement from some of the example programs above to see how _LIMIT is beneficial.
Your Turn
Recreate the following screen as shown in Figure 12 below using the graphics commands you learned from this lesson.
Figure 12: Old glory
- The width of the image is 640 pixels.
- Each red and white stripe is 36 pixels high.
- The blue banner is 310 pixels wide by 252 pixels high.
- The upper left hand circle is located at coordinate (30, 25).
- The first circle in the second row is located at coordinate (55, 50).
- All circles are spaced evenly horizontally and vertically 50 pixels apart.
- Look for patterns in the image above to reduce the size of your code using controlled loops.
- 22 lines of code (not counting REM statements) were created to produce the image in Figure 12.
- Try to create the flag in as few lines of code as possible (can you do it in less than 22?)
Save your code as USAFlag.BAS when finished.
Click here to see the solution.
BONUS!
Instead of circles create actual stars as seen in Figure 13 below.
Figure 13: Bonus for stars!
SUPER BONUS!
Create your country's flag using the statements learned in this lesson. When you are finished either post the source code at the QB64 Phoenix Edition forum or email it to me (my email address is at the bottom of this page). I'll create a section in this lesson with the code that creates your country's flag to highlight all of the varying countries the tutorial users hail from.
Commands and Concepts Learned
New commands introduced in this lesson:
SCREEN
LINE
CIRCLE
PAINT
PSET
_RGB32()
_NEWIMAGE()
_DISPLAY
_AUTODISPLAY
_LIMIT
New concepts introduced in this lesson: