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 QuickBasic 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 Lesson 6). 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 Lesson 14.
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 (232 = 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 SCREEN Statement
Up to this point we have been doing everything in a text only screen (known as SCREEN 0) where graphics are not permitted. The first thing we need to do is get QB64 into a graphics screen mode. 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 up 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.
Legacy Screens
The SCREEN statement is used to put QB64 into a graphics mode. 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 graphics modes are also available. However, these legacy SCREEN graphics modes are very limited in both size and colors available when working with today's graphics cards and monitors. As graphics cards evolved over time (MDA, CGA, EGA, VGA) the BASIC language had to add new SCREEN modes to take advantage of them. The syntax for the legacy SCREEN statement is (used in GWBasic, QuickBasic, and supported by QB64):
SCREEN mode%, , active%, visual%
mode% sets the screen into the desired mode (see list below)
active% is the page of video RAM currently being written to (more on this in a bit)
visual% is the page of video RAM currently being displayed on the monitor (again, more in a bit)
NOTE: there is a parameter missing between the first two commas in the syntax above. This was used for color switching (on and off) in GWBasic and QuickBasic and is no longer supported by QB64.
Here is a list of the legacy SCREEN modes supported by QB64:
SCREEN 0: Text mode only (MDA - Monochrome Display Adapter and greater)
The default BASIC screen when no SCREEN mode is supplied.
40 x 25 or 80 x 25 text only with up to 2 colors (16 colors with CGA and greater)
SCREEN 1: 320x200 Color Graphics (CGA - Color Graphics Adapter and greater)
40 x 25 text with up to 16 colors (but only 4 at a time from selected palettes)
SCREEN 2: 640x200 Monochrome Graphics (CGA and greater)
80 x 25 text with up to 2 colors (black and white)
SCREEN 7: 320x200 Color Graphics (EGA - Enhanced Graphics Adapter and greater)
40 x 25 text with up to 16 colors
SCREEN 8: 640x200 Color Graphics (EGA and greater)
80 x 25 text with up to 16 colors
SCREEN 9: 640x350 Color Graphics (EGA and greater)
80 x 25 or 80 x 43 text with up to 16 colors (from a palette of 64 colors)
SCREEN 10: 640x350 Monochrome Graphics (EGA and greater)
80 x 25 or 80 x 43 text with up to 2 colors (black and white)
SCREEN 11: 640x480 Monochrome Graphics (VGA - Video Graphics Array and greater)
80 x 30 or 80x 60 text with up to 2 colors (black and white)
SCREEN 12: 640x480 Color Graphics (VGA and greater)
80 x 30 or 80 x 60 text with up to 16 colors from a palette of 262,144 colors (256K)
SCREEN 13: 320x200 Color Graphics (VGA and greater)
40x25 text with up to 256 colors from a palette of 262,144 colors (256K)
You probably noticed the omission of SCREEN modes 3 through 6. These modes were available on systems with specialized video adapters such as the IBM PCjr computer, the Tandy 1000 series of computers (known as TGA), the Hercules video adapter, the Olivetti M series of computers, and the AT&T 6300 series systems (an Olivetti M24 in disguise). These systems shipped with a modified version of GWBasic to take advantage of the enhanced video adapters they included. QuickBasic never natively supported these specialized legacy SCREEN modes although there were third party libraries available if these modes were needed but it was still quite uncommon.
A program included with the tutorial named LegacyModes.BAS displays all of the above SCREEN legacy modes along with their color capabilities. Load the program into your IDE and run it now. Press the ENTER key to move through the SCREEN modes or the ESC key to exit at any time.
( This program can be found at .\tutorial\Lesson5\LegacyModes.bas )
Figure 0: The legacy SCREEN modes
As you can tell after running the program these legacy screens are quite small and limited in their color capabilities. SCREEN 13, the 320x200 256 color mode, was by far the most popular when writing games in QuickBasic but it would be much too small to be usable on today's large monitors. You can literally fit 30 SCREEN 13s onto a standard 1920x1080 wide screen monitor! Heck, all of them fit into a nice little image as Figure 0 illustrates above. It's also important to note that while some of these SCREEN modes are extremely rectangular, on a CRT monitor they would have been stretched to a 4:3 aspect ratio creating pixels greater in height than in width.
The _NEWIMAGE Statement
SVGA (Super Video Graphics Array) and XGA (eXtended Graphics Array) added the screen modes 800x600, 1024x768, and 1280x1024. Other standards followed to offer even higher resolutions to accommodate wide screen and high definition formats. QB64 allows the creation of these formats by pairing the SCREEN statement with the _NEWIMAGE() statement. _NEWIMAGE creates an image canvas in RAM identified by a numeric handle value that can be used as the output screen. QB64 has modified the SCREEN syntax as follows to allow this:
SCREEN ImageHandle&, ,active%, visual%
ImageHandle& is any image created through the use of QB64's image statements
active% is the page of video RAM currently being written to (more on this in a bit)
visual% is the page of video RAM currently being displayed on the monitor (again, more in a bit)
The syntax for the _NEWIMAGE statement is:
Handle& = _NEWIMAGE(Width%, Height%, Mode% {0|1|2|7|8|9|10|11|12|13|256|32})
Width% is the width of the image in pixels to be created
Height% is the height of the image in pixels to be created
Mode% is the image mode to emulate (0 for text, 1 through 13 for legacy, 256 for 256 color, and 32 for 32 bit color)
To create an SVGA screen of 1024x768 that supports 32 bit color (16+ million colors) the following line of code would be used:
SCREEN _NEWIMAGE(1024, 768, 32) ' create a 1024x768 32 bit color graphics output screen
_NEWIMAGE(1024, 768, 32) creates an image canvas in RAM and assigns a numeric value to it. The SCREEN statement uses that numeric value to identify the output screen. When _NEWIMAGE is paired with the SCREEN statement that numeric value will always be zero (0). Statements that you'll learn about in later lessons allow you to identify the image canvas to draw on. Image zero (0) will always be the output screen.
It's even possible to create a text only screen with custom text character dimensions:
SCREEN _NEWIMAGE(132, 66, 0) ' create a text only output screen of character dimension 132x66
By choosing a Mode% of zero (0) the _NEWIMAGE statement will emulate the text only legacy SCREEN 0. Here we've chosen to make the screen 132 characters wide by 66 characters high. These are the exact dimensions of a wide format sheet of printer paper. Note that when emulating SCREEN 0 the dimensions supplied are in characters and not pixels. Emulating screen modes 1, 2, 7, 8, 9, 10, 11, 12, and 13 need dimensions supplied in pixels. The same is true for modes 256 (256 colors) and 32 (32 bit color).
The _PIXELSIZE statement
At some point in your program you may need to know which mode was used to create an image or SCREEN. The _PIXELSIZE statement returns one of three values that identify the mode your SCREEN or an image is in. The syntax for the _PIXELSIZE statement is:
Psize% = _PIXELSIZE(ImageHandle&)
ImageHandle& is the numeric handle value of the image you wish to test. Psize% will contain one of three values:
0 - the image or output screen is text mode only
1 - the image or output screen is emulating legacy modes 1 through 13 or 256 color
4 - the image or output screen is in 32 bit color mode
Remember, the current output screen will always be zero (0) so just supply a value of zero (0) for ImageHandle& to identify the current SCREEN mode.
SCREEN _NEWIMAGE(640, 480, 32) ' 32 bit color mode
PRINT _PIXELSIZE(0) ' will print the value of 4
While _PIXELSIZE may not seem to be a handy statement at the moment it will become useful later on as your programming skills progress. The value _PIXELSIZE returns is actually the number of bytes needed to store a single pixel's color information in RAM. This information is required when dealing with direct image memory manipulation or routines that need to create new images that match the current mode of existing images or the output screen.
The Graphics SCREEN
Lesson 13 will introduce the different ways that QB64 can create images but in this lesson we'll focus on the _NEWIMAGE() statement to create an image that can be used as an output screen. Type in the following program that illustrates how a graphics screen is created and 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
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.
SCREEN Pages and Flipping
An output screen can have multiple pages associated with it. Remember these two methods of creating output screens?
Legacy SCREEN:
SCREEN mode%, , active%, visual%
mode% sets the screen into the desired mode
active% is the page of video RAM currently being written to
visual% is the page of video RAM currently being displayed on the monitor
QB64 modified SCREEN:
SCREEN ImageHandle&, ,active%, visual%
ImageHandle& is any image created through the use of QB64's image statements
active% is the page of video RAM currently being written to
visual% is the page of video RAM currently being displayed on the monitor
It's time to discuss the active% and visual% video RAM pages associated with them. Legacy SCREENs were created in the video card's RAM. In many cases an output screen would only use a portion of that video RAM. That extra video RAM could be used to create multiple instances of the output screen that could be called upon.
For instance, If you had an EGA video card that contained 256K of RAM and created a game using SCREEN 7 (320x200 with 16 color) that output screen only required 32K of video RAM. The remaining video RAM could be broken into 32K chunks containing more output screens for a total of 8 screens (256K divided by 32K). Those 7 extra screens could then be written to and viewed at will. Many GWBasic programmers took advantage of this and used these hidden pages to hold sprite images instead of using the precious little system RAM that early computers had. Through the use of the GET and PUT graphics statements these sprite images could be copied from a hidden screen and written to the output screen.
I used this to my advantage while writing my Connect Four game back in 1992 (more on that here). When the game was first started page 0, the output screen, remained blank until page 1 was finished drawing the title screen. Page 1 was then copied to page 0 to reveal the title screen while page 2 drew the sprites needed for the game. When that was finished an image of the playing field was drawn on page 3. The whole time this drawing was going on in the background page 0 was still showing the title screen. Page 3 now contained a clean image of the playing field whenever I needed it, page 1 held a copy of the title screen, and page 2 a copy of the sprites. Once all the drawing was finished page 3 was copied to page 0 and the game began. When the game finished I could simply copy page 3 back to page 0 to clear the screen with a fresh playing field to start a new game. Or, if the player decided to exit the game back the title screen I could simply copy page 1 which contains a copy of the title screen back to page 0. Drawing graphics on a Tandy 1000EX 8088 computer was painfully slow in GWBasic but I took advantage of that by using that time to display just the title screen. The title screen would display for about 5 seconds, the time needed to draw all the graphics assets needed in the background. If you were to run that same program in QB64 today the title screen would blip by so fast you wouldn't even see it!
Traditional animation, film, and video is achieved by flipping images very quickly in front of the eye. This same "page flipping" technique was very common in QuickBasic using one page to draw on while the other was being shown, then the pages were flipped, with the opposite pages being drawn and shown. Here is a quick example of page flipping:
'---------------
' Page flipping
'---------------
x% = 159 ' x coordinate
y% = 99 ' y coordinate
a% = 1 ' active page being written to
v% = 0 ' visual page being shown
DO ' begin animation loop
SCREEN 7, , a%, v% ' set active and visual pages
IF y% > 0 THEN ' draw another square?
LINE (x%, y%)-(319 - x%, 199 - y%), 14, B ' yes, draw yellow square
x% = x% - 3 ' decrement x coordinate
y% = y% - 3 ' decrement y coordinate
END IF
a% = 1 - a% ' flip active page
v% = 1 - v% ' flip visual page
_DELAY .1 ' 1/10th second delay (10 FPS)
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
While some legacy screens had more pages than others, and some of them only having one, QB64 has removed these limitations completely. All SCREENs, regardless of mode (this includes legacy SCREENs), can have as many video pages as available system RAM allows. This even includes SCREEN 0 and an emulated SCREEN 0 created with _NEWIMAGE.
The PCOPY Statement
The PCOPY statement allows the programmer to take advantage of the multiple pages associated with a SCREEN as discussed in the section above. The PCOPY statement allows any SCREEN page to be copied to a different SCREEN page. The syntax for the PCOPY statement is:
PCOPY SourcePage%, DesinationPage%
SourcePage% is the SCREEN page you wish to copy and DestinationPage% is the SCREEN page to receive the copy. Let's look at an example of this in code first.
Type the following program into your IDE.
( This code can be found at .\tutorial\Lesson5\PCOPYexample.bas )
'---------------------------------------------------
' PCOPY Example
'
' by Terry Ritchie 04/22/24
'
' A simple game using PCOPY to update the screen
'
' Page 0 - the main viewing page
' Page 1 - a copy of the title screen (animation 1)
' Page 2 - a copy of the title screen (animation 2)
' Page 3 - a copy of the game screen
'---------------------------------------------------
CONST WHITE~& = _RGB32(255, 255, 255) ' define the color white
CONST BLACK~& = _RGB32(0, 0, 0) ' define the color black
DIM ScreenImage& ' the main graphics screen
DIM ActivePage% ' page being written to
DIM ViewPage% ' page being viewed
DIM KeyPress$ ' any key press on keyboard
DIM x% ' player x coordinate
DIM y% ' player y coordinate
DIM ex! ' enemy x coordinate
DIM ey! ' enemy y coordinate
DIM Distance! ' distance from player to enemy
ScreenImage& = _NEWIMAGE(640, 640, 32) ' create main graphics screen
SCREEN ScreenImage&, , 1, 0 ' page 1 active and viewing page 0
'+-----------------------------------------------------+
'| Create an animated title screen using pages 1 and 2 |
'+-----------------------------------------------------+
LOCATE 15, 21 ' position text cursor
PRINT "WELCOME TO MY AWESOME MONOCHROME GAME!" ' print title screen text
LOCATE 18, 24
PRINT "PRESS ANY KEY TO BEGIN THE GAME!" ' print instructions
LOCATE 21, 33
PRINT "(ESC to Exit)"
PCOPY 1, 2 ' copy this page to page 2
ActivePage% = 0 ' set active page
FOR x% = 0 TO 640 STEP 64 ' cycle horizontally in steps of 64
SCREEN ScreenImage&, , ActivePage% + 1, 0 ' set active screen page
LINE (x%, 0)-(x% + 63, 63), WHITE, BF ' draw a filled box at top of screen
LINE (639 - x%, 575)-(575 - x%, 639), WHITE, BF ' draw a filled box at bottom of screen
ActivePage% = 1 - ActivePage% ' flip active page
NEXT x%
'+------------------------------+
'| Draw a game screen on page 3 |
'+------------------------------+
SCREEN ScreenImage&, , 3, 0 ' make page 3 the active page
CLS , WHITE ' clear the page white
LINE (10, 10)-(629, 629), BLACK, BF ' draw black box (creates border)
LOCATE 2, 18: PRINT "DON'T LET THE SOLID CIRCLE CATCH YOUR CIRCLE"
LOCATE 3, 27: PRINT "USE THE ARROW KEYS TO MOVE"
LINE (0, 50)-(639, 59), WHITE, BF ' draw border line under text
'+-----------------+
'| Begin main code |
'+-----------------+
SCREEN ScreenImage&, , 0, 0 ' active and view page set to 0
DO ' begin main program loop
ViewPage% = 0 ' set view page
DO ' begin title screen loop
_LIMIT 5 ' 5 frames per second
PCOPY ViewPage% + 1, 0 ' copy page 1 or 2 to page 0
ViewPage% = 1 - ViewPage% ' flip view page
KeyPress$ = INKEY$ ' get any key that was pressed
_DISPLAY ' update screen (remove flicker)
LOOP UNTIL KeyPress$ <> "" ' leave loop when key pressed
IF KeyPress$ = CHR$(27) THEN SYSTEM ' return to OS if ESC pressed
x% = 539 ' initialize player position
y% = 539
ex! = 99 ' initialize enemy position
ey! = 99
DO ' begin game play loop
_LIMIT 240 ' 240 frames per second
PCOPY 3, 0 ' clear screen with page 3 image
CIRCLE (x%, y%), 10, WHITE ' draw player
CIRCLE (ex!, ey!), 10, WHITE ' draw enemy
PAINT (ex!, ey!), WHITE, WHITE ' paint enemy
IF _KEYDOWN(19712) THEN x% = x% + 1 ' move player right
IF _KEYDOWN(19200) THEN x% = x% - 1 ' move player left
IF _KEYDOWN(18432) THEN y% = y% - 1 ' move player up
IF _KEYDOWN(20480) THEN y% = y% + 1 ' move player down
IF x% < 20 THEN x% = 20 ELSE IF x% > 619 THEN x% = 619 ' keep player x coordinate within limits
IF y% < 70 THEN y% = 70 ELSE IF y% > 619 THEN y% = 619 ' keep player y coordinate within limits
Distance! = _HYPOT(x% - ex!, y% - ey!) ' distance between player and enemy
ex! = ex! + (x% - ex!) / Distance! ' add normalized x vector to enemy
ey! = ey! + (y% - ey!) / Distance! ' add normalized y vector to enemy
SOUND (1660 - Distance! * 2), .03 ' higher pitch as enemy gets closer
_DISPLAY ' update screen (remove flicker)
LOOP UNTIL Distance! <= 20 ' leave when enemy catches player
PLAY "O4L8CO3L16GGL8A-GRBO4C" ' shave and a haircut 2 bits
_KEYCLEAR ' clear the keyboard buffer
LOOP ' end main program loop
Lines 34 through 39 of the code print the text instructions to the active page 1 as set in line 28. Since the title screen is going to be animated through the use of pages 1 and 2 line 40 of the code copies the text currently on page 1 to page 2.
SCREEN ScreenImage&, , 1, 0 ' page 1 active and viewing page 0
'+-----------------------------------------------------+
'| Create an animated title screen using pages 1 and 2 |
'+-----------------------------------------------------+
LOCATE 15, 21 ' position text cursor
PRINT "WELCOME TO MY AWESOME MONOCHROME GAME!" ' print title screen text
LOCATE 18, 24
PRINT "PRESS ANY KEY TO BEGIN THE GAME!" ' print instructions
LOCATE 21, 33
PRINT "(ESC to Exit)"
PCOPY 1, 2 ' copy this page to page 2
Lines 42 through 47 of the code draws solid boxes across the top and bottom of the screen. While these boxes are being drawn the active page is alternated between page 1 and page 2. This causes the boxes to be drawn in alternate positions on each active page and creates the animation effect. We now have our two title screen pages that can be flipped back and forth.
FOR x% = 0 TO 640 STEP 64 ' cycle horizontally in steps of 64
SCREEN ScreenImage&, , ActivePage% + 1, 0 ' set active screen page
LINE (x%, 0)-(x% + 63, 63), WHITE, BF ' draw a filled box at top of screen
LINE (639 - x%, 575)-(575 - x%, 639), WHITE, BF ' draw a filled box at bottom of screen
ActivePage% = 1 - ActivePage% ' flip active page
NEXT x%
Lines 53 through 58 of the code draws the game play screen on page 3. Page 3 can now be used to clear the screen during the game updates and retain the look of the playing field without having to redraw it over and over again.
SCREEN ScreenImage&, , 3, 0 ' make page 3 the active page
CLS , WHITE ' clear the page white
LINE (10, 10)-(629, 629), BLACK, BF ' draw black box (creates border)
LOCATE 2, 18: PRINT "DON'T LET THE SOLID CIRCLE CATCH YOUR CIRCLE"
LOCATE 3, 27: PRINT "USE THE ARROW KEYS TO MOVE"
LINE (0, 50)-(639, 59), WHITE, BF ' draw border line under text
Lines 67 through 73 in the main code section simply copies page 1 and page 2 using PCOPY repeatedly while waiting for the player to press a key. Since page 1 and page 2 have alternating graphics the view screen appears animated. These 2 pages eliminate the need to constantly redraw the animated squares while waiting for the player.
DO ' begin title screen loop
_LIMIT 5 ' 5 frames per second
PCOPY ViewPage% + 1, 0 ' copy page 1 or 2 to page 0
ViewPage% = 1 - ViewPage% ' flip view page
KeyPress$ = INKEY$ ' get any key that was pressed
_DISPLAY ' update screen (remove flicker)
LOOP UNTIL KeyPress$ <> "" ' leave loop when key pressed
Finally, line 81 of the code uses the image of the game playing field on page 3 to clear the screen in preparation for player and enemy movement and object redraws. Once again, no need to redraw the static parts of the playing field over and over again.
PCOPY 3, 0 ' clear screen with page 3 image
The PSET Statement
The PSET() statement (Pixel Set) 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 explained above. In the previous 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.
PSET (0, 0), WHITE ' turn on white pixels
PSET (319, 239), WHITE
PSET (639, 479), 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
This is possible because a graphics cursor is kept track of by QB64. The graphics cursor resides at the endpoint of each line that is drawn by the LINE statement.
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.
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 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 that use PAINT.
The POINT Statement
The POINT statement has two functions. First it can be used to retrieve the color of an individual pixel or coordinate position on the graphics screen.
PixelColor~& = POINT(10, 30) ' get the pixel color
The color of the pixel located at coordinate 10, 30 will be saved within PixelColor~&.
Secondly, the POINT statement can report the x and y position of the graphics cursor. A graphics cursor is maintained as you draw graphics using the QB64 graphics commands. It's the reason this is possible:
LINE (299, 219)-(319, 0), YELLOW ' draw a line
LINE -(339, 219), YELLOW ' draw a line from endpoint of last line
When the first LINE statement was executed the graphics cursor position now resides at the endpoint of the line, or coordinate 319, 0. The second LINE statement uses the graphics cursor's current location as its starting point.
The POINT(0) statement retrieves the graphics cursor's current x, or horizontal, position and the POINT(1) statement retrieves the graphics cursor's current y, or vertical, position.
LINE (299, 219)-(319, 0), YELLOW ' draw a line
LINE -(339, 219), YELLOW ' draw a line from endpoint of last line
x% = POINT(0) ' get graphics cursor x location
y% = POINT(1) ' get graphics cursor y location
The variable x% will contain the value of 339 and the variable y% will contain the value of 219. This does not seem very useful because we already know what these values are since they were given within the LINE statement. POINT(0) and POINT(1) are very useful however when used with the DRAW statement below.
The DRAW Statement
The DRAW statement has been a part of BASIC since before even QuickBasic, having its roots in GWBasic as far back as the early 1980s. However, it's one of the most overlooked and underutilized graphics statements available. This is due partly because of the perceived complexity of the statement but it's very powerful for those that wish to use it.
The DRAW statement can be thought of as a small scripting language built into QB64 that allows the programmer to build complex drawings using a wide range of available directives. The DRAW statement allows for the direct manipulation of the graphics cursor discussed earlier. The syntax for the DRAW statement is deceptively simple:
DRAW DrawString$
DrawString$ contains a series of directives that instructs the DRAW statement to move the graphics cursor, draw lines, change line colors, rotate lines, resize lines, and paint areas of the screen. Imagine setting a pen down on a piece of paper and then with one fluid movement drawing an image or using the rotating dials on an Etch-a-Sketch to draw an image to the screen. Let's go through each one of the DrawString$ directives to see how useful this statement can be. I like to use the analogy of a pen on paper when describing the DRAW statement directives.
DRAW: Moving The Graphics Cursor
The "Mx,y" directive is used to move the graphics cursor pen to a new location on the screen.
Type the following into your IDE:
'--------------
' DRAW Example
'--------------
SCREEN _NEWIMAGE(800, 600, 32) ' enter an 800x600 32bit color graphics screen
DRAW "M399,299" ' move the pen to the center of the screen
DRAW "M100,100" ' move the pen to coordinate 100,100 on the screen
The first DRAW statement moved the graphics cursor pen to the center of the 800x600 graphics screen. The second DRAW statement then moved the graphics cursor pen to coordinate 100,100 on the screen resulting in a line being drawn between the two points (the pen was dragged across the paper). While you can draw lines by simply moving the graphics cursor there are specific directives designed to draw lines that are much more powerful. Most of the time you'll want to move the graphics cursor by picking the pen up, moving it, and then putting the pen back down resulting in no line being drawn between the cursor movements.
Modify your code like so:
'--------------
' DRAW Example
'--------------
SCREEN _NEWIMAGE(800, 600, 32) ' enter an 800x600 32bit color graphics screen
DRAW "BM399,299" ' move the pen to the center of the screen
DRAW "BM100,100" ' move the pen to coordinate 100,100 on the screen
By adding the "B" directive in front of the "Mx,y" directive, "BMx,y", you instructed the DRAW statement to do a blind move. This results in no line between drawn from the last graphics cursor position to the new position. The graphics cursor is now sitting at coordinate 100,100 awaiting the next directive.
DRAW: Drawing Lines
Straight lines can be drawn in eight directions using letter designated directive directions. Figure 12 below shows the eight directions available.
Figure 12: Line drawing directives
Using these directives lets draw a square that has 100 pixel side lengths. To draw a line simply choose the direction you wish to draw followed by the number of pixels to move in that direction. For example, "D100" would draw a line in the downward direction at a length of 100 pixels.
Modify your code as follows:
'--------------
' DRAW Example
'--------------
SCREEN _NEWIMAGE(800, 600, 32) ' enter an 800x600 32bit color graphics screen
DRAW "BM399,299" ' move the pen to the center of the screen
DRAW "BM100,100" ' move the pen to coordinate 100,100 on the screen
' Draw a square with 100 pixel side lengths
DRAW "R100" ' move the pen right 100 pixels to draw a line
DRAW "D100" ' move the pen down 100 pixels to draw a line
DRAW "L100" ' move the pen left 100 pixels to draw a line
DRAW "U100" ' move the pen up 100 pixels to draw a line
Directives can be linked together into one large string holding a complex shape that can then be called upon and drawn.
Once again, modify your code:
'--------------
' DRAW Example
'--------------
SCREEN _NEWIMAGE(800, 600, 32) ' enter an 800x600 32bit color graphics screen
DRAW "BM399,299" ' move the pen to the center of the screen
DRAW "BM100,100" ' move the pen to coordinate 100,100 on the screen
' Draw a square with 100 pixel side lengths
Square$ = "R100D100L100U100" ' directive string containing a square to draw
DRAW Square$ ' draw the square
This time the line drawing directives were combined into one string named Square$. Spacing the directives apart in the string is not required as DRAW will parse out the individual directives. Using the eight line drawing directives you can create simple primitive shapes.
NOTE: Even though spaces are not required or needed within the strings you can add them for clarity. For example, this line of code would be acceptable as well:
Square$ = "R100 D100 L100 U100" ' directive string containing a square to draw
( This code can be found at .\tutorial\Lesson5\DrawPrimitives.bas )
'--------------
' DRAW Example
'--------------
SCREEN _NEWIMAGE(800, 600, 32) ' enter an 800x600 32bit color graphics screen
DRAW "BM20,100" ' move pen to coordinate 20,100
' Create four primitive shapes
Square$ = "R100D100L100U100" ' a square
Triangle$ = "F100L200E100" ' a triangle
RightTriangle$ = "F100L100U100" ' a right triangle
Hexagon$ = "R33F33D33G33L33H33U33E33" ' a hexagon
' Draw the primitive shapes in a row
DRAW Square$
DRAW "BM230,100"
DRAW Triangle$
DRAW "BM340,100"
DRAW RightTriangle$
DRAW "BM460,100"
DRAW Hexagon$
Figure 13: Primitive Shapes
DRAW: Rotating Lines
While the "E", "F", "G", and "H" line directives offer slanted line drawing they are restricted to 45 (E), 135 (F), 225 (G), and 315 (H) degree angles respectively. What if you wanted to draw a 30 degree line, or a 60 degree line, is this possible? Yes and it's done by rotating the pen using the "A" and "TA" angle directives. Let's discuss the "TA" directive (turn angle) first by starting off with a simple program that draws a straight line upward.
Start a new program in your IDE and type the following in:
'---------------------------
' DRAW Example Using Angles
'---------------------------
SCREEN _NEWIMAGE(800, 600, 32) ' enter an 800x600 32bit color graphics screen
' Draw two lines, one at 0 degrees and the other at 30 degrees
DRAW "BM399,299" ' center the pen on the screen
DRAW "NU250" ' draw a line going up 250 pixels long (0 degrees)
DRAW "TA30" ' rotate pen by 30 degrees counter-clockwise
DRAW "NU250" ' draw a line going up 250 pixels long (30 degrees)
Before we get into rotation did you notice the "N" directive added to the "U" line directives above?
DRAW "NU250" ' draw a line going up 250 pixels long (0 degrees)
DRAW "NU250" ' draw a line going up 250 pixels long (30 degrees)
When an "N" directive is added to one of the eight line direction drawing directives seen in Figure 12 the line is drawn but then the graphics cursor pen is returned to the line starting position. Both lines in the example program above need to be drawn from the center of the screen. So, instead of issuing the "BM399,299" directive twice, once before each line drawn, we can simply tell DRAW to return the pen back the beginning of the line after it has been drawn.
Both lines of code that draw the upward line are identical. Why did the first one draw a line straight up as expected but the second line of identical code draw a line 30 degrees to the left? The "TA30" directive rotated all the drawing directives counter-clockwise by 30 degrees as seen in Figure 14 below.
Figure 14: All line directives rotated by 30 degrees
As you can see in Figure 14 above the "U" line directive now points in the 30 degree direction. If you were to issue the directive:
DRAW "TA180" ' rotate pen by 180 degrees counter-clockwise
then UP would become DOWN and LEFT would become RIGHT. This rotation of the entire compass structure does take some getting used to but some very interesting things can be done with this.
The "TA" directive allows rotation ranges from 1 to 359 for counter-clockwise rotation and -1 to -359 for clockwise rotation. Setting the "TA" directive to 0, "TA0", resets the drawing directives to their original position.
Let's revisit drawing the square and add some rotation into the mix.
Modify your code to the following:
'---------------------------
' DRAW Example Using Angles
'---------------------------
SCREEN _NEWIMAGE(800, 600, 32) ' enter an 800x600 32bit color graphics screen
' Spirograph (for those that remember such a thing)
DRAW "BM399,299" ' center the pen on the screen
Square$ = "R100D100L100U100" ' directive string containing a square to draw
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
DRAW "TA30" ' rotate pen to 30 degrees
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
DRAW "TA60" ' rotate pen to 60 degrees
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
DRAW "TA90" ' rotate pen to 90 degrees
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
DRAW "TA120" ' rotate pen to 120 degrees
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
DRAW "TA150" ' rotate pen to 150 degrees
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
DRAW "TA180" ' rotate pen to 180 degrees
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
DRAW "TA210" ' rotate pen to 210 degrees
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
DRAW "TA240" ' rotate pen to 240 degrees
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
DRAW "TA270" ' rotate pen to 270 degrees
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
DRAW "TA300" ' rotate pen to 300 degrees
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
DRAW "TA330" ' rotate pen to 330 degrees
DRAW Square$ ' draw the square
Figure 15: A digital Spirograph image
By rotating the pen at 30 degree intervals and redrawing the square each time a pattern emerges. The SLEEP commands were inserted so you could see the drawing process as it happens. If you are a child of the 70s or perhaps the 80s you'll remember the Spirograph. These types of patterns can be created using complex shapes and line rotation.
By the way, there is a MUCH easier way to do this and get the same result:
'---------------------------
' DRAW Example Using Angles
'---------------------------
SCREEN _NEWIMAGE(800, 600, 32) ' enter an 800x600 32bit color graphics screen
' Spirograph (for those that remember such a thing)
DRAW "BM399,299" ' center the pen on the screen
Square$ = "R100D100L100U100" ' directive string containing a square to draw
FOR Angle% = 0 TO 330 STEP 30 ' cycle from 0 to 330 in steps of 30
DRAW "TA" + STR$(Angle%) ' rotate pen to Angle% degrees
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
NEXT Angle%
NOTE: Save this code as Spirograph.BAS before continuing, you'll need it again shortly.
In this much smaller version of the same code we use a FOR...NEXT loop to count from 0 to 330 in STEPs of 30. The integer variable Angle% will hold the current value of the loop.
The next line of code contains a little string manipulation magic:
DRAW "TA" + STR$(Angle%) ' rotate pen to Angle% degrees
Lesson 9 deals with string manipulation and the STR$() being one of the statements covered. Since the DRAW statement requires strings as directives we can't pass values to it such as those contained in Angle%. We first need to convert the value contained within Angle% to a string and then add that string to the "TA" string directive.
For instance, let's say the current value of Angle% is 180. The STR$() statement will convert that integer value of 180 to a string of characters containing "180". Now this string can be added to the end of "TA" to form a new string that contains the characters "TA180", something that the DRAW statement can understand.
The use of strings for a statement that relies so heavily on numeric values may seem counterintuitive and is probably one of the reasons this statement is often overlooked. The reason for this choice lies with the age of the statement. Remember that the DRAW statement has been around since the early 1980s for those first IBM PCs and PC compatibles through the use of GWBasic. The very first IBM PC could be ordered with as little as 16KB of RAM. Purchasing an IBM PC with 64KB to 256KB of RAM was quite expensive with the standard 640KB of RAM becoming more common years later. GWBasic could only take advantage of up to a 64KB chunk of RAM. However, that full 64KB was not available to to the programmer. The GWBasic language interpreter along with other overhead considerations such as HEAP and stack space took a little more than half of the 64KB available, leaving the programmer with anywhere between 24KB to 29KB of RAM to program in. Imagine having only 24KB of RAM to write your program in! It was common back then and forced BASIC programmers to become very creative and as a consequence created very skilled programmers.
Using a string as a mechanism to pass directives to the DRAW statement allowed the programmer to very tightly pack many directives into a very small amount of RAM. Using multiple lines of code to draw individual directives would have been costly so packing them into a tight string was the solution. That's why spaces are not even needed to separate the directives in the strings; a space character meant another precious byte of RAM used. QB64 doesn't have any of these RAM limitations. If fact, you can create a string of up to 2GB in length (2,147,483,647 bytes!) that contains DRAW statement directives that can create any masterpiece you can dream up.
If you were lucky enough to be around during the arcade boom of the late 1970s and early 1980s you'll remember BattleZone. By using the DRAW statement's rotation feature you can easily recreate the radar screen from that game. Don't worry about any new statements you may see in the code. We'll eventually get to those.
Type in the following program:
( This code can be found at .\tutorial\Lesson5\Radar.bas )
' Radar.BAS
'
' DRAW statement demo
' Create a simulated radar scope using DRAW statements. (Remember BattleZone?)
' by Terry Ritchie 04/05/24
' Note that rotation within the DRAW command moves in a counter-clockwise fashion
DIM Green~& ' bright green
DIM DarkGreen~& ' dark green
DIM GreenStr$ ' bright green as a string value
DIM Degree% ' degree of pen rotation
DIM RadarScreen& ' radar screen overlay image
Green~& = _RGB32(0, 255, 0) ' bright green
DarkGreen~& = _RGB32(0, 127, 0) ' dark green
GreenStr$ = STR$(Green~&) ' bright green in string format
RadarScreen& = _NEWIMAGE(800, 600, 32) ' an image to hold the radar screen
_DEST RadarScreen& ' draw on radar screen overlay image
CLS ' remove transparency from image
CIRCLE (399, 299), 250, Green~& ' draw the radar screen overlay
CIRCLE (399, 299), 200, DarkGreen~&
CIRCLE (399, 299), 150, DarkGreen~&
CIRCLE (399, 299), 100, DarkGreen~&
CIRCLE (399, 299), 50, DarkGreen~&
LINE (149, 299)-(649, 299), DarkGreen~&
LINE (399, 49)-(399, 549), DarkGreen~&
SCREEN _NEWIMAGE(800, 600, 32) ' enter a graphics screen
DRAW "C" + GreenStr$ ' set DRAW pen color to bright green
COLOR Green~& ' set text foreground to bright green
DO ' begin main program loop
_LIMIT 60 ' 6 seconds for a full sweep
_PUTIMAGE , RadarScreen& ' erase last image with radar screen
DRAW "BM399,299" ' move pen to center of screen
DRAW "TA" + STR$(Degree%) ' rotate pen
DRAW "NU250" ' draw line don't update pen location
LOCATE 2, 2 ' set text cursor
PRINT USING "SCANNING ### DEGREES"; ABS(359 - Degree%) ' print current rotation degree
Degree% = Degree% - 1 ' rotate clockwise
IF Degree% = -1 THEN Degree% = 359 ' reset when needed
_DISPLAY ' update screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when the ESC key pressed
_FREEIMAGE RadarScreen& ' remove radar screen overlay from RAM
SYSTEM ' return to the operating system
Figure 16: No enemies spotted on radar
The second DRAW rotation directive, "A", allows quick rotation through 90 degree intervals. The "A" directive allows four 90 degree counter-clockwise rotations:
DRAW "A0" ' reset to normal rotation 0 degrees
DRAW "A1" ' rotate counter-clockwise 90 degrees
DRAW "A2" ' rotate counter-clockwise 180 degrees
DRAW "A3" ' rotate counter-clockwise 270 degrees
This allows for quick rotation to the four 90 degree points on the compass. These four statements are identical to:
DRAW "TA0" ' reset to normal rotation 0 degrees
DRAW "TA90" ' rotate counter-clockwise 90 degrees
DRAW "TA180" ' rotate counter-clockwise 180 degrees
DRAW "TA270" ' rotate counter-clockwise 270 degrees
DRAW: Coloring Lines
All QB64 basic graphic statements will use bright white ( _RGB32(255, 255,255) ) as the default color if no specific color is specified. This is also true for the DRAW statement. The "C" directive is used to change line color when paired with the _RGB32() statement along with the use of the aforementioned STR$() statement:
DRAW "C" + STR$(_RGB32(RED, GREEN, BLUE))
Load the Spirograph code again ( Spirograph.BAS ) into your IDE and modify it a bit to add color.
Make the following changes:
'-------------------------------------
' DRAW Example Using Angles and Color
'-------------------------------------
SCREEN _NEWIMAGE(800, 600, 32) ' enter an 800x600 32bit color graphics screen
' Spirograph (for those that remember such a thing)
Red$ = "C" + STR$(_RGB32(255, 0, 0)) ' directive to change line color to red
Green$ = "C" + STR$(_RGB32(0, 255, 0)) ' directive to change line color to green
Blue$ = "C" + STR$(_RGB32(0, 0, 255)) ' directive to change line color to blue
Yellow$ = "C" + STR$(_RGB32(255, 255, 0)) ' directive to change line color to yellow
Square$ = Red$ + "R100" + Green$ + "D100" + Blue$ + "L100" + Yellow$ + "U100" ' colored square
DRAW "BM399,299" ' center the pen on the screen
FOR Angle% = 0 TO 330 STEP 30 ' cycle from 0 to 330 in steps of 30
DRAW "TA" + STR$(Angle%) ' rotate pen to Angle% degrees
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
NEXT Angle% ' leave when angle equals 330
Figure 17: A dash of color added
Let's put what we've learned so far into a small demonstration of the power of the DRAW statement.
WARNING: Members of the QB64PE forum have reported staring at this for 20 minutes at a time! LOL
Type the following program into your IDE:
( This code can be found at .\tutorial\Lesson5\Hypno.bas )
' Hypno.BAS (you are getting very sleepy)
'
' DRAW statement demo
' by Terry Ritchie 04/05/24
'
DIM Angle% ' pen angle
DIM Length% ' line length
DIM Count% ' line counter
DIM Gray% ' gray color level
DIM Direction% ' gray level direction
SCREEN _NEWIMAGE(800, 600, 32) ' enter a graphics screen
Gray% = 0 ' set gray color level
Direction% = 1 ' set gray level direction
DO ' begin main program loop
_LIMIT 10 ' 10 times a second should be nice
Angle% = 0 ' reset pen angle
Length% = 1 ' reset line length
Count% = 0 ' reset line counter
DRAW "BM399,299" ' move pen to the center of screen
DO ' begin spiral draw loop
DRAW "C" + STR$(_RGB(Gray%, Gray%, Gray%)) ' change gray level of line color
DRAW "TA" + STR$(Angle%) ' rotate pen
DRAW "R" + STR$(Length%) ' draw line to right at rotated angle
Angle% = Angle% + 15 ' increase angle of rotation
IF Angle% MOD 180 = 0 THEN Length% = Length% + 1 ' increase length at top and bottom
IF Angle% = 360 THEN Angle% = 0 ' reset angle when needed
Gray% = Gray% + Direction% ' increase/decrease gray color level
IF Gray% = 255 OR Gray% = 0 THEN Direction% = -Direction% ' reverse gray level direction
Count% = Count% + 1 ' increment line counter
LOOP UNTIL Count% = 1550 ' 1550 lines should do it
_DISPLAY ' update the screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave when ESC pressed
SYSTEM ' return to the operating system
Figure 18: Groovy
DRAW: Painting
The DRAW statement also includes a directive to allow your creations to be painted in the same way the PAINT statement was described earlier using the "Pf,b" directive. Once again you'll need to use the _RGB32() and STR$() statements along with the directive:
DRAW "P" + STR$(_RGB32(RED, GREEN, BLUE) + "," + STR$(_RGB32(RED, GREEN, BLUE))
The first _RGB32() statement is the fill color (f) and the second _RGB32() statement is the border color (b). A little demonstration program first before continuing on.
Type in the following code:
'-------------------------
' DRAW example - Painting
'-------------------------
SCREEN _NEWIMAGE(800, 600, 32)
' The following line creates a paint directive to fill dark green inside
' of a bright green border.
DarkGreenPaint$ = "P" + STR$(_RGB32(0, 127, 0)) + "," + STR$(_RGB32(0, 255, 0))
Green$ = "C" + STR$(_RGB32(0, 255, 0)) ' directive for bright green color
Square$ = "R100D100L100U100" ' directive to draw a square
DRAW "BM349,249" ' position cursor so square gets drawn centered
DRAW Green$ + Square$ ' draw a bright green square
' The cursor needs to be off the line and inside the square to paint.
' Either one of the following methods will work:
'DRAW "BM399,299" ' center the graphics cursor inside the box using blind move
'DRAW "BF10" ' move the cursor diagonally down using a blind draw
DRAW DarkGreenPaint$ ' paint dark green inside the bright green square
When you run the code the bright green square will not get painted. This is because the graphics cursor is still sitting at the end of the last line drawn. The graphics cursor needs to be moved off the line and inside the square for the paint directive to work correctly.
The "B" blind directive discussed earlier is used to perform this move, either by using it along with the "M" move directive or any of the line directional drawing directives. Line 21 uses a blind move to move the graphics cursor to the center of the square:
DRAW "BM399,299" ' center the graphics cursor inside the box using blind move
Line 23 using a blind draw that uses the line drawing directive "F" to move the graphics cursor at a 135 angle for 10 pixels to get the graphics cursor off the line and inside the square:
DRAW "BF10" ' move the cursor diagonally down using a blind draw
Remove the REM ( ' ) from the beginning of either one of these lines to see the result.
Figure 19: A painted square
The DRAW statement makes easy work of doing background effects as you saw with Hypno.BAS that created Figure 18 above. Adding the ability to paint your drawings can create even more elaborate effects as seen in the next example program below.
Type the following code in:
( This code can be found at .\tutorial\Lesson5\DrawBoxes.bas )
' DrawBoxes.BAS
'
' DRAW example using paint feature
' by Terry Ritchie 04/05/24
' Using the paint feature in DRAW can be very taxing on the CPU.
' Change BOXSIZE% to 20 below and see the result.
CONST BOXSIZE% = 100 ' side length of rotating squares
DIM Angle% ' pen rotation angle
DIM Gray% ' paint color
DIM Direction% ' paint color direction
DIM s$ ' side length of square in string format
DIM s2$ ' 1/2 side length of square in string format
DIM Box$ ' string used to draw a square
DIM White$ ' color white in string format
DIM Black$ ' color black in string format
DIM x% ' x coordinate counter
DIM y% ' y coordinate counter
SCREEN _NEWIMAGE(800, 600, 32) ' enter a graphics screen
White$ = STR$(_RGB(255, 255, 255)) ' create white string
Black$ = STR$(_RGB(0, 0, 0)) ' create black string
s$ = STR$(BOXSIZE) ' create side length string
s2$ = STR$(BOXSIZE \ 2) ' create 1/2 side length string
Box$ = "D" + s2$ + "L" + s$ + "U" + s$ + "R" + s$ + "D" + s2$ ' create string to draw a square
Direction% = 1 ' set paint color direction
Gray% = 0 ' set paint color
DO ' begin main program loop
CLS ' clear the screen
_LIMIT 60 ' no faster than 60 FPS
FOR y% = BOXSIZE \ 2 TO 600 - BOXSIZE \ 2 STEP BOXSIZE ' cycle square x center points
FOR x% = BOXSIZE \ 2 TO 800 - BOXSIZE \ 2 STEP BOXSIZE ' cycle square y center points
DRAW "BM" + STR$(x%) + "," + STR$(y%) ' move pen to square center point
DRAW "TA" + STR$(Angle%) ' rotate pen
DRAW "C" + Black$ ' make pen color black
DRAW "R" + s2$ ' move pen to square border
DRAW "C" + White$ ' make pen color white
DRAW Box$ ' draw a square
DRAW "BM" + STR$(x%) + "," + STR$(y%) ' move pen back to center of square
DRAW "P" + STR$(_RGB(Gray%, Gray%, Gray%)) + "," + White$ ' paint inside the square
NEXT x%
NEXT y%
_DISPLAY ' update the screen with changes
Gray% = Gray% + Direction% ' increase/decrease gray color level
IF Gray% = 255 OR Gray% = 0 THEN Direction% = -Direction% ' reverse gray level direction
Angle% = Angle% + 1 ' increase pen angle
IF Angle% = 360 THEN Angle% = 0 ' reset angle when needed
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
SYSTEM ' return to operating system
Figure 20: Rotating and color changing boxes
The paint directive, just like the PAINT statement, can be very taxing on the CPU and considerably slow down your code. Change the value for BOXSIZE% in line 9 from 100 to 20 and then run the code again to see the result. Keep this in mind when using DRAW's paint directive especially if animation is in play.
DRAW: Resizing
The final DRAW directive to be discussed in the "S" directive used for resizing pixel lengths. Load Spirograph.BAS once again into your IDE and add the line:
DRAW "S8" ' increase the pen size by 2
as seen in the code below:
'---------------------------
' DRAW Example Using Angles
'---------------------------
SCREEN _NEWIMAGE(800, 600, 32) ' enter an 800x600 32bit color graphics screen
' Spirograph (for those that remember such a thing)
DRAW "BM399,299" ' center the pen on the screen
DRAW "S8" ' increase the pen size by 2
Square$ = "R100D100L100U100" ' directive string containing a square to draw
FOR Angle% = 0 TO 330 STEP 30 ' cycle from 0 to 330 in steps of 30
DRAW "TA" + STR$(Angle%) ' rotate pen to Angle% degrees
DRAW Square$ ' draw the square
SLEEP ' wait for a key press
NEXT Angle%
The rotated squares are now twice the size almost filling the entire screen.
Resizing works as follows:
DRAW "S1" ' draw at 25% size (zoom out)
DRAW "S2" ' draw at 50% size (zoom out)
DRAW "S3" ' draw at 75% size (zoom out)
DRAW "S4" ' draw at 100% size (normal )
DRAW "S5" ' draw at 125% size (zoom in )
DRAW "S6" ' draw at 150% size (zoom in )
DRAW "S7" ' draw at 175% size (zoom in )
DRAW "S8" ' draw at 200% size (zoom in )
DRAW "S9" ' draw at 225% size (zoom in )
' etc.., etc..
It's also possible to use decimal values to get even more precise with your sizing. For example, these will work just fine:
DRAW "S.5" ' draw at 12.5% size (zoom out)
DRAW "S2.5" ' draw at 62.5% size (zoom out)
DRAW: An Interesting DRAW Example
I'm one of those programmers that has rarely taken advantage of the DRAW statement throughout the years. Funny enough the Connect Four game I had published back in 1992 used the DRAW statement extensively to create the title screen and the checker's star and eagle images. (You can view more about that here.) So I knew how powerful the command was but for some reason still mostly ignored it. And that's a shame, because while adding this section to the tutorial I again realized just how powerful this statement can be.
Also, while adding the DRAW and POINT() statements to the tutorial an idea hit me where I could use DRAW to perform rotational point calculations around a circle. Instead of using trigonometry as is the norm why not have DRAW do the heavy math for me?
The basic idea is simple: Draw a line straight up, use POINT(0) and POINT(1) to get the x,y endpoint of the line (the graphics cursor now sits there), return back to the beginning of the line, rotate one degree, draw the line again, get the endpoint, and on and on around a full 360 degrees. This would gather the x and y values of every endpoint around the circle! No heavy math needed!
I wrote the following program to take advantage of this. The program draws an analog clock on the screen with working moving hands. The hour, minute, and second hand endpoints were all gathered using the method previously described. Furthermore, DRAW's rotational directive is used to draw rotated triangles and squares at the end of each hand. This program highlights many of the other graphics statements discussed previously along with DRAW's powerful directives helping to round out the program.
( This code can be found at .\tutorial\Lesson5\DrawClock.bas )
'---------------------------------
' DRAW example - DrawClock.BAS
' Displays a working analog clock
' by Terry Ritchie - 04/07/24
'---------------------------------
' Note: The entire clock will auto adjust its size based on the SIZE value below
CONST SIZE% = 500 ' screen size (must be 200 or larger)
'+---------------+
'| Define colors |
'+---------------+
CONST WHITE~& = _RGB32(255, 255, 255) ' bright white
CONST BLACK~& = _RGB32(0, 0, 0) ' black
CONST GRAY~& = _RGB32(127, 127, 127) ' gray
CONST DARKGRAY~& = _RGB32(63, 63, 63) ' dark gray
CONST OFFWHITE1~& = _RGB32(255, 254, 255) ' slightly off bright white
CONST OFFWHITE2~& = _RGB32(254, 255, 255) ' slightly off bright white
CONST RED~& = _RGB32(255, 0, 0) ' bright red
CONST OFFRED~& = _RGB32(254, 0, 0) ' slightly off bright red
'+------------------------------+
'| Variable declaration section |
'+------------------------------+
DIM TOx%(360) ' outer tick mark x coordinate
DIM TOy%(360) ' outer tick mark y coordinate
DIM TIx%(360) ' inner tick mark x coordinate
DIM TIy%(360) ' inner tick mark y coordinate
DIM Hx%(360) ' hour and second hand x coordinate
DIM Hy%(360) ' hour and second hand y coordinate
DIM Mx%(360) ' minute hand x coordinate
DIM My%(360) ' minute hand y coordinate
DIM Ex%(360) ' hand opposite end x coordinate
DIM Ey%(360) ' hand opposite end y coordinate
DIM ClockFace& ' image of clock face
DIM Angle% ' 360 degree angle counter
DIM Aa% ' absolute value of a negative angle
DIM Center% ' center coordinate of clock face
DIM CenterPen$ ' pen center point of clock face
DIM OuterTick$ ' line extended out to outer tick mark
DIM HourTick$ ' line extended out to hour hand mark
DIM InnerTick$ ' line extended out to inner tick mark
DIM MinuteTick$ ' line extended out to minute and second hand mark
DIM EndTick$ ' line extended out to the hand opposite end mark
DIM Square$ ' DRAW directive to draw a square
DIM Triangle$ ' DRAW directive to draw a triangle
DIM Aplus1% ' current angle plus 1
DIM Aminus1% ' current angle minus 1
DIM TickColor~& ' color of tick marks around the face perimeter
DIM Hour% ' the current hour value
DIM Minute% ' the current minute value
DIM Second% ' the current second value
DIM OldSecond% ' the previous second value
DIM Scount% ' the current 1/6th of a second
DIM Sdegree% ' degree the second hand points to
DIM Mdegree% ' degree the minute hand points to
DIM Hdegree% ' degree the hour hand points to
DIM Minus180% ' the opposite degree of any hand
DIM l% ' the standard length of a DRAW line
DIM L1$ ' string form of l%
DIM L2$ ' string form of l% * 2
IF SIZE% < 200 THEN ' clock window too small?
PRINT ' yes
PRINT " SIZE of clock must be 200 or greater" ' report error to user
END ' end program
END IF
ClockFace& = _NEWIMAGE(SIZE, SIZE, 32) ' an image to hold the clock's face
'+---------------------------------------------------------+
'| Calculate the DRAW line lengths based on SIZE of window |
'+---------------------------------------------------------+
l% = INT(SIZE / 100) ' standard length of DRAW line
L1$ = STR$(l%) ' string form of standard DRAW line length
L2$ = STR$(l% * 2) ' string form of twice the length of the draw line
'+------------------------------------------------+
'| Create the square and triangle DRAW directives |
'+------------------------------------------------+
Square$ = "U" + L1$ + "R" + L1$ + "D" + L2$ + "L" + L2$ + "U" + L2$ + "R" + L1$ + "BD" + STR$(l% + 1)
Triangle$ = "F" + L2$ + "L" + STR$(l% * 4) + "E" + L2$ + "BD" + L1$
'+------------------------------------------------------+
'| Create the coordinate locations using DRAW and POINT |
'| (No math! No trigonometry! COS and SIN be damned! |
'+------------------------------------------------------+
Center% = SIZE \ 2 - 1 ' calculate center of clock face coordinates
CenterPen$ = "BM" + STR$(Center%) + "," + STR$(Center%) ' center pen on clock face
OuterTick$ = CenterPen$ + "U" + STR$(INT(SIZE * .45)) ' extend outer tick line (90%)
HourTick$ = CenterPen$ + "U" + STR$(INT(SIZE * .30)) ' extend hour hand line (60%)
InnerTick$ = CenterPen$ + "U" + STR$(INT(SIZE * .43)) ' extend inner tick line (86%)
MinuteTick$ = CenterPen$ + "U" + STR$(INT(SIZE * .44)) ' extend minute and second hand line (88%)
EndTick$ = CenterPen$ + "U" + STR$(INT(SIZE * .05)) ' extend opposite hand endpoint line ( 5%)
_DEST ClockFace& ' draw on clock face image in background
_SOURCE ClockFace& ' get coordinates from clock face image
FOR Angle% = 0 TO -359 STEP -1 ' cycle clockwise through 360 degrees
Aa% = ABS(Angle%) ' the absolute value of the angle
DRAW "TA" + STR$(Angle%) ' rotate the pen by the current angle
DRAW OuterTick$ ' extend line to outer tick mark
TOx%(Aa%) = POINT(0) ' get the endpoint coordinates of the line
TOy%(Aa%) = POINT(1) ' these are the outer tick mark coordinates
DRAW HourTick$ ' extend line to hour and second hand mark
Hx%(Aa%) = POINT(0) ' get the endpoint coordinates of the line
Hy%(Aa%) = POINT(1) ' these are the hour and second hand coordinates
DRAW InnerTick$ ' extend line to inner tick mark
TIx%(Aa%) = POINT(0) ' get the endpoint coordinates of the line
TIy%(Aa%) = POINT(1) ' these are the inner tick mark coordinates
DRAW MinuteTick$ ' extend line to minute hand mark
Mx%(Aa%) = POINT(0) ' get the endpoint coordinates of the line
My%(Aa%) = POINT(1) ' these are the minute hand coordinates
DRAW EndTick$ ' extend line to opposite hand endpoint
Ex%(Aa%) = POINT(0) ' get the endpoint coordinates of the line
Ey%(Aa%) = POINT(1) ' these are the hand's opposite side coordinates
NEXT Angle% ' leave loop when angle is -359
CLS ' clear the clock face image
'+-----------------------------------------------+
'| Draw the clock face onto the clock face image |
'+-----------------------------------------------+
CIRCLE (Center%, Center%), SIZE * .46, GRAY ' draw outer edge of tick mark boundary (92%)
CIRCLE (Center%, Center%), SIZE * .42, GRAY ' draw inner edge of tick mark boundary (84%)
PAINT (Mx%(0), My%(0)), GRAY, GRAY ' paint the tick mark area
CIRCLE (Center%, Center%), SIZE * .46, WHITE ' draw the outer edge of the clock face (92%)
FOR Angle% = 0 TO 359 STEP 6 ' cycle through 360 degrees at one second intervals
IF Angle% MOD 30 = 0 THEN ' is this an hour mark?
TickColor~& = WHITE ' yes, make it white
ELSE ' no
TickColor~& = BLACK ' make it black
END IF
Aplus1% = Angle% + 1 ' get the angle just above
Aminus1% = Angle% - 1 ' get the angle just below
IF Aplus1% = 360 THEN Aplus1% = 0 ' correct for angle too high
IF Aminus1% = -1 THEN Aminus1% = 359 ' correct for angle too low
PSET (TOx%(Aminus1%), TOy%(Aminus1%)), TickColor~& ' set graphics cursor location
LINE -(TOx%(Aplus1%), TOy%(Aplus1%)), TickColor~& ' draw tick mark box
LINE -(TIx%(Aplus1%), TIy%(Aplus1%)), TickColor~&
LINE -(TIx%(Aminus1%), TIy%(Aminus1%)), TickColor~&
LINE -(TOx%(Aminus1%), TOy%(Aminus1%)), TickColor~&
PAINT (Mx%(Angle%), My%(Angle%)), TickColor~&, TickColor~& ' paint the tick mark box
NEXT Angle% ' leave loop when angle is 359
PAINT (Center%, Center%), DARKGRAY, GRAY ' paint the inner clock face
'+--------------------+
'| Begin main program |
'+--------------------+
SCREEN _NEWIMAGE(SIZE, SIZE, 32) ' enter a graphics screen
DO ' begin main program loop
_LIMIT 6 ' 6 degrees per second (1/6th of second)
_PUTIMAGE (0, 0), ClockFace& ' use clock face image to clear screen
'+-------------------------------------------------------+
'| Calculate hand rotation degrees from the current time |
'+-------------------------------------------------------+
OldSecond% = Second% ' record the previous second value
Hour% = VAL(LEFT$(TIME$, 2)) ' get the value of the current hour
Minute% = VAL(MID$(TIME$, 4, 2)) ' get the value of the current minute
Second% = VAL(RIGHT$(TIME$, 2)) ' get the value of the current second
IF Hour% >= 12 THEN Hour% = Hour% - 12 ' remove military time
Scount% = Scount% + 1 ' increment 1/6th second counter
IF OldSecond% <> Second% THEN ' has a second passed?
Scount% = 0 ' yes, reset counter
SOUND 1660, .05 ' tick sound
END IF
Sdegree% = Second% * 6 + Scount% ' calculate the second hand degree
Mdegree% = Minute% * 6 + INT(Second% / 12) ' calculate the minute hand degree
Hdegree% = Hour% * 30 + INT(Minute% / 2) ' calculate the hour hand degree
'+--------------------+
'| Draw the hour hand |
'+--------------------+
Minus180% = Hdegree% - 180 ' calculate the hour hand opposite degree
IF Minus180% < 0 THEN Minus180% = 360 + Minus180% ' keep the degree a positive value
LINE (Center%, Center%)-(Hx%(Hdegree%), Hy%(Hdegree%)), WHITE ' draw the hour hand
DRAW "TA" + STR$(-Hdegree%) ' rotate pen to same degree as hour hand
DRAW "C" + STR$(OFFWHITE1) + Triangle$ ' draw an off white triangle at end of hand
DRAW "P" + STR$(OFFWHITE1) + "," + STR$(OFFWHITE1) ' paint the triangle off white
LINE (Center%, Center%)-(Ex%(Minus180%), Ey%(Minus180%)), WHITE ' draw the hour hand opposite side
DRAW "TA" + STR$(-Hdegree%) ' rotate pen to same degree as hour hand
DRAW "C" + STR$(OFFWHITE1) + Square$ ' draw an off white square at opposite end
DRAW "P" + STR$(OFFWHITE1) + "," + STR$(OFFWHITE1) ' paint the square off white
'+----------------------+
'| Draw the minute hand |
'+----------------------+
Minus180% = Mdegree% - 180 ' calculate the minute hand opposite degree
IF Minus180% < 0 THEN Minus180% = 360 + Minus180% ' keep the degree a positive value
LINE (Center%, Center%)-(TIx%(Mdegree%), TIy%(Mdegree%)), WHITE ' draw the minute hand
DRAW "TA" + STR$(-Mdegree%) ' rotate pen to same degree as minute hand
DRAW "C" + STR$(OFFWHITE2) + Triangle$ ' draw an off white triangle at end of hand
DRAW "P" + STR$(OFFWHITE2) + "," + STR$(OFFWHITE2) ' paint the triangle off white
LINE (Center%, Center%)-(Ex%(Minus180%), Ey%(Minus180%)), WHITE ' draw the minute hand opposite side
DRAW "TA" + STR$(-Mdegree%) ' rotate pen to same degree as minute hand
DRAW "C" + STR$(OFFWHITE2) + Square$ ' draw an off white square at opposite end
DRAW "P" + STR$(OFFWHITE2) + "," + STR$(OFFWHITE2) ' paint the square off white
'+----------------------+
'| Draw the second hand |
'+----------------------+
Minus180% = Sdegree% - 180 ' calculate the second hand opposite degree
IF Minus180% < 0 THEN Minus180% = 360 + Minus180% ' keep the degree a positive value
LINE (Center%, Center%)-(TIx%(Sdegree%), TIy%(Sdegree%)), RED ' draw the second hand
DRAW "TA" + STR$(-Sdegree%) ' rotate pen to same degree as second hand
DRAW "C" + STR$(OFFRED) + Triangle$ ' draw an off red triangle at end of hand
DRAW "P" + STR$(OFFRED) + "," + STR$(OFFRED) ' paint the triangle off red
LINE (Center%, Center%)-(Ex%(Minus180%), Ey%(Minus180%)), RED ' draw the second hand opposite side
CIRCLE STEP(0, 0), l%, OFFRED ' draw an off red circle at opposite end
PAINT STEP(0, 0), OFFRED, OFFRED ' paint the circle off red
'+----------------------+
'| Draw the center post |
'+----------------------+
CIRCLE (Center%, Center%), l% * 2, BLACK ' draw a black circle at center of screen
PAINT (Center%, Center%), BLACK, BLACK ' paint the circle black
CIRCLE (Center%, Center%), l%, DARKGRAY ' draw dark gray circle at center of screen
_DISPLAY ' update the screen with changes
LOOP UNTIL _KEYDOWN(27) ' leave main loop when ESC key pressed
_FREEIMAGE ClockFace& ' remove clock face image from RAM
SYSTEM ' return to the operating system
Figure 21: Clock drawn with the aid of DRAW
There are a few statements and concepts in the program that will be discussed in later lessons, but as you can see the DRAW statement is a very powerful command and in the right situation is very useful too. Take some time to play around with DRAW's feature rich set of directive options available. Above all, don't forget this statement exists!
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:
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 22: 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 22.
- 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 23 below.
Figure 23: 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
POINT
DRAW
_RGB32()
_NEWIMAGE()
_PIXELSIZE
_DISPLAY
_AUTODISPLAY
_LIMIT
New concepts introduced in this lesson: