.........1.........2.........3.........4.........5.........6.........7.........8

LIGHT SCREEN DESIGNER
ZX Computing, April 1986
Part 11 of 13

Toni Baker brings you her graphics mega-program.


This month's article is all about COLOURING IN. The general idea is
that you should be able to position the cursor in the interior of an
outline on the screen and then, hey presto - a touch of machine code
and the entire outline is filled with whichever colour you choose.

Having completed the colouring-in program, I subsequently discovered a
bug in the earlier LINE DRAWING routine. It turned out that if you
used Light Screen Designer to draw a shape composed of straight lines
then the line drawing routine would leave "holes" in the outline at
the corners. Of course no colouring-in program can ever fill an
outline with holes in it - mine got confused and coloured-in parts of
the outside as well as the inside. I realised then that the only way
out of the dilemma was to cure the line- drawing bug, so that outlines
can be drawn without holes. The solution was to alter the line drawing
routine so that an extra point is plotted at the end of a straight
line. This means of course alterations to the program. You should
amend your program by overwriting the old draw line routine by the new
one in Figure 1.

Next, it will be necessary to link the colouring bits and the various
other parts into the main program. To do this you should enter the
following:

DBC2 98                 DEFB 98
DB76 12EA               DEFW EA12,CURS_TYPE (CURSOR_TYPE)
DB7A 7EE8               DEFW E87E,FILL                         [not 7AE8]
DB8A 74E8               DEFW E874,PAINT                        [not 70E8]

OK - now everything's sorted out [Except for the errors that were in
the above table. JimG] we can get on with this month's bit of program.

-----------------------------------------------------------------------------
DF04 CDEFDE   DRAW_LINE CALL MOVE_X        ;Move origin cursor to new position
DF07 CDE6DE             CALL ADJUST_BD     ;Adjust B and D to ROM convention
DF0A C5                 PUSH BC            ;Stack cursor coordinates
DF0B EB                 EX   DE,HL         ;HL= previous coordinates
DF0C 227D5C             LD   (COORDS),HL   ;Store as "last point plotted"
DF0F 110101             LD   DE,#0101
DF12 78                 LD   A,B           ;A= y coord of cursor
DF13 94                 SUB  H             ;A= vertical DRAW parameter
DF14 3003               JR   NC,DL_1       ;Jump if positive or zero
DF16 2F                 CPL                ;A= ABS(vertical parameter)
DF17 16FF               LD   D,#FF         ;D indicates that vertical
                                           ;parameter is negative
DF19 47       DL_1      LD   B,A
DF1A 79                 LD   A,C           ;A= x coord of cursor
DF1B 95                 SUB  L             ;A= horizontal DRAW parameter
DF1C 3003               JR   NC,DL_2       ;Jump if positive or zero
DF1E 2F                 CPL                ;A= ABS(horizontal parameter)
DF1F 1EFF               LD   E,#FF         ;E indicates that horizontal
                                           ;parameter is negative
DF21 4F       DL_2      LD   C,A
DF22 CDBA24             CALL DRAW_3        ;Draw the line
DF25 C1                 POP  BC            ;BC= cursor coordinates
DF26 18D9               JR   J_PLOT        ;Jump to plot last point
-----------------------------------------------------------------------------


Paint away!

The program gives you two new procedures for Light Screen Designer.
PAINT, on key 1, and FILL on key 2. Although they perform essentially
the same task (i.e. they both effectively colour-in an area) the two
are subtly different.

PAINT is intended for creating colour designs. When an area is filled
in this is largely done by altering the attribute bytes. Only at the
outline itself is any additional plotting required. This means that it
is possible to paint two adjacent areas in two different colours.
Provided that you never try to paint three different colours on the
same character square you should be alright. The program will set or
reset pixels and adjust attribute bytes automatically.

FILL, on the other hand, is intended for working in black and white.
Unlike PAINT it will fill an area by INKING every point in the
interior with the current ink colour. This means that it is not
possible to fill an area adjacent to another FILLed or PAINTed area
(PAINT does not suffer from this disadvantage). FILL has an advantage,
however, which PAINT does not have - if areas are FILLED then every
interior point will be SET, so that you can COPY a picture from the
screen to the ZX Printer.

The program works by first of all determining which pixels constitute
the interior, and then by changing the colour of these points. The
interior is determined as follows:
(1) The pixel at the cursor position is an interior point.
(2) Any pixel which is adjacent to, and the same colour as, an
    interior point is also an interior point.

