PAINT is a multi-mode pixel art drawing program that supports Draw, Line, Ray, Box, Brush, Circle, Copy, and Fill modes, all driven by joystick or keyboard. It uses the STICK command for joystick input and stores directional delta values in two-dimensional arrays for eight-way cursor movement at three speeds. The fill routine implements a scan-line flood-fill algorithm entirely in BASIC, with solid, 50%, and striped variants controlled by a grid-parity variable. The Copy mode can scale a rectangular region up or down by an integer factor, reading pixel-by-pixel with POINT and replotting. ON ERR is used extensively throughout as a boundary-collision and error-recovery mechanism, replacing conventional range checking.
Program Structure
The program is organized into a dispatcher loop and a collection of subroutines, each prefaced by a REM label. The main loop runs from lines 30–110, reading joystick state and keyboard input each frame, updating cursor position, and branching to whichever drawing subroutine is currently loaded into the variable mode. A long REM block from line 1570 onward serves as embedded documentation and can be deleted after reading.
| Line range | Role |
|---|---|
| 20 | Initialization entry point (POKE caps-lock off, branch to startup) |
| 30–110 | Main event loop: joystick read, key dispatch, cursor movement, draw call |
| 130–150 | Draw mode subroutine |
| 160–240 | Line / Arc mode subroutine |
| 250–260 | Brush mode (delegates to Line) |
| 270–320 | Box mode subroutine |
| 330–380 | Circle mode subroutine |
| 390–510 | Copy mode subroutine (with scaling) |
| 520–540 | Copy helper: redraw bounding box |
| 550–750 | Fill modes (solid, 50%, striped) — scan-line flood fill |
| 760–790 | Brush draw helper |
| 800–890 | Copy pixel-transfer subroutine |
| 900–990 | Wash / recolor subroutine |
| 1000–1290 | Keyboard handler |
| 1300–1410 | Ink/border cycling, tape load/save |
| 1420–1560 | Startup, array initialization, DATA |
| 1570–1740 | Embedded documentation REMs |
Cursor Movement and Direction Arrays
Direction deltas are stored in two parallel arrays x(14,3) and y(14,3) initialized from DATA at lines 1440–1450. Each row corresponds to one of 14 joystick/key directions; each column represents a speed (1 = slow, 2 = medium, 3 = fast). The variable b selects the column (speed), and a selects the row (direction). After adding the delta at line 70, the absolute-value trick at line 80 is used to wrap negative coordinates back to positive — a simplified modulo that works only because the coordinate space is symmetric around zero, and is an acknowledged quirk rather than a correct wrap.
Joystick input uses STICK(1,1) for direction and STICK(2,1) for the fire button, read each iteration at line 40. Key presses “5”–”8″ are mapped to direction indices by adding 6 to the digit value (line 1010), placing them at rows 11–14 in the direction arrays.
Drawing Modes and the Mode Variable
The active drawing subroutine is stored as a line number in mode, which is called via GO SUB mode at line 90. Switching modes sets mode to the appropriate entry point (e.g., 130 for Draw, 160 for Line, 270 for Box, 330 for Circle, 390 for Copy) and branches to line 120 to update the status display. This is a compact dispatch table pattern that avoids a large IF/ELSE chain in the main loop.
ON ERR as a Guard Mechanism
ON ERR is used pervasively as a boundary and error guard instead of explicit range checks. Before any PLOT, DRAW, or CIRCLE that might go out of bounds, an ON ERR GO TO redirects execution to the next safe recovery point. This chains through lines 210→220→230→240 in the arc-draw section, and similarly through the Copy and Fill sections, allowing the program to silently skip illegal operations and continue. Line 240’s final ON ERR GO TO 100 re-enters the main loop on any unhandled error.
Flood Fill Algorithm
The fill routine (lines 550–750) implements a scan-line flood fill entirely in BASIC using POINT to test pixels. Starting from cursor position (xp,yp), it sweeps horizontally to find xmin and xmax for each row, then advances one row in direction fm (+1 or -1). After completing a downward pass, it reverses at line 720 and fills upward. The grid-parity variable g selects fill style: g=0 solid (line 610), g=1 50% checkerboard (line 740 tests (x*g+y)/2 = INT(…)), and g=2 horizontal stripes.
Copy Mode with Scaling
Copy mode (lines 390–510, 800–890) reads pixels from a source rectangle defined by (x1,y1) to (x1+x2,y1+y2) using POINT and replots them at a destination offset, optionally scaled by an integer factor fm = 1 + ABS arc. Enlargement uses x*fm and y*fm for destination and x/fm for source lookup; reduction reverses this. The sign of arc selects enlargement (SGN arc ≠ -1) vs. reduction (SGN arc = -1). A Space keypress at line 840 aborts the pixel transfer early.
Status Line Display
The subroutine at lines 1500–1520 prints a one-line status bar at row 22 showing the current mode name, an “(erase)” indicator when e is active, the current ink color name from array c$(), and the arc/size value when relevant. POKE 23659,0 disables the lower-screen scroll guard before printing, then restores it to 2 afterward — a standard technique for writing to the bottom lines without triggering a scroll prompt.
Wash and Recolor Operations
Lines 900–990 provide four wash operations: replace all paper colors of a given value with the current ink paper (t), replace all ink colors (u), global paper replace (y, lines 930–950), and global ink replace (i, lines 960–980). These iterate over the 22×32 character grid using ATTR to read the color byte, then reprint the cell with the new color. The paper component is extracted as INT(ATTR/8) and the ink as ATTR - 8*INT(ATTR/8).
Notable Techniques and Idioms
NOT PIevaluates to 0 (since PI is nonzero), used as a readable zero initializer at line 1430 and elsewhere.DIM x(14, PI)at line 1440 usesPI(≈3.14159) as the second dimension; BASIC truncates it to 3, giving three speed columns.- Conditional string expressions like
("(erase)" AND e)at line 1510 output the string wheneis true and an empty string when false — a standard Spectrum BASIC boolean-string idiom. - The arc parameter doubles as a copy scale selector: positive arc values enlarge, negative values reduce, and zero means 1:1.
SAVE i$ SCREEN$at line 1410 saves the current pixel display as a raw screen file, andLOAD ""SCREEN$at line 1370 loads any screen from tape.
Bugs and Anomalies
- Line 80’s
ABS x: ABS ywrapping is incorrect for non-symmetric coordinate spaces — negative cursor positions fold back to positive rather than clamping or wrapping correctly, potentially causing cursor jumps. - Line 880 contains a stray
{19}{0}token (an ink/paper control sequence artifact) immediately beforePOINT, which may cause a syntax or attribute bleed issue in the reduction copy path. - Line 730 contains both a
GO TO 100and a trailingRETURNafter it; the RETURN is unreachable. - Line 1280 tests
i$="STOP "with a trailing space, which will never match a single INKEY$ character — this key binding is effectively dead code. - Line 340 plots
x,ythen draws a circle; the PLOT before CIRCLE is redundant since CIRCLE does not require a prior PLOT on this platform.
Content
Source Code
10 REM "Paint"\*Rheesware 1984 \{20}\{1} LIST 1570 for documentation \{20}\{0}
20 POKE 23658,0:GO TO 1420
30 REM :{GO TO 200:REM -joystick
40 LET a=STICK (1,1):IF s <>STICK (2,1) THEN LET s=STICK (2,1)
50 LET b=1:IF INKEY$ <>"" THEN GO SUB 1000
60 IF NOT a THEN GO TO 90
70 LET x=x+x(a,b):LET y=y+y(a,b)
80 LET x= ABS x:LET y= ABS y
90 PLOT x,y:GO SUB mode:GO TO 40
100 IF NOT a THEN GO TO 40
110 LET x=x-x(a,b):LET y=y-y(a,b):GO TO 30
120 LET e=0:GO TO 1500
130 REM -DRAW -
140 PLOT x,y:IF s THEN PLOT OVER 0; INVERSE e; INK ink;x,y
150 RETURN
160 REM -line/ray-
170 LET x2=x1-x:LET y2=y1-y:IF arc\{20}\{1}\{20}\{1}\{20}\{0} THEN GO TO 210
180 DRAW x2,y2:PLOT x,y:DRAW x2,y2
190 IF s THEN PLOT OVER 0; INVERSE e; INK ink;x,y:DRAW OVER 0; INVERSE e; INK ink;x2,y2::IF NOT r THEN LET x1=x:LET y1=y
200 RETURN
210 ON ERR GO TO 220:DRAW x2,y2,arc
220 ON ERR GO TO 230:PLOT x,y:DRAW x2,y2,arc
230 ON ERR GO TO 240:IF s THEN PLOT OVER 0; INVERSE e; INK ink;x,y:DRAW OVER 0; INVERSE e; INK ink,x2,y2,arc:IF NOT r THEN LET x1=x:LET y1=y:LET arc=0:GO TO 1500
240 ON ERR GO TO 100:RETURN
250 REM -brush-
260 ON ERR GO TO 760:GO SUB 180:ON ERR GO TO 100:RETURN
270 REM -box-
280 LET x2=x1-x:LET y2=y1-y
290 DRAW x2,0:DRAW 0,y2:DRAW -x2,0:DRAW 0,-y2
300 PLOT x,y:DRAW x2,0:DRAW 0,y2:DRAW -x2,0:DRAW 0,-y2
310 IF s THEN OVER 0:LET x1=x:LET y1=y:DRAW x2,0:DRAW 0,y2:DRAW -x2,0:DRAW 0,-y2
320 INVERSE 0:INK 8:OVER 1:RETURN
330 REM -CIRCLE -
340 ON ERR GO TO 350:PLOT x,y:CIRCLE x,y, ABS x2
350 ON ERR GO TO 360:CIRCLE x,y, ABS x2
360 ON ERR GO TO 380:IF NOT s THEN STOP
370 CIRCLE INK ink; INVERSE e; OVER 0;x,y, ABS x2
380 ON ERR GO TO 100:RETURN
390 REM -COPY -
400 LET fm=1+ ABS arc
410 IF SGN arc=-1 THEN GO TO 450
420 ON ERR GO TO 430:DRAW x2*fm,0:DRAW 0,y2*fm:DRAW -x2*fm,0:DRAW 0,-y2*fm
430 GO SUB 520
440 GO TO 490
450 ON ERR GO TO 460:DRAW x2/fm,0:DRAW 0,y2/fm:DRAW -x2/fm,0:DRAW 0,-y2/fm
460 GO SUB 520
470 ON ERR GO TO 480:PLOT x,y:DRAW x2/fm,0:DRAW 0,y2/fm:DRAW -x2/fm,0:DRAW 0,-y2/fm
480 GO TO 500
490 ON ERR GO TO 500:PLOT x,y:DRAW x2*fm,0:DRAW 0,y2*fm:DRAW -x2*fm,0:DRAW 0,-y2*fm
500 GO SUB 520:IF s THEN GO SUB 800
510 ON ERR GO TO 100:RETURN
520 PLOT x1,y1
530 ON ERR GO TO 540:DRAW x2,0:DRAW 0,y2:DRAW -x2,0:DRAW 0,-y2
540 ON ERR GO TO 100:RETURN
550 REM -fill-
560 INVERSE e:LET e= NOT e:INK ink:ON ERR GO TO 730:LET fm=1:OVER 0
570 LET xmin=x:LET xp=x:LET yp=y
580 IF POINT (x,y)=e THEN STOP
590 GO TO 670
600 IF g THEN GO TO 740
610 PLOT x,y:LET x=x-1
620 IF POINT (x,y)=e THEN LET xmin=x+1:LET x=xmax:GO TO 640
630 GO TO 600
640 LET y=y+fm:IF POINT (x,y)= NOT e THEN GO TO 670
650 LET x=x-1:IF POINT (x,y)=e THEN GO TO 690
660 LET xmax=x:GO TO 600
670 LET x=x+1:IF POINT (x,y)=e THEN LET xmax=x-1:GO TO 600
680 GO TO 670
690 IF x=xmin THEN GO TO 720
700 FOR x=x TO xmin STEP -1:IF NOT POINT (x,y)=e THEN LET xmax=x:GO TO 600
710 NEXT x
720 IF fm=1 THEN LET fm=-1:LET x=xp:LET y=yp-1:GO TO 670
730 INVERSE 0:LET e= NOT e:INK 8:LET x=xp:LET y=yp:OVER 1:BEEP .4,-10:GO TO 100:RETURN
740 LET i=(x*g+y)/2:IF i= INT i THEN PLOT x,y
750 LET x=x-1:GO TO 620
760 ON ERR GO TO 770:PLOT x,y:DRAW x2,y2
770 IF NOT s THEN GO TO 790
780 ON ERR GO TO 790:GO TO 190
790 ON ERR GO TO 100:RETURN
800 ON ERR RESET :IF y2=0 OR x2=0 THEN RETURN :REM -copy2-
810 INK ink:OVER 0:INVERSE e:LET x3=x:LET y3=y:IF SGN arc=-1 THEN GO TO 870
820 FOR y=0 TO fm*y2 STEP SGN y2:FOR x=0 TO fm*x2 STEP SGN x2
830 ON ERR GO TO 840:IF POINT (x1+x/fm,y1+y/fm) THEN PLOT x3+x,y3+y
840 IF INKEY$=" " THEN GO TO 860
850 NEXT x:NEXT y
860 BEEP .4,-10:LET y=y3:LET x=x3:INK 8:INVERSE 0:OVER 1:RETURN
870 FOR y=0 TO y2 STEP SGN y2:FOR x=0 TO x2 STEP SGN x2
880 ON ERR GO TO 890:IF \{19}\{0} POINT (x+x1,y+y1) THEN PLOT x3+x+fm,y3+y/fm
890 GO TO 840
900 REM -wash-
910 PRINT AT 0,0;:FOR i=0 TO 21:PRINT PAPER bord;,,:NEXT i:RETURN
920 PRINT AT 0,0;:FOR i=0 TO 21:PRINT INK ink,,:NEXT i:RETURN
930 ON ERR GO TO 990:FOR i=0 TO 21:FOR g=0 TO 31
940 IF INT (ATTR (i,g)/8)=bord THEN PRINT AT i,g; PAPER ink;" "
950 NEXT g:NEXT i:GO TO 990
960 ON ERR GO TO 990:FOR i=0 TO 21:FOR g=0 TO 31
970 IF ATTR (i,g)-8* INT (ATTR (i,g)/8)=bord THEN PRINT AT i,g; INK ink;" "
980 NEXT g:NEXT i
990 BEEP .4,-10:ON ERR GO TO 100:RETURN
1000 REM -keyread-
1010 LET i$= INKEY$:IF i$>"4" AND i$<"9" THEN LET a= VAL i$+6:RETURN
1020 IF i$="z" THEN LET s=1:LET b=2:RETURN
1030 IF i$="x" THEN LET s=1:LET b=3:RETURN
1040 IF i$="1" THEN GO TO 1300
1050 IF i$="2" THEN GO TO 1320
1060 IF i$="m" THEN PLOT INK ink;x,y:PLOT x,y:LET b=3:RETURN
1070 IF i$="p" THEN PLOT PAPER bord;x,y:PLOT x,y:LET b=3:RETURN
1080 IF i$="3" THEN ON ERR GO TO 1340:GO TO 1390
1090 IF i$="o" THEN ON ERR GO TO 1340:COPY :STOP
1100 IF i$= CHR$ 12 THEN PAPER bord:INK 9:CLS :PAPER 8:INK 8:GO TO 1500
1110 IF i$="e" THEN LET e= NOT e:GO TO 1500
1120 IF i$="d" THEN LET m$="Draw":LET arc1=0:LET mode=130:GO TO 120
1130 IF i$="l" THEN LET x1=x:LET r=0:LET arc1=.2:LET arc=0:LET y1=y:LET m$="Line":LET mode=160:GO TO 120
1140 IF i$="r" THEN LET x1=x:LET arc=0:LET arc1=.2:LET r=1:LET y1=y:LET m$="Ray":LET mode=160:GO TO 120
1150 IF i$="b" THEN IF m$="Line" THEN LET m$="Brush":LET r=1:LET arc1=0::LET mode=250:GO TO 120
1160 IF arc1 THEN IF i$="a" OR i$="s" THEN LET arc=arc+(arc1 AND i$="a")-(arc1 AND i$="s"):LET a$="Arc/Size="+ STR$ (INT (arc*10)/10)+" ":GO TO 1500
1170 IF i$="q" THEN LET x1=x:LET y1=y:LET m$="Box":LET mode=270:LET arc1=0:GO TO 120
1180 IF i$="f" THEN LET g=0:GO TO 550
1190 IF i$="g" THEN LET g=1:GO TO 550
1200 IF i$="h" THEN LET g=2:GO TO 550
1210 IF i$="4" THEN GO TO 1370
1220 IF i$="c" THEN IF m$="Line" THEN LET m$="Circle":LET mode=330:LET arc1=0:GO TO 120
1230 IF i$="w" THEN IF m$="Box" THEN GO TO 1350
1240 IF i$="t" THEN GO TO 900
1250 IF i$="y" THEN GO TO 930
1260 IF i$="u" THEN GO TO 920
1270 IF i$="i" THEN GO TO 960
1280 IF i$="STOP " THEN ON ERR RESET :STOP :GO TO 1500
1290 RETURN
1300 LET ink=ink+1:IF ink>7 THEN LET ink=0
1310 GO SUB 1500:RETURN
1320 LET bord=bord+1:IF bord>7 THEN LET bord=0
1330 BORDER bord:RETURN
1340 POKE 23658,0:BEEP .5,-10:GO TO 1500
1350 LET arc1=1:LET arc=0:LET mode=390:LET m$="Copy"
1360 LET x1=x:LET y1=y:GO TO 120
1370 LET i$=m$:LET m$="Start tape":GO SUB 1500:ON ERR GO TO 1380:OVER 0:PRINT AT 0,0; INK 9:LOAD ""SCREEN$
1380 INK 8:OVER 1:LET m$=i$:GO TO 1340
1390 INPUT "NAME? "; LINE i$
1400 IF i$="" THEN LET i$="paint"
1410 SAVE i$ SCREEN$ :STOP
1420 REM -startup-
1430 BORDER NOT PI:LET bord= NOT PI:LET ink= NOT PI:PAPER 7:INK 9:OVER 1:INVERSE NOT PI:BRIGHT NOT PI:CLS :INK 8:PAPER 8
1440 DIM x(14, PI):DIM y(14, PI):FOR i=1 TO 10:FOR a=1 TO PI:READ x(i,a),y(i,a):NEXT a:NEXT i
1450 FOR i=11 TO 14:READ x(i,1),y(i,1):NEXT i
1460 LET e= NOT PI:LET x=100:LET y=x:LET s=e
1470 DIM c$(8,7):FOR i=1 TO 8:READ c$(i):NEXT i
1480 LET arc=e
1490 LET i$="d":GO SUB 1020:GO TO 30
1500 REM -modePRINT -
1510 ON ERR GO TO 1520:POKE 23659,0:PRINT AT 22,0; OVER 0; INK 0; PAPER 7;m$,("(erase)" AND e),c$(ink+1),(a$ AND (arc1 AND arc))
1520 POKE 23659,2:ON ERR GO TO 100:RETURN
1530 DATA 0,1,0,2,0,3,0,-1,0,-2,0,-3,0,0,0,0,0,0,-1,0,-2,0,-3,0,-1,1,-2,2,-3,3,-1,-1,-2,-2,-3,-3,0,0,0,0,0,0,1,0,2,0,3,0,1,1,2,2,3,3,1,-1,2,-2,3,-3
1540 DATA -8,0,0,-8,0,8,8,0
1550 DATA "Black","Blue","Red","Magenta","Green","Cyan","Yellow","White"
1560 SAVE "PAINT" LINE 0
1570 REM instructions delete when done-COPY TO 2040-\{6}Copied from the "Hacker"TSUG of Las Vegas, NV March 1986
1580 \{20}\{0} REM 2405 Howard Dr.,Las Vegas, NV 89104-copied by P. Hill, SINCUS, 1229 Rhodes Rd. Johnson Cty, NY13790
1590 REM -the Modify Paper+Ink modify modes are copied exact-but with mono color monitor, it is hard to tell if they work.
1600 REM copyrighted-given to public domain-you are free to copybut not to profit from.
1610 REM to SAVE run 9999 STOP to break into prg Need joystick left side
1620 REM small cursor need monitor to see
1630 REM MODES-DRAW-"d"-use joy stick or arrow keys, fire button draws-"z" causes skip space-"x" causes two space skip
1640 REM MODES-ERASE-"e"to enteror exit erase mode. Can enter from other modes.
1650 REM MODES-LINE-"l"flashing line drawn to cursor from last point drawn."z" or"x" will ink inline.
1660 REM MODES-ARC-"a"arc value shown at bottom and flashing line will curve.More "a" increases arc,"s" dereases arc. "l" resets arc to zero.
1670 REM MODES-RAY-"r" works like line-lines radiate from common pt to follow cursor."z" and "x"work with ray to space lines. arc modes work with ray.
1680 REM MODES-BOX-"q" move cursor diagonally to strech a box to size.fire button,cursor keys, "z","x","e" work similiar as with line mode.
1690 REM MODES-BRUSH-draw. Enter"l"mode,form brush size by length of brush, then "b". if line isvertical to start, length of brush is only that wide in horizontsweeps, vertical sweeps will be one pixel wide.
1700 REM BRUSH cont-can enter "e" from brush, erase wide sweeps
1710 REM MODES-FILL-works in anymode, "f"=solid fill, "g"= 50% fill,"h"=stripes. must be inside area to be filled. Must be a right side. tone sounds on complete.
1720 REM MODES-COPY- to copy an object on screen,"q" to form flashing box around object. while still flashing, and around objecthit "w" for copy-move box with arrow or jystck-increase or decrease size with "a"or"s", firebotton places object, takes awhile towork, wait 30 secs or so."d" to get out of copy mode.
1730 REM KEYS USED\{6}"1"-INK SELECT\{6}\{6}"2"-BORDER SELECT\{6}"3"-SAVE page\{6}\{6}"4"-LOAD page\{6}\{6}"5"- LEFT arrow\{6}\{6}"6"- DOWN arrow\{6}\{6}"7"- UP arrow\{6}\{6}"8"- RIGHT arrow\{6}"0"-CLS with CAPS SHIFT\{6}"q"- BOX mode\{6}\{6}"w"-COPY mode\{6}\{6}"e"-ERASE mode \{6}"r"- RAY mode\{6}\{6}"t"-PAPER mode \{6}"y"- GlobalPAPER \{6}"u"-INK change\{6}\{6}"i"- GlobalINK \{6}"o"-PRINT page TO printer\{6}"p"- ModifyPAPER \{6}"a"- increase arc/copy\{6}"s"- decrease " "
1740 REM \{6}"d"-DRAW mode\{6}\{6}"f"- FILL mode\{6}\{6}"g"- 50% Fill mode\{6}"h"- FILL striped\{6}"l"- LINE mode-arc reset\{6}"z"- skip 1 space\{6}"x"- skip 2 space\{6}"c"-CIRCLE mode\{6}"b"- BRUSH mode\{6}\{6}"m"-INK modify
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.
