A drawing lasted exactly one frame. The verbs are immediate, they went to the back buffer, SDL double-buffers and the frontend never clears -- so the only way to keep a picture was to capture it with `SSHAPE` and install it as a sprite, which is what `examples/breakout/sprites/breakout.bas` spends two of its eight sprites doing. That was TODO.md section 9 item 9. The drawing verbs now render into a layer texture the frame composites under the text and the sprites. Draw once; it is there on every frame after. **Bracketed around the step phase, not around each verb.** One pair of `SDL_SetRenderTarget` calls a frame instead of one per `DRAW`, and it is also what makes `SSHAPE` read back what the program has just drawn rather than whatever the last frame left. **The layer is transparent where nothing was drawn.** It covers the whole window and composites underneath, so an opaque one would black out the frame the moment a program issued a single `DRAW`. And a fresh SDL target texture's contents are undefined, so it is cleared on creation -- skipping that puts uninitialised memory under the first frame's text and looks like a driver bug rather than a missing memset. **The line editor forced a wrinkle worth naming.** `akbasic_frontend_akgl_pump()` is called from two places with different answers to "is a render target current": the frame loop calls it between steps, and the sink's editor calls it from *inside* a step, borrowing a frame while it waits for a typed line. SDL refuses to present while a target is current, so the pump ends the layer, presents, and puts it back only if it was the one that ended it. `akgl_frontend` caught this -- it drives a REPL session, and it failed with "You can't present on a render target" the first time the brackets went in. This does not make a drawing *visible* on its own. The text layer still repaints every row it owns, opaque, every frame, and by default it owns the whole window; `WINDOW` shrinks it and that half was already fixed. The two together are what a picture needed, and the tests assert both -- a pixel still there a frame later with nothing redrawn, and a pixel below a shrunk text area surviving the text repaint. The second assertion wipes to a non-black colour first, because against black it could not tell a transparent layer from an opaque one. The tests found two of their own bugs on the way: `stop_runtime()` was not tearing the graphics backend down, so re-initialising it dropped a live texture on the floor; and a first draft called `begin()` before `start_runtime()`, which re-inits the backend, so the assertion read back off an orphaned render target and passed while proving nothing. Chapters 6 and 13 stop saying a drawing has to be redrawn every frame, because it does not. The batch-boundary tear stays documented -- it bites an `SSHAPE` capture, which matters much less now that capturing is not the only way to keep a picture. Both games still run clean. 111 with akgl, 110 without. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
268 lines
8.7 KiB
Markdown
268 lines
8.7 KiB
Markdown
# 6. Graphics
|
|
|
|
Everything in this chapter needs the SDL build and a graphics device. Without one each
|
|
verb refuses by name:
|
|
|
|
```basic requires=noakgl
|
|
10 DRAW 1, 0, 0 TO 100, 100
|
|
```
|
|
|
|
```output
|
|
? 10 : RUNTIME ERROR DRAW needs a graphics device and this runtime has none
|
|
|
|
```
|
|
|
|
## The coordinate space
|
|
|
|
**A drawing coordinate is a pixel of the host's window**, with (0, 0) at the top left.
|
|
On the standalone interpreter's 800 by 600 window, `DRAW 1, 799, 599` lands on the
|
|
bottom-right pixel and everything in between is reachable. A game embedding the
|
|
interpreter gets whatever size its own renderer is.
|
|
|
|
`RGR` is how a program finds out:
|
|
|
|
```basic requires=akgl
|
|
10 PRINT "THE SCREEN IS"
|
|
20 PRINT RGR(1)
|
|
30 PRINT "BY"
|
|
40 PRINT RGR(2)
|
|
50 DRAW 1, 0, 0 TO RGR(1) - 1, RGR(2) - 1
|
|
```
|
|
|
|
Subtracting one is not a wart, it is the last pixel: a window `RGR(1)` wide has
|
|
columns 0 through `RGR(1) - 1`.
|
|
|
|
**A C128 listing assumes 320 by 200 and will draw in the top-left corner.** Give it
|
|
the whole window by naming the space it was written for:
|
|
|
|
```basic requires=akgl
|
|
10 SCALE 1, 319, 199
|
|
20 BOX 1, 0, 0, 319, 199
|
|
```
|
|
|
|
That box is now the border of the window whatever size the window is. When no device
|
|
answers the size question at all, 320 by 200 is what the interpreter assumes — the
|
|
space a C128 listing was written for is the right thing to fall back to.
|
|
|
|
## Colour
|
|
|
|
`COLOR` binds a *source* to a palette index, and the drawing verbs name the source
|
|
rather than the colour:
|
|
|
|
```basic requires=akgl
|
|
10 COLOR 1, 3
|
|
20 DRAW 1, 10, 20
|
|
```
|
|
|
|
Sources are numbered 0 to 6; palette indices are 1 to 16, as on a C128. That
|
|
indirection is BASIC 7.0's, and it is why every drawing verb's first argument is a
|
|
small number that is not a colour.
|
|
|
|
## The verbs
|
|
|
|
### GRAPHIC
|
|
|
|
`GRAPHIC mode` chooses a screen mode; `GRAPHIC CLR` clears it. Mode 0 is text and
|
|
refuses to draw.
|
|
|
|
Every picture in this chapter is generated by running the listing above it; see
|
|
`MAINTENANCE.md` if you are editing one.
|
|
|
|
### DRAW
|
|
|
|
```basic requires=akgl screenshot=draw
|
|
10 COLOR 1, 8
|
|
20 DRAW 1, 20, 180 TO 90, 40 TO 160, 150 TO 230, 20 TO 300, 120
|
|
30 COLOR 2, 6
|
|
40 DRAW 2, 20, 190 TO 300, 190
|
|
50 LOCATE 160, 100
|
|
60 COLOR 3, 3
|
|
70 DRAW 3
|
|
```
|
|
|
|

