ExFont 1.1

Developer(s): Randy Moulder
Date: 1985
Type: Program
Platform(s): TS 2068

ExFont 1.1 is a character set editor that allows users to redesign both the main character set (CHR$ 32–127) and user-defined graphics (CHR$ 144–164) by directly POKEing new bitmap data into memory. The program stores a replacement font as a binary CODE block loaded at address 64600, and switches between the built-in and custom character sets by POKEing the system variable at addresses 23606–23607 (the CHARS pointer). Navigation within the 8×8 pixel grid editor is handled via the STICK command, with FIRE toggling individual bits between set (█) and clear (space). Each row of a character is represented as an 8-character string using block graphics for 1-bits, and subroutines at lines 100 and 200 convert between these binary string representations and decimal byte values for PEEK/POKE operations. A short ascending BEEP sequence at line 9100 serves as an audio prompt before each INPUT statement.


Program Analysis

Program Structure

The program is organized into clearly labeled subroutine blocks, each preceded by a REM banner. Execution begins at line 8000 (initialization), which loads the CODE block, sets up variables, displays notes, and falls through to the main menu at line 8100. The broad structure is:

  1. Lines 100–190: Binary string → decimal byte conversion (bin→dec) subroutine
  2. Lines 200–400: Decimal byte → binary string conversion (dec→bin) subroutine
  3. Lines 1000–1260: Character range selection loop
  4. Lines 1500–1620: Per-character display and editing driver
  5. Lines 3000–3950: Interactive pixel editor (STICK-based)
  6. Lines 5002–5050: Commit edits, POKE bytes back, refresh display
  7. Lines 8000–8090: Initialization
  8. Lines 8100–8300: Main menu
  9. Lines 8400–8430: SAVE routines
  10. Lines 8500–8640: Notes/startup screen
  11. Lines 9000–9070: Character set toggle
  12. Lines 9100–9150: Audio prompt subroutine
  13. Lines 9200–9450: Print character set comparison screen

Memory and Font Addressing

The program exploits the TS2068/Spectrum system variable at addresses 23606–23607, which holds a pointer to the character set bitmap data (offset by 256 from CHR$ 32). Two font bases are used:

FontCHARS pointer valueBase address
Built-in ROM font15360 (POKE 23606,0 : POKE 23607,60)15616
Custom font (RAM)64344 (POKE 23606,88 : POKE 23607,251)64600

The custom CODE block occupies 936 bytes starting at 64600, covering CHR$ 32–127 (768 bytes) and UDGs 144–164 (168 bytes). Address calculation at line 1520 handles UDGs separately: add=64472+((c-32)*8), while the main set uses add=64600+((c-32)*8) at line 1525. CLEAR 64599 at line 8010 protects this area from BASIC’s heap.

Binary String Representation

Each 8×8 character is held in a two-dimensional string array N$(8,8). Each row is an 8-character string where a set pixel is represented by the block graphic █ (\::, character 143) and a clear pixel by UDG \a (character 144, displayed as a space-equivalent). The dec→bin subroutine (line 200) extracts bits with repeated halving and remainder testing; the bin→dec subroutine (line 100) reconstructs bytes by scanning the string and treating the block graphic as “1” and the UDG as “0” via CODE(B$(p))-48 — though this relies on the specific code values of those characters rather than a true ASCII “0”/”1″ comparison.

The Pixel Editor

The interactive editor (lines 3200–3950) places an 8×8 grid at screen rows 14–21, columns 15–22. A flashing cursor character \d (UDG d, code 157) marks the current cell. The editor polls STICK(1,1) for directional movement and STICK(2,1) for the fire button simultaneously with INKEY$ for ENTER to quit.

Movement is handled by four labeled goto targets (3600=up, 3700=down, 3800=left, 3900=right), each printing the underlying cell content before repositioning the cursor and wrapping at boundaries. Bit toggling at lines 3400 and 3500 spins on STICK(2,1)>0 to wait for button release before returning to the main poll loop, preventing repeated triggers.

The cursor position in screen coordinates (x,y) maps to the N$ array via N$(x-13, y-14): row index x-13 (range 1–8) and column index y-14 (range 1–8).

Notable Techniques

  • CHARS pointer toggling: The toggle routine at line 9000 reads the current pointer value and swaps between the two known states, allowing live comparison of ROM and custom glyphs.
  • Side-by-side font comparison (line 9200): For each character, the routine temporarily POKEs the ROM base, prints the ROM glyph, then POKEs the custom base and prints the custom glyph, yielding a two-column comparison in a single loop pass.
  • Audio prompt (line 9100): An eight-step ascending BEEP sequence (semitones 2,5,7,10,12,15,17,20) plays before every INPUT, providing consistent audio feedback without interrupting screen layout.
  • Separator string Z$: Initialized at line 8020 as 32 cursor-left characters (\b = CHR$ 8), used as a horizontal rule by overprinting the current line.
  • Skipping undefined codes: Lines 1512–1515 skip codes 124, 126, and 128–143 (the block graphics range), since these are not user-redefinable in the same way, and the loop variable c is advanced directly rather than using a conditional skip flag.

Bugs and Anomalies

  • Missing line 5000: Line 3280 executes GO TO 5000 on ENTER, but line 5000 does not exist; execution continues at line 5002. This is an intentional technique — the interpreter advances to the next available line.
  • CODE K$ at line 8215: The condition CODE INKEY$>5 OR CODE INKEY$<1 calls INKEY$ twice in a single statement with no guarantee the same key is read both times; however, since this line has no effect (it falls through to 8220 regardless), it is functionally harmless dead code.
  • bin→dec code arithmetic: Line 130 uses CODE(B$(p))-48. After substitution, B$(p) is either “1” (CODE 49, giving 1) or “0” (CODE 48, giving 0), so the arithmetic is correct — but only because the subroutine first replaces the graphic characters with literal digit characters at lines 122–123.
  • UDG address formula: Line 1520 uses 64472+((c-144)*8+8*112) simplified to 64472+((c-32)*8). Since UDGs start at code 144 and their bitmaps are placed after the 96 main-set characters (96×8=768 bytes from 64600 = 65368), the formula actually points 896 bytes past 64600 for code 144, which is 65368 — matching a layout where UDG data immediately follows the main set. This is consistent with the 936-byte save length (768+168=936).

Content

Appears On

Related Products

Related Articles

And now, here are a few more words about the last contest. First, we would like to thank all of...

Related Content

Image Gallery

Source Code

    1 REM \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b          existo:ray software 85          \b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b        
    2 REM   ExFont 1.1
    3 REM   R. L. Moulder
    4 REM   206 Yeonas Dr. SW
    5 REM   Vienna, Va  22180             
  100 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c            bin->dec                 
  108 FOR x=1 TO 8
  109 LET B$=N$(x)
  110 LET n(2,x)=0
  120 FOR p=1 TO 8
  122 IF B$(p)="\::" THEN LET B$(p)="1"
  123 IF B$(p)="\a" THEN LET B$(p)="0"
  130 LET n(2,x)=n(2,x)*2+(CODE (B$(p))-48)
  140 NEXT p
  145 POKE n(1,x),n(2,x)
  150 NEXT x
  190 RETURN 
  200 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c            dec->bin                 
  210 DIM B$(8)
  220 LET p=8
  230 LET B$="\a\a\a\a\a\a\a\a"
  240 LET d=n(2,x)
  250 LET n=INT (d/2)
  260 IF d-2*n=1 THEN LET B$(p)="\::"
  280 LET d=n
  290 LET p=p-1
  300 IF d>0 THEN GO TO 250
  390 LET N$(x)=B$
  400 RETURN 
 1000 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c            set CHR$ and loop         
 1010 CLS 
 1020 PRINT AT 3,0;" Enter code # of FIRST and LAST  characters to be re-designed."
 1030 INK 3: PRINT AT 5,0;Z$: INK 5
 1040 PRINT AT 6,0;" Main Character Set  32 \g\h 127                                   User Graphics      144 \g\h 164"
 1045 PRINT AT 10,0;" NOTE: Program skips code #'s    128 thru 143, 124 & 126         automatically"
 1050 PRINT AT 13,0;" To change one character only    use its code # for both inputs."
 1060 INK 3: PRINT AT 15,0;Z$
 1200 GO SUB 9100: INPUT "Enter First Code # ";c1
 1205 IF c1<32 OR c1>164 THEN GO TO 1200
 1210 INK 5: PRINT AT 17,1;"CODE ";c1;" (";CHR$ c1;")";" thru ";
 1220 GO SUB 9100: INPUT "Enter Second Code # ";c2
 1225 IF c2<32 OR c2>164 OR c2<c1 THEN GO TO 1220
 1230 PRINT "CODE ";c2;" (";CHR$ c2;")"
 1240 GO SUB 9100: PRINT AT 20,1;"Is This Correct? (y/n) "
 1245 LET K$=INKEY$
 1250 IF K$="n" THEN GO TO 1000
 1255 IF K$="y" THEN GO TO 1500
 1260 GO TO 1245
 1500 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c           print address & cont.         
 1505 CLS : INK 3
 1510 FOR c=c1 TO c2
 1512 IF c=124 THEN LET c=125
 1513 IF c=126 THEN LET c=127
 1515 IF c>127 AND c<144 THEN LET c=144
 1520 IF c>=144 THEN LET add=64472+((c-32)*8)
 1525 IF c<=127 THEN LET add=64600+((c-32)*8)
 1528 INK 6: PRINT AT 1,3;"CHR$ CODE #": PRINT AT 1,14;c: INK 6: PAPER 1: PRINT AT 1,18;CHR$ c: INK 3: PAPER 0
 1530 FOR x=1 TO 8
 1535 LET n(1,x)=add+x-1: LET n(2,x)=PEEK (add+x-1)
 1540 PRINT AT 2+x,1;n(1,x);TAB 8;n(2,x);
 1550 GO SUB 200
 1555 PRINT TAB 15;N$(x)
 1560 NEXT x
 1570 GO SUB 3000
 1605 CLS 
 1610 NEXT c
 1620 GO TO 8100
 3000 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c              change CHR$ ?       
 3005 GO SUB 9100
 3010 INK 5: PRINT AT 15,0;"Change character? (y/n or ENTER)": INK 3
 3020 LET K$=INKEY$
 3030 IF K$="y" THEN GO TO 3200
 3040 IF K$="n" THEN RETURN 
 3045 IF CODE K$=13 THEN RETURN 
 3050 GO TO 3020
 3200 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c            Redesign CHR$        
 3205 INK 6: PRINT AT 15,0;"                                "
 3210 PRINT AT 13,0;"Use STICK to";AT 14,0;"move cursor."
 3211 PRINT AT 16,0;"Press FIRE";AT 17,0;"button to";AT 18,0;"flip bits."
 3212 PRINT AT 20,0;"Press ENTER";AT 21,0;"to quit."
 3215 INK 3: PRINT AT 13,15;"76543210": INK 5
 3220 FOR x=1 TO 8
 3230 INK 3: PRINT AT 13+x,14;x-1: INK 5: PRINT AT 13+x,15;N$(x)
 3240 NEXT x
 3250 LET x=14: LET y=15: FLASH 1: PRINT AT x,y;"\d": FLASH 0
 3260 LET st= STICK (1,1)
 3270 LET bt= STICK (2,1)
 3280 IF CODE INKEY$=13 THEN GO TO 5000
 3290 IF bt=1 AND N$(x-13,y-14)="\a" THEN GO TO 3500
 3300 IF bt=1 AND N$(x-13,y-14)="\::" THEN GO TO 3400
 3310 IF st=1 THEN GO TO 3600
 3320 IF st=2 THEN GO TO 3700
 3330 IF st=4 THEN GO TO 3800
 3340 IF st=8 THEN GO TO 3900
 3390 GO TO 3260
 3400 LET N$(x-13,y-14)="\a"
 3410 FLASH 1: PRINT AT x,y;"\d": FLASH 0
 3420 IF STICK (2,1)>0 THEN GO TO 3420
 3430 GO TO 3260
 3500 LET N$(x-13,y-14)="\::"
 3510 FLASH 1: PRINT AT x,y;"\d": FLASH 0
 3520 IF STICK (2,1)>0 THEN GO TO 3520
 3530 GO TO 3260
 3600 PRINT AT x,y;N$(x-13,y-14)
 3610 LET x=x-1
 3620 IF x<14 THEN LET x=21
 3640 FLASH 1: PRINT AT x,y;"\d": FLASH 0
 3650 GO TO 3260
 3700 PRINT AT x,y;N$(x-13,y-14)
 3710 LET x=x+1
 3720 IF x>21 THEN LET x=14
 3740 FLASH 1: PRINT AT x,y;"\d": FLASH 0
 3750 GO TO 3260
 3800 PRINT AT x,y;N$(x-13,y-14)
 3810 LET y=y-1
 3820 IF y<15 THEN LET y=22
 3840 FLASH 1: PRINT AT x,y;"\d": FLASH 0
 3850 GO TO 3260
 3900 PRINT AT x,y;N$(x-13,y-14)
 3910 LET y=y+1
 3920 IF y>22 THEN LET y=15
 3940 FLASH 1: PRINT AT x,y;"\d": FLASH 0
 3950 GO TO 3260
 5002 FOR x=12 TO 21
 5004 PRINT AT x,0;"              "
 5006 NEXT x
 5008 GO SUB 100
 5020 FOR x=1 TO 8
 5030 PRINT AT 13+x,1;n(1,x);"  ";n(2,x)
 5040 NEXT x
 5045 PAUSE 45
 5050 RETURN 
 8000 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c          initialize variables      
 8005 BORDER 0: PAPER 0: INK 5
 8010 CLEAR 64599
 8015 LOAD "cd"CODE 64600
 8020 LET Z$="\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b"
 8030 DIM N$(8,8)
 8040 DIM n(2,8)
 8050 POKE 23606,88: POKE 23607,251
 8090 GO SUB 8500
 8100 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c           print menu                
 8105 CLS 
 8110 PRINT AT 4,0;"existo:ray software   1  9  8  5"
 8115 INK 3: PRINT AT 3,0;Z$: INK 5
 8117 PRINT AT 2,0;"ExFont 1.1            2  0  6  8"
 8120 INK 3: PRINT AT 7,2;"1.": INK 5: PRINT AT 7,4;" Design Main Character Set       CHR$ 32 thru 127"
 8130 PRINT AT 9,5;"Or User Graphics                CHR$ 144 thru 164"
 8140 INK 3: PRINT AT 11,2;"2.": INK 5: PRINT AT 11,4;" Print Definable Characters"
 8150 INK 3: PRINT AT 12,2;"3.": INK 5: PRINT AT 12,4;" SAVE Program and Code"
 8160 INK 3: PRINT AT 13,2;"4.": INK 5: PRINT AT 13,4;" SAVE Code Only (64600,936)"
 8170 INK 3: PRINT AT 14,2;"5.": INK 5: PRINT AT 14,4;" Toggle Character Sets"
 8200 FLASH 1: INK 3: PRINT AT 17,2;"\g\h": FLASH 0: INK 5: PRINT AT 17,5;"Enter # 1 thru 5"
 8210 GO SUB 9100
 8215 IF CODE INKEY$>5 OR CODE INKEY$<1 THEN GO TO 8220
 8220 IF INKEY$="1" THEN GO TO 1000
 8230 IF INKEY$="2" THEN GO TO 9200
 8240 IF INKEY$="3" THEN GO TO 8400
 8250 IF INKEY$="4" THEN GO TO 8420
 8260 IF INKEY$="5" THEN GO TO 9000
 8300 GO TO 8220
 8400 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c              SAVE
 8405 PRINT AT 17,0;"                               ": FLASH 1: PRINT AT 17,7;"SAVING": FLASH 0
 8410 SAVE "ef" LINE 8000
 8420 SAVE "cd"CODE 64600,936
 8430 CLS : GO TO 8100
 8500 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c              NOTES      
 8505 CLS 
 8510 PRINT AT 4,0;"existo:ray software   1  9  8  5"
 8520 PRINT AT 2,0;"ExFont 1.1            2  0  6  8"
 8530 INK 3: PRINT AT 3,0;Z$
 8540 PRINT AT 0,0;" notes notes notes notes notes": INK 5
 8550 PRINT AT 7,2;"Use <GO TO 8100> to restart."
 8560 PRINT AT 9,2;"To use a new character set in   other programs:                       CLEAR 64599                     LOAD \fcd\fCODE 64600             POKE 23606,88                   POKE 23607,251"
 8570 PRINT AT 16,2;"To return to TS characters:           POKE 23606,0                    POKE 23607,60"
 8600 PRINT AT 20,0;"<c> to copy                     any other key to continue"
 8610 LET I$=INKEY$
 8620 IF I$="c" THEN COPY 
 8630 IF I$="" THEN GO TO 8610
 8640 RETURN 
 9000 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c              POKE CHARS            
 9010 LET p=PEEK 23606+256*PEEK 23607
 9020 IF p=64344 THEN GO TO 9040
 9030 IF p=15360 THEN GO TO 9060
 9040 POKE 23606,0: POKE 23607,60
 9050 CLS : GO TO 8100
 9060 POKE 23606,88: POKE 23607,251
 9070 CLS : GO TO 8100
 9100 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c           INPUT PROMPT BEEP        
 9105 BEEP .01,2
 9110 BEEP .01,5
 9115 BEEP .01,7
 9120 BEEP .01,10
 9125 BEEP .01,12
 9130 BEEP .01,15
 9135 BEEP .01,17
 9140 BEEP .01,20
 9150 RETURN 
 9200 REM \c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c\c           PRINT character set        
 9201 CLS 
 9210 PRINT "MAIN CHARACTER SET": PRINT "\e\e\e\e\e\e\e\e\e\e\e\e\e\e\e\e\e\e"
 9220 FOR x=32 TO 127
 9222 IF x=124 THEN LET x=125
 9223 IF x=126 THEN LET x=127
 9225 POKE 23606,0: POKE 23607,60
 9230 PRINT CHR$ x;
 9240 POKE 23606,88: POKE 23607,251
 9250 PRINT CHR$ x;"  ";
 9260 NEXT x
 9265 PRINT : PRINT : PRINT "USER GRAPHICS": PRINT "\e\e\e\e\e\e\e\e\e\e\e\e\e"
 9270 FOR x=144 TO 164
 9272 POKE 23606,0: POKE 23607,60
 9273 PRINT CHR$ (x-79);
 9275 POKE 23606,88: POKE 23607,251
 9280 PRINT CHR$ x;"  ";
 9290 NEXT x
 9295 GO SUB 9100
 9300 PRINT : PRINT : PRINT "<c>to COPY any other key to ret"
 9400 LET I$=INKEY$
 9410 IF I$="c" THEN COPY 
 9420 IF I$="" THEN GO TO 9400
 9450 CLS : GO TO 8100

Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.

Scroll to Top