Skip to content

Commit 0316d1c

Browse files
cursoragentomiq
andcommitted
Add SCROLL viewport + tutorial_gfx examples and cheat-sheet page
- GfxVideoState scroll_x/scroll_y; SCROLL dx,dy; SCROLLX()/SCROLLY() - Apply offset in canvas + raylib text/bitmap renderers and sprite composite - examples: tutorial_gfx_scroll/memory/tilemap/gamepad/index + tile PNG - web/tutorial-gfx-features.html; README, CHANGELOG, embedding doc, to-do Co-authored-by: Chris Garrett <chris@chrisg.com>
1 parent 5bb05c3 commit 0316d1c

20 files changed

+326
-19
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
### Unreleased
44

5+
- **Viewport scroll (basic-gfx + canvas WASM):** **`SCROLL dx, dy`** sets pixel pan for the text/bitmap layer and sprites; **`SCROLLX()`** / **`SCROLLY()`** read offsets. `GfxVideoState.scroll_x` / `scroll_y`; Raylib and canvas compositors apply the same shift. Examples: **`examples/tutorial_gfx_scroll.bas`**, overview **`web/tutorial-gfx-features.html`**.
6+
57
- **Gamepad (basic-gfx + canvas WASM):** **`JOY(port, button)`**, alias **`JOYSTICK`**, and **`JOYAXIS(port, axis)`** (`gfx_gamepad.c`). **Native:** Raylib **1–15** button codes; axes **0–5** scaled **-1000..1000**. **Canvas WASM:** `canvas.html` polls **`navigator.getGamepads()`** each frame into exported buffers; Raylib codes map to **Standard Gamepad** indices. **Terminal WASM** (no `GFX_VIDEO`) still returns **0**. **`HEAP16`** + **`_wasm_gamepad_buttons_ptr`** / **`_wasm_gamepad_axes_ptr`** exports. Example **`examples/gfx_joy_demo.bas`**.
68

79
- **Tilemap sprite sheets:** **`LOADSPRITE slot, "path.png", tw, th`** defines a **tw×th** tile grid; **`SPRITETILES(slot)`**, **`DRAWSPRITETILE slot, x, y, tile_index [, z]`** (1-based tile index), **`SPRITEFRAME slot, frame`** / **`SPRITEFRAME(slot)`** (default tile for **`DRAWSPRITE`** when crop omitted), **`gfx_sprite_effective_source_rect`**, **`SPRITEW`/`SPRITEH`** return one tile size when tilemap is active. **`SPRITECOLLIDE`** uses tile size when no explicit crop.

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,10 @@ Releases include **basic-gfx** — a full graphical version of the interpreter b
377377
- Use `SCREENCODES OFF` to restore the default (ASCII strings like `PRINT "HELLO"` map naturally).
378378
- The window closes automatically when the program reaches `END`.
379379

380+
**Viewport scroll (basic-gfx + canvas WASM)**:
381+
382+
- **`SCROLL dx, dy`** sets a **pixel** offset for the **text/bitmap layer and sprites** (positive **dx** pans the world to the left; positive **dy** pans up). Use **`SCROLL 0, 0`** to reset. **`SCROLLX()`** / **`SCROLLY()`** return the current offset (roughly **-32768..32767**). Tutorial: `examples/tutorial_gfx_scroll.bas`.
383+
380384
**PNG sprites / HUD overlay (basic-gfx)**:
381385

382386
- `LOADSPRITE slot, "file.png"` queues loading a PNG from disk. Paths are relative to the **`.bas` file’s directory** (or absolute). Only `.png` is supported here; use `LOAD "bin" INTO …` for raw bytes.
@@ -405,6 +409,7 @@ Releases include **basic-gfx** — a full graphical version of the interpreter b
405409

406410
The `examples` folder (included in release archives) contains:
407411