|
|
|
|
One coordinate pair plots a point. Two or more, separated by `TO`, draw a polyline. A
|
|
bare `DRAW 3` — the last line above, and the single red pixel in the middle of the
|
|
picture — plots wherever `LOCATE` left the pixel cursor.
|
|
|
|
### BOX
|
|
|
|
```basic requires=akgl screenshot=box
|
|
10 COLOR 1, 8
|
|
20 BOX 1, 20, 30, 130, 140
|
|
30 COLOR 2, 6
|
|
40 BOX 2, 180, 30, 290, 140, 30
|
|
50 COLOR 3, 3
|
|
60 LOCATE 300, 190
|
|
70 BOX 3, 20, 160
|
|
```
|
|
|
|

|
|
|
|
Corners, and an optional rotation angle — the green box is the same box turned 30
|
|
degrees about its own centre. Two coordinates instead of four take the other corner
|
|
from the pixel cursor, which is the red box.
|
|
|
|
**`BOX` always outlines; it cannot fill.** BASIC 7.0 selects fill with a seventh
|
|
argument and that is not implemented here — see Chapter 13. `PAINT` is the fill you
|
|
have.
|
|
|
|
### CIRCLE
|
|
|
|
```basic requires=akgl screenshot=circle
|
|
10 COLOR 1, 8
|
|
20 CIRCLE 1, 80, 70, 60, 60
|
|
30 COLOR 2, 6
|
|
40 CIRCLE 2, 230, 70, 75, 45
|
|
50 COLOR 3, 3
|
|
60 CIRCLE 3, 160, 140, 130, 50, 90, 270
|
|
```
|
|
|
|

|
|
|
|
Source, centre, then the two radii — equal radii give a circle and unequal ones an
|
|
ellipse. Two further arguments are a start and an end angle, which is what makes the
|
|
red arc: 90 to 270 is the bottom half, because angles here are degrees clockwise from
|
|
straight up, the same convention `MOVSPR` uses. Beyond those come a rotation and the
|
|
degree increment, and a large increment is what turns a circle into a polygon.
|
|
|
|
### PAINT
|
|
|
|
```basic requires=akgl screenshot=paint
|
|
10 COLOR 1, 8
|
|
20 CIRCLE 1, 100, 100, 70, 70
|
|
30 BOX 1, 180, 50, 290, 150
|
|
40 COLOR 2, 6
|
|
50 PAINT 2, 100, 100
|
|
60 COLOR 3, 3
|
|
70 PAINT 3, 230, 100
|
|
```
|
|
|
|

|
|
|
|
Flood-fills the region containing a point, stopping at whatever is already drawn — so
|
|
the outline you fill inside can come from any verb. If the region is too large for the
|
|
fill's own working space it stops and reports rather than leaving a half-painted screen
|
|
with no explanation.
|
|
|
|
### LOCATE
|
|
|
|
Moves the pixel cursor, which is where a bare `DRAW` plots and where a `BOX` with two
|
|
coordinates finishes.
|
|
|
|
### SCALE
|
|
|
|
```basic requires=akgl screenshot=scale size=640x400
|
|
10 COLOR 1, 3
|
|
20 BOX 1, 0, 0, 319, 199
|
|
30 SCALE 1, 319, 199
|
|
40 COLOR 2, 6
|
|
50 BOX 2, 0, 0, 319, 199
|
|
60 DRAW 2, 0, 0 TO 319, 199
|
|
```
|
|
|
|

