AutoGrafix is a User Defined Graphic (UDG) character editor that lets users design all 21 UDG characters (A–U) either interactively with a joystick/keyboard cursor or by entering raw byte values numerically. The program uses custom character shapes stored at address 64000 to draw a box cursor on screen, manipulating the character definition table pointer at address 23606 (CHARS) to temporarily swap the definition of the block character used for the editing grid. Two 8×8 arrays, S(8,8) for the current pixel state and A(21,8) for all 21 UDG byte values, track edits in memory; completed designs can be saved and reloaded as a named DATA file via tape using SAVE/LOAD with the array. Joystick input is supported on both ports using STICK, allowing simultaneous keyboard and joystick operation for cursor movement and bit toggling.
Program Structure
AutoGrafix is organized as a collection of named subroutines, each preceded by a REM label rendered in inverse video. The main flow is:
- Line 1080 – INITIALIZE: dimensions arrays, sets constants, POKEs custom box characters, reads current UDG values.
- Line 850 – TITLES: splash screen with animated UDG pixels, joystick port selection, and tape-load option.
- Line 570 – MAIN: menu loop dispatching to cursor editor, numeric editor, save, or quit.
- Line 200 – EXISTING: renders old and new UDG bitmaps side by side; prompts Create or Modify.
- Line 80 – CURSOR: interactive pixel editor using joystick/keyboard.
- Line 410 – TURNON: toggles a single pixel bit and updates the byte array.
- Line 490 – NUMERIC: byte-by-byte numeric entry mode.
- Line 790 – STORE: writes all edited
A()values back to UDG RAM viaPOKE USR X$+N. - Line 1020 – SAVE: saves array
A()to tape by name; verifies after writing.
Custom Cursor Character Technique
The program stores two custom 8×8 pixel patterns at address 64000 (a solid box outline and a filled box) using DATA statements at lines 1150–1180. The system variable CHARS (address 23606) normally points 256 bytes before the character set. By POKEing CHARS with BOX1 (248) and CHARS+1 with BOX2 (247), the program temporarily redirects the character lookup so that printing CHR$ 8 (backspace character code) renders the custom box instead. After each operation the pointer is restored to 0/60 to re-establish the normal character set. This avoids consuming a UDG slot for the cursor.
UDG Address Calculation
The address of a UDG byte is computed throughout as:
PEEK UDG + 256 * PEEK (UDG+1) + (code-1)*8 + byte_index
where UDG = 23675, a two-byte system variable holding the base address of the UDG table. This is the canonical two-byte little-endian peek idiom used instead of the simpler USR X$ form, possibly for slight speed or flexibility.
Pixel State Arrays
S(8,8) holds the current on-screen pixel state for one UDG being edited, using the ASCII codes 65 (‘A’) for a set pixel and 66 (‘B’) for a clear pixel. This allows PRINT AT x,y; CHR$ S(r,c) to redraw any cell directly. A(21,8) stores the byte values for all 21 UDGs simultaneously, enabling batch store operations and the numeric entry mode.
Joystick and Keyboard Input
Lines 130 and 140 use compound boolean arithmetic — a hallmark of ZX Spectrum BASIC optimization — to compute cursor delta in a single expression. Both joystick ports and the 5/6/7/8/SPACE keys are checked simultaneously. The variable PORT is set during initialization based on which fire button the user presses first, selecting between STICK port 1 and port 2 for movement, while fire detection always reads port 2.
Bit Manipulation in TURNON (line 410)
When a pixel is toggled at cursor position (X,Y), the corresponding bit in A(code, X-XO) is updated using powers of two:
- Turn off (A→B):
A(code,row) = A(code,row) - 2^(8-(Y-YO)) - Turn on (B→A):
A(code,row) = A(code,row) + 2^(8-(Y-YO))
The column offset 8-(Y-YO) maps screen Y position to bit weight (MSB on the left). This is a straightforward but correct bit-field manipulation without bitwise operators, since Spectrum BASIC lacks them.
Bitmap Rendering in EXISTING (line 250–300)
Bytes are decoded bit-by-bit in the inner loop at line 280 using repeated integer division by 2 to extract each bit LSB-first, then printed in reverse column order (14-C) so that the visual result is MSB-left. When MOD=1, the decoded pixel values are also written back into S() for subsequent cursor-mode editing.
Tape I/O
SAVE E$ DATA A() and LOAD E$ DATA A() serialize the entire 21×8 numeric array to/from tape under a user-supplied filename. After loading, GO SUB 790 with FILE=1 iterates all 21 UDGs and POKEs each byte into the UDG area, making the loaded designs immediately active.
Notable Idioms and Details
F$is a 32-space string (line 1140) used throughout as a fast screen-clearing print for specific rows, avoiding a fullCLS.TITLEflag (set to 1 at line 1130) controls whether EXISTING renders the old UDG column at all during the splash sequence versus normal editing.- Line 870 uses a
FLASHvariable (distinct from the keyword) as a flag — a subtle name collision that works because Spectrum BASIC resolves the keyword from context. - The
MODvariable (line 1110) tracks whether the user chose Modify mode; it is set to 0 after display and to 1 when modifications are applied, controlling the EXISTING subroutine’s dual-purpose behavior. - Line 760 prints UDG preview characters \a through \u (UDGs A–U) in a grid so the user can see current designs at a glance after each editing session.
- Line 1240 saves the program itself with
LINE 1for auto-run.
Potential Issues
- The variable name
VALat line 260 shadows the built-inVALfunction. Spectrum BASIC allows this, but any subsequent use ofVAL "..."would be ambiguous; however, no such use appears in this program. - Similarly,
FLASHis used as both a keyword and a variable name (line 860/870). This works because Spectrum BASIC distinguishes them by context, but it is an unusual and potentially confusing pattern. - The numeric entry in line 530 accepts values via
INPUT A(code,C)without an upper range guard at entry time; the out-of-range check at line 540 aborts the whole session rather than re-prompting for the same byte.
Content
Source Code
10 REM *** Program AUTOGRAFIX
20 REM *** Updated 4/10/84 AM
30 POKE 23658,8:POKE 23609,10
40 GO SUB 1080
50 GO SUB 850
60 LET TITLE=0
70 GO TO 570
80 REM \{20}\{1} CURSOR \{20}\{0}
90 LET X=XO+1:LET Y=YO+1
100 LET X1=X:LET Y1=Y
110 IF INKEY$=" " OR STICK (2,PORT)=1 THEN GO SUB 410
120 LET X1=X:LET Y1=Y
130 LET X=X+((INKEY$="6" OR STICK (1,PORT)=6 OR STICK (1,PORT)=2 OR STICK (1,PORT)=10) AND X<14)-((INKEY$="7" OR STICK (1,PORT)=5 OR STICK (1,PORT)=1 OR STICK (1,PORT)=9) AND X>7)
140 LET Y=Y+((INKEY$="8" OR STICK (1,PORT)=8 OR STICK (1,PORT)=9 OR STICK (1,PORT)=10) AND Y<26)-((INKEY$="5" OR STICK (1,PORT)=4 OR STICK (1,PORT)=5 OR STICK (1,PORT)=6) AND Y>19)
150 IF X <>X1 OR Y <>Y1 THEN BEEP .01, RND*30:POKE CHARS,BOX1:POKE (CHARS+1),BOX2:PRINT AT X1,Y1; CHR$ S(X1-XO,Y1-YO):POKE CHARS,0:POKE (CHARS+1),60
160 PRINT OVER 1; INK 9; AT X,Y;"*"
170 IF INKEY$= CHR$ 13 THEN PRINT AT 2,0;F$; AT 3,0;F$; AT 17,0;F$; AT 18,0;F$:BEEP .02,15:RETURN
180 IF INKEY$="0" THEN FOR J=1 TO 8:LET A(code,J)= PEEK (PEEK UDG+256* PEEK (UDG+1)+(code-1)*8+J-1):NEXT J:PRINT AT 2,0;F$; AT 3,0;F$; AT 17,0;F$; AT 18,0;F$:GO TO 570
190 GO TO 110
200 REM \{20}\{1} EXISTING \{20}\{0}
210 POKE CHARS,BOX1:POKE (CHARS+1),BOX2
220 PRINT AT 6,0:FOR N=1 TO 8
230 BEEP .01, RND*30:PRINT TAB 19-15*TITLE;"BBBBBBBB":NEXT N:IF TITLE=1 THEN POKE CHARS,0:POKE (CHARS+1),60:RETURN
240 IF TITLE=0 THEN POKE CHARS,0:POKE (CHARS+1),60:FOR N=1 TO 8:PRINT AT (6+N),28;"0":NEXT N:POKE CHARS,BOX1:POKE (CHARS+1),BOX2
250 FOR B=0 TO 7
260 LET VAL= PEEK (PEEK UDG+256* PEEK (UDG+1)+B+8*(code-1))
270 IF NUMB=1 THEN LET VAL=A(code,B+1)
280 FOR C=1 TO 8:LET XX= INT (VAL/2):LET AORB=VAL-2*XX:LET VAL=XX
290 PRINT AT B+7,13*MOD+14-C; CHR$ (66-AORB):IF MOD=1 THEN LET S(B+1,27-C-YO)=66-AORB
300 NEXT C:NEXT B
310 POKE CHARS,0:POKE (CHARS+1),60
320 IF MOD=1 THEN LET MOD=0:FOR N=1 TO 8:PRINT AT 6+N,28;A(code,N):NEXT N:RETURN
330 IF NUMB=1 THEN RETURN
340 FOR V=1 TO 4:BEEP .015, RND*20:NEXT V:POKE CHARS,0:PRINT AT 17,8;"\{20}\{1}\{20}\{1}C\{20}\{0}REATE OR \{20}\{1}M\{20}\{0}ODIFY?":PAUSE 0
350 IF INKEY$="M" THEN LET MOD=1:POKE CHARS,BOX1:POKE (CHARS+1),BOX2:BEEP .02,10:PRINT AT 17,0;F$:GO TO 250
360 IF INKEY$ <>"C" THEN GO TO 350
370 BEEP .02,10:PRINT AT 17,8;" "
380 FOR N=1 TO 8:FOR M=1 TO 8:LET S(N,M)=66:NEXT M:LET A(code,N)=0:NEXT N
390 POKE CHARS,0:POKE (CHARS+1),60
400 RETURN
410 REM \{20}\{1} TURNON \{20}\{0}
420 BEEP .02, RND*20-20
430 POKE CHARS,BOX1:POKE (CHARS+1),BOX2
440 IF S(X-XO,Y-YO)=65 THEN PRINT AT X,Y; CHR$ 66:LET S(X-XO,Y-YO)=66:LET A(code,X-XO)=A(code,X-XO)-2^(8-(Y-YO)):GO TO 460
450 IF S(X-XO,Y-YO)=66 THEN PRINT AT X,Y; CHR$ 65:LET S(X-XO,Y-YO)=65:LET A(code,X-XO)=A(code,X-XO)+2^(8-(Y-YO))
460 POKE CHARS,0:POKE (CHARS+1),60
470 PRINT AT X,28;" ":PRINT AT X,28;A(code,X-XO)
480 RETURN
490 REM \{20}\{1} NUMERIC \{20}\{0}
500 FOR C=1 TO 8:PRINT AT 21,(C-1)*4;A(code,C):NEXT C
510 FOR V=1 TO 4:BEEP .015, RND*20:NEXT V
520 FOR C=1 TO 8:FOR B=1 TO 8:PRINT AT 19,(B-1)*4;"#";B:NEXT B
530 PRINT AT 16,0;"ENTER NEW VALUE FOR BYTE:"; AT 17,0;"(OR VALUE >255 TO QUIT)":PRINT FLASH 1; AT 19,(C-1)*4;"#";C:INPUT A(code,C):PRINT AT 21,(C-1)*4;" ":PRINT AT 21,(C-1)*4;A(code,C)
540 BEEP .1, RND*20:PRINT AT 19,28;"#8":IF A(code,C)>255 OR A(code,C)<0 THEN FOR J=1 TO 8:LET A(code,J)= PEEK (PEEK UDG+256* PEEK (UDG+1)+(code-1)*8+J-1):NEXT J:GO TO 570
550 NEXT C:LET MOD=1:POKE CHARS,BOX1:POKE (CHARS+1),BOX2:GO SUB 250
560 RETURN
570 REM \{20}\{1} MAIN \{20}\{0}
580 LET NUMB=0
590 POKE 23658,8
600 CLS :FOR V=1 TO 4:BEEP .015, RND*20:NEXT V:PRINT AT 7,0;"PRESS A-U TO CREATE WITH CURSOR"; AT 9,6;"'X' TO CREATE WITH NUMBERS"
610 PRINT AT 11,6;"'Y' TO SAVE DATA"; AT 13,6;"'Z' TO STOP PROGRAM":PAUSE 0
620 LET X$= INKEY$:IF X$<"A" OR X$>"Z" OR X$="V" OR X$="W" THEN GO TO 620
630 BEEP .1,5:IF X$="Y" THEN GO TO 1020
640 IF X$="Z" THEN CLS :STOP
650 IF X$="X" THEN LET NUMB=1
660 CLS :FOR V=1 TO 4:BEEP .015, RND*20:NEXT V:PRINT AT 0,3;"SPECIFY UDG CHARACTER (A-U)"
670 PAUSE 0:LET X$= INKEY$:IF X$<"A" OR X$>"U" THEN GO TO 670
680 CLS :PRINT AT 0,2;"THIS IS THE UDG CHARACTER '";X$;"'"; AT 5,6;"OLD"; AT 5,19;"NEW"
690 LET code= CODE X$-64
700 GO SUB 200
710 IF NUMB=1 THEN GO SUB 490:PRINT AT 16,0;F$; AT 17,0;F$; AT 19,0;F$; AT 21,0;F$
720 FOR V=1 TO 4:BEEP .015, RND*20:NEXT V:PRINT AT 2,1;"USE JOYSTICK OR KEYS TO MOVE *"; AT 3,1;"USE \{20}\{1}SPACE\{20}\{0} OR \{20}\{1}FIRE\{20}\{0} TO ALTER BIT"; AT 17,6;"PRESS \{20}\{1}ENTER\{20}\{0} TO STORE"; AT 18,6;"PRESS '0' TO QUIT"
730 GO SUB 80
740 GO SUB 790
750 PRINT AT 16,0;F$; AT 19,0;F$
760 PRINT AT 17,0;"A B C D E F G H I J K "; AT 18,0;" \a \b \c \d \e \f \g \h \i \j \k"; AT 20,0;" L M N O P Q R S T U "; AT 21,0;" \l \m \n \o \p \q \r \s \t \u "
770 PRINT #1;" Press any key to continue":PAUSE 0
780 GO TO 570
790 REM \{20}\{1} STORE \{20}\{0}
800 IF FILE=1 THEN FOR J=1 TO 21:LET code=J:LET X$= CHR$ (code+64)
810 FOR N=0 TO 7
820 POKE USR X$+N,A(code,N+1)
830 NEXT N:IF FILE=1 THEN NEXT J
840 RETURN
850 REM \{20}\{1} TITLES \{20}\{0}
860 LET FLASH=0
870 PRINT INVERSE 1; FLASH FLASH; AT 7,14;" "; AT 8,14;" AUTOGRAFIX "; AT 9,14;" "; INVERSE 0; FLASH 0; AT 14,14;"\*1984 B. Schaffer":IF FLASH=1 THEN RETURN
880 GO SUB 200:FOR N=1 TO 25:PRINT AT XO+ INT (RND*8+1),YO+ INT (RND*8+1)-15;"\::":BEEP .10, RND*25:NEXT N
890 LET FLASH=1:GO SUB 870:PAUSE 120:FOR C=1 TO 10:BEEP .008,4*C:NEXT C
900 CLS :PRINT AT 8,2;"PRESS \{20}\{1}FIRE BUTTON\{20}\{1} #2\{20}\{0}\{20}\{0} TO USE"; AT 9,2;"JOYSTICK #2 AND/OR KEYBOARD"; AT 12,2;"\{20}\{1}FIRE BUTTON #1\{20}\{0} OR \{20}\{1}SPACE BAR\{20}\{0}"; AT 13,1;"WILL PERMIT USE OF JOYSTICK #1"; AT 14,1;"AND/OR KEYS (5,6,7,8 AND SPACE)"
910 IF INKEY$=" " OR STICK (2,1)=1 THEN LET PORT=1:BEEP .2,15:GO TO 940
920 IF STICK (2,2)=1 THEN LET PORT=2:BEEP .2,15:GO TO 940
930 GO TO 910
940 CLS :PRINT AT 10,0;"\{20}\{1}L\{20}\{0}OAD A UDG FILE FROM TAPE?"; AT 12,0;"\{20}\{1}C\{20}\{0}REATE NEW / ALTER EXISTING UDG?"
950 PAUSE 0:IF INKEY$ <>"L" AND INKEY$ <>"C" THEN GO TO 950
960 BEEP .01,15:IF INKEY$="C" THEN RETURN
970 CLS :INPUT AT 10,0;"NAME OF INPUT FILE? ";E$:BEEP .1,20
980 BEEP .1,20:CLS :PRINT AT 10,7;"Press recorder \{20}\{1}PLAY\{20}\{0}"; AT 12,4;"Press any key to continue":PAUSE 0
990 CLS :BEEP .1,15:LOAD E$ DATA A()
1000 CLS :PRINT AT 10,7;"Press recorder \{20}\{1}STOP\{20}\{0}"; AT 12,4;"Press any key to continue":PAUSE 0
1010 CLS :BEEP .1,10:LET FILE=1:GO SUB 790:LET FILE=0:RETURN
1020 REM \{20}\{1} SAVE \{20}\{0}
1030 CLS :INPUT AT 10,0;"OUTPUT FILE NAME? ";E$:BEEP .1,20:CLS
1040 BEEP .1,15:SAVE E$ DATA A()
1050 CLS :PRINT AT 8,7;"Press recorder \{20}\{1}STOP\{20}\{0}"; AT 10,4;"\{20}\{1}REWIND\{20}\{0} tape to verify save"; AT 12,10;"Press any key"; AT 14,7;"Press recorder \{20}\{0}\{20}\{0}\{20}\{1}PLAY\{20}\{0}":PAUSE 0
1060 CLS :BEEP .1,10:VERIFY E$ DATA :BEEP .1,10:PRINT AT 10,7;"Press recorder \{20}\{1}STOP\{20}\{0}"; AT 12,4;"Press any key to continue"
1070 PAUSE 0:CLS :BEEP .1,10:GO TO 570
1080 REM \{20}\{1} INITIALIZE \{20}\{0}
1090 DIM S(8,8):DIM A(21,8)
1100 BRIGHT 0:FLASH 0:INVERSE 0:PAPER 1:BORDER 1:INK 9:CLS
1110 LET MOD=0
1120 LET FILE=0:LET G=9900:LET ADDRESS=64000:LET CHARS=23606:LET UDG=23675
1130 LET TITLE=1:LET BOX1=248:LET BOX2=247:LET XO=6:LET YO=18
1140 LET F$=" "
1150 DATA 255,255,255,255,255,255,255,255
1160 DATA 255,129,129,129,129,129,129,255
1170 FOR M=0 TO 1:FOR N=0 TO 7
1180 READ USER:POKE ADDRESS+(8*M)+N,USER:NEXT N:NEXT M
1190 FOR N=1 TO 21:FOR M=1 TO 8:LET A(N,M)= PEEK (PEEK UDG+256* PEEK (UDG+1)+(M-1)+8*(N-1))
1200 NEXT M:NEXT N
1210 RETURN
1220 REM \{20}\{1} CORRECT \{20}\{0}
1230 POKE CHARS,0:POKE (CHARS+1),60
1240 SAVE "AutoGrafix" LINE 1
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.

