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 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 Lesson 14.

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.

The SCREEN Statement

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.

( This code can be found at .\tutorial\Lesson5\ScreenDemo.bas )

'------------------------
'- SCREEN demonstration -
'------------------------

CONST WHITE = _RGB32(255, 255, 255) ' set colors
CONST YELLOW = _RGB32(255, 255, 0)

SCREEN _NEWIMAGE(640, 480, 32) '      switch into a 640x480 graphics screen
CLS '                                 clear the graphics screen
PSET (0, 0), WHITE '                  turn on white pixels
PSET (319, 239), WHITE
PSET (639, 479), WHITE
LINE (5, 5)-(50, 50), YELLOW '        draw yellow lines
LINE (5, 5)-(5, 15), YELLOW
LINE (5, 5)-(15, 5), YELLOW
LINE (319, 234)-(319, 189), YELLOW
LINE (319, 234)-(309, 224), YELLOW
LINE (319, 234)-(329, 224), YELLOW
LINE (634, 474)-(584, 424), YELLOW
LINE (634, 474)-(624, 474), YELLOW
LINE (634, 474)-(634, 464), YELLOW
LINE (0, 279)-(639, 279), YELLOW
LINE (0, 279)-(10, 269), YELLOW
LINE (0, 279)-(10, 289), YELLOW
LINE (639, 279)-(629, 269), YELLOW
LINE (639, 279)-(629, 289), YELLOW
LINE (150, 0)-(150, 479), YELLOW
LINE (150, 0)-(160, 10), YELLOW
LINE (150, 0)-(140, 10), YELLOW
LINE (150, 479)-(160, 469), YELLOW
LINE (150, 479)-(140, 469), YELLOW
LOCATE 4, 2 '                         place text on screen
PRINT "Coordinate 0,0"
LOCATE 5, 3
PRINT "(White dot)"
LOCATE 11, 34
PRINT "Center 319,239"
LOCATE 12, 35
PRINT "(White dot)"
LOCATE 26, 62
PRINT "Coordinate 639,479"
LOCATE 27, 66
PRINT "(White dot)"
LOCATE 18, 23
PRINT "Width of screen 640 pixels (0-639)"
LOCATE 25, 3
PRINT "Height of screen 480 pixels (0-479)"
SLEEP '                               wait for user to press a key
SYSTEM '                              return to Windows

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.

The LINE Statement

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.

( This code can be found at .\tutorial\Lesson5\LineDemo.bas )

'----------------------
'- LINE demonstration -
'----------------------

CONST YELLOW = _RGB32(255, 255, 0)

SCREEN _NEWIMAGE(640, 480, 32)
CLS
LOCATE 2, 30
PRINT "Line Demonstration"
LOCATE 4, 16
PRINT "LINE (69, 89)-(569, 429), _RGB32(255, 255, 0)"
LINE (69, 89)-(569, 429), YELLOW
LOCATE 29, 34
PRINT "PRESS A KEY";
SLEEP
CLS
LOCATE 2, 30
PRINT "Line Demonstration"
LOCATE 4, 16
PRINT "LINE (69, 89)-(569, 429), _RGB32(255, 255, 0), B"
LINE (69, 89)-(569, 429), YELLOW, B
LOCATE 29, 34
PRINT "PRESS A KEY";
SLEEP
CLS
LOCATE 2, 30
PRINT "Line Demonstration"
LOCATE 4, 16
PRINT "LINE (69, 89)-(569, 429), _RGB32(255, 255, 0), BF"
LINE (69, 89)-(569, 429), YELLOW, BF

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.

( This code can be found at .\tutorial\Lesson5\LineDemo2.bas )

'-------------------------
'- LINE demonstration #2 -
'-------------------------

CONST YELLOW = _RGB32(255, 255, 0) ' define yellow

SCREEN _NEWIMAGE(640, 480, 32) '     switch into a 640x480 graphics screen
CLS '                                clear the graphics screen
LINE (299, 219)-(319, 0), YELLOW '   draw a line
LINE -(339, 219), YELLOW '           draw a line from endpoint of last line
LINE -(639, 239), YELLOW '           draw a line from endpoint of last line
LINE -(339, 259), YELLOW '           ""
LINE -(319, 479), YELLOW '           ""
LINE -(299, 259), YELLOW '           etc..
LINE -(0, 239), YELLOW
LINE -(299, 219), YELLOW
LOCATE 16, 36
PRINT "PRESS A KEY"
SLEEP '                              pause computer until key pressed
SYSTEM '                             return control to OS

You can even add a little style to the lines you draw. Type in the next example program.

( This code can be found at .\tutorial\Lesson5\LineDemo3.bas )

'-------------------------
'- LINE demonstration #3 -
'-------------------------

CONST YELLOW = _RGB32(255, 255, 0) ' define yellow
DIM Style& '                         the line's style
DIM Bit% '                           the current bit in style to draw
DIM Dir% '                           style direction

Dir% = -1
SCREEN _NEWIMAGE(640, 480, 32) '                   switch into a 640x480 graphics screen
DO '                                               begin loop
   CLS '                                           clear the graphics screen
   _LIMIT 30 '                                     limit loop to 30 frames per second
   LOCATE 2, 33 '                                  print directions
   PRINT "A Stylized Line"
   LOCATE 4, 21
   PRINT "Press the Space Bar to change direction."
   LOCATE 6, 23
   PRINT "Press the Escape key to exit program."
   IF _KEYHIT = 32 THEN Dir% = -Dir% '             change bit counter direction when space bar pressed
    Bit% = Bit% + Dir% '                           increment bit counter
   IF Bit% = 16 THEN '                             keep bit counter between 0 and 15
        Bit% = 0
   ELSEIF Bit% = -1 THEN
        Bit% = 15
   END IF
    Style& = 2 ^ Bit% '                            calculate the line style
   LINE (99, 129)-(539, 409), YELLOW, B , Style& ' draw the line as a stylized box
   _DISPLAY '                                      update screen with changes
LOOP UNTIL _KEYDOWN(27) '                          leave loop when ESC key pressed

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.

( This code can be found at .\tutorial\Lesson5\LineStyle.bas )

'---------------
'- Line Styles -
'---------------

CONST YELLOW = _RGB32(255, 255, 0) '         define yellow
SCREEN _NEWIMAGE(640, 480, 32) '             switch into a 640x480 graphics screen
CLS
LINE (10, 10)-(629, 10), YELLOW, , 43690 '   a dotted line 1010101010101010
LINE (10, 50)-(629, 50), YELLOW, , 52428 '   a dotted line 1100110011001100
LINE (10, 90)-(629, 90), YELLOW, , 61680 '   a dashed line 1111000011110000
LINE (10, 130)-(629, 130), YELLOW, , 65280 ' a dashed line 1111111100000000

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 to see the LINE statement on digital drugs.

( This code can be found at .\tutorial\Lesson5\Illusion.bas )

CONST CYAN = _RGB32(0, 255, 255) '         define cyan color
DIM x1%, y1% '                             upper left coordinates
DIM x2%, y2% '                             lower right coordinates
DIM Count% '                               FOR...NEXT loop counter
DIM Bit% '                                 pixel to turn on in style
DIM Style& '                               calculated style of line

SCREEN _NEWIMAGE(640, 480, 32) '           640 x 480 graphics screen
DO '                                       begin animation loop
   CLS '                                   clear the screen
   _LIMIT 60 '                             limit animation to 60 frames per second
    x1% = 0 '                              set starting coordinates
    y1% = 0
    x2% = 639
    y2% = 479
   PSET (0, 0), CYAN '                     set pixel to start with
    Bit% = Bit% + 1 '                      increment style bit counter
   IF Bit% = 16 THEN Bit% = 0 '            keep bit between 0 and 15
    Style& = 2 ^ Bit% '                    calculate line style
   FOR Count% = 1 TO 60 '                  cycle through line corkscrew 60 times
       LINE -(x2%, y1%), CYAN, , Style& '  draw lines with style
       LINE -(x2%, y2%), CYAN, , Style&
       LINE -(x1%, y2%), CYAN, , Style&
        y1% = y1% + 4 '                    update coordinates
        x2% = x2% - 4
        y2% = y2% - 4
       LINE -(x1%, y1%), CYAN, , Style& '  draw next line
        x1% = x1% + 4 '                    update coordinate
   NEXT Count%
   _DISPLAY '                              update screen with changes
LOOP UNTIL _KEYHIT '                       leave animation loop when key pressed

Figure 3: An animated illusion made with lines

And here is the code with straight lines.

Warning: This code creates an effect that may induce an epileptic seizure. If you are prone to such seizures do not execute this code.

( This code can be found at .\tutorial\Lesson5\Illusion2.bas )