|
|
|
|
That picture is 640 by 400, and both boxes name the same four numbers. The red one is
|
|
drawn with `SCALE` off, so its coordinates are pixels and it covers exactly the
|
|
top-left 320 by 200 of the window — which is what a C128 listing does here. The green
|
|
one is drawn after `SCALE 1, 319, 199` and fills the window, and the diagonal confirms
|
|
that 319, 199 reaches the last pixel rather than stopping one short of it.
|
|
|
|
Turns on user coordinates and gives their maxima. With it on, your coordinates are
|
|
mapped onto the drawing surface: 0 is the first pixel and the maximum you gave is the
|
|
*last* one, so `DRAW 1, 1023, 1023` above reaches the bottom-right corner rather than
|
|
missing it by a pixel. `SCALE 1` on its own uses 7.0's 1023 by 1023. `SCALE 0` turns
|
|
it off and coordinates go back to being window pixels.
|
|
|
|
### RGR
|
|
|
|
`RGR(0)` is the current `GRAPHIC` mode. `RGR(1)` and `RGR(2)` are the drawing
|
|
surface's width and height in pixels — those two are ours rather than 7.0's, and they
|
|
are what a program needs to use a window whose size it did not choose. They
|
|
refuse when there is no graphics device, unlike `RGR(0)`, which is a mode this
|
|
interpreter recorded rather than a screen it has to go and measure.
|
|
|
|
**`RGR(3)` and `RGR(4)` are a character cell's width and height**, also ours. They come
|
|
from the *text* device rather than the graphics one — a character grid belongs to the
|
|
sink — so they refuse by naming that instead. Between them and
|
|
[`RWINDOW`](12-function-reference.md), which gives the current text window in columns
|
|
and rows, a program can place a character and a sprite at the same spot without
|
|
hardcoding a number measured against whatever font the host loaded:
|
|
|
|
```basic requires=akgl
|
|
10 CW# = RGR(3)
|
|
20 CH# = RGR(4)
|
|
30 COL# = 12
|
|
40 ROW# = 3
|
|
50 CHAR 1, COL#, ROW#, "X"
|
|
60 MOVSPR 1, COL# * CW#, ROW# * CH#
|
|
```
|
|
|
|
`RWINDOW` follows a `WINDOW` call, because it reports the window. `RGR(3)` and `RGR(4)`
|
|
do not, because windowing does not change how big a character is.
|
|
|
|
### WIDTH
|
|
|
|
`WIDTH 1` or `WIDTH 2` sets how thick a drawn line is. A thick line is drawn as
|
|
parallel passes; see Chapter 13.
|
|
|
|
## Saving and stamping regions
|
|
|
|
`SSHAPE` copies a rectangle off the screen and `GSHAPE` stamps it back:
|
|
|
|
```basic requires=akgl screenshot=shapes
|
|
10 COLOR 1, 8
|
|
20 CIRCLE 1, 40, 40, 30, 30
|
|
30 COLOR 2, 6
|
|
40 PAINT 2, 40, 40
|
|
50 SSHAPE A$, 8, 8, 72, 72
|
|
60 GSHAPE A$, 120, 20
|
|
70 GSHAPE A$, 200, 60
|
|
80 GSHAPE A$, 120, 120
|
|
```
|
|
|
|

|
|
|
|
One disc drawn, captured, and stamped three times.
|
|
|
|
A string array works: `SSHAPE SH$(2), ...` writes into element 2 and `GSHAPE SH$(2)`
|
|
stamps what element 2 names, so a program can keep a set of shapes in one array.
|
|
|
|
**`A$` holds a handle, not the pixels.** On a C128 the string holds the bitmap, so a
|
|
program could save it to disk or take its `LEN`. Here a string is a fixed 255 bytes and
|
|
the region is a device surface, so what goes in the string is a reference to it —
|
|
`SHAPE:0`. You can pass it to `GSHAPE` and to `SPRSAV`, which is everything BASIC ever
|
|
does with one, but you cannot store it or measure it.
|
|
|
|
## What is not here
|
|
|
|
`FILTER` parses and then refuses: there is no filter stage to configure. See Chapter 7.
|
|
|
|
**A drawing stays.** The verbs render into a layer the frame composites underneath the
|
|
text and the sprites, so a program draws its picture once and it is there on every frame
|
|
after. It does not have to redraw it, and it does not have to capture it into a sprite.
|
|
|
|
What covers it is the text layer, which repaints every row it owns — opaque, every frame,
|
|
for a reason `akbasic_sink_akgl_render()` explains — and by default it owns the whole
|
|
window. `WINDOW` shrinks it:
|
|
|
|
```basic norun
|
|
10 WINDOW 0, 0, 39, 1
|
|
20 GRAPHIC 1, 1
|
|
30 COLOR 1, 3
|
|
40 BOX 1, 20, 40, 300, 180
|
|
```
|
|
|
|
Two rows of text at the top, the rest of the window for drawing, and the box is still
|
|
there a thousand frames later.
|
|
|
|
**A redraw also has to fit inside one batch.** The host runs a fixed number of source
|
|
lines and then presents, and presenting throws the drawing buffer away — so a run of
|
|
drawing verbs longer than one batch is torn rather than merely transient, and an
|
|
`SSHAPE` at the end of it captures only the part issued since the present. Watching
|
|
`TI#` change is how a program finds the boundary; Chapter 13 has the measured numbers and
|
|
[Chapter 18](18-tutorial-breakout-artwork.md#step-5-find-the-frame-boundary) has a
|
|
routine that uses them.
|