joeylib2/docs/ApiTile.adoc
2026-10-19 15:46:47 -05:00

474 lines
16 KiB
Text

== Tiles and Text
Declared in `<joey/tile.h>`.
A tile is not a separate object. It is the 8x8 region of a surface at block
coordinates `(bx, by)`, where `bx` runs 0..39 and `by` runs 0..24 -- 40x25
blocks across a 320x200 surface. The tile API moves 32-byte blocks between
surfaces or fills them.
The consequence is that everything else works on tiles too. You can paint tile
content with `jlDrawPixel` and `jlFillRect`, generate it procedurally, or edit
it at run time, because a "tileset" is just a surface you agree to treat as
one. Fonts, terrain sets, and spritesheets are all the same thing.
=== Constants and Types
[cols="3,1,4", options="header"]
|===
|Name |Value |Meaning
|`JL_TILE_PIXELS_PER_SIDE` |8 |Tile edge in pixels.
|`JL_TILE_BYTES_PER_ROW` |4 |Bytes per tile row (8 pixels at 4bpp).
|`JL_TILE_BYTES` |32 |Bytes in one tile.
|`JL_TILE_BLOCKS_PER_ROW` |40 |Tile columns on a surface.
|`JL_TILE_BLOCKS_PER_COL` |25 |Tile rows on a surface.
|`JL_TILE_NO_GLYPH` |0xFFFF |`jlDrawText` map entry meaning "skip this character".
|`JL_TILE_MAP_SKIP` |0xFF |`jlTileMapPaste` entry meaning "leave this cell alone"; in a tile layer's map, color 0.
|`JL_TILE_LAYER_MAX_SIDE` |4096 |Largest tile layer map side, in tiles.
|===
[source,c]
----
typedef struct jlTileT {
uint8_t pixels[JL_TILE_BYTES];
} jlTileT;
----
A stack-allocatable 32-byte tile snapshot, so save-under style work does not
need a whole scratch surface.
[IMPORTANT]
====
A `jlTileT` is *target-native*: plane-major on the Amiga and X68000, row by row
with each row's four plane bytes together on the ST, and chunky on the others.
Treat it as opaque. In particular, do not feed a tile from `jlTileSnap` or
`jlTileBankLoad` to `jlTilePasteMono`, which wants a chunky silhouette -- on the
Amiga, ST, and X68000 that is a format mismatch and draws the wrong shape. Use
`jlTilePasteGlyph` for 1-bit glyph data instead.
====
=== jlTileCopy
[source,c]
----
void jlTileCopy(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy);
----
Copies one 8x8 block between surfaces, opaquely.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`dst` |`jlSurfaceT *` |Destination surface.
|`dstBx`, `dstBy` |`uint8_t` |Destination block coordinates.
|`src` |`const jlSurfaceT *` |Source surface. May be the same as `dst`.
|`srcBx`, `srcBy` |`uint8_t` |Source block coordinates.
|===
*Returns:* nothing. Out-of-range block coordinates on either side are silent
no-ops.
=== jlTileCopyMasked
[source,c]
----
void jlTileCopyMasked(jlSurfaceT *dst, uint8_t dstBx, uint8_t dstBy,
const jlSurfaceT *src, uint8_t srcBx, uint8_t srcBy,
uint8_t transparentIndex);
----
As `jlTileCopy`, but source pixels equal to `transparentIndex` are skipped and
the destination keeps its original value. This is the call for fonts and for
any tileset with see-through pixels.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`dst` |`jlSurfaceT *` |Destination surface.
|`dstBx`, `dstBy` |`uint8_t` |Destination block coordinates.
|`src` |`const jlSurfaceT *` |Source surface.
|`srcBx`, `srcBy` |`uint8_t` |Source block coordinates.
|`transparentIndex` |`uint8_t` |Source color index treated as transparent.
|===
*Returns:* nothing.
=== jlTileFill
[source,c]
----
void jlTileFill(jlSurfaceT *s, uint8_t bx, uint8_t by, uint8_t colorIndex);
----
Fills one block with a solid color. Equivalent to
`jlFillRect(s, bx*8, by*8, 8, 8, colorIndex)` but skips the clipping math,
since block coordinates are already known to be in range.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`s` |`jlSurfaceT *` |Target surface.
|`bx`, `by` |`uint8_t` |Block coordinates.
|`colorIndex` |`uint8_t` |Color index 0..15.
|===
*Returns:* nothing.
=== jlTileBankLoad
[source,c]
----
uint16_t jlTileBankLoad(const char *path, jlTileT *outTiles, uint16_t maxTiles,
bool *outValid, uint16_t *outPalette);
----
Loads tiles from a baked `.tbk` file. The file's per-target byte is checked
against the build platform and mismatches are refused.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`path` |`const char *` |File path.
|`outTiles` |`jlTileT *` |Receives the tiles. Caller-owned.
|`maxTiles` |`uint16_t` |Capacity of `outTiles`.
|`outValid` |`bool *` |Optional, may be `NULL`. One flag per slot up to
`maxTiles`: `true` for each loaded tile, `false` for the rest (all of them when
the file is refused), so loaded tiles can be told from leftovers in a
fixed-size array.
|`outPalette` |`uint16_t *` |Optional, may be `NULL`. Receives the embedded
16-entry `$0RGB` palette if the file has one.
|===
*Returns:* the number of tiles written. Returns 0 if the file is missing, the
magic or target byte is wrong, or `maxTiles` is 0.
=== jlTileBankLoadToSurface
[source,c]
----
uint16_t jlTileBankLoadToSurface(const char *path, jlSurfaceT *dst,
uint16_t *outPalette);
----
Streams a baked `.tbk` straight onto a surface's block grid: tile `i` lands at
block `(i % 40, i / 40)`, the row-major layout the bake tools author against.
Use this instead of `jlTileBankLoad` for anything large. A 1000-tile font sheet
is 32000 bytes, which no IIgs program can hold in an array or in bank-0 BSS;
this needs only a small internal scratch buffer and one pass over the file.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`path` |`const char *` |File path.
|`dst` |`jlSurfaceT *` |Destination surface.
|`outPalette` |`uint16_t *` |Optional, may be `NULL`. As `jlTileBankLoad`.
|===
*Returns:* the number of tiles pasted. Blocks beyond the file's tile count keep
their previous content, and tiles past the surface's 1000 blocks are ignored.
=== jlTileSnap
[source,c]
----
void jlTileSnap(const jlSurfaceT *src, uint8_t bx, uint8_t by, jlTileT *out);
----
Captures one block into a caller-supplied `jlTileT`.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`src` |`const jlSurfaceT *` |Source surface.
|`bx`, `by` |`uint8_t` |Block coordinates.
|`out` |`jlTileT *` |Receives the 32-byte snapshot.
|===
*Returns:* nothing.
=== jlTilePaste
[source,c]
----
void jlTilePaste(jlSurfaceT *dst, uint8_t bx, uint8_t by, const jlTileT *in);
----
Pastes a `jlTileT` back onto a surface, always opaquely. Use
`jlTileCopyMasked` if you need transparency.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`dst` |`jlSurfaceT *` |Destination surface.
|`bx`, `by` |`uint8_t` |Block coordinates.
|`in` |`const jlTileT *` |Tile to paste.
|===
*Returns:* nothing.
=== jlTilePasteMono
[source,c]
----
void jlTilePasteMono(jlSurfaceT *dst, uint8_t bx, uint8_t by,
const jlTileT *in, uint8_t fgColor, uint8_t bgColor);
----
Pastes a tile as a silhouette: any non-zero source pixel draws `fgColor`, zero
pixels draw `bgColor`. Useful when the bitmap is a fixed glyph but the color
comes from a separate map, as in C64-style color RAM. Opaque.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`dst` |`jlSurfaceT *` |Destination surface.
|`bx`, `by` |`uint8_t` |Block coordinates.
|`in` |`const jlTileT *` |Source tile, read as a silhouette.
|`fgColor` |`uint8_t` |Color for non-zero pixels. Masked to 4 bits.
|`bgColor` |`uint8_t` |Color for zero pixels. Masked to 4 bits.
|===
*Returns:* nothing.
[WARNING]
====
This call wants a *chunky* silhouette, but a `jlTileT` from `jlTileSnap` or
`jlTileBankLoad` is target-native. Feeding one to this function on the Amiga,
ST, or X68000 is a format mismatch. For glyphs, use `jlTilePasteGlyph`, which
takes unambiguous 1-bit input.
====
=== jlTilePasteGlyph
[source,c]
----
void jlTilePasteGlyph(jlSurfaceT *dst, uint8_t bx, uint8_t by,
const uint8_t *bits, uint8_t fgColor, uint8_t bgColor);
----
Pastes an 8x8 one-bit-per-pixel glyph: `bits[row]` is that row, most
significant bit leftmost. A set bit draws `fgColor`, a clear bit `bgColor`.
Opaque.
This is the shape a character generator already has -- a font ROM, a C64
charset, anything 1bpp -- so nothing needs expanding into a `jlTileT` first.
*Prefer this over `jlTilePasteMono` for glyphs.*
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`dst` |`jlSurfaceT *` |Destination surface.
|`bx`, `by` |`uint8_t` |Block coordinates.
|`bits` |`const uint8_t *` |8 bytes, one per glyph row, MSB leftmost.
|`fgColor` |`uint8_t` |Set-bit color. Masked to 4 bits.
|`bgColor` |`uint8_t` |Clear-bit color. Masked to 4 bits.
|===
*Returns:* nothing.
=== jlDrawText
[source,c]
----
void jlDrawText(jlSurfaceT *dst, uint8_t bx, uint8_t by,
const jlSurfaceT *fontSurface, const uint16_t *asciiMap,
const char *str);
----
Draws a NUL-terminated ASCII string using glyphs from a font surface. Glyph
color 0 is transparent, so the background shows through. The cursor wraps at
the right edge and truncates at the bottom. A start off the 40x25 grid uses up
characters without drawing them as the cursor moves on: a start column past
39 drops the first character and carries on at the start of the next row, and
a start row past 24 draws nothing.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`dst` |`jlSurfaceT *` |Destination surface.
|`bx`, `by` |`uint8_t` |Starting block coordinates.
|`fontSurface` |`const jlSurfaceT *` |Surface holding the glyphs.
|`asciiMap` |`const uint16_t *` |256 entries mapping ASCII code to glyph
position: low byte is source block X, high byte is source block Y. An entry of
`JL_TILE_NO_GLYPH`, or one off the font surface's 40x25 grid, skips that
character, advancing the cursor without drawing.
|`str` |`const char *` |NUL-terminated string.
|===
*Returns:* nothing.
=== jlTileMapPaste
[source,c]
----
void jlTileMapPaste(jlSurfaceT *dst, uint8_t bx, uint8_t by,
uint8_t wTiles, uint8_t hTiles,
const jlTileT *tiles, const uint8_t *map);
----
Pastes a whole rectangle of tiles in one call, so validation, dispatch, and the
dirty-rectangle union are paid once for the rectangle instead of once per tile.
For anything larger than a few tiles this is substantially faster than looping
over `jlTilePaste`.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`dst` |`jlSurfaceT *` |Destination surface.
|`bx`, `by` |`uint8_t` |Top-left block coordinates.
|`wTiles`, `hTiles` |`uint8_t` |Rectangle size in tiles. Zero is a no-op.
|`tiles` |`const jlTileT *` |The tileset.
|`map` |`const uint8_t *` |`wTiles * hTiles` tileset indices, row-major, so
`map[row * wTiles + col]`. `JL_TILE_MAP_SKIP` leaves that cell untouched.
|===
*Returns:* nothing. A rectangle whose edge crosses the surface boundary is
rejected as a no-op.
[WARNING]
====
Any map index other than `JL_TILE_MAP_SKIP` must be a valid tileset index. The
batch does not know the tileset length and does *not* range-check it -- an
out-of-range index reads past the end of your array.
====
=== Scrolling Tile Layers
A tile layer is a map of tile indices, up to `JL_TILE_LAYER_MAX_SIDE` tiles a
side, seen through a band of the stage: whole tile rows, the full width (rows
outside the band are yours, for a status bar or a score). `jlTileLayerDraw`
fills the band with the map as seen from a pixel origin, to the pixel both
ways, so moving the origin a little each frame scrolls it. The map wraps at its
edges: an origin past them, or a map smaller than the view, repeats it.
The layer keeps the tiles around the view drawn, so a draw only draws the edge
tiles the move uncovered (a move of a whole view or more draws them all), then
copies the band. A draw covers the band and never reads what was there, so a
scrolling frame is: draw the layer, draw the sprites with `jlSpriteDraw` (no
save-under, since the next draw covers them), present.
[source,c]
----
static jlTileT tiles[64]; // from jlTileBankLoad
static uint8_t map[256 * 25]; // 256 x 25 tile indices
jlSpriteT *ship; // from jlSpriteCreate, compiled
jlTileLayerT *layer;
uint16_t x = 0;
layer = jlTileLayerCreate(tiles, map, 256, 25, 0, JL_TILE_BLOCKS_PER_COL);
for (;;) {
jlTileLayerDraw(jlStageGet(), layer, x, 0);
jlSpriteDraw(jlStageGet(), ship, 40, 96);
jlStagePresent();
x++;
}
----
The layer keeps a pointer to your tileset and map rather than copying them, so
both must outlive it. Change a cell with `jlTileLayerSetTile`, not by writing
the map: the layer would not see the write until that cell scrolled out of the
view and back. A tile whose pixels you change in the tileset is likewise only
seen where a cell showing it is set again.
What a layer costs in memory is its view: about 1.3 KB per tile row of the band,
plus one, on the Amiga, ST, and X68000 (35 KB for a full-height band), twice that on the
others, which keep a second copy of the view shifted by a pixel so an odd
origin copies whole bytes.
[NOTE]
====
Every whole-band scroll redraws the band and presents it, so a scrolling game
runs at the rate of a full-screen present plus a full-band copy. On the
slowest machines JoeyLib targets that is a few frames a second (the IIgs about
5.8, the plain ST about 8, a 386SX-16 under DOS about 6.5 in the UBER
benchmark; the Amiga and STe copy the band with their blitters, about 20), so
plan scrolling games around a band no taller than the game needs.
====
=== jlTileLayerCreate
[source,c]
----
jlTileLayerT *jlTileLayerCreate(const jlTileT *tiles, uint8_t *map,
uint16_t mapWidth, uint16_t mapHeight,
uint8_t viewBy, uint8_t viewHeight);
----
Makes a layer over `map`. Nothing is drawn until `jlTileLayerDraw`.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`tiles` |`const jlTileT *` |The tileset. Kept, not copied.
|`map` |`uint8_t *` |`mapWidth * mapHeight` tileset indices, row-major, so
`map[row * mapWidth + col]`. `JL_TILE_MAP_SKIP` draws color 0. Kept, not
copied; `jlTileLayerSetTile` writes it.
|`mapWidth`, `mapHeight` |`uint16_t` |The map's size in tiles, 1 to
`JL_TILE_LAYER_MAX_SIDE`.
|`viewBy` |`uint8_t` |The band's first tile row on the stage.
|`viewHeight` |`uint8_t` |The band's height in tile rows, at least 1;
`viewBy + viewHeight` at most `JL_TILE_BLOCKS_PER_COL`.
|===
*Returns:* the layer, or `NULL` if an argument is `NULL` or out of range, or
memory runs out.
[WARNING]
====
Like `jlTileMapPaste`, a layer does not range-check map indices against the
tileset: any index other than `JL_TILE_MAP_SKIP` must be a valid one.
====
=== jlTileLayerDestroy
[source,c]
----
void jlTileLayerDestroy(jlTileLayerT *layer);
----
Frees a layer, not its tileset or map. `NULL` is a no-op.
=== jlTileLayerDraw
[source,c]
----
void jlTileLayerDraw(jlSurfaceT *dst, jlTileLayerT *layer,
uint16_t x, uint16_t y);
----
Fills the layer's band of `dst` with the map seen from pixel `(x, y)`: the
band's top-left pixel shows map pixel `(x, y)`, each taken modulo the map's
size in pixels. Any pixel origin, with no alignment.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`dst` |`jlSurfaceT *` |Destination surface, normally the stage.
|`layer` |`jlTileLayerT *` |The layer.
|`x`, `y` |`uint16_t` |The origin, in map pixels.
|===
*Returns:* nothing.
=== jlTileLayerSetTile
[source,c]
----
void jlTileLayerSetTile(jlTileLayerT *layer, uint16_t tx, uint16_t ty,
uint8_t tile);
----
Sets map cell `(tx, ty)` to `tile`. The next draw shows it.
[cols="1,1,4", options="header"]
|===
|Parameter |Type |Description
|`layer` |`jlTileLayerT *` |The layer.
|`tx`, `ty` |`uint16_t` |The cell, in tiles. Out-of-range cells are ignored.
|`tile` |`uint8_t` |The tileset index, or `JL_TILE_MAP_SKIP` for color 0.
|===
*Returns:* nothing.