CONST CYAN = _RGB32(0, 255, 255) ' define cyan color
DIM x1%, y1% '                     upper left coordinates
DIM x2%, y2% '                     lower right coordinates

SCREEN _NEWIMAGE(640, 480, 32) '   640 x 480 graphics screen
x2% = 639 '                        set starting coordinates
y2% = 479
PSET (0, 0), CYAN '                set pixel to start with
FOR Count% = 1 TO 60 '             cycle through line corkscrew 60 times
   LINE -(x2%, y1%), CYAN '        draw lines
   LINE -(x2%, y2%), CYAN
   LINE -(x1%, y2%), CYAN
    y1% = y1% + 4 '                update coordinates
    x2% = x2% - 4
    y2% = y2% - 4
   LINE -(x1%, y1%), CYAN '        draw next line
    x1% = x1% + 4 '                update coordinate
NEXT Count%
SLEEP '                            leave when key pressed
SYSTEM '                           return control to OS

Figure 4: Makes the eyes go doink!

The CIRCLE Statement

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.

( This code can be found at .\tutorial\Lesson5\Circle.bas )

'-- Circle demonstration

CONST YELLOW = _RGB32(255, 255, 0)

SCREEN _NEWIMAGE(640, 480, 32)
CLS
CIRCLE (319, 239), 100, YELLOW ' a yellow circle with a radius of 100 pixels
LOCATE 2, 33
PRINT "A Yellow Circle"

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.

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.

( This code can be found at .\tutorial\Lesson5\Radians.bas )

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

CONST PI = 3.1415926
CONST RED = _RGB32(255, 0, 0)
CONST YELLOW = _RGB32(255, 255, 0)

DIM StartRadian! ' starting point of circle arc
DIM StopRadian! '  ending point of circle arc
DIM Aspect! '      aspect ratio of circle (ellipses)
DIM Sign% '        holds the sign (-1 or +1) of arc radians

'----------------------------
'- Main Program Begins Here -
'----------------------------

SCREEN _NEWIMAGE(640, 480, 32) '                initiate a graphics screen
StartRadian! = 2 * PI '                         set initial arc starting point
StopRadian! = 0 '                               set initial arc ending point
Aspect! = 1 '                                   set initial aspect ratio of circle
Sign% = 1 '                                     set radian sign

DO '                                            main loop begins here
   CLS '                                        clear the screen
    StopRadian! = StopRadian! + 2 * PI / 360 '  increment stop radian in 360ths
   IF StopRadian! > 2 * PI THEN '               has arc done a full sweep?
        StopRadian! = 0 '                       yes, reset the arc end point
   END IF
   LINE (319, 119)-(319, 359), RED '            draw vertical red line
   LINE (199, 239)-(439, 239), RED '            draw horizontal red line
   CIRCLE (319, 239), 100, YELLOW, Sign% * StartRadian!, Sign% * StopRadian!, Aspect! ' draw yellow arc
   LOCATE 2, 18 '                               display instructions
   PRINT "CIRCLE radian demonstration for creating arcs"
   LOCATE 4, 30
   IF Sign% = 1 THEN
       PRINT "Stop radian ="; StopRadian!
   ELSE
       PRINT "Stop radian ="; -StopRadian!
   END IF
   LOCATE 5, 29
   PRINT "Aspect ratio ="; Aspect!
   LOCATE 7, 32
   PRINT "Half PI 1.5707963"
   LOCATE 24, 32
   PRINT "1.5x PI 4.7123889"
   LOCATE 15, 57
   PRINT "0 or 2x PI 6.2831852"
   LOCATE 15, 13
   PRINT "PI 3.1415926"
   LOCATE 27, 3
   PRINT "Press SPACEBAR to change arc type, hold UP/DOWN arrow keys to change ellipse"
   LOCATE 29, 26
   PRINT "Press ESC key to exit program";
   IF _KEYHIT = 32 THEN '                       did user press space bar?
        Sign% = -Sign% '                        yes, change the sign
   END IF
   IF _KEYDOWN(18432) THEN '                    is user holding UP arrow?
        Aspect! = Aspect! + .005 '              yes, increment aspect ratio
       IF Aspect! > 3 THEN '                    is aspect ratio greater than 3?
            Aspect! = 3 '                       yes, limit its value to 3
       END IF
   END IF
   IF _KEYDOWN(20480) THEN '                    is user holding DOWN arrow?
        Aspect! = Aspect! - .005 '              yes, decrement aspect ratio
       IF Aspect! < .25 THEN '                  is aspect ratio less than .25?
            Aspect! = .25 '                     yes, limit its value to .25
       END IF
   END IF
   _LIMIT 60 '                                  limit loop to 60 FPS
   _DISPLAY '                                   update screen with changes