412+
- **Graphics / WASM feature tours** (run with **`./basic-gfx`** or load into **`web/canvas.html`**): `tutorial_gfx_index.bas` lists short demos — **`tutorial_gfx_scroll.bas`** (**`SCROLL`**), **`tutorial_gfx_memory.bas`** (**`POKE`/`PEEK`** bases), **`tutorial_gfx_tilemap.bas`** (**tile** **`LOADSPRITE`** + **`tutorial_tile_sheet_demo.png`**), **`tutorial_gfx_gamepad.bas`** (**`JOY`**/**`JOYAXIS`**). See also **`web/tutorial-gfx-features.html`** for a static overview (serve the `web/` directory over HTTP).
408413
- `dartmouth.bas`: classic Dartmouth BASIC tutorial; exercises `PRINT`, `INPUT`, `IF`, `FOR/NEXT`, `DEF FN`, `READ`/`DATA`, and more.
409414
- `trek.bas`: Star Trek–style game; exercises `GET`, `ON GOTO`/`GOSUB`, multi-dimensional arrays, and PETSCII-style output.
410415
- `chr.bas`: PETSCII/ANSI color and control-code test (run with `-petscii`).

basic.c

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2173,7 +2173,9 @@ enum func_code {
21732173
FN_JOY = 47,
21742174
FN_JOYAXIS = 48,
21752175
FN_SPRITETILES = 49,
2176-
FN_SPRITEFRAME = 50
2176+
FN_SPRITEFRAME = 50,
2177+
FN_SCROLLX = 51,
2178+
FN_SCROLLY = 52
21772179
};
21782180

21792181
/* Report an error and halt further execution.
@@ -2409,10 +2411,10 @@ static const char *const reserved_words[] = {
24092411
"INKEY", "INPUT", "INSTR", "INT", "INDEXOF", "JSON", "LEFT", "LEN", "LET", "LINE", "LOAD", "LOADSPRITE", "LOCATE", "LOG",
24102412
"LASTINDEXOF", "LCASE", "FIELD", "LTRIM", "MEMCPY", "MEMSET", "MID", "MOD", "NEXT", "OFF", "ON", "OPEN", "OR", "PEEK", "POKE", "PLATFORM", "PRESET", "PSET", "PRINT",
24112413
"XOR",
2412-
"READ", "REM", "REPLACE", "RESTORE", "RETURN", "RIGHT", "RND", "RTRIM", "RVS", "SCREEN", "SCREENCODES", "SPRITECOLLIDE", "SPRITEFRAME", "SPRITETILES", "SPRITEVISIBLE",
2414+
"READ", "REM", "REPLACE", "RESTORE", "RETURN", "RIGHT", "RND", "RTRIM", "RVS", "SCROLL", "SCREEN", "SCREENCODES", "SPRITECOLLIDE", "SPRITEFRAME", "SPRITETILES", "SPRITEVISIBLE",
24132415
"JOIN",
24142416
"SGN", "SIN", "SLEEP", "SORT", "SPC", "SPLIT", "SPRITEH", "SPRITEW", "SQR", "STEP", "STOP", "STR", "STRING",
2415-
"DRAWSPRITE", "DRAWSPRITETILE", "JOY", "JOYAXIS", "JOYSTICK", "SYSTEM", "TAB", "TAN", "TEXTAT", "THEN", "TI", "TO", "TRIM", "UCASE", "UNLOADSPRITE", "VAL", "WEND", "WHILE",
2417+
"DRAWSPRITE", "DRAWSPRITETILE", "JOY", "JOYAXIS", "JOYSTICK", "SCROLLX", "SCROLLY", "SYSTEM", "TAB", "TAN", "TEXTAT", "THEN", "TI", "TO", "TRIM", "UCASE", "UNLOADSPRITE", "VAL", "WEND", "WHILE",
24162418
"DO", "LOOP", "UNTIL", "EXIT",
24172419
NULL
24182420
};
@@ -3219,6 +3221,10 @@ static int function_lookup(const char *name, int len)
32193221
if (len == 11 && name[0] == 'S' && name[1] == 'P' && name[2] == 'R' && name[3] == 'I' &&
32203222
name[4] == 'T' && name[5] == 'E' && name[6] == 'F' && name[7] == 'R' && name[8] == 'A' &&
32213223
name[9] == 'M' && name[10] == 'E') return FN_SPRITEFRAME;
3224+
if (len == 7 && name[0] == 'S' && name[1] == 'C' && name[2] == 'R' && name[3] == 'O' &&
3225+
name[4] == 'L' && name[5] == 'L' && name[6] == 'X') return FN_SCROLLX;
3226+
if (len == 7 && name[0] == 'S' && name[1] == 'C' && name[2] == 'R' && name[3] == 'O' &&
3227+
name[4] == 'L' && name[5] == 'L' && name[6] == 'Y') return FN_SCROLLY;
32223228
return FN_NONE;
32233229
case 'C':
32243230
if ((len == 3 && name[0] == 'C' && name[1] == 'H' && name[2] == 'R') ||
@@ -3654,6 +3660,52 @@ static void statement_line(char **p)
36543660
#endif
36553661
}
36563662

3663+
/* SCROLL x, y — viewport offset in pixels (text/bitmap layer + sprites; basic-gfx / canvas WASM). */
3664+
static void statement_scroll(char **p)
3665+
{
3666+
#ifndef GFX_VIDEO
3667+
(void)p;
3668+
runtime_error_hint("SCROLL is only available in basic-gfx or canvas WASM",
3669+
"SCROLL dx, dy shifts the visible framebuffer (camera-style).");
3670+
#else
3671+
struct value vx, vy;
3672+
int dx, dy;
3673+
skip_spaces(p);
3674+
vx = eval_expr(p);
3675+
ensure_num(&vx);
3676+
skip_spaces(p);
3677+
if (**p != ',') {
3678+
runtime_error_hint("SCROLL expects dx, dy",
3679+
"Example: SCROLL 8, 0 (pan one character width right).");
3680+
return;
3681+
}
3682+
(*p)++;
3683+
skip_spaces(p);
3684+
vy = eval_expr(p);
3685+
ensure_num(&vy);
3686+
if (!gfx_vs) {
3687+
runtime_error_hint("SCROLL requires basic-gfx or canvas WASM", NULL);
3688+
return;
3689+
}
3690+
dx = (int)vx.num;
3691+
dy = (int)vy.num;
3692+
if (dx < -32768) {
3693+
dx = -32768;
3694+
}
3695+
if (dx > 32767) {
3696+
dx = 32767;
3697+
}
3698+
if (dy < -32768) {
3699+
dy = -32768;
3700+
}
3701+
if (dy > 32767) {
3702+
dy = 32767;
3703+
}
3704+
gfx_vs->scroll_x = (int16_t)dx;
3705+
gfx_vs->scroll_y = (int16_t)dy;
3706+
#endif
3707+
}
3708+
36573709
static void statement_screen(char **p)
36583710
{
36593711
#ifndef GFX_VIDEO
@@ -4574,6 +4626,17 @@ static struct value eval_function(const char *name, char **p)
45744626
runtime_error_hint("JOY/JOYAXIS require basic-gfx (native)", "Terminal basic has no gamepad.");
45754627
return make_num(0.0);
45764628
}
4629+
if (code == FN_SCROLLX || code == FN_SCROLLY) {
4630+
if (**p != ')') {
4631+
runtime_error_hint("Missing ')'", "SCROLLX() and SCROLLY() take no arguments.");
4632+
return make_num(0.0);
4633+
}
4634+
(*p)++;
4635+
skip_spaces(p);
4636+
runtime_error_hint("SCROLLX/SCROLLY require basic-gfx or canvas WASM",
4637+
"Viewport scroll applies to the PETSCII/bitmap layer in graphics builds.");
4638+
return make_num(0.0);
4639+
}
45774640
#endif
45784641
#ifdef GFX_VIDEO
45794642
if (code == FN_SPRITEW || code == FN_SPRITEH) {
@@ -4667,6 +4730,18 @@ static struct value eval_function(const char *name, char **p)
46674730
}
46684731
return make_num(0.0);
46694732
}
4733+
if (code == FN_SCROLLX || code == FN_SCROLLY) {
4734+
if (**p != ')') {
4735+
runtime_error_hint("Missing ')'", "SCROLLX() and SCROLLY() take no arguments.");
4736+
return make_num(0.0);
4737+
}
4738+
(*p)++;
4739+
skip_spaces(p);
4740+
if (gfx_vs) {
4741+
return make_num((double)(code == FN_SCROLLX ? gfx_vs->scroll_x : gfx_vs->scroll_y));
4742+
}
4743+
return make_num(0.0);
4744+
}
46704745
if (code == FN_JOY || code == FN_JOYAXIS) {
46714746
struct value vp, vb;
46724747
int port, btn, ax;
@@ -6131,6 +6206,7 @@ static struct value eval_factor(char **p)
61316206
starts_with_kw(*p, "PEEK") || starts_with_kw(*p, "INKEY") ||
61326207
starts_with_kw(*p, "SPRITEW") || starts_with_kw(*p, "SPRITEH") ||
61336208
starts_with_kw(*p, "SPRITECOLLIDE") || starts_with_kw(*p, "SPRITETILES") || starts_with_kw(*p, "SPRITEFRAME") ||
6209+
starts_with_kw(*p, "SCROLLX") || starts_with_kw(*p, "SCROLLY") ||
61346210
starts_with_kw(*p, "JOY") || starts_with_kw(*p, "JOYSTICK") || starts_with_kw(*p, "JOYAXIS")) {
61356211
char namebuf[32];
61366212
char *q;
@@ -9196,6 +9272,11 @@ static void execute_statement(char **p)
91969272
statement_screen(p);
91979273
return;
91989274
}
9275+
if (c == 'S' && starts_with_kw(*p, "SCROLL")) {
9276+
*p += 6;
9277+
statement_scroll(p);
9278+
return;
9279+
}
91999280
if (c == 'S' && starts_with_kw(*p, "SCREENCODES")) {
92009281
*p += 11;
92019282
statement_screencodes(p);

docs/sprite-features-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Sprite Features – Planning & Specification
22

3-
**Status**: **Partially implemented** in `basic-gfx` + canvas WASM: `LOADSPRITE`, optional **tile sheet** `LOADSPRITE slot, "sheet.png", tw, th`, `DRAWSPRITETILE`, `SPRITETILES`, **`SPRITEFRAME`** (default tile for `DRAWSPRITE` without crop), `UNLOADSPRITE`, `DRAWSPRITE` (persistent pose per slot, `z` and optional source rectangle), `SPRITEVISIBLE`, `SPRITEW`/`SPRITEH`, `SPRITECOLLIDE`. **Worked example**: `examples/gfx_game_shell.bas` (PETSCII tile map via `POKE` + PNG player/enemy/HUD); **minimal HUD demo**: `examples/gfx_sprite_hud_demo.bas`. **Gamepad**: `JOY`/`JOYAXIS` in **basic-gfx** and **canvas WASM** (`examples/gfx_joy_demo.bas`). Full **world tilemap engine** (scroll, layers) still planning.
3+
**Status**: **Partially implemented** in `basic-gfx` + canvas WASM: `LOADSPRITE`, optional **tile sheet** `LOADSPRITE slot, "sheet.png", tw, th`, `DRAWSPRITETILE`, `SPRITETILES`, **`SPRITEFRAME`** (default tile for `DRAWSPRITE` without crop), `UNLOADSPRITE`, `DRAWSPRITE` (persistent pose per slot, `z` and optional source rectangle), `SPRITEVISIBLE`, `SPRITEW`/`SPRITEH`, `SPRITECOLLIDE`. **Worked example**: `examples/gfx_game_shell.bas` (PETSCII tile map via `POKE` + PNG player/enemy/HUD); **minimal HUD demo**: `examples/gfx_sprite_hud_demo.bas`. **Gamepad**: `JOY`/`JOYAXIS` in **basic-gfx** and **canvas WASM** (`examples/gfx_joy_demo.bas`). Full **world tilemap engine** (layers beyond a single viewport) still planning. **Viewport** **`SCROLL dx, dy`** + **`SCROLLX()`/`SCROLLY()`** implement shared camera-style pan for text/bitmap/sprites (see **`examples/tutorial_gfx_scroll.bas`**).
44

55
This document outlines a sprite subsystem for `basic-gfx`, designed for modern hardware with no C64-style limits. The goal is to let BASIC programs load PNG images as sprites, draw them at arbitrary positions with depth ordering, and perform collision detection.
66

docs/tutorial-embedding.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ This produces:
2020
| `web/vfs-helpers.js` | Shared **upload/download** helpers for `Module.FS` (loaded automatically by tutorial embeds) |
2121
| `web/tutorial-example.html` | Minimal page with **two** embeds (smoke test reference) |
2222
| `web/tutorial.html` | **Getting started** walkthrough with **thirteen** lesson embeds (serve `web/` over HTTP) |
23+
| `web/tutorial-gfx-features.html` | **Static** cheat sheet for **basic-gfx** / **canvas WASM** features (scroll, memory, sprites, gamepad); links to `examples/tutorial_gfx_*.bas` |
2324

2425
Serve the `web/` directory over **HTTP** (WASM is blocked on many browsers for `file://`).
2526

examples/tutorial_gfx_gamepad.bas

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
1 REM ============================================================
2+
2 REM Tutorial: JOY / JOYAXIS (basic-gfx native or canvas WASM)
3+
3 REM Native: ./basic-gfx examples/tutorial_gfx_gamepad.bas
4+
4 REM Canvas: connect a controller; see also examples/gfx_joy_demo.bas
5+
5 REM Button codes 1-15 (Raylib); axes 0-5 scaled to about -1000..1000
6+
6 REM ============================================================
7+
10 PRINT "{CLR}{HOME}GAMEPAD — press buttons / move sticks"
8+
20 PRINT "Port 0, Face A (code 7):"; JOY(0, 7)
9+
30 PRINT "Left stick X (axis 0):"; JOYAXIS(0, 0)
10+
40 PRINT "Left stick Y (axis 1):"; JOYAXIS(0, 1)
11+
50 PRINT "(Values update each frame in a real game loop.)"
12+
60 END

examples/tutorial_gfx_index.bas

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
1 REM ============================================================
2+
2 REM Index of graphics / WASM tutorial examples (repo: examples/)
3+
3 REM Run demos with: ./basic-gfx examples/<file>
4+
4 REM ============================================================
5+
5 REM tutorial_gfx_scroll.bas — SCROLL, SCROLLX(), SCROLLY()
6+
6 REM tutorial_gfx_memory.bas — POKE/PEEK memory bases (#OPTION memory)
7+
7 REM tutorial_gfx_tilemap.bas — LOADSPRITE tw,th, DRAWSPRITETILE, SPRITEFRAME
8+
8 REM tutorial_tile_sheet_demo.png — 16x8 asset for tilemap demo
9+
9 REM gfx_joy_demo.bas — full gamepad poll loop
10+
10 REM gfx_poke_demo.bas — screen RAM from BASIC
11+
11 REM gfx_jiffy_game_demo.bas — TI$, INKEY$, PEEK keyboard matrix
12+
12 REM ============================================================
13+
20 PRINT "{CLR}{HOME}See REM lines in this file for a feature list."
14+
30 END

examples/tutorial_gfx_memory.bas

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
1 REM ============================================================
2+
2 REM Tutorial: virtual memory bases for POKE/PEEK (C64-style layout)
3+
3 REM Run: ./basic-gfx examples/tutorial_gfx_memory.bas
4+
4 REM Default: screen $0400, color $D800, charset $3000, keys $DC00, bitmap $2000
5+
5 REM Use #OPTION in a real project, or -memory on the command line.
6+
6 REM ============================================================
7+
10 REM --- Default screen RAM at 1024 (0x0400), same as C64 first screen line
8+
20 POKE 1024, 32
9+
30 POKE 1025, 32
10+
40 REM White letter A (screencode 1) at top-left
11+
50 POKE 1024, 1
12+
60 POKE 55296, 1
13+
70 REM Read back
14+
80 PRINT "{CLR}{HOME}PEEK screen @1024 ="; PEEK(1024)
15+
90 PRINT "PEEK colour @55296 ="; PEEK(55296)
16+
100 PRINT
17+
110 PRINT "Tip: #OPTION memory c64 | pet | default"
18+
120 PRINT " #OPTION screen 1024 (override base)"
19+
130 END

examples/tutorial_gfx_scroll.bas

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
1 REM ============================================================
2+
2 REM Tutorial: viewport SCROLL (camera-style pan in basic-gfx / canvas)
3+
3 REM Run: ./basic-gfx examples/tutorial_gfx_scroll.bas
4+
4 REM or open web/canvas.html and load this file into the VFS.
5+
5 REM SCROLL dx, dy moves the visible layer left/up by dx, dy pixels.
6+
6 REM SCROLLX() / SCROLLY() return the current offset (see status line).
7+
7 REM ============================================================
8+
10 REM Draw a label so you can see the screen shift
9+
20 COLOR 1
10+
30 PRINT "{CLR}{HOME}SCROLL DEMO — watch text slide"
11+
40 PRINT "SCROLL 16,0 pans one column (8px) to the right"
12+
50 PRINT "SCROLL 0,8 pans one text row up"
13+
60 REM Pan right in small steps (8 px = one character cell)
14+
70 FOR I = 1 TO 5
15+
80 SCROLL I * 8, 0
16+
90 TEXTAT 0, 24, "SCROLLX=" + STR$(SCROLLX())
17+
100 SLEEP 0.25
18+
110 NEXT I
19+
120 REM Reset then pan down
20+
130 SCROLL 0, 0
21+
140 FOR J = 1 TO 3
22+
150 SCROLL 0, J * 8
23+
160 TEXTAT 0, 24, "SCROLLY=" + STR$(SCROLLY()) + " "
24+
170 SLEEP 0.25
25+
180 NEXT J
26+
190 SCROLL 0, 0
27+
200 PRINT
28+
210 PRINT "Done — SCROLL 0,0 resets the view."
29+
220 END

examples/tutorial_gfx_tilemap.bas

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
1 REM ============================================================
2+
2 REM Tutorial: tile sheet LOADSPRITE, DRAWSPRITETILE, SPRITEFRAME
3+
3 REM Requires: examples/tutorial_tile_sheet_demo.png (16x8 = two 8x8 tiles)
4+
4 REM Run: ./basic-gfx examples/tutorial_gfx_tilemap.bas
5+
5 REM ============================================================
6+
10 REM Two tiles: red (index 1) and green (index 2)
7+
20 LOADSPRITE 0, "tutorial_tile_sheet_demo.png", 8, 8
8+
30 PRINT "{CLR}{HOME}Tiles in sheet:"; SPRITETILES(0)
9+
40 REM Draw tile 2 at pixel (100, 80), z=50
10+
50 DRAWSPRITETILE 0, 100, 80, 2, 50
11+
60 REM Set default frame for DRAWSPRITE (same as tile 1)
12+
70 SPRITEFRAME 0, 1
13+
80 DRAWSPRITE 0, 180, 80, 50
14+
90 PRINT "SPRITEFRAME(0)="; SPRITEFRAME(0)
15+
100 SLEEP 1
16+
110 END

0 commit comments

Comments
 (0)