Skip to content

Commit 1a8018d

Browse files
cursoragentomiq
andcommitted
feat: SPRITEMODULATE for sprite alpha, RGB tint, and scale (gfx + WASM)
- Add gfx_sprite_set_modulate + per-slot state; Raylib DrawTexturePro tint/dest size - Canvas: bilinear-ish scaled blit with modulate + premultiplied blend - BASIC: SPRITEMODULATE slot, alpha [, r,g,b [, sx [, sy]]]; document in README/CHANGELOG Co-authored-by: Chris Garrett <chris@chrisg.com>
1 parent fc7e0a3 commit 1a8018d

6 files changed

Lines changed: 341 additions & 26 deletions

File tree

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+
- **`SPRITEMODULATE` (basic-gfx + canvas WASM):** Per-slot draw tint and scale until **`LOADSPRITE`** / **`UNLOADSPRITE`**. Syntax: **`SPRITEMODULATE slot, alpha [, r, g, b [, scale_x [, scale_y]]]`****`alpha`** and **`r`/`g`/`b`** are **0–255** (**`alpha`** multiplies PNG alpha); **`scale_x`/`scale_y`** stretch the drawn quad (default **1**; one scale sets both). Implemented with Raylib **`Color`** + destination size; canvas uses bilinear-ish sampling + tint. **`gfx_sprite_set_modulate`** in **`basic_api.h`**.
6+
57
- **Load normalization / identifiers containing `IF`:** **`normalize_keywords_line`** treated **`IF`** inside identifiers as **`IF …`** (e.g. **`JIFFIES_PER_FRAME`****`J IF FIES_PER_FRAME`**, breaking assignments). The same **`prev_ident`** guard used for **`FOR`** (avoid splitting **`PLATFORM`**) now applies to the **`IF`** insertion rule. Regression: **`tests/if_inside_ident_normalize.bas`**.
68

79
- **`gfx_peek()` keyboard vs colour RAM**: With default bases, **`GFX_KEY_BASE` (0xDC00)** lies inside the colour RAM window **(0xD800 + 2000 bytes)**. **`gfx_peek()`** must resolve **keyboard before colour** or **`PEEK(56320+n)`** reads colour bytes (e.g. **14** for light blue). **`gfx_video.c`** documents the required order (**text → keyboard → colour → charset → bitmap**). **`gfx_video_test`** asserts key wins over colour at the aliased address.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,7 @@ Releases include **basic-gfx** — a full graphical version of the interpreter b
390390
- **Tile sheet / tilemap:** `LOADSPRITE slot, "tiles.png", tw, th` treats the image as a grid of **tw×th** pixels per tile (row-major, left-to-right). Use **`DRAWSPRITETILE slot, x, y, tile_index [, z]`** with **1-based** `tile_index` (first tile = 1). **`SPRITETILES(slot)`** returns how many tiles are in the sheet. **`SPRITEW`/`SPRITEH`** return one tile’s size when a tilemap is loaded. **`DRAWSPRITE`** with **`sx, sy, sw, sh`** still works for arbitrary crops.
391391
- **Gamepad (basic-gfx and canvas WASM):** **`JOY(port, button)`** returns **1** if pressed, else **0**. **`JOYSTICK`** is an alias for **`JOY`**. **`JOYAXIS(port, axis)`** returns stick/trigger movement scaled to about **-1000..1000** (axes **0–5**: left X/Y, right X/Y, left trigger, right trigger). Port **0** is the first controller. **Native basic-gfx** uses Raylib button codes **1–15** (DPAD/face/triggers; **0** is unknown). **Canvas WASM** maps those codes to the browser **Standard Gamepad** layout via `navigator.getGamepads()` (polls each frame in `canvas.html`). **Terminal** `./basic` has no gamepad. Example: `./basic-gfx examples/gfx_joy_demo.bas`
392392
- **Tile animation frame:** After **`LOADSPRITE slot, "sheet.png", tw, th`**, **`SPRITEFRAME slot, frame`** sets the **1-based** tile index used when **`DRAWSPRITE`** omits **`sx, sy, sw, sh`** (same as choosing that tile with **`DRAWSPRITETILE`**). **`SPRITEFRAME(slot)`** returns the current frame.
393+
- **Tint / opacity / scale:** **`SPRITEMODULATE slot, alpha [, r, g, b [, scale_x [, scale_y]]]`****`alpha`** and **`r`/`g`/`b`** are **0–255** (defaults **255**; **`alpha`** multiplies the PNG’s alpha). Optional **`scale_x`/`scale_y`** stretch the drawn sprite (default **1**; one number sets both). Resets to opaque white at **** on each **`LOADSPRITE`** / **`UNLOADSPRITE`**.
393394
- `UNLOADSPRITE slot` frees that slot’s texture and clears its draw state so you can `LOADSPRITE` again (e.g. swap art or reclaim memory). No-op if the slot was empty.
394395
- `DRAWSPRITE slot, x, y [, z [, sx, sy [, sw, sh ]]]` sets the **persistent** pose for that slot: the same image is drawn **every frame** until you call `DRAWSPRITE` again for that slot or the program exits (textures are freed when the window closes). **`x`, `y`** are **pixel** coordinates on the 320×200 framebuffer (not character rows): row *r* starts at **`y = r × 8`**. **`z`**: larger values paint **on top** (e.g. text/bitmap at 0, HUD at 200). Omit **`sx, sy`** to use the top-left of the image; omit **`sw, sh`** (or use ≤0) to use the rest of the texture from `(sx,sy)`. **Alpha** in the PNG is respected (transparency over text or bitmap).
395396
- `SPRITEVISIBLE slot, 0|1` hides or shows a loaded sprite without unloading.