LOOP UNTIL _KEYDOWN(27) '                       leave loop when ESC key pressed
SYSTEM '                                        return to Windows

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.

( This code can be found at .\tutorial\Lesson5\CircleClock.bas )

'-------------------------------------------------------
'Description: Draws a functioning clock using circles
'Author     : Terry Ritchie
'Date       : 11/12/13
'Updated    : 04/09/20
'Version    : 2.1
'Rights     : Open source, free to modify and distribute
'Terms      : Original author's name must remain intact
'-------------------------------------------------------

'--------------------------------
'- Variable Declaration Section -
'--------------------------------

CONST GRAY = _RGB32(127, 127, 127) '    define colors
CONST DARKGRAY = _RGB32(63, 63, 63)
CONST LIGHTGRAY = _RGB32(191, 191, 191)
CONST BLACK = _RGB32(0, 0, 0)
CONST WHITE = _RGB32(255, 255, 255)
CONST RED = _RGB32(127, 0, 0)
CONST YELLOW = _RGB32(255, 255, 0)

DIM Clock!(60) ' 60 radian points around circle
DIM Tick% '      counter to keep track of tick marks
DIM Tc~& '       tick mark color
DIM Radian! '    FOR...NEXT radian counter
DIM h% '         value of hour extracted from TIME$
DIM m% '         value of minute extracted from TIME$
DIM s% '         value of second extracted from TIME$
DIM Tm$ '        current TIME$ value

'----------------------------
'- Main Program Begins Here -
'----------------------------

SCREEN _NEWIMAGE(640, 480, 32) '                                     initiate graphics screen
Tick% = 15 '                                                         first position is 15 seconds
FOR Radian! = 6.2831852 TO 0 STEP -.10471975 '                       clockwise from 2*Pi in steps of -60ths
    Clock!(Tick%) = Radian! '                                        save circle radian seconds position
    Tick% = Tick% + 1 '                                              move to next seconds position
   IF Tick% = 60 THEN Tick% = 0 '                                    reset to 0 seconds position if needed
NEXT Radian!
DO '                                                                 begin main loop
   CLS '                                                             clear the screen
   _LIMIT 5 '                                                        5 updates per second
   CIRCLE (319, 239), 120, GRAY '                                    draw outer circle of clock
   PAINT (319, 239), DARKGRAY, GRAY '                                paint circle dark gray
   CIRCLE (319, 239), 110, BLACK '                                   draw inner circle of clock
   PAINT (319, 239), WHITE, BLACK '                                  paint face bright white
   FOR Tick% = 0 TO 59 '                                             cycle through radian seconds positions
       IF Tick% MOD 5 = 0 THEN Tc~& = BLACK ELSE Tc~& = LIGHTGRAY '  5 second ticks are black in color
       CIRCLE (319, 239), 109, Tc~&, -Clock!(Tick%), Clock!(Tick%) ' draw radian line from center of circle
   NEXT Tick%
   CIRCLE (319, 239), 102, DARKGRAY '                                draw circle to cut off radian lines
   PAINT (319, 239), WHITE, DARKGRAY '                               paint face again to remove radian lines
   CIRCLE (319, 239), 102, WHITE '                                   fill in cut off circle
   CIRCLE (319, 239), 4, BLACK '                                     draw small black circle in center
   PAINT (319, 239), 0, BLACK '                                      paint small black circle
    Tm$ = TIME$ '                                                    get current time from TIME$
    s% = VAL(RIGHT$(Tm$, 2)) '                                       get numeric value of seconds
    m% = VAL(MID$(Tm$, 4, 2)) '                                      get numeric value of minutes
    h% = VAL(LEFT$(Tm$, 2)) '                                        get numeric value of hours
   IF h% >= 12 THEN h% = h% - 12 '                                   convert from military time
   COLOR BLACK, WHITE '                                              black text on bright white background
   LOCATE 19, 37 '                                                   position cursor on screen
   PRINT RIGHT$("0" + LTRIM$(STR$(h%)), 2) + RIGHT$(Tm$, 6) '        print current time in face of clock
   COLOR GRAY, BLACK '                                               white text on black background
    Tick% = (h% * 5) + (m% \ 12) '                                   calculate which hour hand radian to use
   CIRCLE (319, 239), 80, BLACK, -Clock(Tick%), Clock(Tick%) '       display hour hand
    h% = h% + 6 '                                                    move to opposite hour on clock face
   IF h% >= 12 THEN h% = h% - 12 '                                   adjust hour if necessary
    Tick% = (h% * 5) + (m% \ 12) '                                   calculate which hour hand radian to use
   CIRCLE (319, 239), 15, BLACK, -Clock(Tick%), Clock(Tick%) '       display small opposite tail of hour hand
   CIRCLE (319, 239), 95, BLACK, -Clock(m%), Clock(m%) '             display minute hand
    m% = m% + 30 '                                                   move to opposite minute on clock face
   IF m% > 59 THEN m% = m% - 60 '                                    adjust minute if necessary
   CIRCLE (319, 239), 15, BLACK, -Clock(m%), Clock(m%) '             display small opposite tail of min hand
   CIRCLE (319, 239), 2, RED '                                       draw small red circle in center
   PAINT (319, 239), RED, RED '                                      paint small red circle
   CIRCLE (319, 239), 100, RED, -Clock(s%), Clock(s%) '              draw second hand
    s% = s% + 30 '                                                   move to opposite second on clock face
   IF s% > 59 THEN s% = s% - 60 '                                    adjust second if necessary
   CIRCLE (319, 239), 25, RED, -Clock(s%), Clock(s%) '               display small opposite tail of sec hand
   CIRCLE (319, 239), 1, YELLOW '                                    draw small yellow circle in center
   PSET (319, 239), YELLOW '                                         fill in small yellow circle
   _DISPLAY '                                                        update screen with loop's changes
