This program renders a 3D “saddle-cosine” surface using a perspective projection, iterating over a parameter k to produce a family of related surfaces. It implements a full 3D rotation pipeline in lines 20–30, converting world coordinates through rotation angles theta and phi into screen coordinates via a perspective divide. Two 12-byte machine code routines are POKEd into RAM at addresses 32000 and 32050; these use the Z80 LDIR instruction (opcode ED B0) to perform fast block memory copies, saving and restoring the Spectrum’s display file to/from pages in RAM above 26000. The surface function is defined with DEF FN as COS(0.1*(x²+y²)) + (x²-y²)/k, and k loops from 10 to 40 to animate a morphing shape. The program saves both the machine code block and itself to tape via line 9000 onwards.
Program Structure
The program divides into four logical sections:
- Initialisation (lines 1–8): Sets
CLEAR 31999to protect machine code space, POKEs two 12-byte ML routines at addressess=32000andr=32050, and initialises page counterp=0. - 3D plotting loop (lines 35–140): Iterates
kfrom 10 to 40, sets up projection parameters, and plots the surface by sweepingxandy. - Page-save section (lines 1000–1050): After each surface is plotted, calls the LDIR save routine via
USR sto copy the display file to a RAM page. - Page-recall slideshow (lines 2000–2060): Loops
qfrom −3 to 4, recalls stored display pages viaUSR rwith briefPAUSE 1delays, looping indefinitely.
Machine Code Routines
Two 12-byte Z80 routines are loaded via READ/POKE from DATA statements at lines 6–7:
| Address | Bytes (decimal) | Z80 Mnemonic | Purpose |
|---|---|---|---|
| 32000 (save) | 17,232,128 → LD DE,32744(?) | LD DE,nn | Destination address (patched at runtime) |
| +3 | 33,0,64 → LD HL,16384 | LD HL,nn | Source = display file base |
| +6 | 1,0,27 → LD BC,6912 | LD BC,nn | Byte count = full display file |
| +9 | 237,176 → LDIR | LDIR | Block copy display→RAM page |
| +11 | 201 → RET | RET | Return to BASIC |
| 32050 (restore) | 17,0,64 → LD DE,16384 | LD DE,nn | Destination = display file |
| +3 | 33,232,128 → LD HL,33000(?) | LD HL,nn | Source address (patched at runtime) |
| +6 | 1,0,27 → LD BC,6912 | LD BC,nn | Byte count = full display file |
| +9 | 237,176 → LDIR | LDIR | Block copy RAM page→display |
| +11 | 201 → RET | RET | Return to BASIC |
The destination/source addresses in these routines are patched at runtime: lines 1030–1031 overwrite bytes at offsets +1/+2 of the save routine (POKE 32001,a1: POKE 32002,a2), and lines 2030–2031 patch the restore routine similarly. This allows the same LDIR stub to copy the 6912-byte display file to/from any calculated RAM address.
Page addresses are computed as a = p*7000 + 26000, giving pages starting at 26000, 33000, 40000, 47000 for p=0–3. However, page 4 (a=54000) would overlap with the machine code at 32000–32061 for some pages, and addresses above ~49151 are in ROM on an unexpanded Spectrum — this scheme likely assumes a 128 KB or RAM-expanded machine, or the page count is intended to stay within safe bounds.
3D Projection Pipeline
Lines 20–30 implement a classic perspective projection with rotation. Given world coordinates (x, y, z), the routine computes rotated eye-space coordinates using angles t (theta, azimuth) and phi (elevation), with precomputed sines/cosines s1, c1, s2, c2. The perspective divide uses focal length d=350 and centre (cx,cy)=(127,87):
xe = -x*s1 + y*c1— rotated Xye = -x*c1*c2 - y*s1*c2 + z*s2— rotated Yze = -x*s2*c1 - y*s2*s1 - z*c2 + rho— depth (with viewer distancerho=50)sx = d*xe/ze + cx,sy = cy + d*ye/ze— screen coordinates
Surface Function and Animation
The surface is defined at line 50: DEF FN z(x) = COS(0.1*(x*x+y*y)) + (x*x-y*y)/k. The first term is a radially symmetric cosine ripple; the second is a hyperbolic paraboloid (saddle) term scaled by 1/k. As k increases from 10 to 40 in the outer loop (line 35), the saddle contribution diminishes, morphing the shape. The variable y is used as a free variable inside FN z, relying on its current value from the enclosing FOR y loop — a standard but implicit BASIC closure technique.
Plotting and Clipping
Line 110 clips points outside the screen bounds (0–255 horizontally, 0–175 vertically) and resets the fl (flag) variable to 0 on out-of-bounds points. Line 120 uses IF fl=0 THEN LET l=1: PLOT sx,sy — the assignment LET l=1 here appears vestigial (a remnant of a DRAW-based approach visible in commented-out lines 125–130). The variable l is set but never subsequently used in the active code path.
Notable Anomalies and Remnants
- Line 9 (
GO TO 2000) followed by line 10 (GO TO 35) means line 10 is dead code on first run; the REM notes it should be removed for initial plotting. - Lines 2 and 8 contain initialisation that is bypassed after the first run via the
GO TO 2000at line 9 — the program enters the recall slideshow directly on subsequent runs if the display pages are already populated. - The recall loop at line 2000 uses
qfrom −3 to 4 and computesp=ABS q, skippingp=0andq=1via line 2017. This effectively recalls pages 3, 2, 1, skips 0, skips q=1 (p=1), then recalls 2, 3, 4 — an unusual asymmetric sequence likely producing a non-obvious display order. - Line 2 is a REM but contains executable-looking code (
BORDER 0: PAPER 0: INK 7: CLS); it is never executed and serves as a comment-placeholder for setup commands. - Several FOR loops and DEF FN alternatives are present as REMs (lines 52–78), suggesting experimentation with torus/Lissajous figures.
Save/Load Sequence
Lines 9000–9010 save the machine code block (30000 bytes from address 32000) and then the BASIC program itself with LINE 1 autostart. The large byte count of 30000 for the CODE block is almost certainly intentional overkill — it captures the ML routines plus any data pages stored above them, providing a complete snapshot for tape distribution.
Content
Image Gallery
Source Code
1 REM this poqram 6.4
2 REM BORDER 0: PAPER 0: INK 7: CLS : CLS
3 CLEAR 31999: LET s=32000: LET r=32050
4 RESTORE : FOR j=0 TO 11: READ a: POKE s+j,a: NEXT j
5 FOR j=0 TO 11: READ a: POKE r+j,a: NEXT j
6 DATA 17,232,128,33,0,64,1,0,27,237,176,201
7 DATA 17,0,64,33,232,128,1,0,27,237,176,201
8 LET p=0
9 GO TO 2000: REM TAKE OUT TO PLOT PICTURES FOR THE FIRST TIME
10 GO TO 35
12 REM SADDLECOS
15 REM by James N. Jones 2242 Locust Amarillo, Texas 79109
20 LET xe=-x*s1+y*c1: LET ye=-x*c1*c2-y*s1*c2+z*s2: LET ze=-x*s2*c1-y*s2*s1-z*c2+rho
30 LET sx=d*xe/ze+cx: LET sy=cy+d*ye/ze: RETURN
35 FOR k=10 TO 40
38 LET t=PI/4
40 LET rho=50: LET d=350: LET phi=PI/4: LET cx=127: LET cy=87: LET s1=SIN (t): LET s2=SIN (phi): LET c1=COS (t): LET c2=COS (phi)
50 DEF FN z(x)=COS (.1*(x*x+y*y))+(x*x-y*y)/(k)
52 REM LET size=8
55 REM DEF FN z(l)=4*SIN l
60 REM color
70 FOR x=10 TO -10 STEP -.05
75 REM FOR m=0 TO PI*2 STEP .05
76 REM LET x=size*COS (1*m): REM LET x=size*SIN l
77 REM LET z=size*1*COS (2*m)
78 REM LET y=size*1*SIN (3*m)
80 LET fl=0
90 FOR y=-10 TO 10
100 LET z=FN z(x): GO SUB 20
105 REM GO SUB 20
110 IF sx<0 OR sx>255 OR sy<0 OR sy>175 THEN LET fl=0: GO TO 140
120 IF fl=0 THEN LET l=1: PLOT sx,sy
123 REM PLOT 127,87: DRAW 127-sx,87-sy
125 REM LET sx1=sx: LET sy1=sy
130 REM DRAW (sx1-sx),(sy1-sy)
140 NEXT y: NEXT x
1000 LET p=p+1: REM INPUT "Stor as page no. (1-4) ";p
1010 LET p=INT p: REM IF p<1 OR p>4 THEN GO TO 1000
1020 LET a=p*7000+26000: LET a2=INT (a/256): LET a1=a-a2*256
1030 POKE 32001,a1: POKE 32002,a2
1040 LET n=USR s
1045 IF p=4 THEN GO TO 2000
1046 CLS
1050 NEXT k
2000 FOR q=-3 TO 4: REM INPUT "Recall page no. (1-4) ";p
2010 LET p=INT q: REM IF p<1 OR p>4 THEN GO TO 2050
2015 LET p=ABS p
2017 IF p=0 OR q=1 THEN GO TO 2050
2020 LET a=p*7000+26000: LET a2=INT (a/256): LET a1=a-a2*256
2030 POKE 32054,a1: POKE 32055,a2
2040 LET n=USR r
2045 PAUSE 1
2050 NEXT q: REM RETURN
2060 GO TO 2000
9000 SAVE "saddlecos1"CODE 32000,30000
9005 BEEP 1,22
9006 SAVE "saddlecos1" LINE 1
9010 BEEP 2,33
Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.