summaryrefslogtreecommitdiff
path: root/src/gfx2d.c
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2025-09-02 19:06:01 -0700
committer3gg <3gg@shellblade.net>2025-09-02 19:06:01 -0700
commitbab4a8169135dfecb2e434cca6825dcb96e1b9ec (patch)
treec8163713c952d8f5844d713276966ddbd00b4e61 /src/gfx2d.c
parent5941948319139b4224df29762d66de2430cc994f (diff)
Rename isogfx -> gfx2d
Diffstat (limited to 'src/gfx2d.c')
-rw-r--r--src/gfx2d.c708
1 files changed, 708 insertions, 0 deletions
diff --git a/src/gfx2d.c b/src/gfx2d.c
new file mode 100644
index 0000000..da265b0
--- /dev/null
+++ b/src/gfx2d.c
@@ -0,0 +1,708 @@
1#include <isogfx/gfx2d.h>
2
3#include <isogfx/asset.h>
4
5#include <filesystem.h>
6#include <memstack.h>
7#include <path.h>
8
9#include <assert.h>
10#include <stdint.h>
11#include <stdio.h>
12#include <string.h>
13
14/// Maximum path length.
15#define MAX_PATH 256
16
17/// Default animation speed.
18#define ANIMATION_FPS 10
19
20/// Time between animation updates.
21#define ANIMATION_UPDATE_DELTA (1.0 / ANIMATION_FPS)
22
23typedef struct ivec2 {
24 int x, y;
25} ivec2;
26
27typedef struct vec2 {
28 double x, y;
29} vec2;
30
31// -----------------------------------------------------------------------------
32// Renderer state.
33// -----------------------------------------------------------------------------
34
35typedef struct CoordSystem {
36 ivec2 o; // Origin.
37 ivec2 x;
38 ivec2 y;
39} CoordSystem;
40
41typedef struct Screen {
42 int width;
43 int height;
44 Pixel* pixels;
45} Screen;
46
47typedef struct SpriteInstance {
48 struct SpriteInstance* next;
49 const Ss_SpriteSheet* sheet;
50 ivec2 position;
51 int animation; // Current animation.
52 int frame; // Current frame of animation.
53} SpriteInstance;
54
55typedef struct IsoGfx {
56 Screen screen;
57 CoordSystem iso_space;
58 ivec2 camera;
59 double last_animation_time;
60 Tile next_tile; // For procedurally-generated tiles.
61 Tm_Map* map;
62 Ts_TileSet* tileset;
63 SpriteInstance* head_sprite; // Head of sprites list.
64 memstack stack;
65 size_t watermark;
66} IsoGfx;
67
68// -----------------------------------------------------------------------------
69// Math and world / tile / screen access.
70// -----------------------------------------------------------------------------
71
72static inline ivec2 ivec2_add(ivec2 a, ivec2 b) {
73 return (ivec2){.x = a.x + b.x, .y = a.y + b.y};
74}
75
76static inline ivec2 ivec2_scale(ivec2 a, int s) {
77 return (ivec2){.x = a.x * s, .y = a.y * s};
78}
79
80static inline ivec2 ivec2_neg(ivec2 a) { return (ivec2){.x = -a.x, .y = -a.y}; }
81
82static inline ivec2 iso2cart(ivec2 iso, int s, int t, int w) {
83 return (ivec2){.x = (iso.x - iso.y) * (s / 2) + (w / 2),
84 .y = (iso.x + iso.y) * (t / 2)};
85}
86
87static inline vec2 vec2_add(vec2 a, vec2 b) {
88 return (vec2){.x = a.x + b.x, .y = a.y + b.y};
89}
90
91static inline vec2 ivec2_to_vec2(ivec2 a) { return (vec2){a.x, a.y}; }
92
93// Method 1.
94// static inline vec2 cart2iso(vec2 cart, int s, int t, int w) {
95// const double x = cart.x - (double)(w / 2);
96// const double xiso = (x * t + cart.y * s) / (double)(s * t);
97// return (vec2){
98// .x = (int)(xiso), .y = (int)((2.0 / (double)t) * cart.y - xiso)};
99//}
100
101// Method 2.
102static inline vec2 cart2iso(vec2 cart, int s, int t, int w) {
103 const double one_over_s = 1. / (double)s;
104 const double one_over_t = 1. / (double)t;
105 const double x = cart.x - (double)(w / 2);
106 return (vec2){.x = (one_over_s * x + one_over_t * cart.y),
107 .y = (-one_over_s * x + one_over_t * cart.y)};
108}
109
110static inline const Pixel* screen_xy_const_ref(
111 const Screen* screen, int x, int y) {
112 assert(screen);
113 assert(x >= 0);
114 assert(y >= 0);
115 assert(x < screen->width);
116 assert(y < screen->height);
117 return &screen->pixels[y * screen->width + x];
118}
119
120static inline Pixel screen_xy(Screen* screen, int x, int y) {
121 return *screen_xy_const_ref(screen, x, y);
122}
123
124static inline Pixel* screen_xy_mut(Screen* screen, int x, int y) {
125 return (Pixel*)screen_xy_const_ref(screen, x, y);
126}
127
128/// Create the basis for the isometric coordinate system with origin and vectors
129/// expressed in the Cartesian system.
130static CoordSystem make_iso_coord_system(
131 const Tm_Map* const map, const Screen* const screen) {
132 assert(map);
133 assert(screen);
134 const ivec2 o = {screen->width / 2, 0};
135 const ivec2 x = {
136 .x = map->base_tile_width / 2, .y = map->base_tile_height / 2};
137 const ivec2 y = {
138 .x = -map->base_tile_width / 2, .y = map->base_tile_height / 2};
139 return (CoordSystem){o, x, y};
140}
141
142// -----------------------------------------------------------------------------
143// Renderer, world and tile management.
144// -----------------------------------------------------------------------------
145
146IsoGfx* isogfx_new(const IsoGfxDesc* desc) {
147 assert(desc->screen_width > 0);
148 assert(desc->screen_height > 0);
149 // Part of our implementation assumes even widths and heights for precision.
150 assert((desc->screen_width & 1) == 0);
151 assert((desc->screen_height & 1) == 0);
152
153 IsoGfx tmp = {0};
154 if (!memstack_make(&tmp.stack, desc->memory_size, desc->memory)) {
155 goto cleanup;
156 }
157 IsoGfx* iso =
158 memstack_alloc_aligned(&tmp.stack, sizeof(IsoGfx), alignof(IsoGfx));
159 *iso = tmp;
160
161 const size_t screen_size_bytes =
162 desc->screen_width * desc->screen_height * sizeof(Pixel);
163 Pixel* screen =
164 memstack_alloc_aligned(&iso->stack, screen_size_bytes, alignof(Pixel));
165
166 iso->screen = (Screen){.width = desc->screen_width,
167 .height = desc->screen_height,
168 .pixels = screen};
169
170 iso->last_animation_time = 0.0;
171 iso->watermark = memstack_watermark(&iso->stack);
172
173 return iso;
174
175cleanup:
176 isogfx_del(&iso);
177 return nullptr;
178}
179
180void isogfx_clear(IsoGfx* iso) {
181 assert(iso);
182 iso->last_animation_time = 0.0;
183 iso->next_tile = 0;
184 iso->map = nullptr;
185 iso->tileset = nullptr;
186 iso->head_sprite = nullptr;
187 // The base of the stack contains the IsoGfx and the screen buffer. Make sure
188 // we don't clear them.
189 memstack_set_watermark(&iso->stack, iso->watermark);
190}
191
192void isogfx_del(IsoGfx** ppIso) {
193 assert(ppIso);
194 IsoGfx* iso = *ppIso;
195 if (iso) {
196 memstack_del(&iso->stack);
197 *ppIso = nullptr;
198 }
199}
200
201void isogfx_make_map(IsoGfx* iso, const MapDesc* desc) {
202 assert(iso);
203 assert(desc);
204 assert(desc->tile_width > 0);
205 assert(desc->tile_height > 0);
206 // Part of our implementation assumes even widths and heights for greater
207 // precision.
208 assert((desc->tile_width & 1) == 0);
209 assert((desc->tile_height & 1) == 0);
210 // World must be non-empty.
211 assert(desc->world_width > 0);
212 assert(desc->world_height > 0);
213 // Must have >0 tiles.
214 assert(desc->num_tiles > 0);
215
216 // Handle recreation by destroying the previous world and sprites.
217 isogfx_clear(iso);
218
219 const int world_size = desc->world_width * desc->world_height;
220 const size_t map_size_bytes = sizeof(Tm_Map) + (world_size * sizeof(Tile));
221
222 // This implies that all tiles are of the base tile dimensions.
223 // We could enhance the API to allow for supertiles as well. Take in max tile
224 // width and height and allocate enough space using those values.
225 const size_t tile_size = desc->tile_width * desc->tile_height;
226 const size_t tile_size_bytes = tile_size * sizeof(Pixel);
227 const size_t tile_data_size_bytes = desc->num_tiles * tile_size_bytes;
228 const size_t tileset_size_bytes = sizeof(Ts_TileSet) +
229 (desc->num_tiles * sizeof(Ts_Tile)) +
230 tile_data_size_bytes;
231
232 iso->map = memstack_alloc_aligned(&iso->stack, map_size_bytes, 4);
233 *iso->map = (Tm_Map){
234 .world_width = desc->world_width,
235 .world_height = desc->world_height,
236 .base_tile_width = desc->tile_width,
237 .base_tile_height = desc->tile_height,
238 .num_layers = 1,
239 };
240
241 iso->tileset = memstack_alloc_aligned(&iso->stack, tileset_size_bytes, 4);
242 *iso->tileset = (Ts_TileSet){
243 .num_tiles = desc->num_tiles,
244 };
245
246 iso->iso_space = make_iso_coord_system(iso->map, &iso->screen);
247}
248
249bool isogfx_load_map(IsoGfx* iso, const char* filepath) {
250 assert(iso);
251 assert(filepath);
252
253 bool success = false;
254
255 // Handle recreation by destroying the previous world and sprites.
256 isogfx_clear(iso);
257
258 // Load the map.
259 printf("Load tile map: %s\n", filepath);
260 WITH_FILE(filepath, {
261 const size_t map_size = get_file_size_f(file);
262 iso->map = memstack_alloc_aligned(&iso->stack, map_size, 4);
263 success = read_file_f(file, iso->map);
264 });
265 if (!success) {
266 goto cleanup;
267 }
268 Tm_Map* const map = iso->map;
269
270 printf("Map orientation: %d\n", ((Tm_Flags*)&map->flags)->orientation);
271
272 // Load the tile set.
273 //
274 // Tile set path is relative to the tile map file. Make it relative to the
275 // current working directory before loading.
276 const char* ts_path = map->tileset_path;
277 char ts_path_cwd[MAX_PATH] = {0};
278 if (!path_make_relative(filepath, ts_path, ts_path_cwd, MAX_PATH)) {
279 goto cleanup;
280 }
281 printf("Load tile set: %s\n", ts_path_cwd);
282 WITH_FILE(ts_path_cwd, {
283 const size_t file_size = get_file_size_f(file);
284 iso->tileset = memstack_alloc_aligned(&iso->stack, file_size, 4);
285 success = read_file_f(file, iso->tileset);
286 });
287 if (!success) {
288 // TODO: Log errors using the log library.
289 goto cleanup;
290 }
291 const Ts_TileSet* const tileset = iso->tileset;
292 printf("Loaded tile set (%u tiles): %s\n", tileset->num_tiles, ts_path_cwd);
293
294 // TODO: These assertions on input data should be library runtime errors.
295 assert(ts_validate_tileset(tileset));
296 assert(tm_validate_map(map, tileset));
297
298 iso->iso_space = make_iso_coord_system(iso->map, &iso->screen);
299
300 success = true;
301
302cleanup:
303 if (!success) {
304 isogfx_clear(iso);
305 }
306 return success;
307}
308
309int isogfx_world_width(const IsoGfx* iso) {
310 assert(iso);
311 return iso->map->world_width;
312}
313
314int isogfx_world_height(const IsoGfx* iso) {
315 assert(iso);
316 return iso->map->world_height;
317}
318
319static void make_tile_from_colour(
320 Pixel colour, const Ts_Tile* tile, Pixel* tile_pixels) {
321 assert(tile);
322 assert(tile_pixels);
323
324 const int width = tile->width;
325 const int height = tile->height;
326 const int r = width / height;
327 for (int y = 0; y < height / 2; ++y) {
328 const int mask_start = width / 2 - r * y - 1;
329 const int mask_end = width / 2 + r * y + 1;
330 for (int x = 0; x < width; ++x) {
331 const bool mask = (mask_start <= x) && (x <= mask_end);
332 const Pixel val = mask ? colour : (Pixel){.r = 0, .g = 0, .b = 0, .a = 0};
333
334 // Top half.
335 *ts_tile_xy_mut(tile_pixels, tile, x, y) = val;
336
337 // Bottom half reflects the top half.
338 const int y_reflected = height - y - 1;
339 *ts_tile_xy_mut(tile_pixels, tile, x, y_reflected) = val;
340 }
341 }
342}
343
344Tile isogfx_make_tile(IsoGfx* iso, const TileDesc* desc) {
345 assert(iso);
346 assert(desc);
347 // Client must create a world first.
348 assert(iso->map);
349 assert(iso->tileset);
350 // Currently, procedural tiles must match the base tile size.
351 assert(desc->width == iso->map->base_tile_width);
352 assert(desc->height == iso->map->base_tile_height);
353 // Cannot exceed max tiles.
354 assert(iso->next_tile < iso->tileset->num_tiles);
355
356 const Tile tile = iso->next_tile++;
357
358 const size_t tile_size_bytes = desc->width * desc->height * sizeof(Pixel);
359
360 switch (desc->type) {
361 case TileFromColour: {
362 assert(desc->width > 0);
363 assert(desc->height > 0);
364
365 Ts_Tile* const ts_tile = ts_tileset_get_tile_mut(iso->tileset, tile);
366
367 *ts_tile = (Ts_Tile){
368 .width = iso->map->base_tile_width,
369 .height = iso->map->base_tile_height,
370 .pixels = tile * tile_size_bytes,
371 };
372
373 Pixel* const tile_pixels =
374 ts_tileset_get_tile_pixels_mut(iso->tileset, tile);
375 make_tile_from_colour(desc->colour, ts_tile, tile_pixels);
376 break;
377 }
378 case TileFromFile:
379 assert(false); // TODO
380 break;
381 case TileFromMemory: {
382 assert(desc->width > 0);
383 assert(desc->height > 0);
384 assert(false); // TODO
385 break;
386 }
387 }
388
389 return tile;
390}
391
392void isogfx_set_tile(IsoGfx* iso, int x, int y, Tile tile) {
393 assert(iso);
394
395 Tm_Layer* const layer = tm_map_get_layer_mut(iso->map, 0);
396 Tile* map_tile = tm_layer_get_tile_mut(iso->map, layer, x, y);
397
398 *map_tile = tile;
399}
400
401void isogfx_set_tiles(IsoGfx* iso, int x0, int y0, int x1, int y1, Tile tile) {
402 assert(iso);
403 for (int y = y0; y < y1; ++y) {
404 for (int x = x0; x < x1; ++x) {
405 isogfx_set_tile(iso, x, y, tile);
406 }
407 }
408}
409
410SpriteSheet isogfx_load_sprite_sheet(IsoGfx* iso, const char* filepath) {
411 assert(iso);
412 assert(filepath);
413
414 bool success = false;
415 SpriteSheet spriteSheet = 0;
416 const size_t watermark = memstack_watermark(&iso->stack);
417
418 // Load sprite sheet file.
419 printf("Load sprite sheet: %s\n", filepath);
420 Ss_SpriteSheet* ss_sheet = nullptr;
421 WITH_FILE(filepath, {
422 const size_t file_size = get_file_size_f(file);
423 ss_sheet =
424 memstack_alloc_aligned(&iso->stack, file_size, alignof(Ss_SpriteSheet));
425 success = read_file_f(file, ss_sheet);
426 });
427 if (!success) {
428 goto cleanup;
429 }
430 assert(ss_sheet);
431
432 spriteSheet = (SpriteSheet)ss_sheet;
433
434cleanup:
435 if (!success) {
436 if (ss_sheet) {
437 memstack_set_watermark(&iso->stack, watermark);
438 }
439 }
440 return spriteSheet;
441}
442
443Sprite isogfx_make_sprite(IsoGfx* iso, SpriteSheet sheet) {
444 assert(iso);
445 assert(sheet);
446
447 // TODO: Remove memstack_alloc() and replace it with a same-name macro that
448 // calls memstack_alloc_aligned() with sizeof/alignof. No real point in
449 // having unaligned allocations.
450 SpriteInstance* sprite = memstack_alloc_aligned(
451 &iso->stack, sizeof(SpriteInstance), alignof(SpriteInstance));
452
453 sprite->sheet = (const Ss_SpriteSheet*)sheet;
454 sprite->next = iso->head_sprite;
455 iso->head_sprite = sprite;
456
457 return (Sprite)sprite;
458}
459
460void isogfx_set_sprite_position(IsoGfx* iso, Sprite hSprite, int x, int y) {
461 assert(iso);
462 SpriteInstance* sprite = (SpriteInstance*)hSprite;
463 sprite->position.x = x;
464 sprite->position.y = y;
465}
466
467void isogfx_set_sprite_animation(IsoGfx* iso, Sprite hSprite, int animation) {
468 assert(iso);
469 SpriteInstance* sprite = (SpriteInstance*)hSprite;
470 sprite->animation = animation;
471}
472
473void isogfx_update(IsoGfx* iso, double t) {
474 assert(iso);
475
476 // If this is the first time update() is called after initialization, just
477 // record the starting animation time.
478 if (iso->last_animation_time == 0.0) {
479 iso->last_animation_time = t;
480 return;
481 }
482
483 if ((t - iso->last_animation_time) >= ANIMATION_UPDATE_DELTA) {
484 // TODO: Consider linking animated sprites in a separate list so that we
485 // only walk over those here and not also the static sprites.
486 for (SpriteInstance* sprite = iso->head_sprite; sprite;
487 sprite = sprite->next) {
488 const Ss_SpriteSheet* sheet = sprite->sheet;
489 const Ss_Row* row = ss_get_sprite_sheet_row(sheet, sprite->animation);
490 sprite->frame = (sprite->frame + 1) % row->num_cols;
491 }
492
493 iso->last_animation_time = t;
494 }
495}
496
497// -----------------------------------------------------------------------------
498// Rendering and picking.
499// -----------------------------------------------------------------------------
500
501/// Get the screen position of the top diamond-corner of the tile at world
502/// (x,y).
503static ivec2 GetTileScreenOrigin(
504 const CoordSystem iso_space, ivec2 camera, int world_x, int world_y) {
505 const ivec2 vx_offset = ivec2_scale(iso_space.x, world_x);
506 const ivec2 vy_offset = ivec2_scale(iso_space.y, world_y);
507 const ivec2 screen_origin =
508 ivec2_add(iso_space.o, ivec2_add(vx_offset, vy_offset));
509 const ivec2 origin_view_space = ivec2_add(screen_origin, ivec2_neg(camera));
510 return origin_view_space;
511}
512
513static Pixel alpha_blend(Pixel src, Pixel dst) {
514 if ((src.a == 255) || (dst.a == 0)) {
515 return src;
516 }
517 const uint16_t one_minus_alpha = 255 - src.a;
518#define blend(s, d) \
519 (Channel)( \
520 (double)((uint16_t)s * (uint16_t)src.a + \
521 (uint16_t)d * one_minus_alpha) / \
522 255.0)
523 return (Pixel){.r = blend(src.r, dst.r),
524 .g = blend(src.g, dst.g),
525 .b = blend(src.b, dst.b),
526 .a = src.a};
527}
528
529/// Draw a rectangle (tile or sprite).
530///
531/// The rectangle's top-left corner is mapped to the screen space position given
532/// by 'top_left'.
533///
534/// The rectangle's pixels are assumed to be arranged in a linear, row-major
535/// fashion.
536///
537/// If indices are given, then the image is assumed to be colour-paletted, where
538/// 'pixels' is the palette and 'indices' the pixel indices. Otherwise, the
539/// image is assumed to be in plain RGBA format.
540static void draw_rect(
541 Screen* screen, ivec2 top_left, int rect_width, int rect_height,
542 const Pixel* pixels, const uint8_t* indices) {
543 assert(screen);
544
545#define rect_pixel(X, Y) \
546 (indices ? pixels[indices[Y * rect_width + X]] : pixels[Y * rect_width + X])
547
548 // Rect origin can be outside screen bounds, so we must offset accordingly to
549 // draw only the visible portion.
550#define max(a, b) (a > b ? a : b)
551 const int px_offset = max(0, -top_left.x);
552 const int py_offset = max(0, -top_left.y);
553
554 // Rect can exceed screen bounds, so clip along Y and X as we draw.
555 for (int py = py_offset;
556 (py < rect_height) && (top_left.y + py < screen->height); ++py) {
557 const int sy = top_left.y + py;
558 for (int px = px_offset;
559 (px < rect_width) && (top_left.x + px < screen->width); ++px) {
560 const Pixel colour = rect_pixel(px, py);
561 if (colour.a > 0) {
562 const int sx = top_left.x + px;
563 const Pixel dst = screen_xy(screen, sx, sy);
564 const Pixel final = alpha_blend(colour, dst);
565 *screen_xy_mut(screen, sx, sy) = final;
566 }
567 }
568 }
569}
570
571/// Draw a tile.
572///
573/// 'screen_origin' is the screen coordinates of the top diamond-corner of the
574/// tile (the base tile for super tiles).
575/// World (0, 0) -> (screen_width / 2, 0).
576static void draw_tile(IsoGfx* iso, ivec2 screen_origin, Tile tile) {
577 assert(iso);
578 assert(iso->tileset);
579
580 const Ts_Tile* pTile = ts_tileset_get_tile(iso->tileset, tile);
581 const Pixel* pixels = ts_tileset_get_tile_pixels(iso->tileset, tile);
582
583 // Move from the top diamond-corner to the top-left corner of the tile image.
584 // For regular tiles, tile height == base tile height, so the y offset is 0.
585 // For super tiles, move as high up as the height of the tile.
586 const ivec2 offset = {
587 -(iso->map->base_tile_width / 2),
588 pTile->height - iso->map->base_tile_height};
589 const ivec2 top_left = ivec2_add(screen_origin, offset);
590
591 draw_rect(
592 &iso->screen, top_left, pTile->width, pTile->height, pixels, nullptr);
593}
594
595static void draw_map(IsoGfx* iso) {
596 assert(iso);
597
598 const int W = iso->screen.width;
599 const int H = iso->screen.height;
600
601 memset(iso->screen.pixels, 0, W * H * sizeof(Pixel));
602
603 const Tm_Layer* layer = tm_map_get_layer(iso->map, 0);
604
605 // TODO: Culling.
606 // Ex: map the screen corners to tile space to cull.
607 // Ex: walk in screen space and fetch the tile.
608 // The tile-centric approach might be more cache-friendly since the
609 // screen-centric approach would juggle multiple tiles throughout the scan.
610 for (int wy = 0; wy < iso->map->world_height; ++wy) {
611 for (int wx = 0; wx < iso->map->world_width; ++wx) {
612 const Tile tile = tm_layer_get_tile(iso->map, layer, wx, wy);
613 const ivec2 screen_origin =
614 GetTileScreenOrigin(iso->iso_space, iso->camera, wx, wy);
615 draw_tile(iso, screen_origin, tile);
616 }
617 }
618}
619
620static void draw_sprite(
621 IsoGfx* iso, ivec2 origin, const SpriteInstance* sprite,
622 const Ss_SpriteSheet* sheet) {
623 assert(iso);
624 assert(sprite);
625 assert(sheet);
626 assert(sprite->animation >= 0);
627 assert(sprite->animation < sheet->num_rows);
628 assert(sprite->frame >= 0);
629
630 const Ss_Row* row = ss_get_sprite_sheet_row(sheet, sprite->animation);
631 const uint8_t* frame = ss_get_sprite_sheet_sprite(sheet, row, sprite->frame);
632 draw_rect(
633 &iso->screen, origin, sheet->sprite_width, sheet->sprite_height,
634 sheet->palette.colours, frame);
635}
636
637static void draw_sprites(IsoGfx* iso) {
638 assert(iso);
639
640 for (const SpriteInstance* sprite = iso->head_sprite; sprite;
641 sprite = sprite->next) {
642 const Ss_SpriteSheet* sheet = sprite->sheet;
643 assert(sheet);
644
645 const ivec2 screen_origin = GetTileScreenOrigin(
646 iso->iso_space, iso->camera, sprite->position.x, sprite->position.y);
647 draw_sprite(iso, screen_origin, sprite, sheet);
648 }
649}
650
651void isogfx_set_camera(IsoGfx* iso, int x, int y) {
652 assert(iso);
653 iso->camera = (ivec2){x, y};
654}
655
656void isogfx_render(IsoGfx* iso) {
657 assert(iso);
658 draw_map(iso);
659 draw_sprites(iso);
660}
661
662void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) {
663 assert(iso);
664 assert(x >= 0);
665 assert(y >= 0);
666 assert(x < iso->map->world_width);
667 assert(y < iso->map->world_height);
668
669 const ivec2 screen_origin =
670 GetTileScreenOrigin(iso->iso_space, iso->camera, x, y);
671 draw_tile(iso, screen_origin, tile);
672}
673
674void isogfx_get_screen_size(const IsoGfx* iso, int* width, int* height) {
675 assert(iso);
676 assert(width);
677 assert(height);
678 *width = iso->screen.width;
679 *height = iso->screen.height;
680}
681
682const Pixel* isogfx_get_screen_buffer(const IsoGfx* iso) {
683 assert(iso);
684 return iso->screen.pixels;
685}
686
687void isogfx_pick_tile(
688 const IsoGfx* iso, double xcart, double ycart, int* xiso, int* yiso) {
689 assert(iso);
690 assert(xiso);
691 assert(yiso);
692
693 const vec2 camera = ivec2_to_vec2(iso->camera);
694 const vec2 xy_cart = vec2_add(camera, (vec2){xcart, ycart});
695
696 const vec2 xy_iso = cart2iso(
697 xy_cart, iso->map->base_tile_width, iso->map->base_tile_height,
698 iso->screen.width);
699
700 if ((0 <= xy_iso.x) && (xy_iso.x < iso->map->world_width) &&
701 (0 <= xy_iso.y) && (xy_iso.y < iso->map->world_height)) {
702 *xiso = (int)xy_iso.x;
703 *yiso = (int)xy_iso.y;
704 } else {
705 *xiso = -1;
706 *yiso = -1;
707 }
708}