LOOP UNTIL INKEY$ <> "" '                                            end program when key pressed
SYSTEM '                                                             return to Windows

Figure 7: A clock made with CIRCLEs

The PAINT Statement

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.

( This code can be found at .\tutorial\Lesson5\Paint.bas )

CONST RED = _RGB32(127, 0, 0) '           define colors
CONST BRIGHTRED = _RGB32(255, 0, 0)
CONST GREEN = _RGB32(0, 127, 0)
CONST BRIGHTGREEN = _RGB32(0, 255, 0)

SCREEN _NEWIMAGE(640, 480, 32) '          initiate a graphics screen
CLS '                                     clear the screen
LINE (50, 50)-(300, 300), BRIGHTRED, BF ' draw a bright red filled box
LINE (50, 50)-(300, 300), RED, B '        draw a darker red border around box
CIRCLE (319, 239), 100, GREEN '           draw a green circle
PAINT (319, 239), BRIGHTGREEN, GREEN '    paint bright green until darker green is encountered

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 '    paint bright green until darker green is encountered

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.

( This code can be found at .\tutorial\Lesson5\Paint2.bas )

CONST RED = _RGB32(127, 0, 0) '           define colors
CONST BRIGHTRED = _RGB32(255, 0, 0)
CONST GREEN = _RGB32(0, 127, 0)
CONST BRIGHTGREEN = _RGB32(0, 255, 0)

SCREEN _NEWIMAGE(640, 480, 32) '          initiate a graphics screen
CLS '                                     clear the screen
CIRCLE (319, 239), 100, GREEN '           draw a green circle
LINE (50, 50)-(300, 300), BRIGHTRED, BF ' draw a bright red filled box
LINE (50, 50)-(300, 300), RED, B '        draw a darker red border around box
PAINT (319, 239), BRIGHTGREEN, GREEN '    paint bright green until darker green is encountered

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:

( This code can be found at .\tutorial\Lesson5\Paint3.bas )

CONST RED = _RGB32(127, 0, 0) '            define colors
CONST BRIGHTRED = _RGB32(255, 0, 0)
CONST GREEN = _RGB32(0, 127, 0)
CONST BRIGHTGREEN = _RGB32(0, 255, 0)

SCREEN _NEWIMAGE(640, 480, 32) '           initiate a graphics screen
CLS '                                      clear the screen
CIRCLE (319, 239), 100, GREEN '            draw a green circle
'LINE (50, 50)-(300, 300), BRIGHTRED, BF ' draw a bright red filled box
LINE (50, 50)-(300, 300), RED, B '         draw a darker red border around box
PAINT (319, 239), BRIGHTGREEN, GREEN '     paint bright green until darker green is encountered

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.

( This code can be found at .\tutorial\Lesson5\CircleBounce.bas )

'--------------------------------
'- Variable declaration section -
'--------------------------------

TYPE CIRCLETYPE '                    CIRCLE DATA
    x AS SINGLE '                    x location of circle
    y AS SINGLE '                    y location of circle
    c AS _UNSIGNED LONG '            color of circle
    r AS INTEGER '                   radius of circle
    xdir AS SINGLE '                 x direction of circle
    ydir AS SINGLE '                 y direction of circle
END TYPE

CONST CIRCLES = 50 '                 maximum number of circles on the screen
CONST SCREENWIDTH = 640 '            graphics screen width
CONST SCREENHEIGHT = 480 '           graphics screen height

DIM Cir(CIRCLES - 1) AS CIRCLETYPE ' circle array
DIM Count% '                         circle counter
DIM Red%, Green%, Blue% '            circle color attributes
DIM OkToPaint% '                     toggle painting flag on/off

'----------------------
'- Begin main program -
'----------------------

RANDOMIZE TIMER '                                       seed the random number generator
FOR Count% = 0 TO CIRCLES - 1 '                         cycle through all circles
    Cir(Count%).x = SCREENWIDTH / 2 - 1 '               calculate x center point of circle
    Cir(Count%).y = SCREENHEIGHT / 2 - 1 '              calculate y center point of circle
    Red% = INT(RND(1) * 256) '                          random red intensity from 0 to 255
    Green% = INT(RND(1) * 256) '                        random green intensity from 0 to 255
    Blue% = INT(RND(1) * 256) '                         random blue intensity from 0 to 255
    Cir(Count%).c = _RGB32(Red%, Green%, Blue%) '       combine colors and save circle color
    Cir(Count%).r = INT(RND(1) * 40) + 11 '             random radius 10 to 50 pixels
    Cir(Count%).xdir = (RND(1) * 2 - RND(1) * 2) * 2 '  random x direction -2 to 2
    Cir(Count%).ydir = (RND(1) * 2 - RND(1) * 2) * 2 '  random y direction -2 to 2
NEXT Count%
OkToPaint% = -1 '                                                               start with painting enabled
SCREEN _NEWIMAGE(SCREENWIDTH, SCREENHEIGHT, 32) '                               create graphics screen
DO '                                                                            begin main program loop
   CLS '                                                                        clear the screen
   _LIMIT 60 '                                                                  limit to 60 frames per second
   FOR Count% = 0 TO CIRCLES - 1 '                                              cycle through all circles
       IF Cir(Count%).x <= Cir(Count%).r THEN '                                 edge of circle hit left wall?
            Cir(Count%).xdir = -Cir(Count%).xdir '                              yes, reverse x direction
       ELSEIF Cir(Count%).x >= SCREENWIDTH - Cir(Count%).r - 1 THEN '           edge of circle hit right wall?
            Cir(Count%).xdir = -Cir(Count%).xdir '                              yes, reverse x direction
       END IF
       IF Cir(Count%).y <= Cir(Count%).r THEN '                                 edge of circle hit top wall?
            Cir(Count%).ydir = -Cir(Count%).ydir '                              yes, change y direction
       ELSEIF Cir(Count%).y >= SCREENHEIGHT - Cir(Count%).r - 1 THEN '          edge of circle hit bottom wall?
            Cir(Count%).ydir = -Cir(Count%).ydir '                              yes, change y direction
       END IF
        Cir(Count%).x = Cir(Count%).x + Cir(Count%).xdir '                      update circle x location
        Cir(Count%).y = Cir(Count%).y + Cir(Count%).ydir '                      update circle y location
       CIRCLE (Cir(Count%).x, Cir(Count%).y), Cir(Count%).r, Cir(Count%).c '    draw circle
       IF OkToPaint% THEN '                                                     paint circles?
           PAINT (Cir(Count%).x, Cir(Count%).y), Cir(Count%).c, Cir(Count%).c ' yes, paint circle
       END IF
   NEXT Count%
   LOCATE 2, 26 '                                                               display directions
   PRINT "PRESS SPACEBAR TO TOGGLE PAINT"
   IF _KEYHIT = 32 THEN OkToPaint% = NOT OkToPaint% '                           toggle paint flag if space bar
   _DISPLAY '                                                                   update screen with changes
LOOP UNTIL _KEYDOWN(27) '                                                       exit loop when ESC key pressed
SYSTEM '                                                                        return control to OS

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 the 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 the graphics changes (dump the queue)
LOOP UNTIL Frame% = 100 ' go back and draw the next frame
_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:

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