basic.c

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2162,6 +2162,7 @@ static void statement_line(char **p);
21622162
static void statement_loadsprite(char **p);
21632163
static void statement_drawsprite(char **p);
21642164
static void statement_spritevisible(char **p);
2165+
static void statement_spritemodulate(char **p);
21652166
#endif
21662167
static void statement_unloadsprite(char **p); /* basic-gfx only; terminal errors */
21672168
static void statement_else(char **p);
@@ -2469,7 +2470,7 @@ static const char *const reserved_words[] = {
24692470
"INKEY", "INPUT", "INSTR", "INT", "INDEXOF", "JSON", "LEFT", "LEN", "LET", "LINE", "LOAD", "LOADSPRITE", "LOCATE", "LOG",
24702471
"LASTINDEXOF", "LCASE", "FIELD", "LTRIM", "MEMCPY", "MEMSET", "MID", "MOD", "NEXT", "OFF", "ON", "OPEN", "OR", "PEEK", "POKE", "PLATFORM", "PRESET", "PSET", "PRINT",
24712472
"XOR",
2472-
"READ", "REM", "REPLACE", "RESTORE", "RETURN", "RIGHT", "RND", "RTRIM", "RVS", "SCROLL", "SCREEN", "SCREENCODES", "SPRITECOLLIDE", "SPRITEFRAME", "SPRITETILES", "SPRITEVISIBLE",
2473+
"READ", "REM", "REPLACE", "RESTORE", "RETURN", "RIGHT", "RND", "RTRIM", "RVS", "SCROLL", "SCREEN", "SCREENCODES", "SPRITECOLLIDE", "SPRITEFRAME", "SPRITEMODULATE", "SPRITETILES", "SPRITEVISIBLE",
24732474
"JOIN",
24742475
"SGN", "SIN", "SLEEP", "SORT", "SPC", "SPLIT", "SPRITEH", "SPRITEW", "SQR", "STEP", "STOP", "STR", "STRING",
24752476
"DRAWSPRITE", "DRAWSPRITETILE", "HTTP", "HTTPSTATUS", "JOY", "JOYAXIS", "JOYSTICK", "SCROLLX", "SCROLLY", "SYSTEM", "TAB", "TAN", "TEXTAT", "THEN", "TI", "TO", "TRIM", "UCASE", "UNLOADSPRITE", "VAL", "WEND", "WHILE",
@@ -4113,6 +4114,84 @@ static void statement_spritevisible(char **p)
41134114
}
41144115
gfx_sprite_enqueue_visible(slot, on);
41154116
}
4117+
4118+
/* SPRITEMODULATE slot, alpha [, r, g, b [, scale_x [, scale_y]]] — alpha/r/g/b 0..255; scales default 1. */
4119+
static void statement_spritemodulate(char **p)
4120+
{
4121+
struct value v;
4122+
int slot;
4123+
int alpha;
4124+
int r = 255, g = 255, b = 255;
4125+
double sx = 1.0, sy = 1.0;
4126+
skip_spaces(p);
4127+
v = eval_expr(p);
4128+
ensure_num(&v);
4129+
slot = (int)v.num;
4130+
skip_spaces(p);
4131+
if (**p != ',') {
4132+
runtime_error_hint("SPRITEMODULATE expects slot, alpha [, r, g, b [, scale_x [, scale_y]]]",
4133+
"Example: SPRITEMODULATE 1, 128 or SPRITEMODULATE 1, 255, 200, 200, 255, 1.2, 0.6");
4134+
return;
4135+
}
4136+
(*p)++;
4137+
skip_spaces(p);
4138+
v = eval_expr(p);
4139+
ensure_num(&v);
4140+
alpha = (int)v.num;
4141+
skip_spaces(p);
4142+
if (**p == ',') {
4143+
(*p)++;
4144+
skip_spaces(p);
4145+
v = eval_expr(p);
4146+
ensure_num(&v);
4147+
r = (int)v.num;
4148+
skip_spaces(p);
4149+
if (**p != ',') {
4150+
runtime_error_hint("SPRITEMODULATE: r, g, b need three values",
4151+
"After alpha: give all three tint channels 0..255.");
4152+
return;
4153+
}
4154+
(*p)++;
4155+
skip_spaces(p);
4156+
v = eval_expr(p);
4157+
ensure_num(&v);
4158+
g = (int)v.num;
4159+
skip_spaces(p);
4160+
if (**p != ',') {
4161+
runtime_error_hint("SPRITEMODULATE: need b after r, g",
4162+
NULL);
4163+
return;
4164+
}
4165+
(*p)++;
4166+
skip_spaces(p);
4167+
v = eval_expr(p);
4168+
ensure_num(&v);
4169+
b = (int)v.num;
4170+
skip_spaces(p);
4171+
if (**p == ',') {
4172+
(*p)++;
4173+
skip_spaces(p);
4174+
v = eval_expr(p);
4175+
ensure_num(&v);
4176+
sx = v.num;
4177+
skip_spaces(p);
4178+
if (**p == ',') {
4179+
(*p)++;
4180+
skip_spaces(p);
4181+
v = eval_expr(p);
4182+
ensure_num(&v);
4183+
sy = v.num;
4184+
} else {
4185+
sy = sx;
4186+
}
4187+
}
4188+
}
4189+
if (!gfx_vs) {
4190+
runtime_error_hint("SPRITEMODULATE requires basic-gfx or canvas WASM", NULL);
4191+
return;
4192+
}
4193+
gfx_sprite_set_modulate(slot, alpha, r, g, b, (float)sx, (float)sy);
4194+
}
41164195
#endif /* GFX_VIDEO */
41174196

