A complete drawing package should allow the user to print characters on the bitmapped display. This month and next I will discuss this topic, and give more examples on the use of the drawing routines presented last month.
There are two methods for printing characters on a bitmapped display. We can POKE the dot patterns of the characters to the bitmapped RAM, or we can draw the characters onto the display.
Let's take a look at the first method, which is faster because no line-drawing routines are required.
POKEing To The Bitmap
The first step in POKEing characters to a bitmapped display is to choose the cell size, or dimensions, of our character set. The choice of the cell size can greatly affect the complexity of the routines which print the characters. If a convenient size is chosen, the routines will be simplified; if you are up for a challenge, you can write the routines to accept a variable cell size.
We will use a cell size of 8 dots high by 8 dots wide, for two reasons. First, a width of 8 dots is the number of dots which can be held in a byte. Second, there already exists a set of 8 x 8 characters in the 64's character ROM.
Actually, the 64's normal character display mode is very similar to what we want to accomplish in a bitmapped mode. The process involves character cells and some method of transferring the character dot patterns to the display. However, in the normal character display mode, the format is determined by the character codes found in a character array called screen memory. In screen memory, you can change only whole 8 x 8 characters, so your effective resolution winds up being 40 columns by 25 lines.
Blending Characters And The Bitmap
When using a bitmap display, you can control each dot. This implies that you can place a character at any X,Y position on the screen. This can certainly be done, though it is more difficult than placing a character code in screen memory. What complicates the task somewhat is that the 64's VIC-II chip organizes a bitmap display as groups of 8 x 8 dot cells.
It's possible for the 8 × 8 dot character pattern to span as many as four of the bitmap cells, two horizontally and two vertically. This doesn't create much of a problem vertically, but horizontally the bytes in the character dot pattern may have to be moved or shifted to span two bytes. In addition, when the bytes are added to the bitmap, the routine must not disturb the dots outside the shifted 8 dots of the character pattern.
Next, we must decide how to transfer the dot patterns so they will be visible against the bitmapped background.
Using Conditional Logic
One way of transferring the dot pattern is to add (logical OR) the dots in the pattern to the dots already in the display. Dots which are on in the character dot pattern are also turned on in the display. Dots which are on in the display remain on. This avoids erasing the background as a character is printed to the bitmapped display, but can result in illegible characters if there are too many dots already turned on in the background.
Another way to transfer the dot pattern is to flip (Exclusive-OR) the dots in the pattern into the bitmapped RAM. Dots in the bitmapped RAM which correspond to on dots in the dot pattern are flipped to the opposite state. The advantage of this technique is that it will make characters visible regardless of whether the background is on or off. However, characters can still be illegible if the background is not predominantly either on or off.
Or the transfer could be accomplished by writing the pattern directly into the bitmapped RAM. This type of transfer replaces the background with the character cell. We will use this technique.
A BASIC Example
Let's first demonstrate how the required routines might be implemented in BASIC. Unfortunately, like the drawing routines presented in earlier columns, the character routines are too slow to be really useful. To enhance their value as an example, we'll try to illustrate modular programming style as well.
One of the main aspects of modular programming is breaking main or primary tasks into smaller, more manageable tasks. Once the tasks have been broken down sufficiently, each may be implemented in a single routine. The more independent each of these separate routines is, the better. This allows you to concentrate on the details involved with the routine as it is written, without being distracted by the details involved with other routines. To show how printing to the bitmapped display might be broken into modules, let's take a look at the logical sub-divisions of this task.
Although this program isn't really complex enough to justify a modular approach, I prefer to keep the functions or tasks in separate routines, so long as the routines don't become embarrassingly simple. This helps while debugging, since the symptoms of the bug often eliminate a majority of the routines from consideration. It also helps keep you from accidentally tangling functions together.
When functions get tangled or intertwined, making one change may require making other changes, leading to a snowball effect. And finally, it is good practice to keep functions divided into separate routines when you write a complex program. How well the tasks are divided up can greatly affect how much effort it takes to write and debug the program.
Breaking Down The Task
Putting a character in a bitmapped display will involve transferring bytes into bitmapped RAM, so we will need a routine which does the transferring. We need another routine to calculate the character's position. We also need to know what to write; this will require two routines. We need a routine which will find and read the appropriate bytes in the character ROM. However, the dot patterns are organized based on screen codes, which are different from the Commodore 64 ASCII codes you normally print. This means we need a routine to convert the ASCII code to the corresponding screen code.
Finally, we need a routine to do the horizontal shifting necessary when the character byte needs to span two bytes in the bitmapped RAM. This gives us five routines to be implemented:
- Convert the character to screen code
- Read the character's dot pattern
- Calculate its position in the bitmap and amount of shift
- Shift a dot-pattern byte
- Put the dot-pattern byte in the bitmap
By dividing the tasks into well-defined and independent sections, it will be a little easier to implement them than if you tried to throw it all together in one routine. For example, converting the ASCII character code to a screen code can be done without concerning ourselves with where the ASCII code came from, or for what the screen code will be used. The shift section does not need to account for where the shift amount came from or what will be done with the shifted bytes.
Combining The Modules
Once we build the character print routine from these five sections, it is simple to build a string print routine using the character print routine. The result might be a BASIC program like the one that accompanies this article. The program uses the machine language routines discussed in this column in previous issues. Before running this program, you must run the BASIC loader presented in the May 1984 issue. The subroutine at line 100 converts ASCII code CH to the equivalent screen code SC. The subroutine at line 200 uses screen code CH to read the associated dot pattern into the array DP(). This subroutine also uses CP which points to the base of the character dot patterns in ROM.
The subroutine at line 300 uses the coordinates in X and Y to calculate an offset OF into the bitmap, and the shift amount SH. The subroutine at line 400 uses the shift amount SH to right-shift the byte in BY partially into B2. This means shifting dots out of the right end of the byte and into the left end of the other byte. This shift routine also makes the mask bytes, M1 and M2.
Finally, the subroutine at line 500 writes the bytes into the bitmap base of the offset OF, calculated earlier. This routine also uses the mask bytes to keep the necessary old bits from the bitmap bytes, before adding the new dot pattern bits. The subroutine at line 600 prints the character at the current coordinates specified by X and Y, and the subroutine at 700 prints a string at X,Y.
I have used logical operators (OR and AND) rather than division and the INT functions. For example, in line 320, the term (X AND -8) gives the same result as INT(X/8)*8. In the subroutine at line 200, the POKEs are required to turn off interrupts and make the character ROM accessible to the BASIC program.
The main routine uses the string-printing routine at line 700 to label the vertical axis for the plot of a sine wave. As you will see, the character printing is pretty slow. This part of the program would be much more useful written in machine language. Next month I will discuss the drawing method of putting characters in the bitmapped display, and present machine language routines for both.
Characters On A Bitmapped Display
10 REM PRINT CHARACTERS TO BIT-MAP : rem 63 20 JV = 49152 : REM JUMP TABLE : rem 6 30 CP = 53248 : REM LOC. OF CHAR. PATTERNS : rem 181 40 POKE 785, PEEK (JV + 28) : REM SETUP USR () : rem 8 50 POKE 786, PEEK (JV + 29) : rem 17 60 GOTO 1000 : rem 96 100 REM CONVERT CHAR. TO SCREEN CODE : rem 96 110 IF CH > 31 AND CH < 64 THEN SC = CH : RETURN : rem 249 120 IF CH > 63 AND CH < 96 THEN SC = CH - 64 : RETURN : rem 155 130 IF CH > 127 AND CH < 128 THEN SC = CH - 32 : RETURN : rem 200 140 IF CH > 127 AND CH < 192 THEN SC = CH - 64 : RETURN : rem 251 150 SC = CH - 128 : RETURN : rem 214 200 REM GET CHARACTER DOT PATTERN : rem 232 210 POKE 56334, PEEK (56344) AND 254 : rem 221 220 POKE 1, PEEK (1) AND 251 : rem 50 230 FOR IX = 0 TO 7 : rem 100 240 DP(IX) = PEEK (CP + SC * 8 + IX) : NEXT : rem 202 250 POKE 1, PEEK (1) OR 4 : rem 159 260 POKE 56334, PEEK (56334) OR 1 : rem 69 270 RETURN : rem 121 300 REM CALC OFFSET AND SHIFT COUNT : rem 43 310 TY = 199 - Y : SH = X AND 7 : rem 27 320 OF = (TYAND - 8) * 40 + (XAND - 8) + (TYAND7) : rem 106 330 RETURN : rem 118 400 REM SHIFT BYTE TO CORRECT POSITION : rem 84 410 B2 = 0 : M1 = 0 : M2 = 255 : IF SH = 0 THEN RETURN : rem 13 420 FOR K = 1 TO SH : B2 = B2/2 : rem 52 430 IF BY AND 1 THEN B2 = B2 OR 128 : rem 85 440 BY = BY/2 : M1 = (M1/2) OR128 : M2 = M2/2 : NEXT : rem 28 450 RETURN : rem 121 500 REM PUT BYTE AT X, Y : rem 24 510 GOSUB 300 : REM CALCULATE OF & SH : rem 171 520 GOSUB 400 : REM SHIFT OVER : rem 131 530 AD = 57344 + OF : REM GET ADDRESS FOR BY : rem 167 540 POKE AD, USR(OF) AND M1 OR BY : rem 230 550 IF SH = 0 THEN RETURN : rem 64 560 POKE AD + 8, USR(OF + 8) AND M2 OR B2 : rem 136 570 RETURN : rem 124 600 REM PUT CHARACTER AT X, Y : rem 114 610 GOSUB 100 : REM CONVERT CH : rem 114 620 GOSUB 200 : REM READ DOT PATTERN : rem 233 630 Y = Y + 8 : REM PUT CHAR. FROM TOP DOWN : rem 173 640 FOR IX = 0 TO 7: Y = Y-1: BY = DP (IX) : rem 136 650 GOSUB 500 : REM PUT BYTE : rem 251 660 NEXT : RETURN : rem 245 700 REM PUT STRING S$ AT X, Y : rem 52 710 FOR SP = 1 TO LEN(S$) : rem 218 720 CH = ASC (MID$(S$, SP,1)) : rem 123 730 GOSUB 600 : REM PUT THE CHARACTER : rem 53 740 X = X + 8 : NEXT : rem 100 750 RETURN : rem 124 1000 REM MAIN ROUTINE : rem 240 1010 SYS JV : SYS JV + 6, 0 : SYS JV + 9, 0, 1 : rem 237 1020 FOR I = 0 TO 10 : rem 100 1030 LB = -1 + I*.2 : S$ = STR$(LB) : rem 213 1050 X = 5 : Y = 46 + 10* I : GOSUB 700 : NEXT : rem 147 1060 SYS JV + 12, 32, 50 : SYS JV + 18, 32, 150 : rem 214 1070 SYS JV = 12, 32, 100 : SYS JV + 18, 319, 100 : rem 54 1080 FOR I = 0 TO 10 : rem 106 1090 X = 30 : Y = 50 + 10 * I : rem 246 1100 SYS JV + 12, X, Y : SYS JV + 18, X + 4, Y : rem 205 1110 NEXT : rem 2 1120 SYS JV + 12, 32, 100 : PI = 3.1416 : rem 124 1130 SX = 256/(2*PI) : SY = 50 : rem 71 1140 FOR I = 0 TO 2*PI STEP 2*PI/100 : rem 236 1150 SYS JV + 18, 32 + I*SX, 100 + SIN(I)*SY : rem 22 1160 NEXT : rem 7 9000 GET Z$ : I Z$ = "" THEN 9000 : rem 231 9010 SYS JV + 3 : rem 199