Thus the "border" of an outline is not needed in the definition of the
interior. To a human it might seem obvious that if you draw a black
circle on a white background then the circle itself is a border
separating the "inside" from the "outside", but to the computer such
glaring obviousness is not glaringly obvious. Its own system
methodically works each pixel one at a time (actually eight at a time,
but that's a minor point). Since the cursor pixel (inside the circle)
will be white, then the border (which is NOT white) will be considered
by the program to be exterior. It is true that pixels outside the
circle will also be white, but none of these are adjacent to true
interior points, and so will always remain exterior.

When the program comes to the actual colouring-in process, it has to
do even more thinking (for PAINT anyway). For within an individual
character square, the concept of a "border" may have to be considered
anyway. Take a look at Figure 1 [PART11.GIF]. In case (a) the so
called "border" is at the edge of a character square. In this case it
is relatively easy to colour-in the interior without making any
changes to the border. In case (b) however, the "border" runs down the
middle of the character square. Since it is impossible to have three
colours on a character square then an alternative solution is
necessary. In this program both the inside and the border are
recoloured, while the outside is left unchanged.

The program uses the area of memory normally used by Light Screen
Designer as the back-up screen memory to store the interior pixels.
This is the area of memory called SCR2, which runs from address C000
to D7FF inclusive. I shall now go through each of the subroutines in
the program one at a time, and explain them as I come to them.


Subroutines

ATTR_ADDR at address E76E is very short and very simple. It converts
the address of a pixel on the screen (in HL) into the address of the
corresponding attribute byte.

COL_TEST at address E778 tests the attribute byte at address (BC)
against the known (old) interior colour. It returns:
Carry reset / Zero reset if paper and ink are both the required colour.
Carry reset / Zero set if neither paper nor ink is the required colour.
Carry set / Zero reset if ink colour is the required colour.
Carry set / Zero set if paper colour is the required colour. It also
returns either 00 or FF in the A register. This value will always
match that of the zero flag (i.e. 00 if zero, FF if non-zero).

At E79E is another short, sweet and simple routine. Given the address
of a pixel in HL, it will return the address of the corresponding
attribute byte in BC, and the address of the corresponding byte from
SCR2 in DE. HL will remain unchanged.

-----------------------------------------------------------------------------
E76E 7C       ATTR_ADDR LD   A,H           ;A= high part of pixel address
E76F 0F                 RRCA
E770 0F                 RRCA
E771 0F                 RRCA
E772 E603               AND  #03           ;A= 00, 01 or 2, according to which
                                           ;third of the screen the pixel is in
E774 F658               OR   #58
E776 67                 LD   H,A           ;HL= attribute address
E777 C9                 RET

;[The CPs at E77E/E786/E790 used different addresses for OLD_COLOUR]
;[(IX+8/IX+8/IX+6 respectively). All references in the next part   ]
;[use IX+8 (DB48), so I've changed E790 accordingly.           JimG]

E778 0A      COL_TEST   LD   A,(BC)        ;A= attribute byte from screen
E779 1F                 RRA
E77A 1F                 RRA
E77B 1F                 RRA
E77C E607               AND  #07           ;A= paper colour
E77E DDBE08             CP   (OLD_COLOUR)
E781 0A                 LD   A,(BC)        ;A= attribute byte from screen
E782 280A               JR   Z,COLTPAPER   ;Jump if paper colour matches search
                                           ;colour
E784 E607               AND  #07           ;A= ink colour
E786 DDBE08             CP   (OLD_COLOUR)
E789 2011               JR   NZ,COLT_NO    ;Jump if neither paper nor ink
                                           ;colour matches search
E78B 37                 SCF                ;Carry flag set (one match found)
E78C 9F                 SBC  A,A           ;Zero flag reset; A= FF (ink)
E78D C9                 RET
E78E E607     COLTPAPER AND  #07           ;A= ink colour
E790 DDBE08             CP   (OLD_COLOUR)
E793 2803               JR   Z,COLT_BOTH   ;Jump if both paper and ink match
                                           ;search
E795 AF                 XOR  A             ;Zero flag set; A= 00 (paper)
E796 37                 SCF                ;Carry flag set (one match found)
E797 C9                 RET
E798 3EFF     COLT_BOTH LD   A,#FF
E79A A7                 AND  A             ;Carry and zero flags both reset,
E79B C9                 RET                ;indicating paper and ink both match
E79C AF       COLT_NO   XOR  A             ;Zero flag set; Carry flag reset;
E79D C9                 RET                ;A= 00, indicating no match found

E79E E5       SR_PTRS   PUSH HL            ;Stack screen byte address
E79F CD6EE7             CALL ATTR_ADDR     ;HL= attribute byte address
E7A2 44                 LD   B,H
E7A3 4D                 LD   C,L           ;BC= attribute byte address
E7A4 E1                 POP  HL            ;HL= screen byte address
E7A5 54                 LD   D,H
E7A6 5D                 LD   E,L
E7A7 CBFA               SET  7,D           ;DE= SCR2 byte address
E7A9 C9                 RET
-----------------------------------------------------------------------------


SCAN_ROW is where all the tricky bits start. This subroutine lies at
address E7AA, and its purpose is to scan one row of the screen -
either from left to right, or from right to left. It declares as
"internal" any pixel which is to the right of (or to the left of) a
known interior pixel, and is also the same colour. In this way the
interior "field" is extended from a single, or collection of, points,
as far as the right (or left) hand interior edge.

-----------------------------------------------------------------------------
E7AA A7       SCAN_ROW  AND  A             ;Reset carry signals "Not a known
                                           ;interior point"
E7AB 08       SR_LOOP_1 EX   AF,AF'        ;Store in F'
E7AC 1A                 LD   A,(DE)        ;A= byte from SCR2
E7AD A7                 AND  A
E7AE 2850               JR   Z,SR_NEXT     ;Jump if there are no known interior
                                           ;points within this byte
E7B0 FEFF     SR_LOOP_2 CP   #FF
E7B2 37                 SCF
E7B3 284A               JR   Z,SR_NEXT_1   ;Jump if all eight pixels are already
                                           ;known to be interior
E7B5 CD78E7             CALL COL_TEST      ;Search attribute byte for interior
                                           ;colour
E7B8 3804               JR   C,SR_1MATCH   ;Jump if one match found
E7BA 2005               JR   NZ,SRALLBITS  ;Jump if paper and ink both match
E7BC 1841               JR   SR_NEXT_1     ;Otherwise jump with no match
E7BE BE       SR_1MATCH CP   (HL)
E7BF 2004               JR   NZ,SR_SCAN    ;Jump unless all eight bits are the
                                           ;required colour
E7C1 37       SRALLBITS SCF                ;Indicates "Known interior point"
E7C2 9F                 SBC  A,A           ;A= FF (ie. all bits set)
E7C3 182A               JR   SR_STORE      ;Jump to store FF
E7C5 C5       SR_SCAN   PUSH BC            ;Stack attribute byte pointer 
E7C6 0608               LD   B,#08         ;B= number of bits in a byte
E7C8 AE                 XOR  (HL)
E7C9 4F                 LD   C,A           ;C= byte from screen, with each bit
                                           ;reset for a known interior point,
                                           ;and set otherwise
E7CA 08                 EX   AF,AF'        ;Carry indicates whether or not last
                                           ;pixel was a known interior point
E7CB 1A                 LD   A,(DE)        ;A= byte from SCR2
E7CC DDCB006E           BIT  5,(JFLAGS)
E7D0 200F               JR   NZ,SR_SCAN_B  ;Jump if scanning from right to left
E7D2 3006     SR_SCAN_A JR   NC,SR_NBIT_A  ;Jump unless last pixel was a known
                                           ;interior point
E7D4 CB79               BIT  7,C
E7D6 2002               JR   NZ,SR_NBIT_A  ;Jump unless pixel is required colour
E7D8 F680               OR   #80           ;Set current pixel to show that it
                                           ;is a known interior point
E7DA CB01     SR_NBIT_A RLC  C             ;Get next bit into bit 7
E7DC 07                 RLCA               ;Move new bit into bit 0, and
                                           ;reassign carry flag accordingly
E7DD 10F3               DJNZ SR_SCAN_A     ;Repeat for all 8 bits
E7DF 180D               JR   SR_NEW
E7E1 3006     SR_SCAN_B JR   NC,SR_NBIT_B  ;Jump unless last pixel was a
                                           ;known interior point
E7E3 CB41               BIT  0,C
E7E5 2002               JR   NZ,SR_NBIT_B  ;Jump unless pixel is required colour
E7E7 F601               OR   #01           ;Set current pixel to show that it
                                           ;is a known interior point
E7E9 CB09     SR_NBIT_B RRC  C             ;Get next bit into bit 0
E7EB 0F                 RRCA               ;Move next bit into bit 7, and
                                           ;reassign carry flag accordingly
E7EC 10F3               DJNZ SR_SCAN_B     ;Repeat for all 8 bits
E7EE C1       SR_NEW    POP  BC            ;Restore attribute pointer
E7EF F5       SR_STORE  PUSH AF            ;Stack carry flag
E7F0 C5                 PUSH BC            ;Stack attribute pointer
E7F1 4F                 LD   C,A           ;C= new value to store in SCR2
E7F2 1A                 LD   A,(DE)        ;A= old value from SCR2
E7F3 2F                 CPL
E7F4 A1                 AND  C
E7F5 2804               JR   Z,SR_OLD      ;Jump unless at least one pixel is a
                                           ;newly discovered interior point
E7F7 DDCB00F6           SET  6,(JFLAGS)    ;Signal "Not finished yet"
E7FB 79       SR_OLD    LD   A,C
E7FC 12                 LD   (DE),A        ;Store new value in SCR2
E7FD C1                 POP  BC            ;Restore attribute pointer
E7FE F1                 POP  AF            ;Restore carry flag
E7FF 08       SR_NEXT_1 EX   AF,AF'        ;Store carry flag
E800 7D       SR_NEXT   LD   A,L
E801 E61F               AND  #1F           ;A= column number of char square
E803 DDCB006E           BIT  5,(JFLAGS)
E807 2008               JR   NZ,SR_DEC     ;Jump if scanning from right to left
E809 FE1F               CP   #1F
E80B C8                 RET  Z             ;Return if at right-hand edge of row
E80C 03                 INC  BC            ;Increment attribute pointer
E80D 13                 INC  DE            ;Increment SCR2 pointer
E80E 23                 INC  HL            ;Increment screen pointer
E80F 1805               JR   SR_CONT
E811 A7       SR_DEC    AND  A
E812 C8                 RET  Z             ;Return if at left-hand edge of row
E813 0B                 DEC  BC            ;Decrement attribute pointer
E814 1B                 DEC  DE            ;Decrement SCR2 pointer
E815 2B                 DEC  HL            ;Decrement screen pointer
E816 08       SR_CONT   EX   AF,AF'        ;Retrieve carry flag
E817 3092               JR   NC,SR_LOOP_1  ;Jump unless last point was known
                                           ;to be interior
E819 08                 EX   AF,AF'        ;Store carry flag again
E81A 1A                 LD   A,(DE)        ;A= next byte from SCR2
E81B 1893               JR   SR_LOOP_2
-----------------------------------------------------------------------------


TM_POP (E81D) doesn't make a lot of sense on its own. It basically
POPs SCR2 from the calculator stack, but see also TEST_MEM (below).

-----------------------------------------------------------------------------
E81D 01800D   TM_POP    LD   BC,#0D80      ;BC= half number of bytes in SCR2
E820 2100DB             LD   HL,#DB00      ;HL point to the first byte beyond
                                           ;this area
E823 D1       TMP_LOOP  POP  DE            ;Get next two bytes from stack
E824 2B                 DEC  HL
E825 72                 LD   (HL),D
E826 2B                 DEC  HL
E827 73                 LD   (HL),E        ;Store next two bytes in SCR2
E828 0B                 DEC  BC
E829 78                 LD   A,B
E82A B1                 OR   C
E82B 20F6               JR   NZ,TMP_LOOP   ;Repeat for all bytes
E82D C9                 RET
-----------------------------------------------------------------------------


TEST_MEM is at E82E. On entry, BC must contain the maximum number of
bytes of memory used by a routine. This must include bytes used on the
machine stack, the calculator stack, and the workspace. It doesn't
matter if BC is too great, but it must never be too small. Calling
TEST_MEM will (a) test the memory, and return immediately if there is
not enough room to store an extra copy of SCR2, otherwise (b) PUSH a
copy of SCR2 onto the machine stack, and (c) manipulate the machine
stack so that TM_POP will be called before returning to the main loop.
Control will then be passed to the byte immediately after the CALL
TEST_MEM instruction (i.e. like a normal RETurn).

-----------------------------------------------------------------------------
E82E 2A655C   TEST_MEM  LD   HL,(STKEND)
E831 09                 ADD  HL,BC
E832 380A               JR   C,TM_NOT      ;Jump if not enough memory
E834 01501B             LD   BC,#1B50
E837 09                 ADD  HL,BC         ;Allow 1B50 bytes for SCR2 and
                                           ;machine stack
E838 3804               JR   C,TM_NOT      ;Jump if not enough memory
E83A ED72               SBC  HL,SP         ;Allow for position of machine stack
E83C 381E               JR   C,TM_PUSH     ;Jump if enough memory available
E83E DDCB0166 TM_NOT    BIT  4,(JFLAGS hi)
E842 2813               JR   Z,TM_?        ;Jump if screen memory not being used
E844 2151E8             LD   HL,TM_ESC     ;Load HL with "ESCAPE" address
E847 E5                 PUSH HL
E848 CDCCDC             CALL MESSAGE
E84B 03                 DEFB #03           ;Print message "WARNING Screen Memory
                                           ;Wipe" and await input
E84C E1                 POP  HL            ;Delete ESCAPE address
E84D FE59               CP   "Y"
E84F 2802               JR   Z,TM_WIPE     ;Jump if reply was "Y"
E851 F1       TM_ESC    POP  AF            ;Delete return address into subrt
E852 C9                 RET                ;Return to main loop
E853 DDCB01A6 TM_WIPE   RES  4,(JFLAGS hi) ;Signal "Screen memory not in use"
E857 DDCB01EE TM_?      SET  5,(JFLAGS hi) ;Signal "SCR2 has been corrupted"
E85B C9                 RET                ;Return to carry out procedure
E85C E1       TM_PUSH   POP  HL            ;HL= return address
E85D D9                 EXX                ;HL'= return address
E85E 01800D             LD   BC,#0D80      ;BC= half number of bytes in SCR2
E861 2100C0             LD   HL,#C000      ;HL points to first byte of SCR2
E864 5E       TMP_LOOP2 LD   E,(HL)
E865 23                 INC  HL
E866 56                 LD   D,(HL)        ;Get next two bytes from SCR2
E867 23                 INC  HL
E868 D5                 PUSH DE            ;Store on machine stack
E869 0B                 DEC  BC
E86A 78                 LD   A,B
E86B B1                 OR   C
E86C 20F6               JR   NZ,TMP_LOOP2
E86E 211DE8             LD   HL,TM_POP
E871 E5                 PUSH HL            ;Force future return into TM_POP
E872 D9                 EXX                ;HL= true return address
E873 E9                 JP   (HL)          ;Return to carry out procedure
-----------------------------------------------------------------------------


[The PAINT and FILL routines described in this issue weren't actually]
[printed until the next issue.                                   JimG]

The principal routines, PAINT and FILL begin here. PAINT at E874, and
FILL at E87E. The first difference between the two is apparent
immediately. PAINT uses the MESSAGE subroutine (listed earlier) to
request the required paint colour from the user, whereas FILL
automatically selects the current ink colour. A flag is assigned so
that the program knows whether PAINT or FILL is being carried out, and
then the two routines merge into one. SCR2 is used to store interior
pixels, so this area of memory is cleared, and one pixel set at the
cursor position. The primary loop begins at the label PF_BEGIN. The
current row is scanned - first from left to right, and then from right
to left. At PF_CONT the address of the row below (or above) is
calculated and any pixels which are directly below (or above) known
interior pixels, and which are the same colour, are themselves
declared internal.

The process repeats over and over again - alternately up and down the
screen until all interior points have been discovered. From PF_SQLOOP
the colouring-in process begins. The screen is scanned one character
square at a time and any required pixels are set or reset as needed
whilst the attribute bytes are adjusted. When the whole screen has
been scanned in this manner the process is complete and control will
return to the main loop (possibly via TM_POP).

Finally the old CURSOR_TYPE routine has been moved to address EA12,
since its previous home (DF24) has now been overwritten with the new
DRAW_LINE routine.

The Light Screen Designer program, in its BASIC form, is now complete.
I have spent this article, and indeed every previous article in this
series, concentrating on just one tiny segment of the program. My next
article will be a special Light Screen Designer Extra - in which I
will talk at great length about the whole program (and with no machine
code to worry about I'll have more room to waffle!). I'll also be
talking about ways to improve or extend the program. See you then.