41184197
/* UNLOADSPRITE slot — free texture and clear draw state (basic-gfx only). */
@@ -9303,6 +9382,11 @@ static void execute_statement(char **p)
93039382
statement_spritevisible(p);
93049383
return;
93059384
}
9385+
if (starts_with_kw(*p, "SPRITEMODULATE")) {
9386+
*p += 15;
9387+
statement_spritemodulate(p);
9388+
return;
9389+
}
93069390
if (starts_with_kw(*p, "SPRITEFRAME")) {
93079391
*p += 11;
93089392
statement_spriteframe(p);

basic_api.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ void gfx_sprite_enqueue_unload(int slot); /* BASIC: UNLOADSPRITE slot */
7373
void gfx_sprite_enqueue_visible(int slot, int on);
7474
/* sw/sh <= 0 means use full sub-texture from (sx,sy) to bottom-right. */
7575
void gfx_sprite_enqueue_draw(int slot, float x, float y, int z, int sx, int sy, int sw, int sh);
76+
/* Per-slot draw tint/scale until next LOADSPRITE/UNLOADSPRITE (basic-gfx / canvas WASM).
77+
* alpha 0–255 (255 = opaque), r/g/b 0–255, scale 1.0 = natural pixel size of crop. */
78+
void gfx_sprite_set_modulate(int slot, int alpha, int r, int g, int b, float scale_x, float scale_y);
7679
int gfx_sprite_slot_width(int slot);
7780
int gfx_sprite_slot_height(int slot);
7881
/* Axis-aligned bounding box overlap of last DRAWSPRITE rects (basic-gfx / canvas). */

gfx/gfx_raylib.c

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ typedef struct {
4949
int tiles_x, tiles_y; /* grid dimensions */
5050
int tile_count; /* tiles_x * tiles_y, or 1 for single */
5151
int draw_frame; /* 1-based tile index for DRAWSPRITE default crop */
52+
/* Per-slot modulation (SPRITEMODULATE); default opaque white, scale 1,1. */
53+
int mod_a, mod_r, mod_g, mod_b;
54+
float mod_sx, mod_sy;
5255
/* Persistent draw state (last DRAWSPRITE for this slot until UNLOAD). */
5356
int draw_active;
5457
float draw_x, draw_y;
@@ -61,6 +64,8 @@ typedef struct {
6164
float x, y;
6265
int z;
6366
int sx, sy, sw, sh;
67+
int mod_a, mod_r, mod_g, mod_b;
68+
float mod_sx, mod_sy;
6469
} GfxSpriteDraw;
6570

6671
static void gfx_sprite_process_queue(void);
@@ -223,6 +228,51 @@ void gfx_sprite_enqueue_draw(int slot, float x, float y, int z, int sx, int sy,
223228
(void)sprite_q_push(&c);
224229
}
225230

231+
void gfx_sprite_set_modulate(int slot, int alpha, int r, int g, int b, float scale_x, float scale_y)
232+
{
233+
if (slot < 0 || slot >= GFX_SPRITE_MAX_SLOTS) {
234+
return;
235+
}
236+
if (alpha < 0) {
237+
alpha = 0;
238+
}
239+
if (alpha > 255) {
240+
alpha = 255;
241+
}
242+
if (r < 0) {
243+
r = 0;
244+
}
245+
if (r > 255) {
246+
r = 255;
247+
}
248+
if (g < 0) {
249+
g = 0;
250+
}
251+
if (g > 255) {
252+
g = 255;
253+
}
254+
if (b < 0) {
255+
b = 0;
256+
}
257+
if (b > 255) {
258+
b = 255;
259+
}
260+
if (scale_x <= 0.0f) {
261+
scale_x = 1.0f;
262+
}
263+
if (scale_y <= 0.0f) {
264+
scale_y = 1.0f;
265+
}
266+
pthread_mutex_lock(&g_sprite_mutex);
267+
g_sprite_slots[slot].mod_a = alpha;
268+
g_sprite_slots[slot].mod_r = r;
269+
g_sprite_slots[slot].mod_g = g;
270+
g_sprite_slots[slot].mod_b = b;
271+
g_sprite_slots[slot].mod_sx = scale_x;
272+
g_sprite_slots[slot].mod_sy = scale_y;
273+
pthread_mutex_unlock(&g_sprite_mutex);
274+
}
275+
226276
int gfx_sprite_slot_width(int slot)
227277
{
228278
int w = 0;
@@ -446,12 +496,12 @@ int gfx_sprite_slots_overlap_aabb(int slot_a, int slot_b)
446496
}
447497
ax = a->draw_x;
448498
ay = a->draw_y;
449-
aw = swa;
450-
ah = sha;
499+
aw = swa * a->mod_sx;
500+
ah = sha * a->mod_sy;
451501
bx = b->draw_x;
452502
by = b->draw_y;
453-
bw = swb;
454-
bh = shb;
503+
bw = swb * b->mod_sx;
504+
bh = shb * b->mod_sy;
455505
}
456506
pthread_mutex_unlock(&g_sprite_mutex);
457507

@@ -537,6 +587,12 @@ static void gfx_sprite_process_queue(void)
537587
g_sprite_slots[c->slot].tile_h = 0;
538588
g_sprite_slots[c->slot].draw_frame = 1;
539589
}
590+
g_sprite_slots[c->slot].mod_a = 255;
591+
g_sprite_slots[c->slot].mod_r = 255;
592+
g_sprite_slots[c->slot].mod_g = 255;
593+
g_sprite_slots[c->slot].mod_b = 255;
594+
g_sprite_slots[c->slot].mod_sx = 1.0f;
595+
g_sprite_slots[c->slot].mod_sy = 1.0f;
540596
}
541597
pthread_mutex_unlock(&g_sprite_mutex);
542598
break;
@@ -559,6 +615,12 @@ static void gfx_sprite_process_queue(void)
559615
g_sprite_slots[c->slot].tiles_x = g_sprite_slots[c->slot].tiles_y = 0;
560616
g_sprite_slots[c->slot].tile_count = 0;
561617
g_sprite_slots[c->slot].draw_frame = 0;
618+
g_sprite_slots[c->slot].mod_a = 255;
619+
g_sprite_slots[c->slot].mod_r = 255;
620+
g_sprite_slots[c->slot].mod_g = 255;
621+
g_sprite_slots[c->slot].mod_b = 255;
622+
g_sprite_slots[c->slot].mod_sx = 1.0f;
623+
g_sprite_slots[c->slot].mod_sy = 1.0f;
562624
}
563625
pthread_mutex_unlock(&g_sprite_mutex);
564626
break;
@@ -634,6 +696,12 @@ static void gfx_sprite_composite(const GfxVideoState *vs, RenderTexture2D target
634696
draws[nd].sy = g_sprite_slots[i].draw_sy;
635697
draws[nd].sw = g_sprite_slots[i].draw_sw;
636698
draws[nd].sh = g_sprite_slots[i].draw_sh;
699+
draws[nd].mod_a = g_sprite_slots[i].mod_a;
700+
draws[nd].mod_r = g_sprite_slots[i].mod_r;
701+
draws[nd].mod_g = g_sprite_slots[i].mod_g;
702+
draws[nd].mod_b = g_sprite_slots[i].mod_b;
703+
draws[nd].mod_sx = g_sprite_slots[i].mod_sx;
704+
draws[nd].mod_sy = g_sprite_slots[i].mod_sy;
637705
nd++;
638706
}
639707
pthread_mutex_unlock(&g_sprite_mutex);
@@ -680,14 +748,24 @@ static void gfx_sprite_composite(const GfxVideoState *vs, RenderTexture2D target
680748
continue;
681749
}
682750
src = (Rectangle){ sx, sy, sw, sh };
683-
dest = (Rectangle){ d->x - scx, d->y - scy, sw, sh };
751+
dest = (Rectangle){
752+
d->x - scx,
753+
d->y - scy,
754+
sw * d->mod_sx,
755+
sh * d->mod_sy
756+
};
684757
DrawTexturePro(
685758
t,
686759
src,
687760
dest,
688761
(Vector2){ 0, 0 },
689762
0.0f,
690-
WHITE);
763+
(Color){
764+
(unsigned char)d->mod_r,
765+
(unsigned char)d->mod_g,
766+
(unsigned char)d->mod_b,
767+
(unsigned char)d->mod_a
768+
});
691769
}
692770
EndBlendMode();
693771
EndTextureMode();

0 commit comments

Comments
 (0)