ROCoding #1: Rectangles
Coming back to RISC OS has been interesting. I’ve had to re-learn a number of things, like using the WIMP and, in particular, graphics programming. I hope this article will be the first of a series explaining my learning curve, and hopefully providing some useful programs. It’s fairly basic — in all senses! — but does assume some familiarity with RISC OS and BBC BASIC.
We’ll start with some things you can do when drawing rectangles. While writing the Solar application I wanted to provide some better backgrounds for the graphs. A plain background is easy, of course: just use RECTANGLE FILL with some appropriate colour, like this.
GCOL 0,&ff,&dd,&ff RECTANGLE FILL 0,0,256 |
…which draws a 256×256 square at the graphics origin. More generally, RECTANGLE accepts both width and height, which can be negative.
First up, we’ll generate a blend between two colours, from the bottom to the top of the rectangle. We’re assuming a full-colour display here, which is necessary for displaying blends with any fidelity. The Solar application graphs are built up by redirecting all graphics output to a sprite, then letting the Wimp handle displaying them on screen; this takes care of any mismatch between the screen and graph colour depths.
It’s useful when defining a routine to make it as general as possible. For graphics operations, that means finding out how OS coordinates map to pixels. We do this by reading the so-called eigen values like this, in PROCinit (which will be run before any of the demos here):
DEF PROCinit SYS ”OS_ReadModeVariable”,-1,4 TO ,,xeig% SYS ”OS_ReadModeVariable”,-1,5 TO ,,yeig% ENDPROC
By using BASIC’s shift operations, we can convert OS coordinates to pixels like this:
xpix% = xos%>>xeig%: ypix% = yos%>>yeig%
Note that we use an arithmetic shift (>> rather than >>>) to preserve any sign.
So, first we work out how to blend between two colours. The height of the rectangle (in pixels) gives us the number of horizontal lines we need to draw, and for each line we need to modify each of the red, green and blue channel values. To get the modifications for each step, we’ll define PROCblend_steps like this:
DEF PROCblend_steps(r0,g0,b0, r1,g1,b1, steps, RETURN rs,RETURN gs,RETURN bs) rs=(r1-r0)/steps: gs=(g1-g0)/steps: bs=(b1-b0)/steps ENDPROC
The first three input variables r0, g0 and b0 give the starting colour, and the next three the end colour. The steps variable specifies the number of lines, and we RETURN three values, the stepping quantities for each channel. Note that we need to use real variables for everything; it will quickly go wrong if you use integers. And even using reals may accumulate rounding errors, so the last colour used might not perfectly match r1, g1 and b1. For these purposes though it’ll be close enough. This gives us a blend between black and white:
PROCinit ysp%=128: REM The height of the rectangle in pixels s%=1<<yeig%: REM The height of a single line in OS units x%=0: y%=0: REM The rectangle’s corner r=0: g=0: b=0: REM Black, blending to white… PROCblend_steps(r,g,b, 255,255,255, ysp%, rs,gs,bs) FOR n% = 1 TO ysp%: REM For each line… GCOL r,g,b: REM …set the colour… LINE x%,y%,256,y%: REM …draw the line… y%+=s%: REM …step to next line… r+=rs: g+=gs: b+=bs: REM …and step each colour channel NEXT |
Incidentally, all these images are saved in WebP format. This one started out as a 64k sprite, which as a WebP image takes up … 60 bytes.
For a nice sky-type blend, which I’ve used for some Solar graphs, try this:
… r=234: g=234: b=255 PROCblend_steps(r,g,b, 188,188,255, ysp%, rs,gs,bs) … |
Modifying this for a horizontal blend, along the X axis, is easy. This does green to white:
PROCinit xsp%=128: REM The width of the rectangle s%=1<<xeig%: REM The width of a single line in OS units x%=0: y%=0: REM The rectangle’s corner r=0: g=255: b=0: REM Green, blending to white… PROCblend_steps(r,g,b, 255,255,255, xsp%, rs,gs,bs) FOR n% = 1 TO xsp%: REM For each line… GCOL r,g,b: REM …set the colour… LINE x%,y%,x%,256: REM …draw the line… x%+=s%: REM …step to next line… r+=rs: g+=gs: b+=bs: REM …and step each colour channel NEXT |
Generalising
To generalise this and construct a useful library procedure, we’ll start by defining colours as palette entries, rather than separate red, green and blue components. This is the form usually used by the ColourTrans module, and encodes a colour into a single word &bbggrr00, where b, g and r are hex digits (the 00 is often used for transparency). So &ffffff00 is white, and &00400000 is a dark green. Colours are a lot easier to pass around in this form.
Here’s a function to encode a triplet, and a procedure to split an encoded colour into its components:
DEF FNencode_col(r%,g%,b%)=b%<<24 OR (g%AND&FF)<<16 OR (r%AND&FF)<<8 DEF PROCdecode_col(c%,RETURN r%,RETURN g%,RETURN b%) r%=c%>>8AND&FF: g%=c%>>16AND&FF: b%=c%>>>24 ENDPROC
Note the use of a logical shift >>> for b%, which means we don’t need the AND operation.
Define some colours in PROCinit, and get the SWI number for ColourTrans_SetGCOL which we’ll be using later on:
DEF PROCinit SYS ”OS_ReadModeVariable”,-1,4 TO ,,xeig% SYS ”OS_ReadModeVariable”,-1,5 TO ,,yeig% SYS ”OS_SWINumberFromString”,,”ColourTrans_SetGCOL” TO CT_SetGCOL% black%=0: white%=&ffffff00: red%=&0000ff00: green%=&00ff0000: blue%=&ff000000 cyan%=&ffff0000: magenta%=&ff00ff00: yellow%=&00ffff00 ENDPROC
Now we’ll redefine PROCblend_steps to accept the encoded colours s% and e% (start and end):
DEF PROCblend_steps(s%,e%,steps,RETURN r,RETURN g,RETURN b,RETURN rs,RETURN gs,RETURN bs) LOCAL r2,g2,b2 PROCdecode_col(s%,r,g,b): PROCdecode_col(e%,r2,g2,b2) rs=(r2-r)/steps: gs=(g2-g)/steps: bs=(b2-b)/steps ENDPROC
The RETURNed values are the starting r, g and b colours, and the step values.
Now we can define a generalised procedure. We assume that the variables xeig% and yeig% are defined globally:
DEF PROCrectangle_blend(x%,y%,w%,h%,startcol%,endcol%,flags%) LOCAL p%,s%,r,g,b,rs,gs,bs,n% IF w%<0 w%=ABSw%: x%-=w% IF h%<0 h%=ABSh%: y%-=h% IF flags% AND 1: p%=w%>>yeig%: s%=1<<xeig% ELSE p%=h%>>xeig%: s%=1<<yeig% PROCblend_steps(startcol%,endcol%,p%,r,g,b,rs,gs,bs) IF flags% AND 1 THEN FOR n%=0 TO p%: GCOL r,g,b: MOVE x%,y%: DRAW BY 0,h%: x%+=s%: r+=rs: g+=gs: b+=bs: NEXT ELSE FOR n%=0 TO p%: GCOL r,g,b: MOVE x%,y%: DRAW BY w%,0: y%+=s%: r+=rs: g+=gs: b+=bs: NEXT ENDIF ENDPROC
There are a couple of points to note here. Firstly, BASIC’s RECTANGLE statement accepts negative widths/heights, so we make ours do that too by adjusting the coordinates so that the rectangle’s origin is at the bottom left. Secondly, BASIC’s RECTANGLE’s coordinates are inclusive. If there are 2 OS coordinates for each pixel (as there are for many modes), the command RECTANGLE 0,0,16 will actually draw a square 9×9 pixels, not 8×8. The same applies to DRAW BY, so MOVE 0,0: DRAW BY 16,0 will draw a 9-pixel line. To match RECTANGLE’s behaviour we start the FOR loop at zero, and use DRAW BY.
The flags% parameter is used to specify the blend type: if bit 0 is zero it’s a vertical blend, otherwise it’s horizontal. (Wouldn’t it be nice if BASIC allowed optional parameters with default values?) Note that you could use another bit of flags% to specify exclusive coordinates, as described above; modifying the code is left as an exercise.
Here’s an example, using some negative sizes and various orientations:
PROCinit PROCrectangle_blend(128,128,96,96,white%,blue%,1) PROCrectangle_blend(128,128,-96,96,green%,white%,1) PROCrectangle_blend(128,128,-96,-96,red%,white%,0) PROCrectangle_blend(128,128,96,-96,white%,magenta%,0) |
Generalising more
So far we can only blend between two colours. This is useful in itself, so we’ll define a new procedure to let us use more colours in a different fashion. We’ll also add a way to specify the GCOL action — we can then use logical operations to combine the blend with what’s already on the screen. Here it is:
DEF PROCrectangle_gradient(x%,y%,w%,h%,nc%,gradient%(),flags%) LOCAL a%,p%,s%,gs,k,n% a%=flags%>>8AND&FF: REM Extract the GCOL action IF w%<0 w%=ABSw%: x%-=w% IF h%<0 h%=ABSh%: y%-=h% IF flags% AND 1: p%=w%>>yeig%: s%=1<<xeig% ELSE p%=h%>>xeig%: s%=1<<yeig% gs=nc%/p%: k=0 IF flags% AND 2: k=nc%-1: gs=-gs: REM Invert the gradient IF flags% AND 1 THEN FOR n%=1 TO p%: SYS CT_SetGCOL%,gradient%(k),,,,a%: MOVE x%,y%: PLOT9,0,h%: x%+=s%: k+=gs: NEXT ELSE FOR n%=1 TO p%: SYS CT_SetGCOL%,gradient%(k),,,,a%: MOVE x%,y%: PLOT9,w%,0: y%+=s%: k+=gs: NEXT ENDIF ENDPROC
Instead of start and end colours, we’re supplying gradient%(), a vector of encoded colours; this can be of any length, but we’re using 256 here (you’ll see why later). The nc% parameter specifies the number of entries to use.
The flags% parameter has been extended:
- Bit 0 = direction: 0 = vertical, 1 = horizontal.
- Bit 1 = invert gradient. To add this to PROCrectangle_blend, just add the line:
IF flags% AND 2 SWAP startcol%,endcol% - Bits 8-15 (ie byte 1) = GCOL action code. Again, easy to add to PROCrectangle_blend.
And now we’re using the ColourTrans module to set the colour.
For my purposes I wanted a 16×16 square to be exactly 8×8 pixels (so it no longer works like RECTANGLE — see above). Hence the use of PLOT 9, which does a relative solid line excluding the end point, and the FOR loop starting at 1.
Here’s a simple demonstration. The gradient g%() is initialised to blend the red channel from black to red, then this gradient is moved to the green and blue channels by the array multiplication. The green channel is inverted:
PROCinit DIM g%(256) FOR i%=0 TO 127: g%(i%)=FNencode_col(i%*2,0,0): NEXT PROCrectangle_gradient(0,0, 256,72, 128, g%(), %01) g%()=g%()*256 PROCrectangle_gradient(0,92, 256,72, 128, g%(), %11) g%()=g%()*256 PROCrectangle_gradient(0,184, 256,72, 128, g%(), %01) |
You could do that with PROCrectangle_blend of course, but you couldn’t do this next example. Set up a gradient with eight alternating black and white colours, and draw two overlapping gradients, one vertical, one horizontal. The second uses the EOR action (GCOL 3) … which will give us a chessboard:
PROCinit g%()=black%,white%,black%,white%,black%,white%,black%,white% PROCrectangle_gradient(0,0,256,256,8,g%(),0) PROCrectangle_gradient(0,0,256,256,8,g%(),3<<8 OR 1) |
RISC OS palette files contain colour definitions, usually (though not necessarily) for 256 colours. If we read in a palette file we can then pass it to PROCrectangle_gradient. Here’s a function to do this:
DEF FNload_palette_file(f$,RETURN pal%()) REM Load a RISC OS palette file into pal%() REM Returns number of colours in palette LOCAL h%,d%,ncols%,palsize% SYS ”OS_File”,17,f$ TO h%,,d% IF NOT h%=1 AND d%>>>8=&ffffed ERROR 100,”Not a palette file” IF DIM(pal%())<>1 ERROR 100,”Palette array must be a vector” pal%()=0: palsize%=DIM(pal%(),1) h%=OPENIN f$ REPEAT IF BGET#h%<>19 CLOSE#h%: ERROR 100,”Bad palette file” d%=BGET#h%: REM Colour number, which is ignored IF BGET#h%=16 THEN pal%(ncols%)=BGET#h%<<8 OR BGET#h%<<16 OR BGET#h%<<24 ncols%+=1 IF ncols%>palsize% CLOSE#h%: ERROR 100,”Too many colours” ELSE REM We ignore mouse/border etc stuff d%=BGET#h%: d%=BGET#h%: d%=BGET#h%: ENDIF UNTIL EOF#h% CLOSE#h% =ncols%
This takes a full filename and a vector as parameters. The returned value is the number of colours read, and the vector now contains the encoded colour definitions.
Here’s an example, using a rainbow palette file (this assumes a system variable Palettes$Path has been set appropriately). It draws both horizontal and vertical gradients, so you can see how the colour gradient is stretched or shrunk to fill the area:
PROCinit DIM rainbow%(256) pf$=”Palettes:bow2/pal” ncols%=FNload_palette_file(pf$,rainbow%()) PROCrectangle_gradient(0,300,1280,128,ncols%,rainbow%(),1) PROCrectangle_gradient(0,0,1280,128,ncols%,rainbow%(),0) |
Incidentally, these palette files were all generated by my Gradgrind program.
Finally, this uses a pastel palette shown in four orientations:
PROCinit pf$=”Palettes:pastel/pal” ncols%=FNload_palette_file(pf$,g%()) PROCrectangle_gradient(128,128,128,128,ncols%,g%(),%11) PROCrectangle_gradient(128,128,-128,128,ncols%,g%(),%01) PROCrectangle_gradient(128,128,-128,-128,ncols%,g%(),%10) PROCrectangle_gradient(128,128,128,-128,ncols%,g%(),%00) |









