The graphics verbs documented a coordinate transform that did not exist. With SCALE off a coordinate went straight to akgl_draw_* as a pixel address, so an 800x600 window drew a C128 listing into its corner and left the rest unused -- while the chapter said coordinates were 320x200 and stretching to fit was the host's business. akbasic_GraphicsBackend gains a size entry point, require_graphics() asks it before every verb that draws so a resized window is honoured between two statements, and 320x200 becomes the fallback for a backend that leaves it NULL. It is the record's one optional member, so a host written against the old header keeps the behaviour it had. SCALE now maps onto the device, and RGR(1)/RGR(2) report the drawing surface so a program can use a window whose size it did not choose. RGR(0) is BASIC 7.0's own field, the GRAPHIC mode. SCALE also mapped xmax onto the width rather than onto the last pixel, so SCALE 1, 319, 199 followed by DRAW 1, 319, 199 drew nothing at all -- one pixel past the surface. Fixed in the same line, because it is what makes "SCALE gives a C128 listing the whole window" true rather than nearly true. The akgl test renders against a 128x128 target, deliberately smaller than the old constants: a SCALE still dividing by them misses it entirely rather than landing somewhere plausible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
186 lines
8.4 KiB
C
186 lines
8.4 KiB
C
/**
|
|
* @file graphics.h
|
|
* @brief Declares the graphics backend: where DRAW, BOX, CIRCLE and PAINT land.
|
|
*
|
|
* The core library is free of SDL and builds with no libakgl present, so the
|
|
* graphics verbs cannot call akgl_draw_* directly. They call through a record of
|
|
* function pointers instead -- the same shape as akbasic_TextSink, and the same
|
|
* shape libakgl itself uses for akgl_RenderBackend and akgl_PhysicsBackend.
|
|
*
|
|
* akbasic_graphics_init_akgl() lives in the separate akbasic_akgl target and
|
|
* draws through whatever renderer the host game already initialized. A host that
|
|
* renders some other way supplies its own record and never links libakgl at all.
|
|
*
|
|
* A runtime with no graphics backend is the normal case -- the standalone driver
|
|
* has none -- so every graphics verb refuses with AKBASIC_ERR_DEVICE rather than
|
|
* dereferencing a NULL vtable.
|
|
*/
|
|
|
|
#ifndef _AKBASIC_GRAPHICS_H_
|
|
#define _AKBASIC_GRAPHICS_H_
|
|
|
|
#include <akerror.h>
|
|
|
|
#include <akbasic/types.h>
|
|
|
|
/**
|
|
* @brief The coordinate space assumed when the device will not say how big it is.
|
|
*
|
|
* A coordinate reaching the backend is a *device pixel*: `DRAW 1, 799, 599` lands
|
|
* at the bottom-right of an 800x600 window, and every pixel of that window is
|
|
* addressable from BASIC. The interpreter still does not own the renderer -- it
|
|
* asks, through akbasic_GraphicsBackend::size(), and the backend that does own
|
|
* one answers.
|
|
*
|
|
* These constants are what is assumed when nobody answers: a backend that leaves
|
|
* `size` NULL, or reports a nonsense size, gets BASIC 7.0's 320x200 hi-res
|
|
* screen. That is the right fallback rather than a guess, because it is the space
|
|
* a C128 listing was written for.
|
|
*/
|
|
#define AKBASIC_GRAPHICS_WIDTH 320
|
|
#define AKBASIC_GRAPHICS_HEIGHT 200
|
|
|
|
/** @brief How many COLOR sources there are. BASIC 7.0 numbers them 0 through 6. */
|
|
#define AKBASIC_COLOR_SOURCES 7
|
|
|
|
/**
|
|
* @brief An RGBA color, in the shape SDL_Color has without requiring SDL.
|
|
*
|
|
* BASIC never names a color this way -- COLOR takes a source and a 1-16 palette
|
|
* index -- so this is what the palette table in src/graphics_tables.c converts
|
|
* to, and what the backend receives.
|
|
*/
|
|
typedef struct
|
|
{
|
|
uint8_t r;
|
|
uint8_t g;
|
|
uint8_t b;
|
|
uint8_t a;
|
|
} akbasic_Color;
|
|
|
|
/**
|
|
* @brief The graphics verbs' own state, which lives on the runtime.
|
|
*
|
|
* DRAW, BOX, CIRCLE and PAINT name a *color source*, not a color: COLOR binds a
|
|
* source to a palette index and the drawing verbs reference the source. That
|
|
* indirection is BASIC 7.0's, not an invention here, and it is why `source` is a
|
|
* table of palette indices rather than a single current color.
|
|
*/
|
|
typedef struct
|
|
{
|
|
int mode; /* GRAPHIC mode; 0 is text, and refuses to draw */
|
|
int source[AKBASIC_COLOR_SOURCES]; /* palette index bound to each COLOR source */
|
|
double x; /* pixel cursor: where LOCATE put it, or where */
|
|
double y; /* the last DRAW ended */
|
|
bool scaling; /* SCALE on */
|
|
int linewidth; /* WIDTH: 1 or 2 pixels per drawn line */
|
|
double xmax; /* user-coordinate maxima SCALE maps from */
|
|
double ymax;
|
|
int devwidth; /* what the device last said it is, in pixels; */
|
|
int devheight; /* the AKBASIC_GRAPHICS_* fallback until then */
|
|
} akbasic_GraphicsState;
|
|
|
|
/**
|
|
* @brief Where the graphics verbs draw.
|
|
*
|
|
* Coordinates are `double` rather than an integer pixel address because SCALE
|
|
* makes them fractional: a program running at a 1023x1023 logical scale on a
|
|
* 320x200 screen produces non-integer pixel positions, and rounding at each verb
|
|
* rather than once at the backend accumulates visible drift along a polyline.
|
|
*
|
|
* There is deliberately no circle entry point. BASIC 7.0's CIRCLE takes two
|
|
* radii, a start and end angle, a rotation and a degree increment -- it is a
|
|
* polygon by definition -- so it is built from `line` calls in the verb handler.
|
|
* See TODO.md section 5.
|
|
*/
|
|
typedef struct akbasic_GraphicsBackend
|
|
{
|
|
void *self;
|
|
|
|
/**
|
|
* How big the drawing surface is, in pixels.
|
|
*
|
|
* **Optional, and the only optional entry point in the record.** It was
|
|
* added after the rest, so a host that filled one of these in by hand
|
|
* before it existed leaves it NULL and keeps the 320x200 fallback rather
|
|
* than crashing; and a backend drawing somewhere with no meaningful size
|
|
* has an honest way to say so. Everything else here is required.
|
|
*
|
|
* Called before each verb draws rather than once at attach, because a
|
|
* window is resizable and a script that runs for an hour should not be
|
|
* drawing to the size the window was when it started.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*size)(struct akbasic_GraphicsBackend *self, int *width, int *height);
|
|
|
|
/** Plot one pixel. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*point)(struct akbasic_GraphicsBackend *self, double x, double y, akbasic_Color color);
|
|
/** Draw a line between two points. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*line)(struct akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color);
|
|
/** Outline an axis-aligned rectangle. A rotated BOX becomes four `line` calls. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*rect)(struct akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color);
|
|
/** Fill an axis-aligned rectangle. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*filled_rect)(struct akbasic_GraphicsBackend *self, double x1, double y1, double x2, double y2, akbasic_Color color);
|
|
/**
|
|
* Flood-fill the region containing (x, y).
|
|
*
|
|
* May report AKERR_OUTOFBOUNDS having filled only part of the region: the
|
|
* akgl implementation walks spans from a fixed stack and gives up rather
|
|
* than growing it. PAINT reports that to the program instead of leaving a
|
|
* half-painted screen unexplained.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*paint)(struct akbasic_GraphicsBackend *self, int x, int y, akbasic_Color color);
|
|
/** Clear the whole drawing surface to one color. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*clear)(struct akbasic_GraphicsBackend *self, akbasic_Color color);
|
|
|
|
/**
|
|
* Save a rectangular region and yield an opaque handle to it.
|
|
*
|
|
* SSHAPE stores the handle in a BASIC string, not the pixels -- see TODO.md
|
|
* section 5 for what that deviation costs.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*save_shape)(struct akbasic_GraphicsBackend *self, int x1, int y1, int x2, int y2, int *handle);
|
|
/** Blit a saved region back at (x, y). */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*paste_shape)(struct akbasic_GraphicsBackend *self, int handle, double x, double y);
|
|
/** Release every saved region. NEW and CLR call this; nothing else reclaims a slot. */
|
|
akerr_ErrorContext AKERR_NOIGNORE *(*free_shapes)(struct akbasic_GraphicsBackend *self);
|
|
} akbasic_GraphicsBackend;
|
|
|
|
/**
|
|
* @brief Convert a BASIC color index to the RGBA the backend draws with.
|
|
*
|
|
* BASIC 7.0 numbers its sixteen colors from 1, not 0; index 17 is the "current"
|
|
* color on a real C128 and has no meaning here.
|
|
*
|
|
* @param index Palette index, 1 through 16.
|
|
* @param dest Output destination populated by the function.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `dest` is NULL.
|
|
* @throws AKBASIC_ERR_BOUNDS When `index` is outside 1..16.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_palette(int index, akbasic_Color *dest);
|
|
|
|
/**
|
|
* @brief Reset the graphics state to its power-on values.
|
|
*
|
|
* Text mode, the default color-source bindings, the pixel cursor at the origin
|
|
* and scaling off. GRAPHIC CLR and NEW both come back through here.
|
|
*
|
|
* @param obj Object to initialize, inspect, or modify.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `obj` is NULL.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_state_init(akbasic_GraphicsState *obj);
|
|
|
|
/**
|
|
* @brief Resolve a COLOR source to the RGBA the backend draws with.
|
|
* @param obj Graphics state to read the binding from.
|
|
* @param source COLOR source number, 0 through 6.
|
|
* @param dest Output destination populated by the function.
|
|
* @return `NULL` on success, otherwise an error context owned by the caller.
|
|
* @throws AKERR_NULLPOINTER When `obj` or `dest` is NULL.
|
|
* @throws AKBASIC_ERR_BOUNDS When `source` is outside 0..6.
|
|
*/
|
|
akerr_ErrorContext AKERR_NOIGNORE *akbasic_graphics_source_color(akbasic_GraphicsState *obj, int source, akbasic_Color *dest);
|
|
|
|
#endif // _AKBASIC_GRAPHICS_H_
|