/* * sprite.h * MontaukOS 2D Game Engine - Sprite and Animation System * PNG spritesheet loading, frame extraction, alpha-blended rendering * Copyright (c) 2026 Daniel Hammer */ #pragma once #include #include #include #include "engine/engine.h" extern "C" { #include } namespace engine { // ============================================================================ // Convert stb_image RGBA output to MontaukOS ARGB pixel format // stb on little-endian x86: pixel bytes in memory are R,G,B,A // When read as uint32_t: 0xAABBGGRR // MontaukOS format: 0xAARRGGBB // ============================================================================ inline void rgba_to_argb(uint32_t* data, int count) { for (int i = 0; i < count; i++) { uint32_t px = data[i]; uint8_t r = px & 0xFF; uint8_t g = (px >> 8) & 0xFF; uint8_t b = (px >> 16) & 0xFF; uint8_t a = (px >> 24) & 0xFF; data[i] = ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) | b; } } // ============================================================================ // Spritesheet // ============================================================================ struct Spritesheet { uint32_t* pixels = nullptr; int width = 0; int height = 0; int frame_w = 0; int frame_h = 0; int cols = 0; int rows = 0; // Load a PNG spritesheet from VFS and split into frames of given size. // If frame_w/frame_h are 0, treat the entire image as a single frame. bool load(const char* vfs_path, int fw = 0, int fh = 0) { FileData file; if (!file.load(vfs_path)) return false; int w, h, channels; uint32_t* img = (uint32_t*)stbi_load_from_memory( file.data, (int)file.size, &w, &h, &channels, 4); file.free(); if (!img) return false; // Convert RGBA to ARGB rgba_to_argb(img, w * h); pixels = img; width = w; height = h; frame_w = fw > 0 ? fw : w; frame_h = fh > 0 ? fh : h; cols = w / frame_w; rows = h / frame_h; return true; } void unload() { if (pixels) { stbi_image_free(pixels); pixels = nullptr; } } // Blit a single frame to the destination buffer with scaling and alpha. // frame_col/frame_row select which frame from the spritesheet. // dst_x/dst_y is the screen position. scale is the integer scale factor. // flip_h mirrors the sprite horizontally. void draw_frame(uint32_t* dst, int dst_w, int dst_h, int frame_col, int frame_row, int dst_x, int dst_y, int scale = 1, bool flip_h = false) const { if (!pixels) return; if (frame_col < 0 || frame_col >= cols) return; if (frame_row < 0 || frame_row >= rows) return; int src_ox = frame_col * frame_w; int src_oy = frame_row * frame_h; int out_w = frame_w * scale; int out_h = frame_h * scale; for (int py = 0; py < out_h; py++) { int dy = dst_y + py; if (dy < 0 || dy >= dst_h) continue; int sy = src_oy + py / scale; for (int px = 0; px < out_w; px++) { int dx = dst_x + px; if (dx < 0 || dx >= dst_w) continue; int sx_local = px / scale; if (flip_h) sx_local = frame_w - 1 - sx_local; int sx = src_ox + sx_local; uint32_t src_px = pixels[sy * width + sx]; uint8_t sa = (src_px >> 24) & 0xFF; if (sa == 0) continue; if (sa == 255) { dst[dy * dst_w + dx] = src_px; } else { uint32_t d = dst[dy * dst_w + dx]; uint8_t sr = (src_px >> 16) & 0xFF; uint8_t sg = (src_px >> 8) & 0xFF; uint8_t sb = src_px & 0xFF; uint8_t dr = (d >> 16) & 0xFF; uint8_t dg = (d >> 8) & 0xFF; uint8_t db = d & 0xFF; uint32_t inv = 255 - sa; uint32_t rr = (sa * sr + inv * dr + 128) / 255; uint32_t gg = (sa * sg + inv * dg + 128) / 255; uint32_t bb = (sa * sb + inv * db + 128) / 255; dst[dy * dst_w + dx] = 0xFF000000 | (rr << 16) | (gg << 8) | bb; } } } } // Draw an arbitrary sub-rectangle of the spritesheet (not frame-aligned) void draw_region(uint32_t* dst, int dst_w, int dst_h, int src_x, int src_y, int src_w, int src_h, int dst_x, int dst_y, int scale = 1) const { if (!pixels) return; int out_w = src_w * scale; int out_h = src_h * scale; for (int py = 0; py < out_h; py++) { int dy = dst_y + py; if (dy < 0 || dy >= dst_h) continue; int sy = src_y + py / scale; if (sy < 0 || sy >= height) continue; for (int px = 0; px < out_w; px++) { int dx = dst_x + px; if (dx < 0 || dx >= dst_w) continue; int sx = src_x + px / scale; if (sx < 0 || sx >= width) continue; uint32_t src_px = pixels[sy * width + sx]; uint8_t sa = (src_px >> 24) & 0xFF; if (sa == 0) continue; if (sa == 255) { dst[dy * dst_w + dx] = src_px; } else { uint32_t d = dst[dy * dst_w + dx]; uint8_t sr = (src_px >> 16) & 0xFF; uint8_t sg = (src_px >> 8) & 0xFF; uint8_t sb = src_px & 0xFF; uint8_t dr = (d >> 16) & 0xFF; uint8_t dg = (d >> 8) & 0xFF; uint8_t db = d & 0xFF; uint32_t inv = 255 - sa; uint32_t rr = (sa * sr + inv * dr + 128) / 255; uint32_t gg = (sa * sg + inv * dg + 128) / 255; uint32_t bb = (sa * sb + inv * db + 128) / 255; dst[dy * dst_w + dx] = 0xFF000000 | (rr << 16) | (gg << 8) | bb; } } } } }; // ============================================================================ // Animation // ============================================================================ struct Animation { int row = 0; // spritesheet row for this animation int start_col = 0; // first frame column int num_frames = 1; // number of frames float speed = 8.0f; // frames per second float timer = 0.0f; int current = 0; void update(float dt) { timer += dt * speed; while (timer >= 1.0f) { timer -= 1.0f; current++; if (current >= num_frames) current = 0; } } void reset() { timer = 0.0f; current = 0; } int frame_col() const { return start_col + current; } int frame_row() const { return row; } }; // ============================================================================ // Animated Sprite - combines a spritesheet with named animations // ============================================================================ static constexpr int MAX_ANIMS = 16; struct AnimatedSprite { Spritesheet* sheet = nullptr; Animation anims[MAX_ANIMS]; int anim_count = 0; int current_anim = 0; bool flip_h = false; int add_anim(int row, int start_col, int num_frames, float speed = 8.0f) { if (anim_count >= MAX_ANIMS) return -1; int idx = anim_count++; anims[idx].row = row; anims[idx].start_col = start_col; anims[idx].num_frames = num_frames; anims[idx].speed = speed; return idx; } void play(int anim_idx) { if (anim_idx < 0 || anim_idx >= anim_count) return; if (current_anim != anim_idx) { current_anim = anim_idx; anims[current_anim].reset(); } } void update(float dt) { if (current_anim >= 0 && current_anim < anim_count) anims[current_anim].update(dt); } void draw(uint32_t* dst, int dst_w, int dst_h, int x, int y, int scale = 1) const { if (!sheet || current_anim < 0 || current_anim >= anim_count) return; const Animation& a = anims[current_anim]; sheet->draw_frame(dst, dst_w, dst_h, a.frame_col(), a.frame_row(), x, y, scale, flip_h); } }; } // namespace engine