add networking, 3d improvements, reorganize src structure

This commit is contained in:
asrael 2026-01-17 22:52:36 -06:00
parent 39b604b333
commit 415d424057
122 changed files with 5358 additions and 721 deletions

214
src/gfx/pxl8_3d_camera.c Normal file
View file

@ -0,0 +1,214 @@
#include "pxl8_3d_camera.h"
#include <math.h>
#include <stdlib.h>
struct pxl8_3d_camera {
pxl8_vec3 position;
f32 pitch;
f32 roll;
f32 yaw;
pxl8_3d_camera_mode mode;
f32 aspect;
f32 far;
f32 fov;
f32 near;
f32 ortho_bottom;
f32 ortho_left;
f32 ortho_right;
f32 ortho_top;
f32 shake_duration;
f32 shake_intensity;
pxl8_vec3 shake_offset;
f32 shake_timer;
};
pxl8_3d_camera* pxl8_3d_camera_create(void) {
pxl8_3d_camera* cam = calloc(1, sizeof(pxl8_3d_camera));
if (!cam) return NULL;
cam->position = (pxl8_vec3){0, 0, 0};
cam->pitch = 0;
cam->yaw = 0;
cam->roll = 0;
cam->mode = PXL8_3D_CAMERA_PERSPECTIVE;
cam->fov = 1.0f;
cam->aspect = 16.0f / 9.0f;
cam->near = 1.0f;
cam->far = 1000.0f;
return cam;
}
void pxl8_3d_camera_destroy(pxl8_3d_camera* cam) {
free(cam);
}
void pxl8_3d_camera_lookat(pxl8_3d_camera* cam, pxl8_vec3 eye, pxl8_vec3 target, pxl8_vec3 up) {
if (!cam) return;
cam->position = eye;
pxl8_vec3 forward = pxl8_vec3_normalize(pxl8_vec3_sub(target, eye));
cam->pitch = asinf(-forward.y);
cam->yaw = atan2f(forward.x, forward.z);
cam->roll = 0;
(void)up;
}
void pxl8_3d_camera_set_ortho(pxl8_3d_camera* cam, f32 left, f32 right, f32 bottom, f32 top, f32 near, f32 far) {
if (!cam) return;
cam->mode = PXL8_3D_CAMERA_ORTHO;
cam->ortho_left = left;
cam->ortho_right = right;
cam->ortho_bottom = bottom;
cam->ortho_top = top;
cam->near = near;
cam->far = far;
}
void pxl8_3d_camera_set_perspective(pxl8_3d_camera* cam, f32 fov, f32 aspect, f32 near, f32 far) {
if (!cam) return;
cam->mode = PXL8_3D_CAMERA_PERSPECTIVE;
cam->fov = fov;
cam->aspect = aspect;
cam->near = near;
cam->far = far;
}
void pxl8_3d_camera_set_position(pxl8_3d_camera* cam, pxl8_vec3 pos) {
if (!cam) return;
cam->position = pos;
}
void pxl8_3d_camera_set_rotation(pxl8_3d_camera* cam, f32 pitch, f32 yaw, f32 roll) {
if (!cam) return;
cam->pitch = pitch;
cam->yaw = yaw;
cam->roll = roll;
}
pxl8_vec3 pxl8_3d_camera_get_forward(const pxl8_3d_camera* cam) {
if (!cam) return (pxl8_vec3){0, 0, -1};
f32 cp = cosf(cam->pitch);
f32 sp = sinf(cam->pitch);
f32 cy = cosf(cam->yaw);
f32 sy = sinf(cam->yaw);
return (pxl8_vec3){
cp * sy,
-sp,
cp * cy
};
}
pxl8_vec3 pxl8_3d_camera_get_position(const pxl8_3d_camera* cam) {
if (!cam) return (pxl8_vec3){0, 0, 0};
return pxl8_vec3_add(cam->position, cam->shake_offset);
}
pxl8_mat4 pxl8_3d_camera_get_projection(const pxl8_3d_camera* cam) {
if (!cam) return pxl8_mat4_identity();
if (cam->mode == PXL8_3D_CAMERA_PERSPECTIVE) {
return pxl8_mat4_perspective(cam->fov, cam->aspect, cam->near, cam->far);
} else {
return pxl8_mat4_ortho(
cam->ortho_left, cam->ortho_right,
cam->ortho_bottom, cam->ortho_top,
cam->near, cam->far
);
}
}
pxl8_vec3 pxl8_3d_camera_get_right(const pxl8_3d_camera* cam) {
if (!cam) return (pxl8_vec3){1, 0, 0};
f32 cy = cosf(cam->yaw);
f32 sy = sinf(cam->yaw);
return (pxl8_vec3){cy, 0, -sy};
}
pxl8_vec3 pxl8_3d_camera_get_up(const pxl8_3d_camera* cam) {
if (!cam) return (pxl8_vec3){0, 1, 0};
pxl8_vec3 forward = pxl8_3d_camera_get_forward(cam);
pxl8_vec3 right = pxl8_3d_camera_get_right(cam);
return pxl8_vec3_cross(forward, right);
}
pxl8_mat4 pxl8_3d_camera_get_view(const pxl8_3d_camera* cam) {
if (!cam) return pxl8_mat4_identity();
pxl8_vec3 pos = pxl8_3d_camera_get_position(cam);
pxl8_vec3 forward = pxl8_3d_camera_get_forward(cam);
pxl8_vec3 target = pxl8_vec3_add(pos, forward);
pxl8_vec3 up = (pxl8_vec3){0, 1, 0};
return pxl8_mat4_lookat(pos, target, up);
}
void pxl8_3d_camera_blend(pxl8_3d_camera* dest, const pxl8_3d_camera* a, const pxl8_3d_camera* b, f32 t) {
if (!dest || !a || !b) return;
dest->position = pxl8_vec3_lerp(a->position, b->position, t);
dest->pitch = a->pitch + (b->pitch - a->pitch) * t;
dest->yaw = a->yaw + (b->yaw - a->yaw) * t;
dest->roll = a->roll + (b->roll - a->roll) * t;
dest->fov = a->fov + (b->fov - a->fov) * t;
dest->aspect = a->aspect + (b->aspect - a->aspect) * t;
dest->near = a->near + (b->near - a->near) * t;
dest->far = a->far + (b->far - a->far) * t;
dest->mode = (t < 0.5f) ? a->mode : b->mode;
}
void pxl8_3d_camera_follow(pxl8_3d_camera* cam, pxl8_vec3 target, pxl8_vec3 offset, f32 smoothing, f32 dt) {
if (!cam) return;
pxl8_vec3 desired = pxl8_vec3_add(target, offset);
f32 t = 1.0f - powf(1.0f - smoothing, dt * 60.0f);
cam->position = pxl8_vec3_lerp(cam->position, desired, t);
pxl8_vec3 forward = pxl8_vec3_normalize(pxl8_vec3_sub(target, cam->position));
cam->pitch = asinf(-forward.y);
cam->yaw = atan2f(forward.x, forward.z);
}
void pxl8_3d_camera_shake(pxl8_3d_camera* cam, f32 intensity, f32 duration) {
if (!cam) return;
cam->shake_intensity = intensity;
cam->shake_duration = duration;
cam->shake_timer = duration;
}
void pxl8_3d_camera_update(pxl8_3d_camera* cam, f32 dt) {
if (!cam) return;
if (cam->shake_timer > 0) {
cam->shake_timer -= dt;
f32 decay = cam->shake_timer / cam->shake_duration;
f32 intensity = cam->shake_intensity * decay;
cam->shake_offset.x = ((f32)rand() / (f32)RAND_MAX * 2.0f - 1.0f) * intensity;
cam->shake_offset.y = ((f32)rand() / (f32)RAND_MAX * 2.0f - 1.0f) * intensity;
cam->shake_offset.z = ((f32)rand() / (f32)RAND_MAX * 2.0f - 1.0f) * intensity;
if (cam->shake_timer <= 0) {
cam->shake_offset = (pxl8_vec3){0, 0, 0};
}
}
}

40
src/gfx/pxl8_3d_camera.h Normal file
View file

@ -0,0 +1,40 @@
#pragma once
#include "pxl8_math.h"
#include "pxl8_types.h"
typedef enum pxl8_3d_camera_mode {
PXL8_3D_CAMERA_ORTHO,
PXL8_3D_CAMERA_PERSPECTIVE
} pxl8_3d_camera_mode;
typedef struct pxl8_3d_camera pxl8_3d_camera;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_3d_camera* pxl8_3d_camera_create(void);
void pxl8_3d_camera_destroy(pxl8_3d_camera* cam);
void pxl8_3d_camera_lookat(pxl8_3d_camera* cam, pxl8_vec3 eye, pxl8_vec3 target, pxl8_vec3 up);
void pxl8_3d_camera_set_ortho(pxl8_3d_camera* cam, f32 left, f32 right, f32 bottom, f32 top, f32 near, f32 far);
void pxl8_3d_camera_set_perspective(pxl8_3d_camera* cam, f32 fov, f32 aspect, f32 near, f32 far);
void pxl8_3d_camera_set_position(pxl8_3d_camera* cam, pxl8_vec3 pos);
void pxl8_3d_camera_set_rotation(pxl8_3d_camera* cam, f32 pitch, f32 yaw, f32 roll);
pxl8_vec3 pxl8_3d_camera_get_forward(const pxl8_3d_camera* cam);
pxl8_vec3 pxl8_3d_camera_get_position(const pxl8_3d_camera* cam);
pxl8_mat4 pxl8_3d_camera_get_projection(const pxl8_3d_camera* cam);
pxl8_vec3 pxl8_3d_camera_get_right(const pxl8_3d_camera* cam);
pxl8_vec3 pxl8_3d_camera_get_up(const pxl8_3d_camera* cam);
pxl8_mat4 pxl8_3d_camera_get_view(const pxl8_3d_camera* cam);
void pxl8_3d_camera_blend(pxl8_3d_camera* dest, const pxl8_3d_camera* a, const pxl8_3d_camera* b, f32 t);
void pxl8_3d_camera_follow(pxl8_3d_camera* cam, pxl8_vec3 target, pxl8_vec3 offset, f32 smoothing, f32 dt);
void pxl8_3d_camera_shake(pxl8_3d_camera* cam, f32 intensity, f32 duration);
void pxl8_3d_camera_update(pxl8_3d_camera* cam, f32 dt);
#ifdef __cplusplus
}
#endif

432
src/gfx/pxl8_anim.c Normal file
View file

@ -0,0 +1,432 @@
#include "pxl8_anim.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_ase.h"
#include "pxl8_atlas.h"
#include "pxl8_gfx.h"
#include "pxl8_log.h"
#define PXL8_ANIM_MAX_STATES 32
typedef struct pxl8_anim_state {
char* name;
pxl8_anim* anim;
} pxl8_anim_state;
typedef struct pxl8_anim_state_machine {
pxl8_anim_state states[PXL8_ANIM_MAX_STATES];
u16 state_count;
u16 current_state;
} pxl8_anim_state_machine;
static inline pxl8_anim* pxl8_anim_get_active(pxl8_anim* anim) {
if (anim->state_machine &&
anim->state_machine->current_state < anim->state_machine->state_count) {
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
if (state_anim) return state_anim;
}
return anim;
}
pxl8_anim* pxl8_anim_create(const u32* frame_ids, const u16* frame_durations, u16 frame_count) {
if (!frame_ids || frame_count == 0) {
pxl8_error("Invalid animation parameters");
return NULL;
}
pxl8_anim* anim = (pxl8_anim*)calloc(1, sizeof(pxl8_anim));
if (!anim) {
pxl8_error("Failed to allocate animation");
return NULL;
}
anim->frame_ids = (u32*)malloc(frame_count * sizeof(u32));
if (!anim->frame_ids) {
pxl8_error("Failed to allocate frame IDs");
free(anim);
return NULL;
}
memcpy(anim->frame_ids, frame_ids, frame_count * sizeof(u32));
if (frame_durations) {
anim->frame_durations = (u16*)malloc(frame_count * sizeof(u16));
if (!anim->frame_durations) {
pxl8_error("Failed to allocate frame durations");
free(anim->frame_ids);
free(anim);
return NULL;
}
memcpy(anim->frame_durations, frame_durations, frame_count * sizeof(u16));
} else {
anim->frame_durations = (u16*)calloc(frame_count, sizeof(u16));
if (!anim->frame_durations) {
pxl8_error("Failed to allocate frame durations");
free(anim->frame_ids);
free(anim);
return NULL;
}
for (u16 i = 0; i < frame_count; i++) {
anim->frame_durations[i] = 100;
}
}
anim->frame_count = frame_count;
anim->current_frame = 0;
anim->time_accumulator = 0.0f;
anim->loop = true;
anim->playing = true;
anim->reverse = false;
anim->speed = 1.0f;
anim->state_machine = NULL;
anim->on_complete = NULL;
anim->on_frame_change = NULL;
anim->userdata = NULL;
return anim;
}
pxl8_anim* pxl8_anim_create_from_ase(pxl8_gfx* gfx, const char* path) {
if (!gfx || !path) {
pxl8_error("Invalid parameters for ASE animation creation");
return NULL;
}
pxl8_ase_file ase_file;
pxl8_result result = pxl8_ase_load(path, &ase_file);
if (result != PXL8_OK) {
pxl8_error("Failed to load ASE file: %s", path);
return NULL;
}
if (ase_file.frame_count == 0) {
pxl8_error("ASE file has no frames: %s", path);
pxl8_ase_destroy(&ase_file);
return NULL;
}
u32* frame_ids = (u32*)malloc(ase_file.frame_count * sizeof(u32));
u16* frame_durations = (u16*)malloc(ase_file.frame_count * sizeof(u16));
if (!frame_ids || !frame_durations) {
pxl8_error("Failed to allocate frame arrays");
free(frame_ids);
free(frame_durations);
pxl8_ase_destroy(&ase_file);
return NULL;
}
for (u32 i = 0; i < ase_file.frame_count; i++) {
pxl8_ase_frame* frame = &ase_file.frames[i];
result = pxl8_gfx_create_texture(gfx, frame->pixels, frame->width, frame->height);
if (result != PXL8_OK) {
pxl8_error("Failed to create texture for frame %u", i);
free(frame_ids);
free(frame_durations);
pxl8_ase_destroy(&ase_file);
return NULL;
}
frame_ids[i] = i;
frame_durations[i] = frame->duration;
}
pxl8_anim* anim = pxl8_anim_create(frame_ids, frame_durations, ase_file.frame_count);
free(frame_ids);
free(frame_durations);
pxl8_ase_destroy(&ase_file);
return anim;
}
void pxl8_anim_destroy(pxl8_anim* anim) {
if (!anim) return;
if (anim->state_machine) {
for (u16 i = 0; i < anim->state_machine->state_count; i++) {
free(anim->state_machine->states[i].name);
pxl8_anim_destroy(anim->state_machine->states[i].anim);
}
free(anim->state_machine);
}
free(anim->frame_ids);
free(anim->frame_durations);
free(anim);
}
pxl8_result pxl8_anim_add_state(pxl8_anim* anim, const char* name, pxl8_anim* state_anim) {
if (!anim || !name || !state_anim) {
return PXL8_ERROR_NULL_POINTER;
}
if (!anim->state_machine) {
anim->state_machine = (pxl8_anim_state_machine*)calloc(1, sizeof(pxl8_anim_state_machine));
if (!anim->state_machine) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
}
if (anim->state_machine->state_count >= PXL8_ANIM_MAX_STATES) {
pxl8_error("Cannot add more states, maximum %d reached", PXL8_ANIM_MAX_STATES);
return PXL8_ERROR_OUT_OF_MEMORY;
}
u16 idx = anim->state_machine->state_count;
anim->state_machine->states[idx].name = strdup(name);
if (!anim->state_machine->states[idx].name) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
anim->state_machine->states[idx].anim = state_anim;
anim->state_machine->state_count++;
return PXL8_OK;
}
u16 pxl8_anim_get_current_frame(const pxl8_anim* anim) {
if (!anim) return 0;
if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) {
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
if (state_anim) {
return state_anim->current_frame;
}
}
return anim->current_frame;
}
u32 pxl8_anim_get_current_frame_id(const pxl8_anim* anim) {
if (!anim || !anim->frame_ids) return 0;
if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) {
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
if (state_anim && state_anim->frame_ids) {
return state_anim->frame_ids[state_anim->current_frame];
}
}
return anim->frame_ids[anim->current_frame];
}
const char* pxl8_anim_get_state(const pxl8_anim* anim) {
if (!anim || !anim->state_machine) return NULL;
if (anim->state_machine->current_state < anim->state_machine->state_count) {
return anim->state_machine->states[anim->state_machine->current_state].name;
}
return NULL;
}
bool pxl8_anim_has_state_machine(const pxl8_anim* anim) {
return anim && anim->state_machine && anim->state_machine->state_count > 0;
}
bool pxl8_anim_is_complete(const pxl8_anim* anim) {
if (!anim) return true;
if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) {
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
if (state_anim) {
return pxl8_anim_is_complete(state_anim);
}
}
if (anim->loop) return false;
if (anim->reverse) {
return anim->current_frame == 0;
} else {
return anim->current_frame >= anim->frame_count - 1;
}
}
bool pxl8_anim_is_playing(const pxl8_anim* anim) {
if (!anim) return false;
if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) {
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
if (state_anim) {
return state_anim->playing;
}
}
return anim->playing;
}
void pxl8_anim_pause(pxl8_anim* anim) {
if (!anim) return;
if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) {
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
if (state_anim) {
state_anim->playing = false;
return;
}
}
anim->playing = false;
}
void pxl8_anim_play(pxl8_anim* anim) {
if (!anim) return;
if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) {
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
if (state_anim) {
state_anim->playing = true;
return;
}
}
anim->playing = true;
}
void pxl8_anim_render_sprite(const pxl8_anim* anim, pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, bool flip_x, bool flip_y) {
if (!anim || !gfx) return;
u32 sprite_id = pxl8_anim_get_current_frame_id(anim);
pxl8_2d_sprite(gfx, sprite_id, x, y, w, h, flip_x, flip_y);
}
void pxl8_anim_reset(pxl8_anim* anim) {
if (!anim) return;
if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) {
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
if (state_anim) {
pxl8_anim_reset(state_anim);
return;
}
}
anim->current_frame = anim->reverse ? anim->frame_count - 1 : 0;
anim->time_accumulator = 0.0f;
}
void pxl8_anim_set_frame(pxl8_anim* anim, u16 frame) {
if (!anim) return;
pxl8_anim* target = pxl8_anim_get_active(anim);
if (frame < target->frame_count) {
target->current_frame = frame;
target->time_accumulator = 0.0f;
}
}
void pxl8_anim_set_loop(pxl8_anim* anim, bool loop) {
if (!anim) return;
pxl8_anim_get_active(anim)->loop = loop;
}
void pxl8_anim_set_reverse(pxl8_anim* anim, bool reverse) {
if (!anim) return;
pxl8_anim_get_active(anim)->reverse = reverse;
}
void pxl8_anim_set_speed(pxl8_anim* anim, f32 speed) {
if (!anim) return;
pxl8_anim_get_active(anim)->speed = speed;
}
pxl8_result pxl8_anim_set_state(pxl8_anim* anim, const char* name) {
if (!anim || !name) {
return PXL8_ERROR_NULL_POINTER;
}
if (!anim->state_machine) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
for (u16 i = 0; i < anim->state_machine->state_count; i++) {
if (strcmp(anim->state_machine->states[i].name, name) == 0) {
if (anim->state_machine->current_state != i) {
anim->state_machine->current_state = i;
pxl8_anim_reset(anim->state_machine->states[i].anim);
}
return PXL8_OK;
}
}
pxl8_error("Animation state not found: %s", name);
return PXL8_ERROR_INVALID_ARGUMENT;
}
void pxl8_anim_stop(pxl8_anim* anim) {
if (!anim) return;
if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) {
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
if (state_anim) {
pxl8_anim_stop(state_anim);
return;
}
}
anim->playing = false;
pxl8_anim_reset(anim);
}
void pxl8_anim_update(pxl8_anim* anim, f32 dt) {
if (!anim || !anim->playing) return;
if (anim->state_machine && anim->state_machine->current_state < anim->state_machine->state_count) {
pxl8_anim* state_anim = anim->state_machine->states[anim->state_machine->current_state].anim;
if (state_anim) {
pxl8_anim_update(state_anim, dt);
return;
}
}
if (anim->frame_count == 0 || !anim->frame_durations) return;
anim->time_accumulator += dt * anim->speed * 1000.0f;
u16 current_duration = anim->frame_durations[anim->current_frame];
if (current_duration == 0) return;
while (anim->time_accumulator >= current_duration) {
anim->time_accumulator -= current_duration;
u16 old_frame = anim->current_frame;
if (anim->reverse) {
if (anim->current_frame > 0) {
anim->current_frame--;
} else {
if (anim->loop) {
anim->current_frame = anim->frame_count - 1;
} else {
anim->playing = false;
if (anim->on_complete) {
anim->on_complete(anim->userdata);
}
break;
}
}
} else {
if (anim->current_frame < anim->frame_count - 1) {
anim->current_frame++;
} else {
if (anim->loop) {
anim->current_frame = 0;
} else {
anim->playing = false;
if (anim->on_complete) {
anim->on_complete(anim->userdata);
}
break;
}
}
}
if (old_frame != anim->current_frame && anim->on_frame_change) {
anim->on_frame_change(anim->current_frame, anim->userdata);
}
current_duration = anim->frame_durations[anim->current_frame];
if (current_duration == 0) break;
}
}

56
src/gfx/pxl8_anim.h Normal file
View file

@ -0,0 +1,56 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_types.h"
typedef struct pxl8_anim_state_machine pxl8_anim_state_machine;
typedef struct pxl8_anim {
u32* frame_ids;
u16* frame_durations;
u16 frame_count;
u16 current_frame;
f32 time_accumulator;
bool loop;
bool playing;
bool reverse;
f32 speed;
pxl8_anim_state_machine* state_machine;
void (*on_complete)(void* userdata);
void (*on_frame_change)(u16 frame, void* userdata);
void* userdata;
} pxl8_anim;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_anim* pxl8_anim_create(const u32* frame_ids, const u16* frame_durations, u16 frame_count);
pxl8_anim* pxl8_anim_create_from_ase(pxl8_gfx* gfx, const char* path);
void pxl8_anim_destroy(pxl8_anim* anim);
pxl8_result pxl8_anim_add_state(pxl8_anim* anim, const char* name, pxl8_anim* state_anim);
u16 pxl8_anim_get_current_frame(const pxl8_anim* anim);
u32 pxl8_anim_get_current_frame_id(const pxl8_anim* anim);
const char* pxl8_anim_get_state(const pxl8_anim* anim);
bool pxl8_anim_has_state_machine(const pxl8_anim* anim);
bool pxl8_anim_is_complete(const pxl8_anim* anim);
bool pxl8_anim_is_playing(const pxl8_anim* anim);
void pxl8_anim_pause(pxl8_anim* anim);
void pxl8_anim_play(pxl8_anim* anim);
void pxl8_anim_render_sprite(const pxl8_anim* anim, pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, bool flip_x, bool flip_y);
void pxl8_anim_reset(pxl8_anim* anim);
void pxl8_anim_set_frame(pxl8_anim* anim, u16 frame);
void pxl8_anim_set_loop(pxl8_anim* anim, bool loop);
void pxl8_anim_set_reverse(pxl8_anim* anim, bool reverse);
void pxl8_anim_set_speed(pxl8_anim* anim, f32 speed);
pxl8_result pxl8_anim_set_state(pxl8_anim* anim, const char* name);
void pxl8_anim_stop(pxl8_anim* anim);
void pxl8_anim_update(pxl8_anim* anim, f32 dt);
#ifdef __cplusplus
}
#endif

392
src/gfx/pxl8_atlas.c Normal file
View file

@ -0,0 +1,392 @@
#include "pxl8_atlas.h"
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "pxl8_color.h"
#include "pxl8_log.h"
typedef struct pxl8_skyline_fit {
bool found;
u32 node_idx;
pxl8_point pos;
} pxl8_skyline_fit;
typedef struct pxl8_skyline_node {
i32 x, y, width;
} pxl8_skyline_node;
typedef struct pxl8_skyline {
pxl8_skyline_node* nodes;
u32 count;
u32 capacity;
} pxl8_skyline;
struct pxl8_atlas {
u32 height, width;
u8* pixels;
bool dirty;
u32 entry_capacity, entry_count;
pxl8_atlas_entry* entries;
u32 free_capacity, free_count;
u32* free_list;
pxl8_skyline skyline;
};
static pxl8_skyline_fit pxl8_skyline_find_position(
const pxl8_skyline* skyline,
u32 atlas_w,
u32 atlas_h,
u32 rect_w,
u32 rect_h
) {
pxl8_skyline_fit result = {.found = false};
i32 best_y = INT32_MAX;
i32 best_x = 0;
u32 best_idx = 0;
for (u32 i = 0; i < skyline->count; i++) {
i32 x = skyline->nodes[i].x;
i32 y = skyline->nodes[i].y;
if (x + (i32)rect_w > (i32)atlas_w) continue;
i32 max_y = y;
for (u32 j = i; j < skyline->count && skyline->nodes[j].x < x + (i32)rect_w; j++) {
if (skyline->nodes[j].y > max_y) {
max_y = skyline->nodes[j].y;
}
}
if (max_y + (i32)rect_h > (i32)atlas_h) continue;
if (max_y < best_y || (max_y == best_y && x < best_x)) {
best_y = max_y;
best_x = x;
best_idx = i;
}
}
if (best_y != INT32_MAX) {
result.found = true;
result.pos.x = best_x;
result.pos.y = best_y;
result.node_idx = best_idx;
}
return result;
}
static bool pxl8_skyline_add_rect(pxl8_skyline* skyline, pxl8_point pos, u32 w, u32 h) {
u32 node_idx = 0;
for (u32 i = 0; i < skyline->count; i++) {
if (skyline->nodes[i].x == pos.x) {
node_idx = i;
break;
}
}
u32 nodes_to_remove = 0;
for (u32 i = node_idx; i < skyline->count; i++) {
if (skyline->nodes[i].x < pos.x + (i32)w) {
nodes_to_remove++;
} else {
break;
}
}
if (skyline->count - nodes_to_remove + 1 > skyline->capacity) {
u32 new_capacity = (skyline->count - nodes_to_remove + 1) * 2;
pxl8_skyline_node* new_nodes = (pxl8_skyline_node*)realloc(
skyline->nodes,
new_capacity * sizeof(pxl8_skyline_node)
);
if (!new_nodes) return false;
skyline->nodes = new_nodes;
skyline->capacity = new_capacity;
}
if (nodes_to_remove > 0) {
memmove(
&skyline->nodes[node_idx + 1],
&skyline->nodes[node_idx + nodes_to_remove],
(skyline->count - node_idx - nodes_to_remove) * sizeof(pxl8_skyline_node)
);
}
skyline->nodes[node_idx] = (pxl8_skyline_node){pos.x, pos.y + (i32)h, (i32)w};
skyline->count = skyline->count - nodes_to_remove + 1;
return true;
}
static void pxl8_skyline_compact(pxl8_skyline* skyline) {
for (u32 i = 0; i < skyline->count - 1; ) {
if (skyline->nodes[i].y == skyline->nodes[i + 1].y) {
skyline->nodes[i].width += skyline->nodes[i + 1].width;
memmove(
&skyline->nodes[i + 1],
&skyline->nodes[i + 2],
(skyline->count - i - 2) * sizeof(pxl8_skyline_node)
);
skyline->count--;
} else {
i++;
}
}
}
pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_pixel_mode pixel_mode) {
pxl8_atlas* atlas = (pxl8_atlas*)calloc(1, sizeof(pxl8_atlas));
if (!atlas) return NULL;
atlas->height = height;
atlas->width = width;
i32 bytes_per_pixel = pxl8_bytes_per_pixel(pixel_mode);
atlas->pixels = (u8*)calloc(width * height, bytes_per_pixel);
if (!atlas->pixels) {
free(atlas);
return NULL;
}
atlas->entry_capacity = PXL8_DEFAULT_ATLAS_ENTRY_CAPACITY;
atlas->entries = (pxl8_atlas_entry*)calloc(atlas->entry_capacity, sizeof(pxl8_atlas_entry));
if (!atlas->entries) {
free(atlas->pixels);
free(atlas);
return NULL;
}
atlas->free_capacity = 16;
atlas->free_list = (u32*)calloc(atlas->free_capacity, sizeof(u32));
if (!atlas->free_list) {
free(atlas->entries);
free(atlas->pixels);
free(atlas);
return NULL;
}
atlas->skyline.capacity = 16;
atlas->skyline.nodes =
(pxl8_skyline_node*)calloc(atlas->skyline.capacity, sizeof(pxl8_skyline_node));
if (!atlas->skyline.nodes) {
free(atlas->free_list);
free(atlas->entries);
free(atlas->pixels);
free(atlas);
return NULL;
}
atlas->skyline.nodes[0] = (pxl8_skyline_node){0, 0, (i32)width};
atlas->skyline.count = 1;
return atlas;
}
void pxl8_atlas_destroy(pxl8_atlas* atlas) {
if (!atlas) return;
free(atlas->entries);
free(atlas->free_list);
free(atlas->pixels);
free(atlas->skyline.nodes);
free(atlas);
}
void pxl8_atlas_clear(pxl8_atlas* atlas, u32 preserve_count) {
if (!atlas) return;
for (u32 i = preserve_count; i < atlas->entry_count; i++) {
atlas->entries[i].active = false;
}
atlas->entry_count = preserve_count;
atlas->free_count = 0;
atlas->skyline.nodes[0] = (pxl8_skyline_node){0, 0, (i32)atlas->width};
atlas->skyline.count = 1;
atlas->dirty = true;
}
bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_pixel_mode pixel_mode) {
if (!atlas || atlas->width >= 4096) return false;
i32 bytes_per_pixel = pxl8_bytes_per_pixel(pixel_mode);
u32 new_size = atlas->width * 2;
u32 old_width = atlas->width;
u8* new_pixels = (u8*)calloc(new_size * new_size, bytes_per_pixel);
if (!new_pixels) return false;
pxl8_skyline new_skyline;
new_skyline.nodes = (pxl8_skyline_node*)calloc(16, sizeof(pxl8_skyline_node));
if (!new_skyline.nodes) {
free(new_pixels);
return false;
}
new_skyline.nodes[0] = (pxl8_skyline_node){0, 0, (i32)new_size};
new_skyline.count = 1;
new_skyline.capacity = 16;
for (u32 i = 0; i < atlas->entry_count; i++) {
if (!atlas->entries[i].active) continue;
pxl8_skyline_fit fit = pxl8_skyline_find_position(
&new_skyline,
new_size,
new_size,
atlas->entries[i].w,
atlas->entries[i].h
);
if (!fit.found) {
free(new_skyline.nodes);
free(new_pixels);
return false;
}
for (u32 y = 0; y < (u32)atlas->entries[i].h; y++) {
for (u32 x = 0; x < (u32)atlas->entries[i].w; x++) {
u32 src_idx = (atlas->entries[i].y + y) * old_width + (atlas->entries[i].x + x);
u32 dst_idx = (fit.pos.y + y) * new_size + (fit.pos.x + x);
if (bytes_per_pixel == 2) {
((u16*)new_pixels)[dst_idx] = ((u16*)atlas->pixels)[src_idx];
} else {
new_pixels[dst_idx] = atlas->pixels[src_idx];
}
}
}
atlas->entries[i].x = fit.pos.x;
atlas->entries[i].y = fit.pos.y;
if (!pxl8_skyline_add_rect(&new_skyline, fit.pos, atlas->entries[i].w, atlas->entries[i].h)) {
free(new_skyline.nodes);
free(new_pixels);
return false;
}
pxl8_skyline_compact(&new_skyline);
}
free(atlas->pixels);
free(atlas->skyline.nodes);
atlas->pixels = new_pixels;
atlas->skyline = new_skyline;
atlas->width = new_size;
atlas->height = new_size;
atlas->dirty = true;
pxl8_debug("Atlas expanded to %ux%u", atlas->width, atlas->height);
return true;
}
u32 pxl8_atlas_add_texture(
pxl8_atlas* atlas,
const u8* pixels,
u32 w,
u32 h,
pxl8_pixel_mode pixel_mode
) {
if (!atlas || !pixels) return UINT32_MAX;
pxl8_skyline_fit fit =
pxl8_skyline_find_position(&atlas->skyline, atlas->width, atlas->height, w, h);
if (!fit.found) {
if (!pxl8_atlas_expand(atlas, pixel_mode)) {
return UINT32_MAX;
}
fit = pxl8_skyline_find_position(&atlas->skyline, atlas->width, atlas->height, w, h);
if (!fit.found) return UINT32_MAX;
}
u32 texture_id;
if (atlas->free_count > 0) {
texture_id = atlas->free_list[--atlas->free_count];
} else {
if (atlas->entry_count >= atlas->entry_capacity) {
u32 new_capacity = atlas->entry_capacity * 2;
pxl8_atlas_entry* new_entries = (pxl8_atlas_entry*)realloc(
atlas->entries,
new_capacity * sizeof(pxl8_atlas_entry)
);
if (!new_entries) return UINT32_MAX;
atlas->entries = new_entries;
atlas->entry_capacity = new_capacity;
}
texture_id = atlas->entry_count++;
}
pxl8_atlas_entry* entry = &atlas->entries[texture_id];
entry->active = true;
entry->texture_id = texture_id;
entry->x = fit.pos.x;
entry->y = fit.pos.y;
entry->w = w;
entry->h = h;
i32 bytes_per_pixel = pxl8_bytes_per_pixel(pixel_mode);
for (u32 y = 0; y < h; y++) {
for (u32 x = 0; x < w; x++) {
u32 src_idx = y * w + x;
u32 dst_idx = (fit.pos.y + y) * atlas->width + (fit.pos.x + x);
if (bytes_per_pixel == 2) {
((u16*)atlas->pixels)[dst_idx] = ((const u16*)pixels)[src_idx];
} else {
atlas->pixels[dst_idx] = pixels[src_idx];
}
}
}
if (!pxl8_skyline_add_rect(&atlas->skyline, fit.pos, w, h)) {
entry->active = false;
return UINT32_MAX;
}
pxl8_skyline_compact(&atlas->skyline);
atlas->dirty = true;
return texture_id;
}
const pxl8_atlas_entry* pxl8_atlas_get_entry(const pxl8_atlas* atlas, u32 id) {
if (!atlas || id >= atlas->entry_count) return NULL;
return &atlas->entries[id];
}
u32 pxl8_atlas_get_entry_count(const pxl8_atlas* atlas) {
return atlas ? atlas->entry_count : 0;
}
u32 pxl8_atlas_get_height(const pxl8_atlas* atlas) {
return atlas ? atlas->height : 0;
}
const u8* pxl8_atlas_get_pixels(const pxl8_atlas* atlas) {
return atlas ? atlas->pixels : NULL;
}
u32 pxl8_atlas_get_width(const pxl8_atlas* atlas) {
return atlas ? atlas->width : 0;
}
bool pxl8_atlas_is_dirty(const pxl8_atlas* atlas) {
return atlas ? atlas->dirty : false;
}
void pxl8_atlas_mark_clean(pxl8_atlas* atlas) {
if (atlas) {
atlas->dirty = false;
}
}

35
src/gfx/pxl8_atlas.h Normal file
View file

@ -0,0 +1,35 @@
#pragma once
#include "pxl8_types.h"
typedef struct pxl8_atlas pxl8_atlas;
typedef struct pxl8_atlas_entry {
bool active;
u32 texture_id;
i32 x, y, w, h;
} pxl8_atlas_entry;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_atlas* pxl8_atlas_create(u32 width, u32 height, pxl8_pixel_mode pixel_mode);
void pxl8_atlas_destroy(pxl8_atlas* atlas);
const pxl8_atlas_entry* pxl8_atlas_get_entry(const pxl8_atlas* atlas, u32 id);
u32 pxl8_atlas_get_entry_count(const pxl8_atlas* atlas);
u32 pxl8_atlas_get_height(const pxl8_atlas* atlas);
const u8* pxl8_atlas_get_pixels(const pxl8_atlas* atlas);
u32 pxl8_atlas_get_width(const pxl8_atlas* atlas);
bool pxl8_atlas_is_dirty(const pxl8_atlas* atlas);
void pxl8_atlas_mark_clean(pxl8_atlas* atlas);
u32 pxl8_atlas_add_texture(pxl8_atlas* atlas, const u8* pixels, u32 w, u32 h, pxl8_pixel_mode pixel_mode);
void pxl8_atlas_clear(pxl8_atlas* atlas, u32 preserve_count);
bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_pixel_mode pixel_mode);
#ifdef __cplusplus
}
#endif

19
src/gfx/pxl8_backend.h Normal file
View file

@ -0,0 +1,19 @@
#ifndef PXL8_BACKEND_H
#define PXL8_BACKEND_H
#include "pxl8_cpu.h"
typedef enum {
PXL8_GFX_BACKEND_CPU,
PXL8_GFX_BACKEND_GPU,
} pxl8_gfx_backend_type;
typedef struct {
pxl8_gfx_backend_type type;
union {
pxl8_cpu_backend* cpu;
void* gpu;
};
} pxl8_gfx_backend;
#endif

194
src/gfx/pxl8_blend.c Normal file
View file

@ -0,0 +1,194 @@
#include "pxl8_blend.h"
#include "pxl8_colormap.h"
#include <stdlib.h>
struct pxl8_palette_cube {
u8 colors[PXL8_PALETTE_SIZE * 3];
u8 table[PXL8_CUBE_ENTRIES];
u8 stable[PXL8_CUBE_ENTRIES];
};
struct pxl8_additive_table {
u8 table[PXL8_BLEND_TABLE_SIZE];
};
struct pxl8_overbright_table {
u8 table[PXL8_OVERBRIGHT_TABLE_SIZE];
};
static u8 find_closest_stable(const pxl8_palette* pal, u8 r, u8 g, u8 b) {
u8 best_idx = 1;
u32 best_dist = 0xFFFFFFFF;
u8 dynamic_end = PXL8_DYNAMIC_RANGE_START + PXL8_DYNAMIC_RANGE_COUNT;
for (u32 i = 1; i < PXL8_FULLBRIGHT_START; i++) {
if (i >= PXL8_DYNAMIC_RANGE_START && i < dynamic_end) {
continue;
}
u8 pr, pg, pb;
pxl8_palette_get_rgb(pal, (u8)i, &pr, &pg, &pb);
i32 dr = (i32)r - (i32)pr;
i32 dg = (i32)g - (i32)pg;
i32 db = (i32)b - (i32)pb;
u32 dist = (u32)(dr * dr + dg * dg + db * db);
if (dist < best_dist) {
best_dist = dist;
best_idx = (u8)i;
if (dist == 0) break;
}
}
return best_idx;
}
pxl8_palette_cube* pxl8_palette_cube_create(const pxl8_palette* pal) {
pxl8_palette_cube* cube = calloc(1, sizeof(pxl8_palette_cube));
if (!cube) return NULL;
pxl8_palette_cube_rebuild(cube, pal);
return cube;
}
void pxl8_palette_cube_destroy(pxl8_palette_cube* cube) {
free(cube);
}
void pxl8_palette_cube_rebuild(pxl8_palette_cube* cube, const pxl8_palette* pal) {
if (!cube || !pal) return;
for (u32 i = 0; i < PXL8_PALETTE_SIZE; i++) {
u8 r, g, b;
pxl8_palette_get_rgb(pal, (u8)i, &r, &g, &b);
cube->colors[i * 3 + 0] = r;
cube->colors[i * 3 + 1] = g;
cube->colors[i * 3 + 2] = b;
}
for (u32 bi = 0; bi < PXL8_CUBE_SIZE; bi++) {
for (u32 gi = 0; gi < PXL8_CUBE_SIZE; gi++) {
for (u32 ri = 0; ri < PXL8_CUBE_SIZE; ri++) {
u32 idx = (bi * PXL8_CUBE_SIZE * PXL8_CUBE_SIZE) + (gi * PXL8_CUBE_SIZE) + ri;
u8 r8 = (u8)((ri * 255) / (PXL8_CUBE_SIZE - 1));
u8 g8 = (u8)((gi * 255) / (PXL8_CUBE_SIZE - 1));
u8 b8 = (u8)((bi * 255) / (PXL8_CUBE_SIZE - 1));
cube->table[idx] = pxl8_palette_find_closest(pal, r8, g8, b8);
cube->stable[idx] = find_closest_stable(pal, r8, g8, b8);
}
}
}
}
u8 pxl8_palette_cube_lookup(const pxl8_palette_cube* cube, u8 r, u8 g, u8 b) {
u32 ri = (r * (PXL8_CUBE_SIZE - 1)) / 255;
u32 gi = (g * (PXL8_CUBE_SIZE - 1)) / 255;
u32 bi = (b * (PXL8_CUBE_SIZE - 1)) / 255;
u32 idx = (bi * PXL8_CUBE_SIZE * PXL8_CUBE_SIZE) + (gi * PXL8_CUBE_SIZE) + ri;
return cube->table[idx];
}
u8 pxl8_palette_cube_lookup_stable(const pxl8_palette_cube* cube, u8 r, u8 g, u8 b) {
u32 ri = (r * (PXL8_CUBE_SIZE - 1)) / 255;
u32 gi = (g * (PXL8_CUBE_SIZE - 1)) / 255;
u32 bi = (b * (PXL8_CUBE_SIZE - 1)) / 255;
u32 idx = (bi * PXL8_CUBE_SIZE * PXL8_CUBE_SIZE) + (gi * PXL8_CUBE_SIZE) + ri;
return cube->stable[idx];
}
void pxl8_palette_cube_get_rgb(const pxl8_palette_cube* cube, u8 idx, u8* r, u8* g, u8* b) {
*r = cube->colors[idx * 3 + 0];
*g = cube->colors[idx * 3 + 1];
*b = cube->colors[idx * 3 + 2];
}
pxl8_additive_table* pxl8_additive_table_create(const pxl8_palette* pal) {
pxl8_additive_table* table = calloc(1, sizeof(pxl8_additive_table));
if (!table) return NULL;
pxl8_additive_table_rebuild(table, pal);
return table;
}
void pxl8_additive_table_destroy(pxl8_additive_table* table) {
free(table);
}
void pxl8_additive_table_rebuild(pxl8_additive_table* table, const pxl8_palette* pal) {
if (!table || !pal) return;
for (u32 src = 0; src < 256; src++) {
u8 sr, sg, sb;
pxl8_palette_get_rgb(pal, (u8)src, &sr, &sg, &sb);
for (u32 dst = 0; dst < 256; dst++) {
u32 idx = src * 256 + dst;
if (src == PXL8_TRANSPARENT) {
table->table[idx] = (u8)dst;
continue;
}
u8 dr, dg, db;
pxl8_palette_get_rgb(pal, (u8)dst, &dr, &dg, &db);
u16 ar = (u16)sr + (u16)dr;
u16 ag = (u16)sg + (u16)dg;
u16 ab = (u16)sb + (u16)db;
if (ar > 255) ar = 255;
if (ag > 255) ag = 255;
if (ab > 255) ab = 255;
table->table[idx] = find_closest_stable(pal, (u8)ar, (u8)ag, (u8)ab);
}
}
}
u8 pxl8_additive_blend(const pxl8_additive_table* table, u8 src, u8 dst) {
return table->table[(u32)src * 256 + dst];
}
pxl8_overbright_table* pxl8_overbright_table_create(const pxl8_palette* pal) {
pxl8_overbright_table* table = calloc(1, sizeof(pxl8_overbright_table));
if (!table) return NULL;
pxl8_overbright_table_rebuild(table, pal);
return table;
}
void pxl8_overbright_table_destroy(pxl8_overbright_table* table) {
free(table);
}
void pxl8_overbright_table_rebuild(pxl8_overbright_table* table, const pxl8_palette* pal) {
if (!table || !pal) return;
for (u32 level = 0; level < PXL8_OVERBRIGHT_LEVELS; level++) {
f32 overbright = (f32)level / (f32)(PXL8_OVERBRIGHT_LEVELS - 1);
for (u32 pal_idx = 0; pal_idx < 256; pal_idx++) {
u32 idx = level * 256 + pal_idx;
if (pal_idx == PXL8_TRANSPARENT) {
table->table[idx] = PXL8_TRANSPARENT;
continue;
}
u8 r, g, b;
pxl8_palette_get_rgb(pal, (u8)pal_idx, &r, &g, &b);
u8 or = (u8)(r + (255 - r) * overbright);
u8 og = (u8)(g + (255 - g) * overbright);
u8 ob = (u8)(b + (255 - b) * overbright);
table->table[idx] = find_closest_stable(pal, or, og, ob);
}
}
}
u8 pxl8_overbright_lookup(const pxl8_overbright_table* table, u8 pal_idx, f32 emissive) {
u32 level = (u32)(emissive * (PXL8_OVERBRIGHT_LEVELS - 1));
if (level >= PXL8_OVERBRIGHT_LEVELS) level = PXL8_OVERBRIGHT_LEVELS - 1;
return table->table[level * 256 + pal_idx];
}

39
src/gfx/pxl8_blend.h Normal file
View file

@ -0,0 +1,39 @@
#pragma once
#include "pxl8_palette.h"
#include "pxl8_types.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PXL8_CUBE_SIZE 32
#define PXL8_CUBE_ENTRIES (PXL8_CUBE_SIZE * PXL8_CUBE_SIZE * PXL8_CUBE_SIZE)
#define PXL8_BLEND_TABLE_SIZE (256 * 256)
#define PXL8_OVERBRIGHT_LEVELS 16
#define PXL8_OVERBRIGHT_TABLE_SIZE (256 * PXL8_OVERBRIGHT_LEVELS)
typedef struct pxl8_additive_table pxl8_additive_table;
typedef struct pxl8_overbright_table pxl8_overbright_table;
typedef struct pxl8_palette_cube pxl8_palette_cube;
pxl8_additive_table* pxl8_additive_table_create(const pxl8_palette* pal);
void pxl8_additive_table_destroy(pxl8_additive_table* table);
void pxl8_additive_table_rebuild(pxl8_additive_table* table, const pxl8_palette* pal);
u8 pxl8_additive_blend(const pxl8_additive_table* table, u8 src, u8 dst);
pxl8_overbright_table* pxl8_overbright_table_create(const pxl8_palette* pal);
void pxl8_overbright_table_destroy(pxl8_overbright_table* table);
void pxl8_overbright_table_rebuild(pxl8_overbright_table* table, const pxl8_palette* pal);
u8 pxl8_overbright_lookup(const pxl8_overbright_table* table, u8 pal_idx, f32 emissive);
pxl8_palette_cube* pxl8_palette_cube_create(const pxl8_palette* pal);
void pxl8_palette_cube_destroy(pxl8_palette_cube* cube);
void pxl8_palette_cube_rebuild(pxl8_palette_cube* cube, const pxl8_palette* pal);
u8 pxl8_palette_cube_lookup(const pxl8_palette_cube* cube, u8 r, u8 g, u8 b);
u8 pxl8_palette_cube_lookup_stable(const pxl8_palette_cube* cube, u8 r, u8 g, u8 b);
void pxl8_palette_cube_get_rgb(const pxl8_palette_cube* cube, u8 idx, u8* r, u8* g, u8* b);
#ifdef __cplusplus
}
#endif

57
src/gfx/pxl8_blit.c Normal file
View file

@ -0,0 +1,57 @@
#include "pxl8_blit.h"
void pxl8_blit_hicolor(u16* fb, u32 fb_width, const u16* sprite, u32 atlas_width,
i32 x, i32 y, u32 w, u32 h) {
u16* dest_base = fb + y * fb_width + x;
const u16* src_base = sprite;
for (u32 row = 0; row < h; row++) {
u16* dest_row = dest_base + row * fb_width;
const u16* src_row = src_base + row * atlas_width;
u32 col = 0;
u32 count2 = w / 2;
for (u32 i = 0; i < count2; i++) {
u32 pixels = ((const u32*)src_row)[i];
if (pixels == 0) {
col += 2;
continue;
}
dest_row[col] = pxl8_blend_hicolor((u16)(pixels), dest_row[col]);
dest_row[col + 1] = pxl8_blend_hicolor((u16)(pixels >> 16), dest_row[col + 1]);
col += 2;
}
if (w & 1) {
dest_row[col] = pxl8_blend_hicolor(src_row[col], dest_row[col]);
}
}
}
void pxl8_blit_indexed(u8* fb, u32 fb_width, const u8* sprite, u32 atlas_width,
i32 x, i32 y, u32 w, u32 h) {
u8* dest_base = fb + y * fb_width + x;
const u8* src_base = sprite;
for (u32 row = 0; row < h; row++) {
u8* dest_row = dest_base + row * fb_width;
const u8* src_row = src_base + row * atlas_width;
u32 col = 0;
u32 count4 = w / 4;
for (u32 i = 0; i < count4; i++) {
u32 pixels = ((const u32*)src_row)[i];
if (pixels == 0) {
col += 4;
continue;
}
dest_row[col] = pxl8_blend_indexed((u8)(pixels), dest_row[col]);
dest_row[col + 1] = pxl8_blend_indexed((u8)(pixels >> 8), dest_row[col + 1]);
dest_row[col + 2] = pxl8_blend_indexed((u8)(pixels >> 16), dest_row[col + 2]);
dest_row[col + 3] = pxl8_blend_indexed((u8)(pixels >> 24), dest_row[col + 3]);
col += 4;
}
for (; col < w; col++) {
dest_row[col] = pxl8_blend_indexed(src_row[col], dest_row[col]);
}
}
}

32
src/gfx/pxl8_blit.h Normal file
View file

@ -0,0 +1,32 @@
#pragma once
#include "pxl8_types.h"
#ifdef __cplusplus
extern "C" {
#endif
static inline u8 pxl8_blend_indexed(u8 src, u8 dst) {
u8 m = (u8)(-(src != 0));
return (src & m) | (dst & ~m);
}
static inline u16 pxl8_blend_hicolor(u16 src, u16 dst) {
u16 m = (u16)(-(src != 0));
return (src & m) | (dst & ~m);
}
void pxl8_blit_hicolor(
u16* fb, u32 fb_width,
const u16* sprite, u32 atlas_width,
i32 x, i32 y, u32 w, u32 h
);
void pxl8_blit_indexed(
u8* fb, u32 fb_width,
const u8* sprite, u32 atlas_width,
i32 x, i32 y, u32 w, u32 h
);
#ifdef __cplusplus
}
#endif

71
src/gfx/pxl8_color.h Normal file
View file

@ -0,0 +1,71 @@
#pragma once
#include "pxl8_types.h"
static inline i32 pxl8_bytes_per_pixel(pxl8_pixel_mode mode) {
return (i32)mode;
}
static inline u16 pxl8_rgb565_pack(u8 r, u8 g, u8 b) {
return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
}
static inline void pxl8_rgb565_unpack(u16 color, u8* r, u8* g, u8* b) {
*r = (color >> 11) << 3;
*g = ((color >> 5) & 0x3F) << 2;
*b = (color & 0x1F) << 3;
}
static inline u16 pxl8_rgba32_to_rgb565(u32 rgba) {
return pxl8_rgb565_pack(rgba & 0xFF, (rgba >> 8) & 0xFF, (rgba >> 16) & 0xFF);
}
static inline u32 pxl8_rgb565_to_rgba32(u16 color) {
u8 r, g, b;
pxl8_rgb565_unpack(color, &r, &g, &b);
return r | ((u32)g << 8) | ((u32)b << 16) | 0xFF000000;
}
static inline void pxl8_rgba32_unpack(u32 color, u8* r, u8* g, u8* b, u8* a) {
*r = color & 0xFF;
*g = (color >> 8) & 0xFF;
*b = (color >> 16) & 0xFF;
*a = (color >> 24) & 0xFF;
}
static inline u32 pxl8_rgba32_pack(u8 r, u8 g, u8 b, u8 a) {
return r | ((u32)g << 8) | ((u32)b << 16) | ((u32)a << 24);
}
static inline u32 pxl8_color_to_rgba(u32 abgr) {
u8 r = abgr & 0xFF;
u8 g = (abgr >> 8) & 0xFF;
u8 b = (abgr >> 16) & 0xFF;
u8 a = (abgr >> 24) & 0xFF;
return ((u32)r << 24) | ((u32)g << 16) | ((u32)b << 8) | a;
}
static inline u32 pxl8_color_from_rgba(u32 rgba) {
u8 r = (rgba >> 24) & 0xFF;
u8 g = (rgba >> 16) & 0xFF;
u8 b = (rgba >> 8) & 0xFF;
u8 a = rgba & 0xFF;
return r | ((u32)g << 8) | ((u32)b << 16) | ((u32)a << 24);
}
static inline u8 pxl8_color_lerp_channel(u8 c1, u8 c2, f32 t) {
return c1 + (i32)((c2 - c1) * t);
}
static inline u8 pxl8_rgb332_pack(u8 r, u8 g, u8 b) {
return ((r >> 5) << 5) | ((g >> 5) << 2) | (b >> 6);
}
static inline void pxl8_rgb332_unpack(u8 c, u8* r, u8* g, u8* b) {
u8 ri = (c >> 5) & 0x07;
u8 gi = (c >> 2) & 0x07;
u8 bi = c & 0x03;
*r = (ri << 5) | (ri << 2) | (ri >> 1);
*g = (gi << 5) | (gi << 2) | (gi >> 1);
*b = (bi << 6) | (bi << 4) | (bi << 2) | bi;
}

116
src/gfx/pxl8_colormap.c Normal file
View file

@ -0,0 +1,116 @@
#include "pxl8_colormap.h"
#include <string.h>
static void rgb_to_hsl(u8 r, u8 g, u8 b, i32* h, i32* s, i32* l) {
i32 max = r > g ? (r > b ? r : b) : (g > b ? g : b);
i32 min = r < g ? (r < b ? r : b) : (g < b ? g : b);
i32 chroma = max - min;
*l = (max + min) / 2;
if (chroma == 0) {
*h = 0;
*s = 0;
} else {
i32 denom = 255 - (*l > 127 ? 2 * (*l) - 255 : 255 - 2 * (*l));
if (denom <= 0) denom = 1;
*s = (chroma * 255) / denom;
if (*s > 255) *s = 255;
if (max == (i32)r) {
*h = (((i32)g - (i32)b) * 60) / chroma;
if (*h < 0) *h += 360;
} else if (max == (i32)g) {
*h = 120 + (((i32)b - (i32)r) * 60) / chroma;
} else {
*h = 240 + (((i32)r - (i32)g) * 60) / chroma;
}
}
}
static u8 find_closest_color(const u32* palette, u8 target_r, u8 target_g, u8 target_b) {
u8 best_idx = 1;
u32 best_dist = 0xFFFFFFFF;
u8 dynamic_end = PXL8_DYNAMIC_RANGE_START + PXL8_DYNAMIC_RANGE_COUNT;
i32 th, ts, tl;
rgb_to_hsl(target_r, target_g, target_b, &th, &ts, &tl);
for (u32 i = 1; i < PXL8_FULLBRIGHT_START; i++) {
if (i >= PXL8_DYNAMIC_RANGE_START && i < dynamic_end) {
continue;
}
u32 c = palette[i];
u8 pr = c & 0xFF;
u8 pg = (c >> 8) & 0xFF;
u8 pb = (c >> 16) & 0xFF;
i32 ph, ps, pl;
rgb_to_hsl(pr, pg, pb, &ph, &ps, &pl);
i32 dh = th - ph;
if (dh > 180) dh -= 360;
if (dh < -180) dh += 360;
i32 ds = ts - ps;
i32 dl = tl - pl;
u32 dist = (u32)(dh * dh * 4 + ds * ds + dl * dl);
if (dist < best_dist) {
best_dist = dist;
best_idx = (u8)i;
if (dist == 0) break;
}
}
return best_idx;
}
void pxl8_colormap_generate(pxl8_colormap* cm, const u32* palette, const pxl8_level_tint* tint) {
if (!cm || !palette) return;
u8 dark_r, dark_g, dark_b;
if (tint && tint->tint_strength > 0.0f) {
f32 t = tint->tint_strength;
f32 inv = 1.0f - t;
dark_r = (u8)(tint->dark_r * inv + tint->tint_r * t);
dark_g = (u8)(tint->dark_g * inv + tint->tint_g * t);
dark_b = (u8)(tint->dark_b * inv + tint->tint_b * t);
} else if (tint) {
dark_r = tint->dark_r;
dark_g = tint->dark_g;
dark_b = tint->dark_b;
} else {
dark_r = dark_g = dark_b = 0;
}
for (u32 light = 0; light < PXL8_LIGHT_LEVELS; light++) {
f32 brightness = (f32)light / (f32)(PXL8_LIGHT_LEVELS - 1);
for (u32 pal_idx = 0; pal_idx < 256; pal_idx++) {
u8 result_idx;
if (pal_idx == PXL8_TRANSPARENT) {
result_idx = PXL8_TRANSPARENT;
} else if (pal_idx >= PXL8_FULLBRIGHT_START) {
result_idx = (u8)pal_idx;
} else {
u32 c = palette[pal_idx];
u8 r = c & 0xFF;
u8 g = (c >> 8) & 0xFF;
u8 b = (c >> 16) & 0xFF;
u8 target_r = (u8)(dark_r + (r - dark_r) * brightness);
u8 target_g = (u8)(dark_g + (g - dark_g) * brightness);
u8 target_b = (u8)(dark_b + (b - dark_b) * brightness);
result_idx = find_closest_color(palette, target_r, target_g, target_b);
}
cm->table[light * 256 + pal_idx] = result_idx;
}
}
}

43
src/gfx/pxl8_colormap.h Normal file
View file

@ -0,0 +1,43 @@
#pragma once
#include "pxl8_dither.h"
#include "pxl8_types.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PXL8_LIGHT_LEVELS 64
#define PXL8_COLORMAP_SIZE (256 * PXL8_LIGHT_LEVELS)
#define PXL8_FULLBRIGHT_START 240
#define PXL8_TRANSPARENT 0
#define PXL8_DYNAMIC_RANGE_START 144
#define PXL8_DYNAMIC_RANGE_COUNT 16
typedef struct {
u8 table[PXL8_COLORMAP_SIZE];
} pxl8_colormap;
typedef struct {
u8 dark_r, dark_g, dark_b;
u8 tint_r, tint_g, tint_b;
f32 tint_strength;
} pxl8_level_tint;
void pxl8_colormap_generate(pxl8_colormap* cm, const u32* palette, const pxl8_level_tint* tint);
static inline u8 pxl8_colormap_lookup(const pxl8_colormap* cm, u8 pal_idx, u8 light) {
u32 light_idx = light >> 2;
return cm->table[light_idx * 256 + pal_idx];
}
static inline u8 pxl8_colormap_lookup_dithered(const pxl8_colormap* cm, u8 pal_idx, u8 light, u32 x, u32 y) {
u8 dithered = pxl8_dither_light(light, x, y);
u32 light_idx = dithered >> 2;
return cm->table[light_idx * 256 + pal_idx];
}
#ifdef __cplusplus
}
#endif

1490
src/gfx/pxl8_cpu.c Normal file

File diff suppressed because it is too large Load diff

97
src/gfx/pxl8_cpu.h Normal file
View file

@ -0,0 +1,97 @@
#pragma once
#include "pxl8_atlas.h"
#include "pxl8_blend.h"
#include "pxl8_colormap.h"
#include "pxl8_gfx.h"
#include "pxl8_gfx3d.h"
#include "pxl8_math.h"
#include "pxl8_mesh.h"
#include "pxl8_types.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct pxl8_cpu_backend pxl8_cpu_backend;
typedef struct pxl8_cpu_render_target pxl8_cpu_render_target;
typedef struct pxl8_cpu_render_target_desc {
u32 width;
u32 height;
bool with_depth;
bool with_lighting;
} pxl8_cpu_render_target_desc;
pxl8_cpu_backend* pxl8_cpu_create(u32 width, u32 height);
void pxl8_cpu_destroy(pxl8_cpu_backend* cpu);
void pxl8_cpu_begin_frame(pxl8_cpu_backend* cpu, const pxl8_3d_frame* frame);
void pxl8_cpu_end_frame(pxl8_cpu_backend* cpu);
void pxl8_cpu_clear(pxl8_cpu_backend* cpu, u8 color);
void pxl8_cpu_clear_depth(pxl8_cpu_backend* cpu);
void pxl8_cpu_set_colormap(pxl8_cpu_backend* cpu, const pxl8_colormap* cm);
void pxl8_cpu_set_palette(pxl8_cpu_backend* cpu, const u32* palette);
void pxl8_cpu_draw_pixel(pxl8_cpu_backend* cpu, i32 x, i32 y, u8 color);
u8 pxl8_cpu_get_pixel(pxl8_cpu_backend* cpu, i32 x, i32 y);
void pxl8_cpu_draw_line_2d(pxl8_cpu_backend* cpu, i32 x0, i32 y0, i32 x1, i32 y1, u8 color);
void pxl8_cpu_draw_rect(pxl8_cpu_backend* cpu, i32 x, i32 y, i32 w, i32 h, u8 color);
void pxl8_cpu_draw_rect_fill(pxl8_cpu_backend* cpu, i32 x, i32 y, i32 w, i32 h, u8 color);
void pxl8_cpu_draw_circle(pxl8_cpu_backend* cpu, i32 cx, i32 cy, i32 radius, u8 color);
void pxl8_cpu_draw_circle_fill(pxl8_cpu_backend* cpu, i32 cx, i32 cy, i32 radius, u8 color);
void pxl8_cpu_draw_line_3d(pxl8_cpu_backend* cpu, pxl8_vec3 v0, pxl8_vec3 v1, u8 color);
void pxl8_cpu_draw_mesh(
pxl8_cpu_backend* cpu,
const pxl8_mesh* mesh,
const pxl8_mat4* model,
const pxl8_material* material,
const pxl8_atlas* textures
);
void pxl8_cpu_draw_mesh_wireframe(
pxl8_cpu_backend* cpu,
const pxl8_mesh* mesh,
pxl8_mat4 model,
u8 color
);
u8* pxl8_cpu_get_framebuffer(pxl8_cpu_backend* cpu);
u32* pxl8_cpu_get_output(pxl8_cpu_backend* cpu);
u32 pxl8_cpu_get_height(const pxl8_cpu_backend* cpu);
u32 pxl8_cpu_get_width(const pxl8_cpu_backend* cpu);
void pxl8_cpu_render_glows(
pxl8_cpu_backend* cpu,
const pxl8_glow_source* glows,
u32 glow_count,
const pxl8_additive_table* additive,
const pxl8_palette_cube* palette_cube,
const pxl8_overbright_table* overbright
);
void pxl8_cpu_resolve(pxl8_cpu_backend* cpu);
pxl8_cpu_render_target* pxl8_cpu_create_render_target(const pxl8_cpu_render_target_desc* desc);
void pxl8_cpu_destroy_render_target(pxl8_cpu_render_target* target);
pxl8_cpu_render_target* pxl8_cpu_get_target(pxl8_cpu_backend* cpu);
void pxl8_cpu_set_target(pxl8_cpu_backend* cpu, pxl8_cpu_render_target* target);
void pxl8_cpu_blit(pxl8_cpu_backend* cpu, pxl8_cpu_render_target* src, i32 x, i32 y, u8 transparent_idx);
bool pxl8_cpu_push_target(pxl8_cpu_backend* cpu);
void pxl8_cpu_pop_target(pxl8_cpu_backend* cpu);
u32 pxl8_cpu_get_target_depth(const pxl8_cpu_backend* cpu);
u8* pxl8_cpu_render_target_get_framebuffer(pxl8_cpu_render_target* target);
u32 pxl8_cpu_render_target_get_height(const pxl8_cpu_render_target* target);
u32 pxl8_cpu_render_target_get_width(const pxl8_cpu_render_target* target);
u32* pxl8_cpu_render_target_get_light_accum(pxl8_cpu_render_target* target);
#ifdef __cplusplus
}
#endif

8
src/gfx/pxl8_dither.c Normal file
View file

@ -0,0 +1,8 @@
#include "pxl8_dither.h"
const u8 PXL8_BAYER_4X4[16] = {
0, 8, 2, 10,
12, 4, 14, 6,
3, 11, 1, 9,
15, 7, 13, 5,
};

41
src/gfx/pxl8_dither.h Normal file
View file

@ -0,0 +1,41 @@
#pragma once
#include "pxl8_types.h"
#ifdef __cplusplus
extern "C" {
#endif
extern const u8 PXL8_BAYER_4X4[16];
static inline i8 pxl8_bayer_offset(u32 x, u32 y) {
return (i8)(PXL8_BAYER_4X4[(y & 3) * 4 + (x & 3)]) - 8;
}
static inline u8 pxl8_ordered_dither(u8 value, u32 x, u32 y) {
u8 bayer = PXL8_BAYER_4X4[(y & 3) * 4 + (x & 3)];
u16 result = (u16)value + (bayer >> 1);
return result > 255 ? 255 : (u8)result;
}
static inline u8 pxl8_dither_light(u8 light, u32 x, u32 y) {
i8 offset = pxl8_bayer_offset(x, y) >> 1;
i16 result = (i16)light + offset;
if (result < 0) return 0;
if (result > 255) return 255;
return (u8)result;
}
static inline u8 pxl8_dither_float(f32 value, u32 x, u32 y) {
u8 floor_val = (u8)value;
f32 frac = value - (f32)floor_val;
f32 threshold = (PXL8_BAYER_4X4[(y & 3) * 4 + (x & 3)] + 0.5f) * (1.0f / 16.0f);
if (frac > threshold) {
return floor_val < 255 ? floor_val + 1 : 255;
}
return floor_val;
}
#ifdef __cplusplus
}
#endif

61
src/gfx/pxl8_font.c Normal file
View file

@ -0,0 +1,61 @@
#include "pxl8_font.h"
#include <stdlib.h>
#include <string.h>
pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32* atlas_width, i32* atlas_height) {
if (!font || !atlas_data || !atlas_width || !atlas_height) {
return PXL8_ERROR_NULL_POINTER;
}
i32 glyphs_per_row = 16;
i32 rows_needed = (font->glyph_count + glyphs_per_row - 1) / glyphs_per_row;
*atlas_width = glyphs_per_row * font->default_width;
*atlas_height = rows_needed * font->default_height;
i32 atlas_size = (*atlas_width) * (*atlas_height);
*atlas_data = (u8*)malloc(atlas_size);
if (!*atlas_data) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
memset(*atlas_data, 0, atlas_size);
for (u32 i = 0; i < font->glyph_count; i++) {
const pxl8_glyph* glyph = &font->glyphs[i];
i32 atlas_x = (i % glyphs_per_row) * font->default_width;
i32 atlas_y = (i / glyphs_per_row) * font->default_height;
for (i32 y = 0; y < glyph->height && y < font->default_height; y++) {
for (i32 x = 0; x < glyph->width && x < font->default_width; x++) {
i32 atlas_idx = (atlas_y + y) * (*atlas_width) + (atlas_x + x);
if (glyph->format == PXL8_FONT_FORMAT_INDEXED) {
u8 pixel_byte = glyph->data.indexed[y];
u8 pixel_bit = (pixel_byte >> x) & 1;
(*atlas_data)[atlas_idx] = pixel_bit ? 255 : 0;
} else {
i32 glyph_idx = y * 8 + x;
u32 rgba_pixel = glyph->data.rgba[glyph_idx];
(*atlas_data)[atlas_idx] = (rgba_pixel >> 24) & 0xFF;
}
}
}
}
return PXL8_OK;
}
const pxl8_glyph* pxl8_font_find_glyph(const pxl8_font* font, u32 codepoint) {
if (!font || !font->glyphs) return NULL;
for (u32 i = 0; i < font->glyph_count; i++) {
if (font->glyphs[i].codepoint == codepoint) {
return &font->glyphs[i];
}
}
return NULL;
}

130
src/gfx/pxl8_font.h Normal file
View file

@ -0,0 +1,130 @@
#pragma once
#include "pxl8_types.h"
#define PXL8_FONT_FORMAT_INDEXED 0
#define PXL8_FONT_FORMAT_RGBA 1
typedef struct pxl8_glyph {
u32 codepoint;
u8 width;
u8 height;
u8 format;
union {
u8 indexed[64];
u32 rgba[64];
} data;
} pxl8_glyph;
typedef struct pxl8_font {
const pxl8_glyph* glyphs;
u32 glyph_count;
u8 default_width;
u8 default_height;
} pxl8_font;
static const pxl8_glyph pxl8_ascii_glyphs[] = {
{ 32, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } },
{ 33, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x18, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x00 } } },
{ 34, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } } },
{ 35, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x36, 0x36, 0x7F, 0x36, 0x7F, 0x36, 0x36, 0x00 } } },
{ 36, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0C, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x0C, 0x00 } } },
{ 37, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x63, 0x33, 0x18, 0x0C, 0x66, 0x63, 0x00 } } },
{ 38, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1C, 0x36, 0x1C, 0x6E, 0x3B, 0x33, 0x6E, 0x00 } } },
{ 39, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x06, 0x06, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00 } } },
{ 40, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x18, 0x0C, 0x06, 0x06, 0x06, 0x0C, 0x18, 0x00 } } },
{ 41, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x06, 0x0C, 0x18, 0x18, 0x18, 0x0C, 0x06, 0x00 } } },
{ 42, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00 } } },
{ 43, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x0C, 0x0C, 0x3F, 0x0C, 0x0C, 0x00, 0x00 } } },
{ 44, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x06, 0x00 } } },
{ 45, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x00, 0x00 } } },
{ 46, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x0C, 0x00 } } },
{ 47, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00 } } },
{ 48, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3E, 0x63, 0x73, 0x7B, 0x6F, 0x67, 0x3E, 0x00 } } },
{ 49, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0C, 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x3F, 0x00 } } },
{ 50, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x30, 0x1C, 0x06, 0x33, 0x3F, 0x00 } } },
{ 51, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x30, 0x1C, 0x30, 0x33, 0x1E, 0x00 } } },
{ 52, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x38, 0x3C, 0x36, 0x33, 0x7F, 0x30, 0x78, 0x00 } } },
{ 53, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x03, 0x1F, 0x30, 0x30, 0x33, 0x1E, 0x00 } } },
{ 54, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1C, 0x06, 0x03, 0x1F, 0x33, 0x33, 0x1E, 0x00 } } },
{ 55, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x33, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x00 } } },
{ 56, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x33, 0x1E, 0x33, 0x33, 0x1E, 0x00 } } },
{ 57, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x33, 0x3E, 0x30, 0x18, 0x0E, 0x00 } } },
{ 58, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x0C, 0x00 } } },
{ 59, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x0C, 0x0C, 0x00, 0x00, 0x0C, 0x06, 0x00 } } },
{ 60, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x18, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x18, 0x00 } } },
{ 61, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0x00, 0x00 } } },
{ 62, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x06, 0x0C, 0x18, 0x30, 0x18, 0x0C, 0x06, 0x00 } } },
{ 63, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x30, 0x18, 0x0C, 0x00, 0x0C, 0x00 } } },
{ 64, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3E, 0x63, 0x7B, 0x7B, 0x7B, 0x03, 0x1E, 0x00 } } },
{ 65, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0C, 0x1E, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x00 } } },
{ 66, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x66, 0x66, 0x3E, 0x66, 0x66, 0x3F, 0x00 } } },
{ 67, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3C, 0x66, 0x03, 0x03, 0x03, 0x66, 0x3C, 0x00 } } },
{ 68, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1F, 0x36, 0x66, 0x66, 0x66, 0x36, 0x1F, 0x00 } } },
{ 69, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x46, 0x7F, 0x00 } } },
{ 70, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x7F, 0x46, 0x16, 0x1E, 0x16, 0x06, 0x0F, 0x00 } } },
{ 71, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3C, 0x66, 0x03, 0x03, 0x73, 0x66, 0x7C, 0x00 } } },
{ 72, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x33, 0x33, 0x33, 0x3F, 0x33, 0x33, 0x33, 0x00 } } },
{ 73, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 } } },
{ 74, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x78, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E, 0x00 } } },
{ 75, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x67, 0x66, 0x36, 0x1E, 0x36, 0x66, 0x67, 0x00 } } },
{ 76, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0F, 0x06, 0x06, 0x06, 0x46, 0x66, 0x7F, 0x00 } } },
{ 77, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x63, 0x77, 0x7F, 0x7F, 0x6B, 0x63, 0x63, 0x00 } } },
{ 78, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x63, 0x67, 0x6F, 0x7B, 0x73, 0x63, 0x63, 0x00 } } },
{ 79, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1C, 0x36, 0x63, 0x63, 0x63, 0x36, 0x1C, 0x00 } } },
{ 80, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x66, 0x66, 0x3E, 0x06, 0x06, 0x0F, 0x00 } } },
{ 81, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x33, 0x33, 0x3B, 0x1E, 0x38, 0x00 } } },
{ 82, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x66, 0x66, 0x3E, 0x36, 0x66, 0x67, 0x00 } } },
{ 83, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1E, 0x33, 0x07, 0x0E, 0x38, 0x33, 0x1E, 0x00 } } },
{ 84, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x3F, 0x2D, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 } } },
{ 85, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x3F, 0x00 } } },
{ 86, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x33, 0x33, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 } } },
{ 87, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x63, 0x63, 0x63, 0x6B, 0x7F, 0x77, 0x63, 0x00 } } },
{ 88, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x63, 0x63, 0x36, 0x1C, 0x1C, 0x36, 0x63, 0x00 } } },
{ 89, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x0C, 0x1E, 0x00 } } },
{ 90, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x7F, 0x63, 0x31, 0x18, 0x4C, 0x66, 0x7F, 0x00 } } },
{ 97, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1E, 0x30, 0x3E, 0x33, 0x6E, 0x00 } } },
{ 98, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x07, 0x06, 0x06, 0x3E, 0x66, 0x66, 0x3B, 0x00 } } },
{ 99, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1E, 0x33, 0x03, 0x33, 0x1E, 0x00 } } },
{ 100, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x38, 0x30, 0x30, 0x3E, 0x33, 0x33, 0x6E, 0x00 } } },
{ 101, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1E, 0x33, 0x3F, 0x03, 0x1E, 0x00 } } },
{ 102, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x1C, 0x36, 0x06, 0x0F, 0x06, 0x06, 0x0F, 0x00 } } },
{ 103, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x1F } } },
{ 104, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x07, 0x06, 0x36, 0x6E, 0x66, 0x66, 0x67, 0x00 } } },
{ 105, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0C, 0x00, 0x0E, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 } } },
{ 106, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x30, 0x00, 0x30, 0x30, 0x30, 0x33, 0x33, 0x1E } } },
{ 107, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x07, 0x06, 0x66, 0x36, 0x1E, 0x36, 0x67, 0x00 } } },
{ 108, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x0E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x1E, 0x00 } } },
{ 109, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x33, 0x7F, 0x7F, 0x6B, 0x63, 0x00 } } },
{ 110, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1F, 0x33, 0x33, 0x33, 0x33, 0x00 } } },
{ 111, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x1E, 0x33, 0x33, 0x33, 0x1E, 0x00 } } },
{ 112, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3B, 0x66, 0x66, 0x3E, 0x06, 0x0F } } },
{ 113, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x6E, 0x33, 0x33, 0x3E, 0x30, 0x78 } } },
{ 114, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3B, 0x6E, 0x66, 0x06, 0x0F, 0x00 } } },
{ 115, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3E, 0x03, 0x1E, 0x30, 0x1F, 0x00 } } },
{ 116, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x08, 0x0C, 0x3E, 0x0C, 0x0C, 0x2C, 0x18, 0x00 } } },
{ 117, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x33, 0x33, 0x33, 0x33, 0x6E, 0x00 } } },
{ 118, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x33, 0x33, 0x33, 0x1E, 0x0C, 0x00 } } },
{ 119, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x63, 0x6B, 0x7F, 0x7F, 0x36, 0x00 } } },
{ 120, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x63, 0x36, 0x1C, 0x36, 0x63, 0x00 } } },
{ 121, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x33, 0x33, 0x33, 0x3E, 0x30, 0x1F } } },
{ 122, 8, 8, PXL8_FONT_FORMAT_INDEXED, { .indexed = { 0x00, 0x00, 0x3F, 0x19, 0x0C, 0x26, 0x3F, 0x00 } } }
};
static const pxl8_font pxl8_default_font = {
pxl8_ascii_glyphs,
sizeof(pxl8_ascii_glyphs) / sizeof(pxl8_glyph),
8,
8
};
#ifdef __cplusplus
extern "C" {
#endif
pxl8_result pxl8_font_create_atlas(const pxl8_font* font, u8** atlas_data, i32* atlas_width, i32* atlas_height);
const pxl8_glyph* pxl8_font_find_glyph(const pxl8_font* font, u32 codepoint);
#ifdef __cplusplus
}
#endif

798
src/gfx/pxl8_gfx.c Normal file
View file

@ -0,0 +1,798 @@
#include "pxl8_gfx.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_ase.h"
#include "pxl8_atlas.h"
#include "pxl8_backend.h"
#include "pxl8_blend.h"
#include "pxl8_blit.h"
#include "pxl8_color.h"
#include "pxl8_colormap.h"
#include "pxl8_font.h"
#include "pxl8_hal.h"
#include "pxl8_log.h"
#include "pxl8_macros.h"
#include "pxl8_math.h"
#include "pxl8_sys.h"
#include "pxl8_types.h"
typedef struct pxl8_sprite_cache_entry {
char path[256];
u32 sprite_id;
bool active;
} pxl8_sprite_cache_entry;
struct pxl8_gfx {
pxl8_additive_table* additive_table;
pxl8_atlas* atlas;
pxl8_gfx_backend backend;
pxl8_colormap* colormap;
u8* framebuffer;
i32 framebuffer_height;
i32 framebuffer_width;
pxl8_frustum frustum;
const pxl8_hal* hal;
bool initialized;
pxl8_overbright_table* overbright_table;
pxl8_palette* palette;
pxl8_palette_cube* palette_cube;
pxl8_pixel_mode pixel_mode;
void* platform_data;
pxl8_sprite_cache_entry* sprite_cache;
u32 sprite_cache_capacity;
u32 sprite_cache_count;
pxl8_viewport viewport;
};
pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx) {
pxl8_bounds bounds = {0};
if (!gfx) {
return bounds;
}
bounds.w = gfx->framebuffer_width;
bounds.h = gfx->framebuffer_height;
return bounds;
}
pxl8_pixel_mode pxl8_gfx_get_pixel_mode(pxl8_gfx* gfx) {
return gfx ? gfx->pixel_mode : PXL8_PIXEL_INDEXED;
}
u8* pxl8_gfx_get_framebuffer_indexed(pxl8_gfx* gfx) {
if (!gfx || gfx->pixel_mode == PXL8_PIXEL_HICOLOR) return NULL;
return gfx->framebuffer;
}
u16* pxl8_gfx_get_framebuffer_hicolor(pxl8_gfx* gfx) {
if (!gfx || gfx->pixel_mode != PXL8_PIXEL_HICOLOR) return NULL;
return (u16*)gfx->framebuffer;
}
i32 pxl8_gfx_get_height(const pxl8_gfx* gfx) {
return gfx ? gfx->framebuffer_height : 0;
}
i32 pxl8_gfx_get_width(const pxl8_gfx* gfx) {
return gfx ? gfx->framebuffer_width : 0;
}
pxl8_palette* pxl8_gfx_get_palette(pxl8_gfx* gfx) {
return gfx ? gfx->palette : NULL;
}
u8 pxl8_gfx_find_color(pxl8_gfx* gfx, u32 color) {
if (!gfx || !gfx->palette) return 0;
if (color <= 0xFFFFFF) color = (color << 8) | 0xFF;
u8 r = (color >> 24) & 0xFF;
u8 g = (color >> 16) & 0xFF;
u8 b = (color >> 8) & 0xFF;
return pxl8_palette_find_closest(gfx->palette, r, g, b);
}
void pxl8_gfx_set_palette(pxl8_gfx* gfx, pxl8_palette* pal) {
if (!gfx) return;
if (gfx->palette) {
pxl8_palette_destroy(gfx->palette);
}
gfx->palette = pal;
if (gfx->palette && gfx->pixel_mode != PXL8_PIXEL_HICOLOR) {
u32* colors = pxl8_palette_colors(gfx->palette);
if (gfx->colormap) {
pxl8_colormap_generate(gfx->colormap, colors, NULL);
}
if (gfx->backend.type == PXL8_GFX_BACKEND_CPU) {
pxl8_cpu_set_palette(gfx->backend.cpu, colors);
}
}
}
i32 pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* filepath) {
if (!gfx || !filepath) return -1;
pxl8_palette* pal = gfx->palette;
if (!pal) return -1;
pxl8_result result = pxl8_palette_load_ase(pal, filepath);
if (result != PXL8_OK) return (i32)result;
if (gfx->pixel_mode != PXL8_PIXEL_HICOLOR) {
u32* colors = pxl8_palette_colors(pal);
if (gfx->colormap) {
pxl8_colormap_generate(gfx->colormap, colors, NULL);
}
if (gfx->backend.type == PXL8_GFX_BACKEND_CPU) {
pxl8_cpu_set_palette(gfx->backend.cpu, colors);
}
}
return 0;
}
pxl8_gfx* pxl8_gfx_create(
const pxl8_hal* hal,
void* platform_data,
pxl8_pixel_mode mode,
pxl8_resolution resolution
) {
pxl8_gfx* gfx = (pxl8_gfx*)calloc(1, sizeof(pxl8_gfx));
if (!gfx) {
pxl8_error("Failed to allocate graphics context");
return NULL;
}
gfx->hal = hal;
gfx->platform_data = platform_data;
gfx->pixel_mode = mode;
pxl8_size size = pxl8_get_resolution_dimensions(resolution);
gfx->framebuffer_width = size.w;
gfx->framebuffer_height = size.h;
if (!gfx->platform_data) {
pxl8_error("Platform data cannot be NULL");
free(gfx);
return NULL;
}
if (mode != PXL8_PIXEL_HICOLOR) {
gfx->palette = pxl8_palette_create();
}
gfx->backend.type = PXL8_GFX_BACKEND_CPU;
gfx->backend.cpu = pxl8_cpu_create(gfx->framebuffer_width, gfx->framebuffer_height);
if (!gfx->backend.cpu) {
pxl8_error("Failed to create CPU backend");
pxl8_gfx_destroy(gfx);
return NULL;
}
gfx->framebuffer = pxl8_cpu_get_framebuffer(gfx->backend.cpu);
if (mode != PXL8_PIXEL_HICOLOR) {
gfx->colormap = calloc(1, sizeof(pxl8_colormap));
if (gfx->colormap) {
pxl8_cpu_set_colormap(gfx->backend.cpu, gfx->colormap);
}
}
gfx->viewport.offset_x = 0;
gfx->viewport.offset_y = 0;
gfx->viewport.scaled_width = gfx->framebuffer_width;
gfx->viewport.scaled_height = gfx->framebuffer_height;
gfx->viewport.scale = 1.0f;
gfx->initialized = true;
return gfx;
}
void pxl8_gfx_destroy(pxl8_gfx* gfx) {
if (!gfx) return;
pxl8_additive_table_destroy(gfx->additive_table);
pxl8_atlas_destroy(gfx->atlas);
if (gfx->backend.type == PXL8_GFX_BACKEND_CPU) {
pxl8_cpu_destroy(gfx->backend.cpu);
}
free(gfx->colormap);
pxl8_overbright_table_destroy(gfx->overbright_table);
pxl8_palette_cube_destroy(gfx->palette_cube);
pxl8_palette_destroy(gfx->palette);
free(gfx->sprite_cache);
free(gfx);
}
static pxl8_result pxl8_gfx_ensure_atlas(pxl8_gfx* gfx) {
if (gfx->atlas) return PXL8_OK;
gfx->atlas = pxl8_atlas_create(PXL8_DEFAULT_ATLAS_SIZE, PXL8_DEFAULT_ATLAS_SIZE, gfx->pixel_mode);
return gfx->atlas ? PXL8_OK : PXL8_ERROR_OUT_OF_MEMORY;
}
void pxl8_gfx_clear_textures(pxl8_gfx* gfx) {
if (!gfx) return;
if (gfx->atlas) {
pxl8_atlas_clear(gfx->atlas, 0);
}
if (gfx->sprite_cache) {
gfx->sprite_cache_count = 0;
}
}
pxl8_result pxl8_gfx_create_texture(pxl8_gfx* gfx, const u8* pixels, u32 width, u32 height) {
if (!gfx || !gfx->initialized || !pixels) return PXL8_ERROR_INVALID_ARGUMENT;
pxl8_result result = pxl8_gfx_ensure_atlas(gfx);
if (result != PXL8_OK) return result;
u32 texture_id = pxl8_atlas_add_texture(gfx->atlas, pixels, width, height, gfx->pixel_mode);
if (texture_id == UINT32_MAX) {
pxl8_error("Texture doesn't fit in atlas");
return PXL8_ERROR_INVALID_SIZE;
}
return texture_id;
}
pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path) {
if (!gfx || !gfx->initialized || !path) return PXL8_ERROR_INVALID_ARGUMENT;
if (!gfx->sprite_cache) {
gfx->sprite_cache_capacity = PXL8_DEFAULT_SPRITE_CACHE_CAPACITY;
gfx->sprite_cache = (pxl8_sprite_cache_entry*)calloc(
gfx->sprite_cache_capacity, sizeof(pxl8_sprite_cache_entry)
);
if (!gfx->sprite_cache) return PXL8_ERROR_OUT_OF_MEMORY;
}
for (u32 i = 0; i < gfx->sprite_cache_count; i++) {
if (gfx->sprite_cache[i].active && strcmp(gfx->sprite_cache[i].path, path) == 0) {
return gfx->sprite_cache[i].sprite_id;
}
}
pxl8_result result = pxl8_gfx_ensure_atlas(gfx);
if (result != PXL8_OK) return result;
pxl8_ase_file ase_file;
result = pxl8_ase_load(path, &ase_file);
if (result != PXL8_OK) {
pxl8_error("Failed to load ASE file: %s", path);
return result;
}
if (ase_file.frame_count == 0) {
pxl8_error("No frames in ASE file");
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_FORMAT;
}
u32 sprite_id = pxl8_atlas_add_texture(
gfx->atlas,
ase_file.frames[0].pixels,
ase_file.header.width,
ase_file.header.height,
gfx->pixel_mode
);
pxl8_ase_destroy(&ase_file);
if (sprite_id == UINT32_MAX) {
pxl8_error("Sprite doesn't fit in atlas");
return PXL8_ERROR_INVALID_SIZE;
}
if (gfx->sprite_cache_count >= gfx->sprite_cache_capacity) {
u32 new_capacity = gfx->sprite_cache_capacity * 2;
pxl8_sprite_cache_entry* new_cache = (pxl8_sprite_cache_entry*)realloc(
gfx->sprite_cache,
new_capacity * sizeof(pxl8_sprite_cache_entry)
);
if (!new_cache) return PXL8_ERROR_OUT_OF_MEMORY;
gfx->sprite_cache = new_cache;
gfx->sprite_cache_capacity = new_capacity;
}
pxl8_sprite_cache_entry* entry = &gfx->sprite_cache[gfx->sprite_cache_count++];
entry->active = true;
entry->sprite_id = sprite_id;
pxl8_strncpy(entry->path, path, sizeof(entry->path));
return sprite_id;
}
pxl8_atlas* pxl8_gfx_get_atlas(pxl8_gfx* gfx) {
if (!gfx || !gfx->initialized) return NULL;
if (pxl8_gfx_ensure_atlas(gfx) != PXL8_OK) return NULL;
return gfx->atlas;
}
pxl8_result pxl8_gfx_load_font_atlas(pxl8_gfx* gfx) {
(void)gfx;
return PXL8_OK;
}
void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx) {
if (!gfx || !gfx->initialized || !gfx->hal) return;
if (gfx->backend.type == PXL8_GFX_BACKEND_CPU && gfx->pixel_mode == PXL8_PIXEL_INDEXED) {
pxl8_cpu_resolve(gfx->backend.cpu);
u32* output = pxl8_cpu_get_output(gfx->backend.cpu);
if (output) {
gfx->hal->upload_texture(
gfx->platform_data,
output,
gfx->framebuffer_width,
gfx->framebuffer_height,
PXL8_PIXEL_RGBA,
NULL
);
return;
}
}
u32* colors = gfx->palette ? pxl8_palette_colors(gfx->palette) : NULL;
gfx->hal->upload_texture(
gfx->platform_data,
gfx->framebuffer,
gfx->framebuffer_width,
gfx->framebuffer_height,
gfx->pixel_mode,
colors
);
}
void pxl8_gfx_present(pxl8_gfx* gfx) {
if (!gfx || !gfx->initialized || !gfx->hal) return;
gfx->hal->present(gfx->platform_data);
}
pxl8_viewport pxl8_gfx_viewport(pxl8_bounds bounds, i32 width, i32 height) {
pxl8_viewport vp = {0};
vp.scale = fminf(bounds.w / (f32)width, bounds.h / (f32)height);
vp.scaled_width = (i32)(width * vp.scale);
vp.scaled_height = (i32)(height * vp.scale);
vp.offset_x = (bounds.w - vp.scaled_width) / 2;
vp.offset_y = (bounds.h - vp.scaled_height) / 2;
return vp;
}
void pxl8_gfx_set_viewport(pxl8_gfx* gfx, pxl8_viewport vp) {
if (!gfx) return;
gfx->viewport = vp;
}
void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom) {
(void)gfx; (void)left; (void)right; (void)top; (void)bottom;
}
void pxl8_2d_clear(pxl8_gfx* gfx, u32 color) {
if (!gfx) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_clear(gfx->backend.cpu, (u8)color);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
void pxl8_2d_pixel(pxl8_gfx* gfx, i32 x, i32 y, u32 color) {
if (!gfx) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_draw_pixel(gfx->backend.cpu, x, y, (u8)color);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
u32 pxl8_2d_get_pixel(pxl8_gfx* gfx, i32 x, i32 y) {
if (!gfx) return 0;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
return pxl8_cpu_get_pixel(gfx->backend.cpu, x, y);
case PXL8_GFX_BACKEND_GPU:
return 0;
}
return 0;
}
void pxl8_2d_line(pxl8_gfx* gfx, i32 x0, i32 y0, i32 x1, i32 y1, u32 color) {
if (!gfx) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_draw_line_2d(gfx->backend.cpu, x0, y0, x1, y1, (u8)color);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
void pxl8_2d_rect(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) {
if (!gfx) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_draw_rect(gfx->backend.cpu, x, y, w, h, (u8)color);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
void pxl8_2d_rect_fill(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color) {
if (!gfx) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_draw_rect_fill(gfx->backend.cpu, x, y, w, h, (u8)color);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
void pxl8_2d_circle(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color) {
if (!gfx) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_draw_circle(gfx->backend.cpu, cx, cy, radius, (u8)color);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
void pxl8_2d_circle_fill(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color) {
if (!gfx) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_draw_circle_fill(gfx->backend.cpu, cx, cy, radius, (u8)color);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
void pxl8_2d_text(pxl8_gfx* gfx, const char* text, i32 x, i32 y, u32 color) {
if (!gfx || !text) return;
u8* framebuffer = NULL;
u32* light_accum = NULL;
i32 fb_width = 0;
i32 fb_height = 0;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU: {
pxl8_cpu_render_target* render_target = pxl8_cpu_get_target(gfx->backend.cpu);
if (!render_target) return;
framebuffer = pxl8_cpu_render_target_get_framebuffer(render_target);
light_accum = pxl8_cpu_render_target_get_light_accum(render_target);
fb_width = (i32)pxl8_cpu_render_target_get_width(render_target);
fb_height = (i32)pxl8_cpu_render_target_get_height(render_target);
break;
}
case PXL8_GFX_BACKEND_GPU:
return;
}
if (!framebuffer) return;
const pxl8_font* font = &pxl8_default_font;
i32 cursor_x = x;
i32 cursor_y = y;
for (const char* c = text; *c; c++) {
const pxl8_glyph* glyph = pxl8_font_find_glyph(font, (u32)*c);
if (!glyph) continue;
for (i32 gy = 0; gy < glyph->height; gy++) {
for (i32 gx = 0; gx < glyph->width; gx++) {
i32 px = cursor_x + gx;
i32 py = cursor_y + gy;
if (px < 0 || px >= fb_width || py < 0 || py >= fb_height) continue;
u8 pixel_bit = 0;
if (glyph->format == PXL8_FONT_FORMAT_INDEXED) {
u8 pixel_byte = glyph->data.indexed[gy];
pixel_bit = (pixel_byte >> gx) & 1;
} else {
i32 glyph_idx = gy * 8 + gx;
u32 rgba_pixel = glyph->data.rgba[glyph_idx];
pixel_bit = ((rgba_pixel >> 24) & 0xFF) > 128 ? 1 : 0;
}
if (pixel_bit) {
i32 idx = py * fb_width + px;
framebuffer[idx] = (u8)color;
if (light_accum) light_accum[idx] = 0;
}
}
}
cursor_x += font->default_width;
}
}
void pxl8_2d_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h, bool flip_x, bool flip_y) {
if (!gfx || !gfx->atlas) return;
u8* framebuffer = NULL;
i32 fb_width = 0;
i32 fb_height = 0;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU: {
pxl8_cpu_render_target* render_target = pxl8_cpu_get_target(gfx->backend.cpu);
if (!render_target) return;
framebuffer = pxl8_cpu_render_target_get_framebuffer(render_target);
fb_width = (i32)pxl8_cpu_render_target_get_width(render_target);
fb_height = (i32)pxl8_cpu_render_target_get_height(render_target);
break;
}
case PXL8_GFX_BACKEND_GPU:
return;
}
if (!framebuffer) return;
const pxl8_atlas_entry* entry = pxl8_atlas_get_entry(gfx->atlas, sprite_id);
if (!entry || !entry->active) return;
i32 clip_left = (x < 0) ? -x : 0;
i32 clip_top = (y < 0) ? -y : 0;
i32 clip_right = (x + w > fb_width) ? x + w - fb_width : 0;
i32 clip_bottom = (y + h > fb_height) ? y + h - fb_height : 0;
i32 draw_width = w - clip_left - clip_right;
i32 draw_height = h - clip_top - clip_bottom;
if (draw_width <= 0 || draw_height <= 0) return;
i32 dest_x = x + clip_left;
i32 dest_y = y + clip_top;
bool is_1to1_scale = (w == entry->w && h == entry->h);
bool is_unclipped = (clip_left == 0 && clip_top == 0 && clip_right == 0 && clip_bottom == 0);
bool is_flipped = flip_x || flip_y;
u32 atlas_width = pxl8_atlas_get_width(gfx->atlas);
const u8* atlas_pixels = pxl8_atlas_get_pixels(gfx->atlas);
if (is_1to1_scale && is_unclipped && !is_flipped) {
const u8* sprite_data = atlas_pixels + entry->y * atlas_width + entry->x;
pxl8_blit_indexed(framebuffer, fb_width, sprite_data, atlas_width, x, y, w, h);
} else {
for (i32 py = 0; py < draw_height; py++) {
for (i32 px = 0; px < draw_width; px++) {
i32 local_x = (px + clip_left) * entry->w / w;
i32 local_y = (py + clip_top) * entry->h / h;
i32 src_x = flip_x ? entry->x + entry->w - 1 - local_x : entry->x + local_x;
i32 src_y = flip_y ? entry->y + entry->h - 1 - local_y : entry->y + local_y;
i32 src_idx = src_y * atlas_width + src_x;
i32 dest_idx = (dest_y + py) * fb_width + (dest_x + px);
framebuffer[dest_idx] = pxl8_blend_indexed(atlas_pixels[src_idx], framebuffer[dest_idx]);
}
}
}
}
void pxl8_gfx_update(pxl8_gfx* gfx, f32 dt) {
if (!gfx) return;
if (gfx->palette) {
u16 delta_ticks = (u16)(dt * 1000.0f);
pxl8_palette_tick(gfx->palette, delta_ticks);
}
}
pxl8_3d_frame pxl8_3d_frame_from_camera(const pxl8_3d_camera* camera, const pxl8_3d_uniforms* uniforms) {
pxl8_3d_frame frame = {0};
if (!camera) return frame;
frame.view = pxl8_3d_camera_get_view(camera);
frame.projection = pxl8_3d_camera_get_projection(camera);
frame.camera_pos = pxl8_3d_camera_get_position(camera);
frame.camera_dir = pxl8_3d_camera_get_forward(camera);
frame.near_clip = 1.0f;
frame.far_clip = 4096.0f;
if (uniforms) {
frame.uniforms = *uniforms;
}
return frame;
}
void pxl8_3d_begin_frame(pxl8_gfx* gfx, const pxl8_3d_camera* camera, const pxl8_3d_uniforms* uniforms) {
if (!gfx || !camera) return;
pxl8_3d_frame frame = pxl8_3d_frame_from_camera(camera, uniforms);
pxl8_mat4 vp = pxl8_mat4_mul(frame.projection, frame.view);
gfx->frustum = pxl8_frustum_from_matrix(vp);
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_begin_frame(gfx->backend.cpu, &frame);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
const pxl8_frustum* pxl8_3d_get_frustum(pxl8_gfx* gfx) {
if (!gfx) return NULL;
return &gfx->frustum;
}
void pxl8_3d_clear(pxl8_gfx* gfx, u8 color) {
if (!gfx) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_clear(gfx->backend.cpu, color);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
void pxl8_3d_clear_depth(pxl8_gfx* gfx) {
if (!gfx) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_clear_depth(gfx->backend.cpu);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
void pxl8_3d_draw_line(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, u8 color) {
if (!gfx) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_draw_line_3d(gfx->backend.cpu, v0, v1, color);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
void pxl8_3d_draw_mesh(pxl8_gfx* gfx, const pxl8_mesh* mesh, const pxl8_mat4* model, const pxl8_material* material) {
if (!gfx || !mesh || !model || !material) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_draw_mesh(gfx->backend.cpu, mesh, model, material, gfx->atlas);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
void pxl8_3d_draw_mesh_wireframe(pxl8_gfx* gfx, const pxl8_mesh* mesh, pxl8_mat4 model, u8 color) {
if (!gfx || !mesh) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_draw_mesh_wireframe(gfx->backend.cpu, mesh, model, color);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
void pxl8_3d_end_frame(pxl8_gfx* gfx) {
if (!gfx) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_end_frame(gfx->backend.cpu);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
u8* pxl8_3d_get_framebuffer(pxl8_gfx* gfx) {
if (!gfx) return NULL;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
return pxl8_cpu_get_framebuffer(gfx->backend.cpu);
case PXL8_GFX_BACKEND_GPU:
return NULL;
}
return NULL;
}
bool pxl8_gfx_push_target(pxl8_gfx* gfx) {
if (!gfx) return false;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
return pxl8_cpu_push_target(gfx->backend.cpu);
case PXL8_GFX_BACKEND_GPU:
return false;
}
return false;
}
void pxl8_gfx_pop_target(pxl8_gfx* gfx) {
if (!gfx) return;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_pop_target(gfx->backend.cpu);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
}
static void pxl8_gfx_ensure_blend_tables(pxl8_gfx* gfx) {
if (!gfx || !gfx->palette) return;
if (!gfx->additive_table) {
gfx->additive_table = pxl8_additive_table_create(gfx->palette);
}
if (!gfx->overbright_table) {
gfx->overbright_table = pxl8_overbright_table_create(gfx->palette);
}
if (!gfx->palette_cube) {
gfx->palette_cube = pxl8_palette_cube_create(gfx->palette);
}
}
void pxl8_gfx_blend_tables_update(pxl8_gfx* gfx) {
if (!gfx || !gfx->palette) return;
if (gfx->additive_table) {
pxl8_additive_table_rebuild(gfx->additive_table, gfx->palette);
}
if (gfx->overbright_table) {
pxl8_overbright_table_rebuild(gfx->overbright_table, gfx->palette);
}
if (gfx->palette_cube) {
pxl8_palette_cube_rebuild(gfx->palette_cube, gfx->palette);
}
}
void pxl8_gfx_colormap_update(pxl8_gfx* gfx) {
if (!gfx || !gfx->palette || !gfx->colormap) return;
u32* colors = pxl8_palette_colors(gfx->palette);
if (colors) {
pxl8_colormap_generate(gfx->colormap, colors, NULL);
}
}
void pxl8_gfx_apply_effect(pxl8_gfx* gfx, pxl8_gfx_effect effect, const void* params, u32 count) {
if (!gfx || !params || count == 0) return;
switch (effect) {
case PXL8_GFX_EFFECT_GLOWS: {
pxl8_gfx_ensure_blend_tables(gfx);
if (!gfx->additive_table || !gfx->overbright_table || !gfx->palette_cube) return;
const pxl8_glow_source* glows = (const pxl8_glow_source*)params;
switch (gfx->backend.type) {
case PXL8_GFX_BACKEND_CPU:
pxl8_cpu_render_glows(
gfx->backend.cpu,
glows,
count,
gfx->additive_table,
gfx->palette_cube,
gfx->overbright_table
);
break;
case PXL8_GFX_BACKEND_GPU:
break;
}
break;
}
}
}

103
src/gfx/pxl8_gfx.h Normal file
View file

@ -0,0 +1,103 @@
#pragma once
#include "pxl8_gfx2d.h"
#include "pxl8_gfx3d.h"
#include "pxl8_hal.h"
#include "pxl8_palette.h"
#include "pxl8_types.h"
typedef struct pxl8_gfx pxl8_gfx;
typedef enum pxl8_gfx_effect {
PXL8_GFX_EFFECT_GLOWS = 0,
} pxl8_gfx_effect;
#define PXL8_MAX_GLOWS 256
typedef enum pxl8_glow_shape {
PXL8_GLOW_CIRCLE = 0,
PXL8_GLOW_DIAMOND = 1,
PXL8_GLOW_SHAFT = 2,
} pxl8_glow_shape;
typedef struct pxl8_glow_source {
u8 color;
u16 depth;
u8 height;
u16 intensity;
u8 radius;
pxl8_glow_shape shape;
i16 x;
i16 y;
} pxl8_glow_source;
static inline pxl8_glow_source pxl8_glow_create(i32 x, i32 y, u8 radius, u16 intensity, u8 color) {
return (pxl8_glow_source){
.color = color,
.depth = 0xFFFF,
.height = 0,
.intensity = intensity,
.radius = radius,
.shape = PXL8_GLOW_CIRCLE,
.x = (i16)x,
.y = (i16)y,
};
}
static inline pxl8_glow_source pxl8_glow_with_depth(pxl8_glow_source g, u16 depth) {
g.depth = depth;
return g;
}
static inline pxl8_glow_source pxl8_glow_with_shape(pxl8_glow_source g, pxl8_glow_shape shape) {
g.shape = shape;
return g;
}
static inline pxl8_glow_source pxl8_glow_with_height(pxl8_glow_source g, u8 height) {
g.height = height;
return g;
}
#ifdef __cplusplus
extern "C" {
#endif
pxl8_gfx* pxl8_gfx_create(const pxl8_hal* hal, void* platform_data, pxl8_pixel_mode mode, pxl8_resolution resolution);
void pxl8_gfx_destroy(pxl8_gfx* gfx);
void pxl8_gfx_present(pxl8_gfx* gfx);
void pxl8_gfx_update(pxl8_gfx* gfx, f32 dt);
void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx);
u8 pxl8_gfx_find_color(pxl8_gfx* gfx, u32 color);
pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx);
u8* pxl8_gfx_get_framebuffer_indexed(pxl8_gfx* gfx);
u16* pxl8_gfx_get_framebuffer_hicolor(pxl8_gfx* gfx);
i32 pxl8_gfx_get_height(const pxl8_gfx* gfx);
pxl8_palette* pxl8_gfx_get_palette(pxl8_gfx* gfx);
pxl8_pixel_mode pxl8_gfx_get_pixel_mode(pxl8_gfx* gfx);
i32 pxl8_gfx_get_width(const pxl8_gfx* gfx);
i32 pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* filepath);
void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom);
void pxl8_gfx_set_palette(pxl8_gfx* gfx, pxl8_palette* pal);
void pxl8_gfx_set_viewport(pxl8_gfx* gfx, pxl8_viewport vp);
pxl8_viewport pxl8_gfx_viewport(pxl8_bounds bounds, i32 width, i32 height);
void pxl8_gfx_clear_textures(pxl8_gfx* gfx);
pxl8_result pxl8_gfx_create_texture(pxl8_gfx* gfx, const u8* pixels, u32 width, u32 height);
pxl8_result pxl8_gfx_load_font_atlas(pxl8_gfx* gfx);
pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path);
bool pxl8_gfx_push_target(pxl8_gfx* gfx);
void pxl8_gfx_pop_target(pxl8_gfx* gfx);
void pxl8_gfx_apply_effect(pxl8_gfx* gfx, pxl8_gfx_effect effect, const void* params, u32 count);
void pxl8_gfx_blend_tables_update(pxl8_gfx* gfx);
void pxl8_gfx_colormap_update(pxl8_gfx* gfx);
#ifdef __cplusplus
}
#endif

24
src/gfx/pxl8_gfx2d.h Normal file
View file

@ -0,0 +1,24 @@
#pragma once
#include "pxl8_types.h"
typedef struct pxl8_gfx pxl8_gfx;
#ifdef __cplusplus
extern "C" {
#endif
void pxl8_2d_circle(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color);
void pxl8_2d_circle_fill(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color);
void pxl8_2d_clear(pxl8_gfx* gfx, u32 color);
u32 pxl8_2d_get_pixel(pxl8_gfx* gfx, i32 x, i32 y);
void pxl8_2d_line(pxl8_gfx* gfx, i32 x0, i32 y0, i32 x1, i32 y1, u32 color);
void pxl8_2d_pixel(pxl8_gfx* gfx, i32 x, i32 y, u32 color);
void pxl8_2d_rect(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color);
void pxl8_2d_rect_fill(pxl8_gfx* gfx, i32 x, i32 y, i32 w, i32 h, u32 color);
void pxl8_2d_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h, bool flip_x, bool flip_y);
void pxl8_2d_text(pxl8_gfx* gfx, const char* text, i32 x, i32 y, u32 color);
#ifdef __cplusplus
}
#endif

72
src/gfx/pxl8_gfx3d.h Normal file
View file

@ -0,0 +1,72 @@
#pragma once
#include "pxl8_3d_camera.h"
#include "pxl8_math.h"
#include "pxl8_mesh.h"
#include "pxl8_types.h"
typedef struct pxl8_gfx pxl8_gfx;
#define PXL8_MAX_LIGHTS 16
typedef struct pxl8_light {
pxl8_vec3 position;
u8 r, g, b;
u8 intensity;
f32 radius;
f32 radius_sq;
f32 inv_radius_sq;
} pxl8_light;
static inline pxl8_light pxl8_light_create(pxl8_vec3 pos, u8 r, u8 g, u8 b, u8 intensity, f32 radius) {
f32 radius_sq = radius * radius;
return (pxl8_light){
.position = pos,
.r = r, .g = g, .b = b,
.intensity = intensity,
.radius = radius,
.radius_sq = radius_sq,
.inv_radius_sq = radius_sq > 0.0f ? 1.0f / radius_sq : 0.0f,
};
}
typedef struct pxl8_3d_uniforms {
u8 ambient;
pxl8_vec3 celestial_dir;
f32 celestial_intensity;
u8 fog_color;
f32 fog_density;
pxl8_light lights[PXL8_MAX_LIGHTS];
u32 num_lights;
f32 time;
} pxl8_3d_uniforms;
typedef struct pxl8_3d_frame {
pxl8_3d_uniforms uniforms;
pxl8_vec3 camera_dir;
pxl8_vec3 camera_pos;
f32 far_clip;
f32 near_clip;
pxl8_mat4 projection;
pxl8_mat4 view;
} pxl8_3d_frame;
#ifdef __cplusplus
extern "C" {
#endif
void pxl8_3d_begin_frame(pxl8_gfx* gfx, const pxl8_3d_camera* camera, const pxl8_3d_uniforms* uniforms);
void pxl8_3d_clear(pxl8_gfx* gfx, u8 color);
void pxl8_3d_clear_depth(pxl8_gfx* gfx);
void pxl8_3d_draw_line(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, u8 color);
void pxl8_3d_draw_mesh(pxl8_gfx* gfx, const pxl8_mesh* mesh, const pxl8_mat4* model, const pxl8_material* material);
void pxl8_3d_draw_mesh_wireframe(pxl8_gfx* gfx, const pxl8_mesh* mesh, pxl8_mat4 model, u8 color);
void pxl8_3d_end_frame(pxl8_gfx* gfx);
u8* pxl8_3d_get_framebuffer(pxl8_gfx* gfx);
const pxl8_frustum* pxl8_3d_get_frustum(pxl8_gfx* gfx);
pxl8_3d_frame pxl8_3d_frame_from_camera(const pxl8_3d_camera* camera, const pxl8_3d_uniforms* uniforms);
#ifdef __cplusplus
}
#endif

122
src/gfx/pxl8_mesh.c Normal file
View file

@ -0,0 +1,122 @@
#include "pxl8_mesh.h"
#include <stdlib.h>
#include <string.h>
pxl8_mesh* pxl8_mesh_create(u32 vertex_capacity, u32 index_capacity) {
if (vertex_capacity > PXL8_MESH_MAX_VERTICES) vertex_capacity = PXL8_MESH_MAX_VERTICES;
if (index_capacity > PXL8_MESH_MAX_INDICES) index_capacity = PXL8_MESH_MAX_INDICES;
pxl8_mesh* mesh = calloc(1, sizeof(pxl8_mesh));
if (!mesh) return NULL;
mesh->vertices = calloc(vertex_capacity, sizeof(pxl8_vertex));
mesh->indices = calloc(index_capacity, sizeof(u16));
if (!mesh->vertices || !mesh->indices) {
free(mesh->vertices);
free(mesh->indices);
free(mesh);
return NULL;
}
mesh->vertex_capacity = vertex_capacity;
mesh->index_capacity = index_capacity;
mesh->vertex_count = 0;
mesh->index_count = 0;
return mesh;
}
void pxl8_mesh_destroy(pxl8_mesh* mesh) {
if (!mesh) return;
free(mesh->vertices);
free(mesh->indices);
free(mesh);
}
void pxl8_mesh_clear(pxl8_mesh* mesh) {
if (!mesh) return;
mesh->vertex_count = 0;
mesh->index_count = 0;
}
u16 pxl8_mesh_push_vertex(pxl8_mesh* mesh, pxl8_vertex v) {
if (!mesh || mesh->vertex_count >= mesh->vertex_capacity) return 0;
u16 idx = (u16)mesh->vertex_count;
mesh->vertices[mesh->vertex_count++] = v;
return idx;
}
void pxl8_mesh_push_triangle(pxl8_mesh* mesh, u16 i0, u16 i1, u16 i2) {
if (!mesh || mesh->index_count + 3 > mesh->index_capacity) return;
mesh->indices[mesh->index_count++] = i0;
mesh->indices[mesh->index_count++] = i1;
mesh->indices[mesh->index_count++] = i2;
}
void pxl8_mesh_push_quad(pxl8_mesh* mesh, u16 i0, u16 i1, u16 i2, u16 i3) {
pxl8_mesh_push_triangle(mesh, i0, i1, i2);
pxl8_mesh_push_triangle(mesh, i0, i2, i3);
}
void pxl8_mesh_push_box(pxl8_mesh* mesh, pxl8_vec3 center, pxl8_vec3 half, u8 color) {
pxl8_mesh_push_box_uv(mesh, center, half, color, 1.0f, 1.0f);
}
void pxl8_mesh_push_box_uv(pxl8_mesh* mesh, pxl8_vec3 center, pxl8_vec3 half, u8 color, f32 u_scale, f32 v_scale) {
if (!mesh) return;
pxl8_vec3 corners[8] = {
{center.x - half.x, center.y - half.y, center.z - half.z},
{center.x + half.x, center.y - half.y, center.z - half.z},
{center.x + half.x, center.y + half.y, center.z - half.z},
{center.x - half.x, center.y + half.y, center.z - half.z},
{center.x - half.x, center.y - half.y, center.z + half.z},
{center.x + half.x, center.y - half.y, center.z + half.z},
{center.x + half.x, center.y + half.y, center.z + half.z},
{center.x - half.x, center.y + half.y, center.z + half.z},
};
pxl8_vec3 normals[6] = {
{ 0, 0, -1}, // front
{ 0, 0, 1}, // back
{-1, 0, 0}, // left
{ 1, 0, 0}, // right
{ 0, -1, 0}, // bottom
{ 0, 1, 0}, // top
};
i32 faces[6][4] = {
{0, 1, 2, 3}, // front
{5, 4, 7, 6}, // back
{4, 0, 3, 7}, // left
{1, 5, 6, 2}, // right
{4, 5, 1, 0}, // bottom
{3, 2, 6, 7}, // top
};
f32 uvs[4][2] = {
{0, v_scale},
{u_scale, v_scale},
{u_scale, 0},
{0, 0},
};
for (i32 face = 0; face < 6; face++) {
u16 base = (u16)mesh->vertex_count;
for (i32 v = 0; v < 4; v++) {
pxl8_vertex vert = {
.position = corners[faces[face][v]],
.normal = normals[face],
.u = uvs[v][0],
.v = uvs[v][1],
.color = color,
.light = 255,
};
pxl8_mesh_push_vertex(mesh, vert);
}
pxl8_mesh_push_quad(mesh, base, base + 1, base + 2, base + 3);
}
}

124
src/gfx/pxl8_mesh.h Normal file
View file

@ -0,0 +1,124 @@
#pragma once
#include "pxl8_math.h"
#include "pxl8_types.h"
#ifdef __cplusplus
extern "C" {
#endif
#define PXL8_MESH_MAX_VERTICES 65536
#define PXL8_MESH_MAX_INDICES 65536
typedef enum pxl8_blend_mode {
PXL8_BLEND_OPAQUE = 0,
PXL8_BLEND_ALPHA_TEST,
PXL8_BLEND_ALPHA,
PXL8_BLEND_ADDITIVE,
} pxl8_blend_mode;
typedef struct pxl8_material {
u32 texture_id;
u8 alpha;
u8 blend_mode;
bool dither;
bool double_sided;
bool dynamic_lighting;
bool per_pixel;
bool vertex_color_passthrough;
f32 emissive_intensity;
} pxl8_material;
typedef struct pxl8_vertex {
pxl8_vec3 position;
pxl8_vec3 normal;
f32 u, v;
u8 color;
u8 light;
u8 _pad[2];
} pxl8_vertex;
typedef struct pxl8_mesh {
pxl8_vertex* vertices;
u16* indices;
u32 vertex_count;
u32 index_count;
u32 vertex_capacity;
u32 index_capacity;
} pxl8_mesh;
pxl8_mesh* pxl8_mesh_create(u32 vertex_capacity, u32 index_capacity);
void pxl8_mesh_destroy(pxl8_mesh* mesh);
void pxl8_mesh_clear(pxl8_mesh* mesh);
u16 pxl8_mesh_push_vertex(pxl8_mesh* mesh, pxl8_vertex v);
void pxl8_mesh_push_triangle(pxl8_mesh* mesh, u16 i0, u16 i1, u16 i2);
void pxl8_mesh_push_quad(pxl8_mesh* mesh, u16 i0, u16 i1, u16 i2, u16 i3);
void pxl8_mesh_push_box(pxl8_mesh* mesh, pxl8_vec3 center, pxl8_vec3 half_extents, u8 color);
void pxl8_mesh_push_box_uv(pxl8_mesh* mesh, pxl8_vec3 center, pxl8_vec3 half_extents, u8 color, f32 u_scale, f32 v_scale);
static inline u32 pxl8_mesh_triangle_count(const pxl8_mesh* mesh) {
return mesh->index_count / 3;
}
static inline pxl8_material pxl8_material_create(u32 texture_id) {
return (pxl8_material){
.texture_id = texture_id,
.alpha = 255,
.blend_mode = PXL8_BLEND_OPAQUE,
.dither = true,
.double_sided = false,
.dynamic_lighting = false,
.per_pixel = false,
.vertex_color_passthrough = false,
.emissive_intensity = 0.0f,
};
}
static inline pxl8_material pxl8_material_with_alpha(pxl8_material m, u8 alpha) {
m.alpha = alpha;
if (alpha < 255 && m.blend_mode == PXL8_BLEND_OPAQUE) {
m.blend_mode = PXL8_BLEND_ALPHA;
}
return m;
}
static inline pxl8_material pxl8_material_with_blend(pxl8_material m, pxl8_blend_mode mode) {
m.blend_mode = mode;
return m;
}
static inline pxl8_material pxl8_material_with_double_sided(pxl8_material m) {
m.double_sided = true;
return m;
}
static inline pxl8_material pxl8_material_with_emissive(pxl8_material m, f32 intensity) {
m.emissive_intensity = intensity;
return m;
}
static inline pxl8_material pxl8_material_with_lighting(pxl8_material m) {
m.dynamic_lighting = true;
return m;
}
static inline pxl8_material pxl8_material_with_no_dither(pxl8_material m) {
m.dither = false;
return m;
}
static inline pxl8_material pxl8_material_with_passthrough(pxl8_material m) {
m.vertex_color_passthrough = true;
return m;
}
static inline pxl8_material pxl8_material_with_per_pixel(pxl8_material m) {
m.per_pixel = true;
return m;
}
#ifdef __cplusplus
}
#endif

474
src/gfx/pxl8_palette.c Normal file
View file

@ -0,0 +1,474 @@
#include "pxl8_palette.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_ase.h"
#include "pxl8_color.h"
#include "pxl8_log.h"
#define PXL8_PALETTE_HASH_SIZE 512
typedef struct {
u32 color;
i16 index;
} pxl8_palette_hash_entry;
struct pxl8_palette {
u32 base_colors[PXL8_MAX_CYCLES][PXL8_MAX_CYCLE_LEN];
u32 colors[PXL8_PALETTE_SIZE];
u8 color_ramp[PXL8_PALETTE_SIZE];
u16 color_count;
pxl8_cycle_range cycles[PXL8_MAX_CYCLES];
i8 directions[PXL8_MAX_CYCLES];
f32 phases[PXL8_MAX_CYCLES];
pxl8_palette_hash_entry hash[PXL8_PALETTE_HASH_SIZE];
};
static inline u32 pxl8_palette_hash(u32 color) {
color ^= color >> 16;
color *= 0x85ebca6b;
color ^= color >> 13;
return color & (PXL8_PALETTE_HASH_SIZE - 1);
}
static void pxl8_palette_rebuild_hash(pxl8_palette* pal) {
for (u32 i = 0; i < PXL8_PALETTE_HASH_SIZE; i++) {
pal->hash[i].index = -1;
}
for (u32 i = 0; i < PXL8_PALETTE_SIZE; i++) {
u32 color = pal->colors[i];
u32 slot = pxl8_palette_hash(color);
while (pal->hash[slot].index >= 0) {
slot = (slot + 1) & (PXL8_PALETTE_HASH_SIZE - 1);
}
pal->hash[slot].color = color;
pal->hash[slot].index = (i16)i;
}
}
static inline u32 pack_rgb(u8 r, u8 g, u8 b) {
return 0xFF000000 | ((u32)b << 16) | ((u32)g << 8) | r;
}
static inline u32 pack_rgba(u8 r, u8 g, u8 b, u8 a) {
return ((u32)a << 24) | ((u32)b << 16) | ((u32)g << 8) | r;
}
static inline void unpack_rgba(u32 c, u8* r, u8* g, u8* b, u8* a) {
*r = c & 0xFF;
*g = (c >> 8) & 0xFF;
*b = (c >> 16) & 0xFF;
*a = (c >> 24) & 0xFF;
}
static f32 apply_easing(pxl8_easing easing, f32 t) {
switch (easing) {
case PXL8_EASE_IN:
return t * t;
case PXL8_EASE_IN_OUT:
return t < 0.5f ? 2.0f * t * t : 1.0f - (-2.0f * t + 2.0f) * (-2.0f * t + 2.0f) * 0.5f;
case PXL8_EASE_LINEAR:
return t;
case PXL8_EASE_OUT:
return 1.0f - (1.0f - t) * (1.0f - t);
default:
return t;
}
}
static void rgb_to_hsl(u8 r, u8 g, u8 b, f32* h, f32* s, f32* l) {
f32 rf = r / 255.0f;
f32 gf = g / 255.0f;
f32 bf = b / 255.0f;
f32 max = rf > gf ? (rf > bf ? rf : bf) : (gf > bf ? gf : bf);
f32 min = rf < gf ? (rf < bf ? rf : bf) : (gf < bf ? gf : bf);
f32 delta = max - min;
*l = (max + min) / 2.0f;
if (delta < 0.001f) {
*h = 0.0f;
*s = 0.0f;
} else {
*s = *l > 0.5f ? delta / (2.0f - max - min) : delta / (max + min);
if (max == rf) {
*h = (gf - bf) / delta + (gf < bf ? 6.0f : 0.0f);
} else if (max == gf) {
*h = (bf - rf) / delta + 2.0f;
} else {
*h = (rf - gf) / delta + 4.0f;
}
*h /= 6.0f;
}
}
typedef struct {
u8 index;
f32 hue;
f32 sat;
f32 lum;
} palette_sort_entry;
static int palette_sort_cmp(const void* a, const void* b) {
const palette_sort_entry* entry_a = (const palette_sort_entry*)a;
const palette_sort_entry* entry_b = (const palette_sort_entry*)b;
u8 a_gray = entry_a->sat < 0.1f;
u8 b_gray = entry_b->sat < 0.1f;
if (a_gray != b_gray) return a_gray - b_gray;
if (a_gray) {
if (entry_a->lum < entry_b->lum) return -1;
if (entry_a->lum > entry_b->lum) return 1;
return (int)entry_a->index - (int)entry_b->index;
}
if (entry_a->hue < entry_b->hue - 0.02f) return -1;
if (entry_a->hue > entry_b->hue + 0.02f) return 1;
if (entry_a->lum < entry_b->lum) return -1;
if (entry_a->lum > entry_b->lum) return 1;
return (int)entry_a->index - (int)entry_b->index;
}
static void pxl8_palette_sort_colors(pxl8_palette* pal) {
if (!pal || pal->color_count < 2) return;
pal->color_ramp[0] = 0;
u8 count = pal->color_count;
palette_sort_entry entries[PXL8_PALETTE_SIZE - 1];
for (u32 i = 1; i < count; i++) {
u8 r, g, b, a;
unpack_rgba(pal->colors[i], &r, &g, &b, &a);
entries[i - 1].index = (u8)i;
rgb_to_hsl(r, g, b, &entries[i - 1].hue, &entries[i - 1].sat, &entries[i - 1].lum);
}
qsort(entries, count - 1, sizeof(palette_sort_entry), palette_sort_cmp);
for (u32 i = 1; i < count; i++) {
pal->color_ramp[i] = entries[i - 1].index;
}
}
static u32 lerp_color(u32 c0, u32 c1, f32 t) {
u8 r0, g0, b0, a0, r1, g1, b1, a1;
unpack_rgba(c0, &r0, &g0, &b0, &a0);
unpack_rgba(c1, &r1, &g1, &b1, &a1);
u8 r = (u8)(r0 + (r1 - r0) * t);
u8 g = (u8)(g0 + (g1 - g0) * t);
u8 b = (u8)(b0 + (b1 - b0) * t);
u8 a = (u8)(a0 + (a1 - a0) * t);
return pack_rgba(r, g, b, a);
}
static void update_cycle_colors(pxl8_palette* pal, u8 slot) {
pxl8_cycle_range* cycle = &pal->cycles[slot];
f32 phase = pal->phases[slot];
f32 eased = apply_easing(cycle->easing, phase);
u8 len = cycle->len;
u8 start = cycle->start;
if (cycle->interpolate) {
f32 pos = eased * (len - 1);
u8 idx0 = (u8)pos;
if (idx0 >= len - 1) idx0 = len - 1;
f32 frac = pos - idx0;
for (u8 i = 0; i < len; i++) {
u8 src0 = (i + idx0) % len;
u8 src1 = (i + idx0 + 1) % len;
u32 c0 = pal->base_colors[slot][src0];
u32 c1 = pal->base_colors[slot][src1];
pal->colors[start + i] = lerp_color(c0, c1, frac);
}
} else {
u8 offset = (u8)(eased * len) % len;
for (u8 i = 0; i < len; i++) {
u8 src = (i + offset) % len;
pal->colors[start + i] = pal->base_colors[slot][src];
}
}
}
pxl8_palette* pxl8_palette_create(void) {
pxl8_palette* pal = calloc(1, sizeof(pxl8_palette));
if (!pal) return NULL;
pal->colors[0] = 0x00000000;
pal->colors[1] = pack_rgb(0x00, 0x00, 0x00);
pal->colors[2] = pack_rgb(0xFF, 0xFF, 0xFF);
pal->color_count = 3;
pal->color_ramp[0] = 0;
pal->color_ramp[1] = 1;
pal->color_ramp[2] = 2;
for (u8 i = 0; i < PXL8_MAX_CYCLES; i++) {
pal->cycles[i] = pxl8_cycle_range_disabled();
pal->phases[i] = 0.0f;
pal->directions[i] = 1;
}
return pal;
}
void pxl8_palette_destroy(pxl8_palette* pal) {
free(pal);
}
pxl8_result pxl8_palette_load_ase(pxl8_palette* pal, const char* path) {
if (!pal || !path) return PXL8_ERROR_INVALID_ARGUMENT;
pxl8_ase_file ase_file;
pxl8_result result = pxl8_ase_load(path, &ase_file);
if (result != PXL8_OK) {
pxl8_error("Failed to load ASE file for palette: %s", path);
return result;
}
if (ase_file.palette.entry_count == 0 || !ase_file.palette.colors) {
pxl8_error("No palette data in ASE file");
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_FORMAT;
}
u32 copy_count = ase_file.palette.entry_count < PXL8_PALETTE_SIZE
? ase_file.palette.entry_count
: PXL8_PALETTE_SIZE;
memcpy(pal->colors, ase_file.palette.colors, copy_count * sizeof(u32));
pal->color_count = (u16)copy_count;
pxl8_ase_destroy(&ase_file);
pxl8_palette_sort_colors(pal);
pxl8_palette_rebuild_hash(pal);
return PXL8_OK;
}
u32* pxl8_palette_colors(pxl8_palette* pal) {
return pal ? pal->colors : NULL;
}
u8* pxl8_palette_color_ramp(pxl8_palette* pal) {
return pal ? pal->color_ramp : NULL;
}
u16 pxl8_palette_color_count(const pxl8_palette* pal) {
return pal ? pal->color_count : 0;
}
u8 pxl8_palette_ramp_index(const pxl8_palette* pal, u8 position) {
if (!pal || position >= pal->color_count) return 0;
return pal->color_ramp[position];
}
u8 pxl8_palette_ramp_position(const pxl8_palette* pal, u8 index) {
if (!pal) return 0;
for (u32 i = 0; i < pal->color_count; i++) {
if (pal->color_ramp[i] == index) return (u8)i;
}
return 0;
}
u8 pxl8_palette_find_closest(const pxl8_palette* pal, u8 r, u8 g, u8 b) {
if (!pal || pal->color_count < 2) return 1;
u8 best_idx = 1;
u32 best_dist = 0xFFFFFFFF;
for (u32 i = 1; i < pal->color_count; i++) {
u8 pr, pg, pb, pa;
unpack_rgba(pal->colors[i], &pr, &pg, &pb, &pa);
i32 dr = (i32)r - (i32)pr;
i32 dg = (i32)g - (i32)pg;
i32 db = (i32)b - (i32)pb;
u32 dist = (u32)(dr * dr + dg * dg + db * db);
if (dist < best_dist) {
best_dist = dist;
best_idx = (u8)i;
if (dist == 0) break;
}
}
return best_idx;
}
u32 pxl8_palette_color(const pxl8_palette* pal, u8 idx) {
if (!pal) return 0;
return pxl8_color_to_rgba(pal->colors[idx]);
}
i32 pxl8_palette_index(const pxl8_palette* pal, u32 rgba) {
if (!pal) return -1;
if (rgba <= 0xFFFFFF) rgba = (rgba << 8) | 0xFF;
u32 abgr = pxl8_color_from_rgba(rgba);
u32 slot = pxl8_palette_hash(abgr);
for (u32 i = 0; i < PXL8_PALETTE_HASH_SIZE; i++) {
u32 idx = (slot + i) & (PXL8_PALETTE_HASH_SIZE - 1);
if (pal->hash[idx].index < 0) return -1;
if (pal->hash[idx].color == abgr) return pal->hash[idx].index;
}
return -1;
}
void pxl8_palette_get_rgb(const pxl8_palette* pal, u8 idx, u8* r, u8* g, u8* b) {
if (!pal || !r || !g || !b) return;
u8 a;
unpack_rgba(pal->colors[idx], r, g, b, &a);
}
void pxl8_palette_get_rgba(const pxl8_palette* pal, u8 idx, u8* r, u8* g, u8* b, u8* a) {
if (!pal || !r || !g || !b || !a) return;
unpack_rgba(pal->colors[idx], r, g, b, a);
}
void pxl8_palette_set(pxl8_palette* pal, u8 idx, u32 color) {
if (pal) pal->colors[idx] = color;
}
void pxl8_palette_set_rgb(pxl8_palette* pal, u8 idx, u8 r, u8 g, u8 b) {
if (pal) pal->colors[idx] = pack_rgb(r, g, b);
}
void pxl8_palette_set_rgba(pxl8_palette* pal, u8 idx, u8 r, u8 g, u8 b, u8 a) {
if (pal) pal->colors[idx] = pack_rgba(r, g, b, a);
}
void pxl8_palette_fill_gradient(pxl8_palette* pal, u8 start, u8 count, u32 from, u32 to) {
if (!pal || count == 0) return;
u8 r0, g0, b0, a0, r1, g1, b1, a1;
unpack_rgba(from, &r0, &g0, &b0, &a0);
unpack_rgba(to, &r1, &g1, &b1, &a1);
for (u8 i = 0; i < count; i++) {
f32 t = (count > 1) ? (f32)i / (f32)(count - 1) : 0.0f;
u8 r = (u8)(r0 + (r1 - r0) * t);
u8 g = (u8)(g0 + (g1 - g0) * t);
u8 b = (u8)(b0 + (b1 - b0) * t);
u8 a = (u8)(a0 + (a1 - a0) * t);
pal->colors[start + i] = pack_rgba(r, g, b, a);
}
}
void pxl8_palette_fill_gradient_rgb(pxl8_palette* pal, u8 start, u8 count, u8 r0, u8 g0, u8 b0, u8 r1, u8 g1, u8 b1) {
pxl8_palette_fill_gradient(pal, start, count, pack_rgb(r0, g0, b0), pack_rgb(r1, g1, b1));
}
void pxl8_palette_reset_cycle(pxl8_palette* pal, u8 slot) {
if (!pal || slot >= PXL8_MAX_CYCLES) return;
pal->phases[slot] = 0.0f;
pal->directions[slot] = 1;
}
void pxl8_palette_set_cycle(pxl8_palette* pal, u8 slot, pxl8_cycle_range range) {
if (!pal || slot >= PXL8_MAX_CYCLES) return;
pal->cycles[slot] = range;
pal->phases[slot] = 0.0f;
pal->directions[slot] = 1;
u8 start = range.start;
u8 len = range.len;
if (len > PXL8_MAX_CYCLE_LEN) len = PXL8_MAX_CYCLE_LEN;
for (u8 i = 0; i < len; i++) {
pal->base_colors[slot][i] = pal->colors[start + i];
}
}
void pxl8_palette_set_cycle_colors(pxl8_palette* pal, u8 slot, const u32* colors, u8 count) {
if (!pal || !colors || slot >= PXL8_MAX_CYCLES) return;
pxl8_cycle_range* cycle = &pal->cycles[slot];
u8 start = cycle->start;
u8 len = cycle->len;
if (len > count) len = count;
if (len > PXL8_MAX_CYCLE_LEN) len = PXL8_MAX_CYCLE_LEN;
for (u8 i = 0; i < len; i++) {
pal->base_colors[slot][i] = colors[i];
pal->colors[start + i] = colors[i];
}
}
void pxl8_palette_set_cycle_phase(pxl8_palette* pal, u8 slot, f32 phase) {
if (!pal || slot >= PXL8_MAX_CYCLES) return;
if (phase < 0.0f) phase = 0.0f;
if (phase > 1.0f) phase = 1.0f;
pal->phases[slot] = phase;
update_cycle_colors(pal, slot);
}
void pxl8_palette_tick(pxl8_palette* pal, u16 delta_ticks) {
if (!pal) return;
for (u8 slot = 0; slot < PXL8_MAX_CYCLES; slot++) {
pxl8_cycle_range* cycle = &pal->cycles[slot];
if (cycle->period == 0 || cycle->len < 2) continue;
f32 delta = (f32)delta_ticks / (f32)cycle->period;
f32 dir = (f32)pal->directions[slot];
pal->phases[slot] += delta * dir;
switch (cycle->mode) {
case PXL8_CYCLE_LOOP:
while (pal->phases[slot] >= 1.0f) pal->phases[slot] -= 1.0f;
while (pal->phases[slot] < 0.0f) pal->phases[slot] += 1.0f;
break;
case PXL8_CYCLE_PINGPONG:
if (pal->phases[slot] >= 1.0f) {
pal->phases[slot] = 1.0f - (pal->phases[slot] - 1.0f);
pal->directions[slot] = -1;
} else if (pal->phases[slot] <= 0.0f) {
pal->phases[slot] = -pal->phases[slot];
pal->directions[slot] = 1;
}
break;
case PXL8_CYCLE_ONCE:
if (pal->phases[slot] < 0.0f) pal->phases[slot] = 0.0f;
if (pal->phases[slot] > 1.0f) pal->phases[slot] = 1.0f;
break;
}
update_cycle_colors(pal, slot);
}
}
pxl8_cycle_range pxl8_cycle_range_create(u8 start, u8 len, u16 period) {
pxl8_cycle_range range = {
.easing = PXL8_EASE_LINEAR,
.interpolate = true,
.len = len,
.mode = PXL8_CYCLE_LOOP,
.period = period,
.start = start,
};
return range;
}
pxl8_cycle_range pxl8_cycle_range_disabled(void) {
pxl8_cycle_range range = {
.easing = PXL8_EASE_LINEAR,
.interpolate = false,
.len = 0,
.mode = PXL8_CYCLE_LOOP,
.period = 0,
.start = 0,
};
return range;
}

70
src/gfx/pxl8_palette.h Normal file
View file

@ -0,0 +1,70 @@
#pragma once
#include "pxl8_types.h"
#define PXL8_PALETTE_SIZE 256
#define PXL8_MAX_CYCLES 8
#define PXL8_MAX_CYCLE_LEN 16
typedef struct pxl8_palette pxl8_palette;
typedef enum pxl8_cycle_mode {
PXL8_CYCLE_LOOP,
PXL8_CYCLE_ONCE,
PXL8_CYCLE_PINGPONG,
} pxl8_cycle_mode;
typedef enum pxl8_easing {
PXL8_EASE_LINEAR,
PXL8_EASE_IN,
PXL8_EASE_IN_OUT,
PXL8_EASE_OUT,
} pxl8_easing;
typedef struct pxl8_cycle_range {
pxl8_easing easing;
bool interpolate;
u8 len;
pxl8_cycle_mode mode;
u16 period;
u8 start;
} pxl8_cycle_range;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_palette* pxl8_palette_create(void);
void pxl8_palette_destroy(pxl8_palette* pal);
pxl8_result pxl8_palette_load_ase(pxl8_palette* pal, const char* path);
u16 pxl8_palette_color_count(const pxl8_palette* pal);
u8* pxl8_palette_color_ramp(pxl8_palette* pal);
u32* pxl8_palette_colors(pxl8_palette* pal);
u8 pxl8_palette_ramp_index(const pxl8_palette* pal, u8 position);
u8 pxl8_palette_ramp_position(const pxl8_palette* pal, u8 index);
u8 pxl8_palette_find_closest(const pxl8_palette* pal, u8 r, u8 g, u8 b);
u32 pxl8_palette_color(const pxl8_palette* pal, u8 idx);
i32 pxl8_palette_index(const pxl8_palette* pal, u32 color);
void pxl8_palette_get_rgb(const pxl8_palette* pal, u8 idx, u8* r, u8* g, u8* b);
void pxl8_palette_get_rgba(const pxl8_palette* pal, u8 idx, u8* r, u8* g, u8* b, u8* a);
void pxl8_palette_set(pxl8_palette* pal, u8 idx, u32 color);
void pxl8_palette_set_rgb(pxl8_palette* pal, u8 idx, u8 r, u8 g, u8 b);
void pxl8_palette_set_rgba(pxl8_palette* pal, u8 idx, u8 r, u8 g, u8 b, u8 a);
void pxl8_palette_fill_gradient(pxl8_palette* pal, u8 start, u8 count, u32 from, u32 to);
void pxl8_palette_fill_gradient_rgb(pxl8_palette* pal, u8 start, u8 count, u8 r0, u8 g0, u8 b0, u8 r1, u8 g1, u8 b1);
void pxl8_palette_reset_cycle(pxl8_palette* pal, u8 slot);
void pxl8_palette_set_cycle(pxl8_palette* pal, u8 slot, pxl8_cycle_range range);
void pxl8_palette_set_cycle_colors(pxl8_palette* pal, u8 slot, const u32* colors, u8 count);
void pxl8_palette_set_cycle_phase(pxl8_palette* pal, u8 slot, f32 phase);
void pxl8_palette_tick(pxl8_palette* pal, u16 delta_ticks);
pxl8_cycle_range pxl8_cycle_range_create(u8 start, u8 len, u16 period);
pxl8_cycle_range pxl8_cycle_range_disabled(void);
#ifdef __cplusplus
}
#endif

322
src/gfx/pxl8_particles.c Normal file
View file

@ -0,0 +1,322 @@
#include "pxl8_particles.h"
#include <stdlib.h>
#include "pxl8_gfx.h"
#include "pxl8_gfx2d.h"
#include "pxl8_palette.h"
#include "pxl8_rng.h"
struct pxl8_particles {
pxl8_particle* particles;
pxl8_palette* palette;
pxl8_rng* rng;
u32 alive_count;
u32 count;
u32 max_count;
f32 x, y;
f32 spread_x, spread_y;
f32 drag;
f32 gravity_x, gravity_y;
f32 turbulence;
f32 spawn_rate;
f32 spawn_timer;
u8 color_min, color_max;
f32 life_min, life_max;
f32 size_min, size_max;
f32 vx_min, vx_max, vy_min, vy_max;
pxl8_particle_render_fn render_fn;
pxl8_particle_spawn_fn spawn_fn;
pxl8_particle_update_fn update_fn;
void* userdata;
};
pxl8_particles* pxl8_particles_create(u32 max_count, pxl8_rng* rng) {
pxl8_particles* ps = calloc(1, sizeof(pxl8_particles));
if (!ps) return NULL;
ps->particles = calloc(max_count, sizeof(pxl8_particle));
if (!ps->particles) {
free(ps);
return NULL;
}
ps->rng = rng;
ps->max_count = max_count;
ps->drag = 0.98f;
ps->gravity_y = 100.0f;
ps->spawn_rate = 10.0f;
ps->color_min = ps->color_max = 15;
ps->life_min = ps->life_max = 1.0f;
ps->size_min = ps->size_max = 1.0f;
return ps;
}
void pxl8_particles_destroy(pxl8_particles* ps) {
if (!ps) return;
free(ps->particles);
free(ps);
}
void pxl8_particles_clear(pxl8_particles* ps) {
if (!ps || !ps->particles) return;
for (u32 i = 0; i < ps->max_count; i++) {
ps->particles[i].life = 0;
ps->particles[i].flags = 0;
}
ps->alive_count = 0;
ps->spawn_timer = 0;
}
void pxl8_particles_emit(pxl8_particles* ps, u32 count) {
if (!ps || !ps->particles) return;
for (u32 i = 0; i < count && ps->alive_count < ps->max_count; i++) {
for (u32 j = 0; j < ps->max_count; j++) {
if (ps->particles[j].life <= 0) {
pxl8_particle* p = &ps->particles[j];
f32 life = ps->life_min + pxl8_rng_f32(ps->rng) * (ps->life_max - ps->life_min);
p->life = life;
p->max_life = life;
p->x = ps->x + (pxl8_rng_f32(ps->rng) - 0.5f) * ps->spread_x;
p->y = ps->y + (pxl8_rng_f32(ps->rng) - 0.5f) * ps->spread_y;
p->z = 0;
p->vx = ps->vx_min + pxl8_rng_f32(ps->rng) * (ps->vx_max - ps->vx_min);
p->vy = ps->vy_min + pxl8_rng_f32(ps->rng) * (ps->vy_max - ps->vy_min);
p->vz = 0;
p->ax = ps->gravity_x;
p->ay = ps->gravity_y;
p->az = 0;
u8 ramp_range = ps->color_max - ps->color_min + 1;
u8 ramp_pos = ps->color_min + (pxl8_rng_next(ps->rng) % ramp_range);
u8 color = ps->palette
? pxl8_palette_ramp_index(ps->palette, ramp_pos)
: ramp_pos;
p->color = p->start_color = p->end_color = color;
p->size = ps->size_min + pxl8_rng_f32(ps->rng) * (ps->size_max - ps->size_min);
p->angle = 0;
p->spin = 0;
p->flags = 1;
if (ps->spawn_fn) {
ps->spawn_fn(ps, p);
}
ps->alive_count++;
break;
}
}
}
}
void pxl8_particles_render(pxl8_particles* ps, pxl8_gfx* gfx) {
if (!ps || !ps->particles || !gfx) return;
for (u32 i = 0; i < ps->max_count; i++) {
pxl8_particle* p = &ps->particles[i];
if (p->life > 0 && p->flags) {
if (ps->render_fn) {
ps->render_fn(gfx, p, ps->userdata);
} else {
i32 x = (i32)p->x;
i32 y = (i32)p->y;
if (x >= 0 && x < pxl8_gfx_get_width(gfx) && y >= 0 && y < pxl8_gfx_get_height(gfx)) {
pxl8_2d_pixel(gfx, x, y, p->color);
}
}
}
}
}
void pxl8_particles_update(pxl8_particles* ps, f32 dt) {
if (!ps || !ps->particles) return;
if (ps->spawn_rate > 0.0f) {
ps->spawn_timer += dt;
f32 spawn_interval = 1.0f / ps->spawn_rate;
u32 max_spawns_per_frame = ps->max_count / 10;
if (max_spawns_per_frame < 1) max_spawns_per_frame = 1;
u32 spawn_count = 0;
while (ps->spawn_timer >= spawn_interval && spawn_count < max_spawns_per_frame) {
pxl8_particles_emit(ps, 1);
ps->spawn_timer -= spawn_interval;
spawn_count++;
}
}
for (u32 i = 0; i < ps->max_count; i++) {
pxl8_particle* p = &ps->particles[i];
if (p->life > 0) {
if (ps->update_fn) {
ps->update_fn(p, dt, ps->userdata);
} else {
p->vx += p->ax * dt;
p->vy += p->ay * dt;
p->vz += p->az * dt;
p->vx *= ps->drag;
p->vy *= ps->drag;
p->vz *= ps->drag;
p->x += p->vx * dt;
p->y += p->vy * dt;
p->z += p->vz * dt;
p->angle += p->spin * dt;
}
p->life -= dt / p->max_life;
if (p->life <= 0) {
p->flags = 0;
ps->alive_count--;
}
}
}
}
u32 pxl8_particles_count(const pxl8_particles* ps) {
return ps ? ps->alive_count : 0;
}
u32 pxl8_particles_max_count(const pxl8_particles* ps) {
return ps ? ps->max_count : 0;
}
pxl8_particle* pxl8_particles_get(pxl8_particles* ps, u32 index) {
if (!ps || index >= ps->max_count) return NULL;
return &ps->particles[index];
}
pxl8_rng* pxl8_particles_rng(pxl8_particles* ps) {
return ps ? ps->rng : NULL;
}
f32 pxl8_particles_get_drag(const pxl8_particles* ps) {
return ps ? ps->drag : 0.0f;
}
f32 pxl8_particles_get_gravity_x(const pxl8_particles* ps) {
return ps ? ps->gravity_x : 0.0f;
}
f32 pxl8_particles_get_gravity_y(const pxl8_particles* ps) {
return ps ? ps->gravity_y : 0.0f;
}
f32 pxl8_particles_get_spawn_rate(const pxl8_particles* ps) {
return ps ? ps->spawn_rate : 0.0f;
}
f32 pxl8_particles_get_spread_x(const pxl8_particles* ps) {
return ps ? ps->spread_x : 0.0f;
}
f32 pxl8_particles_get_spread_y(const pxl8_particles* ps) {
return ps ? ps->spread_y : 0.0f;
}
f32 pxl8_particles_get_turbulence(const pxl8_particles* ps) {
return ps ? ps->turbulence : 0.0f;
}
void* pxl8_particles_get_userdata(const pxl8_particles* ps) {
return ps ? ps->userdata : NULL;
}
f32 pxl8_particles_get_x(const pxl8_particles* ps) {
return ps ? ps->x : 0.0f;
}
f32 pxl8_particles_get_y(const pxl8_particles* ps) {
return ps ? ps->y : 0.0f;
}
void pxl8_particles_set_colors(pxl8_particles* ps, u8 color_min, u8 color_max) {
if (!ps) return;
ps->color_min = color_min;
ps->color_max = color_max;
}
void pxl8_particles_set_drag(pxl8_particles* ps, f32 drag) {
if (ps) ps->drag = drag;
}
void pxl8_particles_set_gravity(pxl8_particles* ps, f32 gx, f32 gy) {
if (!ps) return;
ps->gravity_x = gx;
ps->gravity_y = gy;
}
void pxl8_particles_set_life(pxl8_particles* ps, f32 life_min, f32 life_max) {
if (!ps) return;
ps->life_min = life_min;
ps->life_max = life_max;
}
void pxl8_particles_set_palette(pxl8_particles* ps, pxl8_palette* palette) {
if (ps) ps->palette = palette;
}
void pxl8_particles_set_position(pxl8_particles* ps, f32 x, f32 y) {
if (!ps) return;
ps->x = x;
ps->y = y;
}
void pxl8_particles_set_render_fn(pxl8_particles* ps, pxl8_particle_render_fn fn) {
if (ps) ps->render_fn = fn;
}
void pxl8_particles_set_size(pxl8_particles* ps, f32 size_min, f32 size_max) {
if (!ps) return;
ps->size_min = size_min;
ps->size_max = size_max;
}
void pxl8_particles_set_spawn_fn(pxl8_particles* ps, pxl8_particle_spawn_fn fn) {
if (ps) ps->spawn_fn = fn;
}
void pxl8_particles_set_spawn_rate(pxl8_particles* ps, f32 rate) {
if (ps) ps->spawn_rate = rate;
}
void pxl8_particles_set_spread(pxl8_particles* ps, f32 spread_x, f32 spread_y) {
if (!ps) return;
ps->spread_x = spread_x;
ps->spread_y = spread_y;
}
void pxl8_particles_set_turbulence(pxl8_particles* ps, f32 turbulence) {
if (ps) ps->turbulence = turbulence;
}
void pxl8_particles_set_update_fn(pxl8_particles* ps, pxl8_particle_update_fn fn) {
if (ps) ps->update_fn = fn;
}
void pxl8_particles_set_userdata(pxl8_particles* ps, void* userdata) {
if (ps) ps->userdata = userdata;
}
void pxl8_particles_set_velocity(pxl8_particles* ps, f32 vx_min, f32 vx_max, f32 vy_min, f32 vy_max) {
if (!ps) return;
ps->vx_min = vx_min;
ps->vx_max = vx_max;
ps->vy_min = vy_min;
ps->vy_max = vy_max;
}

75
src/gfx/pxl8_particles.h Normal file
View file

@ -0,0 +1,75 @@
#pragma once
#include "pxl8_types.h"
typedef struct pxl8_gfx pxl8_gfx;
typedef struct pxl8_palette pxl8_palette;
typedef struct pxl8_particles pxl8_particles;
typedef struct pxl8_rng pxl8_rng;
typedef struct pxl8_particle {
f32 angle;
f32 ax, ay, az;
u32 color;
u32 end_color;
u8 flags;
f32 life;
f32 max_life;
f32 size;
f32 spin;
u32 start_color;
f32 vx, vy, vz;
f32 x, y, z;
} pxl8_particle;
typedef void (*pxl8_particle_render_fn)(pxl8_gfx* gfx, pxl8_particle* p, void* userdata);
typedef void (*pxl8_particle_spawn_fn)(pxl8_particles* ps, pxl8_particle* p);
typedef void (*pxl8_particle_update_fn)(pxl8_particle* p, f32 dt, void* userdata);
#ifdef __cplusplus
extern "C" {
#endif
pxl8_particles* pxl8_particles_create(u32 max_count, pxl8_rng* rng);
void pxl8_particles_destroy(pxl8_particles* ps);
void pxl8_particles_clear(pxl8_particles* ps);
void pxl8_particles_emit(pxl8_particles* ps, u32 count);
void pxl8_particles_render(pxl8_particles* ps, pxl8_gfx* gfx);
void pxl8_particles_update(pxl8_particles* ps, f32 dt);
u32 pxl8_particles_count(const pxl8_particles* ps);
u32 pxl8_particles_max_count(const pxl8_particles* ps);
pxl8_particle* pxl8_particles_get(pxl8_particles* ps, u32 index);
pxl8_rng* pxl8_particles_rng(pxl8_particles* ps);
f32 pxl8_particles_get_drag(const pxl8_particles* ps);
f32 pxl8_particles_get_gravity_x(const pxl8_particles* ps);
f32 pxl8_particles_get_gravity_y(const pxl8_particles* ps);
f32 pxl8_particles_get_spawn_rate(const pxl8_particles* ps);
f32 pxl8_particles_get_spread_x(const pxl8_particles* ps);
f32 pxl8_particles_get_spread_y(const pxl8_particles* ps);
f32 pxl8_particles_get_turbulence(const pxl8_particles* ps);
void* pxl8_particles_get_userdata(const pxl8_particles* ps);
f32 pxl8_particles_get_x(const pxl8_particles* ps);
f32 pxl8_particles_get_y(const pxl8_particles* ps);
void pxl8_particles_set_colors(pxl8_particles* ps, u8 color_min, u8 color_max);
void pxl8_particles_set_drag(pxl8_particles* ps, f32 drag);
void pxl8_particles_set_gravity(pxl8_particles* ps, f32 gx, f32 gy);
void pxl8_particles_set_life(pxl8_particles* ps, f32 life_min, f32 life_max);
void pxl8_particles_set_palette(pxl8_particles* ps, pxl8_palette* palette);
void pxl8_particles_set_position(pxl8_particles* ps, f32 x, f32 y);
void pxl8_particles_set_render_fn(pxl8_particles* ps, pxl8_particle_render_fn fn);
void pxl8_particles_set_size(pxl8_particles* ps, f32 size_min, f32 size_max);
void pxl8_particles_set_spawn_fn(pxl8_particles* ps, pxl8_particle_spawn_fn fn);
void pxl8_particles_set_spawn_rate(pxl8_particles* ps, f32 rate);
void pxl8_particles_set_spread(pxl8_particles* ps, f32 spread_x, f32 spread_y);
void pxl8_particles_set_turbulence(pxl8_particles* ps, f32 turbulence);
void pxl8_particles_set_update_fn(pxl8_particles* ps, pxl8_particle_update_fn fn);
void pxl8_particles_set_userdata(pxl8_particles* ps, void* userdata);
void pxl8_particles_set_velocity(pxl8_particles* ps, f32 vx_min, f32 vx_max, f32 vy_min, f32 vy_max);
#ifdef __cplusplus
}
#endif

614
src/gfx/pxl8_tilemap.c Normal file
View file

@ -0,0 +1,614 @@
#include "pxl8_tilemap.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_ase.h"
#include "pxl8_log.h"
#include "pxl8_macros.h"
#include "pxl8_tilesheet.h"
struct pxl8_tilesheet {
u8* data;
bool* tile_valid;
u32 height;
u32 tile_size;
u32 tiles_per_row;
u32 total_tiles;
u32 width;
pxl8_pixel_mode pixel_mode;
u32 ref_count;
pxl8_tile_animation* animations;
u32 animation_count;
pxl8_tile_properties* properties;
};
struct pxl8_tilemap_layer {
u32 allocated_chunks;
u32 chunk_count;
pxl8_tile_chunk** chunks;
u32 chunks_high;
u32 chunks_wide;
u8 opacity;
bool visible;
};
struct pxl8_tilemap {
u32 active_layers;
i32 camera_x;
i32 camera_y;
u32 height;
pxl8_tilemap_layer layers[PXL8_MAX_TILE_LAYERS];
u32 tile_size;
pxl8_tilesheet* tilesheet;
u32 tile_user_data_capacity;
void** tile_user_data;
u32 width;
};
static inline u32 pxl8_chunk_index(u32 x, u32 y, u32 chunks_wide) {
return (y >> 4) * chunks_wide + (x >> 4);
}
static pxl8_tile_chunk* pxl8_get_or_create_chunk(pxl8_tilemap_layer* layer, u32 x, u32 y) {
u32 chunk_x = x >> 4;
u32 chunk_y = y >> 4;
u32 idx = chunk_y * layer->chunks_wide + chunk_x;
if (idx >= layer->chunks_wide * layer->chunks_high) return NULL;
if (!layer->chunks[idx]) {
layer->chunks[idx] = calloc(1, sizeof(pxl8_tile_chunk));
if (!layer->chunks[idx]) return NULL;
layer->chunks[idx]->chunk_x = chunk_x;
layer->chunks[idx]->chunk_y = chunk_y;
layer->chunks[idx]->empty = true;
layer->allocated_chunks++;
}
return layer->chunks[idx];
}
pxl8_tilemap* pxl8_tilemap_create(u32 width, u32 height, u32 tile_size) {
if (width > PXL8_MAX_TILEMAP_WIDTH || height > PXL8_MAX_TILEMAP_HEIGHT) {
return NULL;
}
pxl8_tilemap* tilemap = calloc(1, sizeof(pxl8_tilemap));
if (!tilemap) return NULL;
tilemap->width = width;
tilemap->height = height;
tilemap->tile_size = tile_size ? tile_size : PXL8_TILE_SIZE;
tilemap->active_layers = 1;
tilemap->tilesheet = pxl8_tilesheet_create(tilemap->tile_size);
if (!tilemap->tilesheet) {
free(tilemap);
return NULL;
}
tilemap->tile_user_data = NULL;
tilemap->tile_user_data_capacity = 0;
u32 chunks_wide = (width + PXL8_CHUNK_SIZE - 1) / PXL8_CHUNK_SIZE;
u32 chunks_high = (height + PXL8_CHUNK_SIZE - 1) / PXL8_CHUNK_SIZE;
for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) {
pxl8_tilemap_layer* layer = &tilemap->layers[i];
layer->chunks_wide = chunks_wide;
layer->chunks_high = chunks_high;
layer->chunk_count = chunks_wide * chunks_high;
layer->visible = (i == 0);
layer->opacity = 255;
layer->chunks = calloc(layer->chunk_count, sizeof(pxl8_tile_chunk*));
if (!layer->chunks) {
for (u32 j = 0; j < i; j++) {
free(tilemap->layers[j].chunks);
}
if (tilemap->tilesheet) pxl8_tilesheet_destroy(tilemap->tilesheet);
free(tilemap);
return NULL;
}
}
return tilemap;
}
void pxl8_tilemap_destroy(pxl8_tilemap* tilemap) {
if (!tilemap) return;
for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) {
pxl8_tilemap_layer* layer = &tilemap->layers[i];
if (layer->chunks) {
for (u32 j = 0; j < layer->chunk_count; j++) {
if (layer->chunks[j]) {
free(layer->chunks[j]);
}
}
free(layer->chunks);
}
}
if (tilemap->tilesheet) pxl8_tilesheet_unref(tilemap->tilesheet);
if (tilemap->tile_user_data) free(tilemap->tile_user_data);
free(tilemap);
}
u32 pxl8_tilemap_get_width(const pxl8_tilemap* tilemap) {
return tilemap ? tilemap->width : 0;
}
u32 pxl8_tilemap_get_height(const pxl8_tilemap* tilemap) {
return tilemap ? tilemap->height : 0;
}
u32 pxl8_tilemap_get_tile_size(const pxl8_tilemap* tilemap) {
return tilemap ? tilemap->tile_size : 0;
}
void pxl8_tilemap_set_tile_user_data(pxl8_tilemap* tilemap, u16 tile_id, void* user_data) {
if (!tilemap || tile_id == 0) return;
if (tile_id >= tilemap->tile_user_data_capacity) {
u32 new_capacity = tile_id + 64;
void** new_data = realloc(tilemap->tile_user_data, new_capacity * sizeof(void*));
if (!new_data) return;
for (u32 i = tilemap->tile_user_data_capacity; i < new_capacity; i++) {
new_data[i] = NULL;
}
tilemap->tile_user_data = new_data;
tilemap->tile_user_data_capacity = new_capacity;
}
tilemap->tile_user_data[tile_id] = user_data;
}
void* pxl8_tilemap_get_tile_user_data(const pxl8_tilemap* tilemap, u16 tile_id) {
if (!tilemap || tile_id == 0 || tile_id >= tilemap->tile_user_data_capacity) return NULL;
return tilemap->tile_user_data[tile_id];
}
pxl8_result pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet) {
if (!tilemap || !tilesheet) return PXL8_ERROR_NULL_POINTER;
if (tilemap->tilesheet) {
pxl8_tilesheet_unref(tilemap->tilesheet);
}
tilemap->tilesheet = tilesheet;
pxl8_tilesheet_ref(tilesheet);
u32 tilesheet_size = pxl8_tilesheet_get_tile_size(tilesheet);
if (tilesheet_size != tilemap->tile_size) {
pxl8_warn("Tilesheet tile size (%d) differs from tilemap tile size (%d)",
tilesheet_size, tilemap->tile_size);
tilemap->tile_size = tilesheet_size;
}
return PXL8_OK;
}
pxl8_result pxl8_tilemap_set_tile(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 tile_id, u8 flags) {
if (!tilemap) return PXL8_ERROR_NULL_POINTER;
if (layer >= PXL8_MAX_TILE_LAYERS) return PXL8_ERROR_INVALID_ARGUMENT;
if (x >= tilemap->width || y >= tilemap->height) return PXL8_ERROR_INVALID_COORDINATE;
pxl8_tilemap_layer* l = &tilemap->layers[layer];
pxl8_tile_chunk* chunk = pxl8_get_or_create_chunk(l, x, y);
if (!chunk) return PXL8_ERROR_OUT_OF_MEMORY;
u32 local_x = x & PXL8_CHUNK_MASK;
u32 local_y = y & PXL8_CHUNK_MASK;
u32 idx = local_y * PXL8_CHUNK_SIZE + local_x;
chunk->tiles[idx] = pxl8_tile_pack(tile_id, flags, 0);
if (tile_id != 0) {
chunk->empty = false;
}
if (layer >= tilemap->active_layers) {
tilemap->active_layers = layer + 1;
l->visible = true;
}
return PXL8_OK;
}
pxl8_tile pxl8_tilemap_get_tile(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y) {
if (!tilemap || layer >= PXL8_MAX_TILE_LAYERS) return 0;
if (x >= tilemap->width || y >= tilemap->height) return 0;
const pxl8_tilemap_layer* l = &tilemap->layers[layer];
u32 chunk_idx = pxl8_chunk_index(x, y, l->chunks_wide);
if (chunk_idx >= l->chunk_count || !l->chunks[chunk_idx]) return 0;
const pxl8_tile_chunk* chunk = l->chunks[chunk_idx];
u32 local_x = x & PXL8_CHUNK_MASK;
u32 local_y = y & PXL8_CHUNK_MASK;
u32 idx = local_y * PXL8_CHUNK_SIZE + local_x;
return chunk->tiles[idx];
}
void pxl8_tilemap_set_camera(pxl8_tilemap* tilemap, i32 x, i32 y) {
if (!tilemap) return;
tilemap->camera_x = x;
tilemap->camera_y = y;
}
void pxl8_tilemap_get_view(const pxl8_tilemap* tilemap, const pxl8_gfx* gfx, pxl8_tilemap_view* view) {
if (!tilemap || !gfx || !view) return;
view->x = -tilemap->camera_x;
view->y = -tilemap->camera_y;
view->width = pxl8_gfx_get_width(gfx);
view->height = pxl8_gfx_get_height(gfx);
view->tile_start_x = pxl8_max(0, tilemap->camera_x / (i32)tilemap->tile_size);
view->tile_start_y = pxl8_max(0, tilemap->camera_y / (i32)tilemap->tile_size);
view->tile_end_x = pxl8_min((i32)tilemap->width,
(tilemap->camera_x + view->width) / (i32)tilemap->tile_size + 1);
view->tile_end_y = pxl8_min((i32)tilemap->height,
(tilemap->camera_y + view->height) / (i32)tilemap->tile_size + 1);
}
void pxl8_tilemap_render_tile(const pxl8_tilemap* tilemap, pxl8_gfx* gfx, u16 tile_id, i32 x, i32 y, u8 flags) {
if (!tilemap || !gfx || !tilemap->tilesheet) return;
pxl8_tilesheet_render_tile(tilemap->tilesheet, gfx, tile_id, x, y, flags);
}
void pxl8_tilemap_render_layer(const pxl8_tilemap* tilemap, pxl8_gfx* gfx, u32 layer) {
if (!tilemap || !gfx || layer >= tilemap->active_layers) return;
if (!tilemap->tilesheet) {
pxl8_warn("No tilesheet set for tilemap");
return;
}
const pxl8_tilemap_layer* l = &tilemap->layers[layer];
if (!l->visible) return;
i32 view_left = tilemap->camera_x / tilemap->tile_size;
i32 view_top = tilemap->camera_y / tilemap->tile_size;
i32 view_right = (tilemap->camera_x + pxl8_gfx_get_width(gfx)) / tilemap->tile_size + 1;
i32 view_bottom = (tilemap->camera_y + pxl8_gfx_get_height(gfx)) / tilemap->tile_size + 1;
u32 chunk_left = pxl8_max(0, view_left >> 4);
u32 chunk_top = pxl8_max(0, view_top >> 4);
u32 chunk_right = pxl8_min((tilemap->width + 15) >> 4, (u32)((view_right >> 4) + 1));
u32 chunk_bottom = pxl8_min((tilemap->height + 15) >> 4, (u32)((view_bottom >> 4) + 1));
for (u32 cy = chunk_top; cy < chunk_bottom; cy++) {
for (u32 cx = chunk_left; cx < chunk_right; cx++) {
u32 chunk_idx = cy * l->chunks_wide + cx;
if (chunk_idx >= l->chunk_count || !l->chunks[chunk_idx]) continue;
const pxl8_tile_chunk* chunk = l->chunks[chunk_idx];
if (chunk->empty) continue;
u32 tile_start_x = pxl8_max(0, view_left - (i32)(cx << 4));
u32 tile_end_x = pxl8_min(PXL8_CHUNK_SIZE, view_right - (i32)(cx << 4));
u32 tile_start_y = pxl8_max(0, view_top - (i32)(cy << 4));
u32 tile_end_y = pxl8_min(PXL8_CHUNK_SIZE, view_bottom - (i32)(cy << 4));
for (u32 ty = tile_start_y; ty < tile_end_y; ty++) {
for (u32 tx = tile_start_x; tx < tile_end_x; tx++) {
u32 idx = ty * PXL8_CHUNK_SIZE + tx;
pxl8_tile tile = chunk->tiles[idx];
u16 tile_id = pxl8_tile_get_id(tile);
if (tile_id == 0) continue;
i32 world_x = (cx << 4) + tx;
i32 world_y = (cy << 4) + ty;
i32 screen_x = world_x * tilemap->tile_size - tilemap->camera_x;
i32 screen_y = world_y * tilemap->tile_size - tilemap->camera_y;
u8 flags = pxl8_tile_get_flags(tile);
if (flags & PXL8_TILE_ANIMATED && tilemap->tilesheet) {
tile_id = pxl8_tilesheet_get_animated_frame(tilemap->tilesheet, tile_id);
}
pxl8_tilesheet_render_tile(tilemap->tilesheet, gfx, tile_id, screen_x, screen_y, flags);
}
}
}
}
}
void pxl8_tilemap_render(const pxl8_tilemap* tilemap, pxl8_gfx* gfx) {
if (!tilemap || !gfx) return;
for (u32 layer = 0; layer < tilemap->active_layers; layer++) {
pxl8_tilemap_render_layer(tilemap, gfx, layer);
}
}
bool pxl8_tilemap_is_solid(const pxl8_tilemap* tilemap, u32 x, u32 y) {
if (!tilemap) return true;
u32 tile_x = x / tilemap->tile_size;
u32 tile_y = y / tilemap->tile_size;
for (u32 layer = 0; layer < tilemap->active_layers; layer++) {
pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tile_x, tile_y);
if (pxl8_tile_get_flags(tile) & PXL8_TILE_SOLID) return true;
}
return false;
}
bool pxl8_tilemap_check_collision(const pxl8_tilemap* tilemap, i32 x, i32 y, i32 w, i32 h) {
if (!tilemap) return true;
i32 left = x / tilemap->tile_size;
i32 top = y / tilemap->tile_size;
i32 right = (x + w - 1) / tilemap->tile_size;
i32 bottom = (y + h - 1) / tilemap->tile_size;
for (i32 ty = top; ty <= bottom; ty++) {
for (i32 tx = left; tx <= right; tx++) {
if (tx < 0 || tx >= (i32)tilemap->width ||
ty < 0 || ty >= (i32)tilemap->height) return true;
for (u32 layer = 0; layer < tilemap->active_layers; layer++) {
pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tx, ty);
if (pxl8_tile_get_flags(tile) & PXL8_TILE_SOLID) return true;
}
}
}
return false;
}
u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y) {
if (!tilemap) return 0;
pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, x, y);
return pxl8_tile_get_id(tile);
}
void pxl8_tilemap_update(pxl8_tilemap* tilemap, f32 delta_time) {
if (!tilemap || !tilemap->tilesheet) return;
pxl8_tilesheet_update_animations(tilemap->tilesheet, delta_time);
}
static u8 pxl8_tilemap_get_neighbors(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 match_id) {
u8 neighbors = 0;
if (y > 0 && pxl8_tilemap_get_tile_id(tilemap, layer, x, y - 1) == match_id)
neighbors |= 1 << 0;
if (x < tilemap->width - 1 && pxl8_tilemap_get_tile_id(tilemap, layer, x + 1, y) == match_id)
neighbors |= 1 << 1;
if (y < tilemap->height - 1 && pxl8_tilemap_get_tile_id(tilemap, layer, x, y + 1) == match_id)
neighbors |= 1 << 2;
if (x > 0 && pxl8_tilemap_get_tile_id(tilemap, layer, x - 1, y) == match_id)
neighbors |= 1 << 3;
if (y > 0 && x < tilemap->width - 1 && pxl8_tilemap_get_tile_id(tilemap, layer, x + 1, y - 1) == match_id)
neighbors |= 1 << 4;
if (y < tilemap->height - 1 && x < tilemap->width - 1 && pxl8_tilemap_get_tile_id(tilemap, layer, x + 1, y + 1) == match_id)
neighbors |= 1 << 5;
if (y < tilemap->height - 1 && x > 0 && pxl8_tilemap_get_tile_id(tilemap, layer, x - 1, y + 1) == match_id)
neighbors |= 1 << 6;
if (y > 0 && x > 0 && pxl8_tilemap_get_tile_id(tilemap, layer, x - 1, y - 1) == match_id)
neighbors |= 1 << 7;
return neighbors;
}
pxl8_result pxl8_tilemap_set_tile_auto(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y,
u16 base_tile_id, u8 flags) {
if (!tilemap || !tilemap->tilesheet) return PXL8_ERROR_NULL_POINTER;
pxl8_result result = pxl8_tilemap_set_tile(tilemap, layer, x, y, base_tile_id, flags | PXL8_TILE_AUTOTILE);
if (result != PXL8_OK) return result;
pxl8_tilemap_update_autotiles(tilemap, layer,
x > 0 ? x - 1 : x, y > 0 ? y - 1 : y,
x < tilemap->width - 1 ? 3 : 2,
y < tilemap->height - 1 ? 3 : 2);
return PXL8_OK;
}
void pxl8_tilemap_update_autotiles(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u32 w, u32 h) {
if (!tilemap || !tilemap->tilesheet) return;
for (u32 ty = y; ty < y + h && ty < tilemap->height; ty++) {
for (u32 tx = x; tx < x + w && tx < tilemap->width; tx++) {
pxl8_tile tile = pxl8_tilemap_get_tile(tilemap, layer, tx, ty);
u8 flags = pxl8_tile_get_flags(tile);
if (flags & PXL8_TILE_AUTOTILE) {
u16 base_id = pxl8_tile_get_id(tile);
u8 neighbors = pxl8_tilemap_get_neighbors(tilemap, layer, tx, ty, base_id);
u16 new_id = pxl8_tilesheet_apply_autotile(tilemap->tilesheet, base_id, neighbors);
if (new_id != base_id) {
pxl8_tilemap_layer* l = &tilemap->layers[layer];
pxl8_tile_chunk* chunk = pxl8_get_or_create_chunk(l, tx, ty);
if (chunk) {
u32 local_x = tx & PXL8_CHUNK_MASK;
u32 local_y = ty & PXL8_CHUNK_MASK;
u32 idx = local_y * PXL8_CHUNK_SIZE + local_x;
chunk->tiles[idx] = pxl8_tile_pack(new_id, flags, pxl8_tile_get_palette(tile));
}
}
}
}
}
}
u32 pxl8_tilemap_get_memory_usage(const pxl8_tilemap* tilemap) {
if (!tilemap) return 0;
u32 total = sizeof(pxl8_tilemap);
for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) {
const pxl8_tilemap_layer* layer = &tilemap->layers[i];
total += layer->chunk_count * sizeof(pxl8_tile_chunk*);
total += layer->allocated_chunks * sizeof(pxl8_tile_chunk);
}
return total;
}
void pxl8_tilemap_compress(pxl8_tilemap* tilemap) {
if (!tilemap) return;
for (u32 i = 0; i < PXL8_MAX_TILE_LAYERS; i++) {
pxl8_tilemap_layer* layer = &tilemap->layers[i];
for (u32 j = 0; j < layer->chunk_count; j++) {
pxl8_tile_chunk* chunk = layer->chunks[j];
if (!chunk) continue;
bool has_tiles = false;
for (u32 k = 0; k < PXL8_CHUNK_SIZE * PXL8_CHUNK_SIZE; k++) {
if (pxl8_tile_get_id(chunk->tiles[k]) != 0) {
has_tiles = true;
break;
}
}
if (!has_tiles) {
free(chunk);
layer->chunks[j] = NULL;
layer->allocated_chunks--;
} else {
chunk->empty = false;
}
}
}
}
pxl8_result pxl8_tilemap_load_ase(pxl8_tilemap* tilemap, const char* filepath, u32 layer) {
if (!tilemap || !filepath) return PXL8_ERROR_NULL_POINTER;
if (!tilemap->tilesheet) return PXL8_ERROR_NULL_POINTER;
if (layer >= PXL8_MAX_TILE_LAYERS) return PXL8_ERROR_INVALID_ARGUMENT;
pxl8_ase_file ase_file = {0};
pxl8_result result = pxl8_ase_load(filepath, &ase_file);
if (result != PXL8_OK) {
pxl8_error("Failed to load ASE file: %s", filepath);
return result;
}
if (ase_file.tileset_count == 0) {
pxl8_error("ASE file has no tileset - must be created as Tilemap in Aseprite: %s", filepath);
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
pxl8_ase_tileset* tileset = &ase_file.tilesets[0];
if (tileset->tile_width != tilemap->tile_size || tileset->tile_height != tilemap->tile_size) {
pxl8_error("Tileset tile size (%ux%u) doesn't match tilemap tile size (%u)",
tileset->tile_width, tileset->tile_height, tilemap->tile_size);
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
pxl8_info("Loading tilemap from %s: %u tiles, %ux%u each",
filepath, tileset->tile_count, tileset->tile_width, tileset->tile_height);
if (!(tileset->flags & 2)) {
pxl8_error("Tileset has no embedded tiles - external tilesets not yet supported");
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
if (!tileset->pixels) {
pxl8_error("Tileset has no pixel data");
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
u32 tiles_per_row = 16;
u32 tilesheet_rows = (tileset->tile_count + tiles_per_row - 1) / tiles_per_row;
u32 tilesheet_width = tiles_per_row * tilemap->tile_size;
u32 tilesheet_height = tilesheet_rows * tilemap->tile_size;
if (tilemap->tilesheet->data) free(tilemap->tilesheet->data);
tilemap->tilesheet->data = calloc(tilesheet_width * tilesheet_height, 1);
if (!tilemap->tilesheet->data) {
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_OUT_OF_MEMORY;
}
tilemap->tilesheet->width = tilesheet_width;
tilemap->tilesheet->height = tilesheet_height;
tilemap->tilesheet->tiles_per_row = tiles_per_row;
tilemap->tilesheet->total_tiles = tileset->tile_count;
tilemap->tilesheet->pixel_mode = PXL8_PIXEL_INDEXED;
if (tilemap->tilesheet->tile_valid) free(tilemap->tilesheet->tile_valid);
tilemap->tilesheet->tile_valid = calloc(tileset->tile_count + 1, sizeof(bool));
for (u32 i = 0; i < tileset->tile_count; i++) {
u32 sheet_row = i / tiles_per_row;
u32 sheet_col = i % tiles_per_row;
u32 src_offset = i * tilemap->tile_size * tilemap->tile_size;
for (u32 y = 0; y < tilemap->tile_size; y++) {
for (u32 x = 0; x < tilemap->tile_size; x++) {
u32 dst_x = sheet_col * tilemap->tile_size + x;
u32 dst_y = sheet_row * tilemap->tile_size + y;
u32 dst_idx = dst_y * tilesheet_width + dst_x;
u32 src_idx = src_offset + y * tilemap->tile_size + x;
if (src_idx < tileset->pixels_size && dst_idx < tilesheet_width * tilesheet_height) {
tilemap->tilesheet->data[dst_idx] = tileset->pixels[src_idx];
}
}
}
tilemap->tilesheet->tile_valid[i] = true;
}
pxl8_info("Loaded %u tiles into tilesheet (%ux%u)", tileset->tile_count, tilesheet_width, tilesheet_height);
if (ase_file.frame_count == 0 || !ase_file.frames) {
pxl8_error("ASE file has no frames");
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
pxl8_ase_frame* frame = &ase_file.frames[0];
pxl8_ase_cel* tilemap_cel = NULL;
for (u32 i = 0; i < frame->cel_count; i++) {
if (frame->cels[i].cel_type == 3) {
tilemap_cel = &frame->cels[i];
break;
}
}
if (!tilemap_cel || !tilemap_cel->tilemap.tiles) {
pxl8_error("No tilemap cel found in frame 0");
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
pxl8_info("Found tilemap cel: %ux%u tiles", tilemap_cel->tilemap.width, tilemap_cel->tilemap.height);
for (u32 ty = 0; ty < tilemap_cel->tilemap.height && ty < tilemap->height; ty++) {
for (u32 tx = 0; tx < tilemap_cel->tilemap.width && tx < tilemap->width; tx++) {
u32 tile_idx = ty * tilemap_cel->tilemap.width + tx;
u32 tile_value = tilemap_cel->tilemap.tiles[tile_idx];
u16 tile_id = (u16)(tile_value & tilemap_cel->tilemap.tile_id_mask);
u8 flags = 0;
if (tile_value & tilemap_cel->tilemap.x_flip_mask) flags |= PXL8_TILE_FLIP_X;
if (tile_value & tilemap_cel->tilemap.y_flip_mask) flags |= PXL8_TILE_FLIP_Y;
pxl8_tilemap_set_tile(tilemap, layer, tx, ty, tile_id, flags);
}
}
pxl8_ase_destroy(&ase_file);
return PXL8_OK;
}

123
src/gfx/pxl8_tilemap.h Normal file
View file

@ -0,0 +1,123 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_tilesheet.h"
#include "pxl8_types.h"
#define PXL8_TILE_SIZE 8
#define PXL8_MAX_TILEMAP_WIDTH 256
#define PXL8_MAX_TILEMAP_HEIGHT 256
#define PXL8_MAX_TILE_LAYERS 4
#define PXL8_CHUNK_SIZE 16
#define PXL8_CHUNK_MASK 15
typedef enum pxl8_tile_flags {
PXL8_TILE_FLIP_X = 1 << 0,
PXL8_TILE_FLIP_Y = 1 << 1,
PXL8_TILE_SOLID = 1 << 2,
PXL8_TILE_TRIGGER = 1 << 3,
PXL8_TILE_ANIMATED = 1 << 4,
PXL8_TILE_AUTOTILE = 1 << 5,
} pxl8_tile_flags;
#define PXL8_TILE_ID_MASK 0x0000FFFF
#define PXL8_TILE_FLAGS_MASK 0x00FF0000
#define PXL8_TILE_PAL_MASK 0xFF000000
#define PXL8_TILE_ID_SHIFT 0
#define PXL8_TILE_FLAGS_SHIFT 16
#define PXL8_TILE_PAL_SHIFT 24
typedef u32 pxl8_tile;
static inline pxl8_tile pxl8_tile_pack(u16 id, u8 flags, u8 palette_offset) {
return (u32)id | ((u32)flags << 16) | ((u32)palette_offset << 24);
}
static inline u16 pxl8_tile_get_id(pxl8_tile tile) {
return tile & PXL8_TILE_ID_MASK;
}
static inline u8 pxl8_tile_get_flags(pxl8_tile tile) {
return (tile & PXL8_TILE_FLAGS_MASK) >> PXL8_TILE_FLAGS_SHIFT;
}
static inline u8 pxl8_tile_get_palette(pxl8_tile tile) {
return (tile & PXL8_TILE_PAL_MASK) >> PXL8_TILE_PAL_SHIFT;
}
typedef struct pxl8_tile_animation {
u16 current_frame;
f32 frame_duration;
u16 frame_count;
u16* frames;
f32 time_accumulator;
} pxl8_tile_animation;
typedef struct pxl8_tile_properties {
i16 collision_offset_x;
i16 collision_offset_y;
u16 collision_height;
u16 collision_width;
u32 property_flags;
void* user_data;
} pxl8_tile_properties;
typedef struct pxl8_autotile_rule {
u8 neighbor_mask;
u16 tile_id;
} pxl8_autotile_rule;
typedef struct pxl8_tile_chunk {
u32 chunk_x;
u32 chunk_y;
bool empty;
pxl8_tile tiles[PXL8_CHUNK_SIZE * PXL8_CHUNK_SIZE];
} pxl8_tile_chunk;
typedef struct pxl8_tilemap_layer pxl8_tilemap_layer;
typedef struct pxl8_tilemap pxl8_tilemap;
typedef struct pxl8_tilemap_view {
i32 height;
i32 tile_end_x, tile_end_y;
i32 tile_start_x, tile_start_y;
i32 width;
i32 x, y;
} pxl8_tilemap_view;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_tilemap* pxl8_tilemap_create(u32 width, u32 height, u32 tile_size);
void pxl8_tilemap_destroy(pxl8_tilemap* tilemap);
u32 pxl8_tilemap_get_height(const pxl8_tilemap* tilemap);
u32 pxl8_tilemap_get_memory_usage(const pxl8_tilemap* tilemap);
pxl8_tile pxl8_tilemap_get_tile(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y);
u16 pxl8_tilemap_get_tile_id(const pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y);
u32 pxl8_tilemap_get_tile_size(const pxl8_tilemap* tilemap);
void* pxl8_tilemap_get_tile_user_data(const pxl8_tilemap* tilemap, u16 tile_id);
void pxl8_tilemap_get_view(const pxl8_tilemap* tilemap, const pxl8_gfx* gfx, pxl8_tilemap_view* view);
u32 pxl8_tilemap_get_width(const pxl8_tilemap* tilemap);
void pxl8_tilemap_set_camera(pxl8_tilemap* tilemap, i32 x, i32 y);
pxl8_result pxl8_tilemap_set_tile(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 tile_id, u8 flags);
pxl8_result pxl8_tilemap_set_tile_auto(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u16 base_tile_id, u8 flags);
void pxl8_tilemap_set_tile_user_data(pxl8_tilemap* tilemap, u16 tile_id, void* user_data);
pxl8_result pxl8_tilemap_set_tilesheet(pxl8_tilemap* tilemap, pxl8_tilesheet* tilesheet);
pxl8_result pxl8_tilemap_load_ase(pxl8_tilemap* tilemap, const char* filepath, u32 layer);
bool pxl8_tilemap_check_collision(const pxl8_tilemap* tilemap, i32 x, i32 y, i32 w, i32 h);
void pxl8_tilemap_compress(pxl8_tilemap* tilemap);
bool pxl8_tilemap_is_solid(const pxl8_tilemap* tilemap, u32 x, u32 y);
void pxl8_tilemap_render(const pxl8_tilemap* tilemap, pxl8_gfx* gfx);
void pxl8_tilemap_render_layer(const pxl8_tilemap* tilemap, pxl8_gfx* gfx, u32 layer);
void pxl8_tilemap_render_tile(const pxl8_tilemap* tilemap, pxl8_gfx* gfx, u16 tile_id, i32 x, i32 y, u8 flags);
void pxl8_tilemap_update(pxl8_tilemap* tilemap, f32 delta_time);
void pxl8_tilemap_update_autotiles(pxl8_tilemap* tilemap, u32 layer, u32 x, u32 y, u32 w, u32 h);
#ifdef __cplusplus
}
#endif

405
src/gfx/pxl8_tilesheet.c Normal file
View file

@ -0,0 +1,405 @@
#include "pxl8_tilesheet.h"
#include <stdlib.h>
#include <string.h>
#include "pxl8_ase.h"
#include "pxl8_color.h"
#include "pxl8_gfx.h"
#include "pxl8_log.h"
#include "pxl8_tilemap.h"
struct pxl8_tilesheet {
u8* data;
bool* tile_valid;
u32 height;
u32 tile_size;
u32 tiles_per_row;
u32 total_tiles;
u32 width;
pxl8_pixel_mode pixel_mode;
u32 ref_count;
pxl8_tile_animation* animations;
u32 animation_count;
pxl8_tile_properties* properties;
pxl8_autotile_rule** autotile_rules;
u32* autotile_rule_counts;
};
pxl8_tilesheet* pxl8_tilesheet_create(u32 tile_size) {
pxl8_tilesheet* tilesheet = calloc(1, sizeof(pxl8_tilesheet));
if (!tilesheet) return NULL;
tilesheet->tile_size = tile_size;
tilesheet->ref_count = 1;
return tilesheet;
}
void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet) {
if (!tilesheet) return;
if (tilesheet->data) {
free(tilesheet->data);
}
if (tilesheet->tile_valid) {
free(tilesheet->tile_valid);
}
if (tilesheet->animations) {
for (u32 i = 0; i < tilesheet->animation_count; i++) {
if (tilesheet->animations[i].frames) {
free(tilesheet->animations[i].frames);
}
}
free(tilesheet->animations);
}
if (tilesheet->properties) {
free(tilesheet->properties);
}
if (tilesheet->autotile_rules) {
for (u32 i = 0; i <= tilesheet->total_tiles; i++) {
if (tilesheet->autotile_rules[i]) {
free(tilesheet->autotile_rules[i]);
}
}
free(tilesheet->autotile_rules);
free(tilesheet->autotile_rule_counts);
}
free(tilesheet);
}
pxl8_result pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, pxl8_gfx* gfx) {
if (!tilesheet || !filepath || !gfx) return PXL8_ERROR_NULL_POINTER;
pxl8_ase_file ase_file;
pxl8_result result = pxl8_ase_load(filepath, &ase_file);
if (result != PXL8_OK) {
pxl8_error("Failed to load tilesheet: %s", filepath);
return result;
}
if (tilesheet->data) {
free(tilesheet->data);
}
u32 width = ase_file.header.width;
u32 height = ase_file.header.height;
if (ase_file.header.grid_width > 0 && ase_file.header.grid_height > 0) {
tilesheet->tile_size = ase_file.header.grid_width;
pxl8_info("Using Aseprite grid size: %dx%d",
ase_file.header.grid_width, ase_file.header.grid_height);
}
tilesheet->width = width;
tilesheet->height = height;
tilesheet->tiles_per_row = width / tilesheet->tile_size;
tilesheet->total_tiles = (width / tilesheet->tile_size) * (height / tilesheet->tile_size);
tilesheet->pixel_mode = pxl8_gfx_get_pixel_mode(gfx);
u32 pixel_count = width * height;
u16 ase_depth = ase_file.header.color_depth;
bool gfx_hicolor = (tilesheet->pixel_mode == PXL8_PIXEL_HICOLOR);
size_t data_size = pixel_count * pxl8_bytes_per_pixel(tilesheet->pixel_mode);
tilesheet->data = malloc(data_size);
if (!tilesheet->data) {
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_OUT_OF_MEMORY;
}
if (ase_file.frame_count > 0 && ase_file.frames[0].pixels) {
const u8* src = ase_file.frames[0].pixels;
if (ase_depth == 8 && !gfx_hicolor) {
memcpy(tilesheet->data, src, pixel_count);
} else if (ase_depth == 32 && gfx_hicolor) {
u16* dst = (u16*)tilesheet->data;
const u32* rgba = (const u32*)src;
for (u32 i = 0; i < pixel_count; i++) {
u32 c = rgba[i];
u8 a = (c >> 24) & 0xFF;
if (a == 0) {
dst[i] = 0;
} else {
dst[i] = pxl8_rgba32_to_rgb565(c);
}
}
} else if (ase_depth == 8 && gfx_hicolor) {
pxl8_warn("Indexed ASE with hicolor gfx - storing as indexed");
tilesheet->pixel_mode = PXL8_PIXEL_INDEXED;
u8* new_data = realloc(tilesheet->data, pixel_count);
if (!new_data) {
free(tilesheet->data);
tilesheet->data = NULL;
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_OUT_OF_MEMORY;
}
tilesheet->data = new_data;
memcpy(tilesheet->data, src, pixel_count);
} else {
pxl8_error("Unsupported ASE color depth %d for gfx mode", ase_depth);
free(tilesheet->data);
tilesheet->data = NULL;
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_INVALID_ARGUMENT;
}
}
tilesheet->tile_valid = calloc(tilesheet->total_tiles + 1, sizeof(bool));
if (!tilesheet->tile_valid) {
free(tilesheet->data);
tilesheet->data = NULL;
pxl8_ase_destroy(&ase_file);
return PXL8_ERROR_OUT_OF_MEMORY;
}
u32 valid_tiles = 0;
bool is_hicolor = (tilesheet->pixel_mode == PXL8_PIXEL_HICOLOR);
for (u32 tile_id = 1; tile_id <= tilesheet->total_tiles; tile_id++) {
u32 tile_x = ((tile_id - 1) % tilesheet->tiles_per_row) * tilesheet->tile_size;
u32 tile_y = ((tile_id - 1) / tilesheet->tiles_per_row) * tilesheet->tile_size;
bool has_content = false;
for (u32 py = 0; py < tilesheet->tile_size && !has_content; py++) {
for (u32 px = 0; px < tilesheet->tile_size; px++) {
u32 idx = (tile_y + py) * width + (tile_x + px);
if (is_hicolor) {
if (((u16*)tilesheet->data)[idx] != 0) {
has_content = true;
break;
}
} else {
if (tilesheet->data[idx] != 0) {
has_content = true;
break;
}
}
}
}
if (has_content) {
tilesheet->tile_valid[tile_id] = true;
valid_tiles++;
}
}
pxl8_info("Loaded tilesheet %s: %dx%d, %d valid tiles out of %d slots (%dx%d each)",
filepath, width, height, valid_tiles, tilesheet->total_tiles,
tilesheet->tile_size, tilesheet->tile_size);
pxl8_ase_destroy(&ase_file);
return PXL8_OK;
}
void pxl8_tilesheet_render_tile(const pxl8_tilesheet* tilesheet, pxl8_gfx* gfx,
u16 tile_id, i32 x, i32 y, u8 flags) {
if (!tilesheet || !gfx || !tilesheet->data) return;
if (tile_id == 0 || tile_id > tilesheet->total_tiles) return;
if (tilesheet->tile_valid && !tilesheet->tile_valid[tile_id]) return;
u32 tile_x = ((tile_id - 1) % tilesheet->tiles_per_row) * tilesheet->tile_size;
u32 tile_y = ((tile_id - 1) / tilesheet->tiles_per_row) * tilesheet->tile_size;
for (u32 py = 0; py < tilesheet->tile_size; py++) {
for (u32 px = 0; px < tilesheet->tile_size; px++) {
u32 src_x = tile_x + px;
u32 src_y = tile_y + py;
if (flags & PXL8_TILE_FLIP_X) src_x = tile_x + (tilesheet->tile_size - 1 - px);
if (flags & PXL8_TILE_FLIP_Y) src_y = tile_y + (tilesheet->tile_size - 1 - py);
u32 src_idx = src_y * tilesheet->width + src_x;
u8 color_idx = tilesheet->data[src_idx];
if (color_idx != 0) {
i32 screen_x = x + px;
i32 screen_y = y + py;
if (screen_x >= 0 && screen_x < pxl8_gfx_get_width(gfx) &&
screen_y >= 0 && screen_y < pxl8_gfx_get_height(gfx)) {
pxl8_2d_pixel(gfx, screen_x, screen_y, color_idx);
}
}
}
}
}
bool pxl8_tilesheet_is_tile_valid(const pxl8_tilesheet* tilesheet, u16 tile_id) {
if (!tilesheet || tile_id == 0 || tile_id > tilesheet->total_tiles) return false;
return tilesheet->tile_valid ? tilesheet->tile_valid[tile_id] : true;
}
void pxl8_tilesheet_ref(pxl8_tilesheet* tilesheet) {
if (!tilesheet) return;
tilesheet->ref_count++;
}
void pxl8_tilesheet_unref(pxl8_tilesheet* tilesheet) {
if (!tilesheet) return;
if (--tilesheet->ref_count == 0) {
pxl8_tilesheet_destroy(tilesheet);
}
}
pxl8_result pxl8_tilesheet_add_animation(pxl8_tilesheet* tilesheet, u16 base_tile_id,
const u16* frames, u16 frame_count, f32 frame_duration) {
if (!tilesheet || !frames || frame_count == 0) return PXL8_ERROR_INVALID_ARGUMENT;
if (base_tile_id == 0 || base_tile_id > tilesheet->total_tiles) return PXL8_ERROR_INVALID_ARGUMENT;
if (!tilesheet->animations) {
tilesheet->animations = calloc(tilesheet->total_tiles + 1, sizeof(pxl8_tile_animation));
if (!tilesheet->animations) return PXL8_ERROR_OUT_OF_MEMORY;
}
pxl8_tile_animation* anim = &tilesheet->animations[base_tile_id];
if (anim->frames) free(anim->frames);
anim->frames = malloc(frame_count * sizeof(u16));
if (!anim->frames) return PXL8_ERROR_OUT_OF_MEMORY;
memcpy(anim->frames, frames, frame_count * sizeof(u16));
anim->frame_count = frame_count;
anim->current_frame = 0;
anim->frame_duration = frame_duration;
anim->time_accumulator = 0;
tilesheet->animation_count++;
return PXL8_OK;
}
void pxl8_tilesheet_update_animations(pxl8_tilesheet* tilesheet, f32 delta_time) {
if (!tilesheet || !tilesheet->animations) return;
for (u32 i = 1; i <= tilesheet->total_tiles; i++) {
pxl8_tile_animation* anim = &tilesheet->animations[i];
if (!anim->frames || anim->frame_count == 0) continue;
anim->time_accumulator += delta_time;
while (anim->time_accumulator >= anim->frame_duration) {
anim->time_accumulator -= anim->frame_duration;
anim->current_frame = (anim->current_frame + 1) % anim->frame_count;
}
}
}
u16 pxl8_tilesheet_get_animated_frame(const pxl8_tilesheet* tilesheet, u16 tile_id) {
if (!tilesheet || !tilesheet->animations || tile_id == 0 || tile_id > tilesheet->total_tiles) {
return tile_id;
}
const pxl8_tile_animation* anim = &tilesheet->animations[tile_id];
if (!anim->frames || anim->frame_count == 0) return tile_id;
return anim->frames[anim->current_frame];
}
pxl8_result pxl8_tilesheet_set_tile_pixels(pxl8_tilesheet* tilesheet, u16 tile_id, const u8* pixels) {
if (!tilesheet || !pixels || tile_id == 0 || tile_id > tilesheet->total_tiles) return PXL8_ERROR_INVALID_ARGUMENT;
if (!tilesheet->data) return PXL8_ERROR_NULL_POINTER;
u32 tile_x = (tile_id - 1) % tilesheet->tiles_per_row;
u32 tile_y = (tile_id - 1) / tilesheet->tiles_per_row;
u32 bytes_per_pixel = pxl8_bytes_per_pixel(tilesheet->pixel_mode);
for (u32 py = 0; py < tilesheet->tile_size; py++) {
for (u32 px = 0; px < tilesheet->tile_size; px++) {
u32 src_idx = py * tilesheet->tile_size + px;
u32 dst_x = tile_x * tilesheet->tile_size + px;
u32 dst_y = tile_y * tilesheet->tile_size + py;
u32 dst_idx = dst_y * tilesheet->width + dst_x;
if (bytes_per_pixel == 2) {
((u16*)tilesheet->data)[dst_idx] = ((const u16*)pixels)[src_idx];
} else {
tilesheet->data[dst_idx] = pixels[src_idx];
}
}
}
if (tilesheet->tile_valid) {
tilesheet->tile_valid[tile_id] = true;
}
return PXL8_OK;
}
void pxl8_tilesheet_set_tile_property(pxl8_tilesheet* tilesheet, u16 tile_id,
const pxl8_tile_properties* props) {
if (!tilesheet || !props || tile_id == 0 || tile_id > tilesheet->total_tiles) return;
if (!tilesheet->properties) {
tilesheet->properties = calloc(tilesheet->total_tiles + 1, sizeof(pxl8_tile_properties));
if (!tilesheet->properties) return;
}
tilesheet->properties[tile_id] = *props;
}
const pxl8_tile_properties* pxl8_tilesheet_get_tile_property(const pxl8_tilesheet* tilesheet, u16 tile_id) {
if (!tilesheet || !tilesheet->properties || tile_id == 0 || tile_id > tilesheet->total_tiles) {
return NULL;
}
return &tilesheet->properties[tile_id];
}
u32 pxl8_tilesheet_get_tile_size(const pxl8_tilesheet* tilesheet) {
return tilesheet ? tilesheet->tile_size : 0;
}
pxl8_result pxl8_tilesheet_add_autotile_rule(pxl8_tilesheet* tilesheet, u16 base_tile_id,
u8 neighbor_mask, u16 result_tile_id) {
if (!tilesheet || base_tile_id == 0 || base_tile_id > tilesheet->total_tiles) {
return PXL8_ERROR_INVALID_ARGUMENT;
}
if (!tilesheet->autotile_rules) {
tilesheet->autotile_rules = calloc(tilesheet->total_tiles + 1, sizeof(pxl8_autotile_rule*));
tilesheet->autotile_rule_counts = calloc(tilesheet->total_tiles + 1, sizeof(u32));
if (!tilesheet->autotile_rules || !tilesheet->autotile_rule_counts) {
return PXL8_ERROR_OUT_OF_MEMORY;
}
}
u32 count = tilesheet->autotile_rule_counts[base_tile_id];
pxl8_autotile_rule* new_rules = realloc(tilesheet->autotile_rules[base_tile_id],
(count + 1) * sizeof(pxl8_autotile_rule));
if (!new_rules) return PXL8_ERROR_OUT_OF_MEMORY;
new_rules[count].neighbor_mask = neighbor_mask;
new_rules[count].tile_id = result_tile_id;
tilesheet->autotile_rules[base_tile_id] = new_rules;
tilesheet->autotile_rule_counts[base_tile_id]++;
return PXL8_OK;
}
u16 pxl8_tilesheet_apply_autotile(const pxl8_tilesheet* tilesheet, u16 base_tile_id, u8 neighbors) {
if (!tilesheet || !tilesheet->autotile_rules || base_tile_id == 0 ||
base_tile_id > tilesheet->total_tiles) {
return base_tile_id;
}
pxl8_autotile_rule* rules = tilesheet->autotile_rules[base_tile_id];
u32 rule_count = tilesheet->autotile_rule_counts[base_tile_id];
for (u32 i = 0; i < rule_count; i++) {
if (rules[i].neighbor_mask == neighbors) {
return rules[i].tile_id;
}
}
return base_tile_id;
}

38
src/gfx/pxl8_tilesheet.h Normal file
View file

@ -0,0 +1,38 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_types.h"
typedef struct pxl8_autotile_rule pxl8_autotile_rule;
typedef struct pxl8_tile_animation pxl8_tile_animation;
typedef struct pxl8_tile_properties pxl8_tile_properties;
typedef struct pxl8_tilesheet pxl8_tilesheet;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_tilesheet* pxl8_tilesheet_create(u32 tile_size);
void pxl8_tilesheet_destroy(pxl8_tilesheet* tilesheet);
u16 pxl8_tilesheet_get_animated_frame(const pxl8_tilesheet* tilesheet, u16 tile_id);
const pxl8_tile_properties* pxl8_tilesheet_get_tile_property(const pxl8_tilesheet* tilesheet, u16 tile_id);
u32 pxl8_tilesheet_get_tile_size(const pxl8_tilesheet* tilesheet);
bool pxl8_tilesheet_is_tile_valid(const pxl8_tilesheet* tilesheet, u16 tile_id);
pxl8_result pxl8_tilesheet_set_tile_pixels(pxl8_tilesheet* tilesheet, u16 tile_id, const u8* pixels);
void pxl8_tilesheet_set_tile_property(pxl8_tilesheet* tilesheet, u16 tile_id, const pxl8_tile_properties* props);
pxl8_result pxl8_tilesheet_load(pxl8_tilesheet* tilesheet, const char* filepath, pxl8_gfx* gfx);
void pxl8_tilesheet_ref(pxl8_tilesheet* tilesheet);
void pxl8_tilesheet_unref(pxl8_tilesheet* tilesheet);
pxl8_result pxl8_tilesheet_add_animation(pxl8_tilesheet* tilesheet, u16 base_tile_id, const u16* frames, u16 frame_count, f32 frame_duration);
pxl8_result pxl8_tilesheet_add_autotile_rule(pxl8_tilesheet* tilesheet, u16 base_tile_id, u8 neighbor_mask, u16 result_tile_id);
u16 pxl8_tilesheet_apply_autotile(const pxl8_tilesheet* tilesheet, u16 base_tile_id, u8 neighbors);
void pxl8_tilesheet_render_tile(const pxl8_tilesheet* tilesheet, pxl8_gfx* gfx, u16 tile_id, i32 x, i32 y, u8 flags);
void pxl8_tilesheet_update_animations(pxl8_tilesheet* tilesheet, f32 delta_time);
#ifdef __cplusplus
}
#endif

252
src/gfx/pxl8_transition.c Normal file
View file

@ -0,0 +1,252 @@
#include "pxl8_transition.h"
#include <math.h>
#include <stdlib.h>
#include "pxl8_gfx.h"
#include "pxl8_log.h"
#include "pxl8_math.h"
pxl8_transition* pxl8_transition_create(pxl8_transition_type type, f32 duration) {
if (duration <= 0.0f) {
pxl8_error("Invalid transition duration: %f", duration);
return NULL;
}
pxl8_transition* transition = (pxl8_transition*)calloc(1, sizeof(pxl8_transition));
if (!transition) {
pxl8_error("Failed to allocate transition");
return NULL;
}
transition->type = type;
transition->duration = duration;
transition->time = 0.0f;
transition->active = false;
transition->reverse = false;
transition->color = 0xFF000000;
transition->on_complete = NULL;
transition->userdata = NULL;
return transition;
}
void pxl8_transition_destroy(pxl8_transition* transition) {
if (!transition) return;
free(transition);
}
f32 pxl8_transition_get_progress(const pxl8_transition* transition) {
if (!transition) return 0.0f;
f32 t = transition->time / transition->duration;
if (t < 0.0f) t = 0.0f;
if (t > 1.0f) t = 1.0f;
return transition->reverse ? 1.0f - t : t;
}
bool pxl8_transition_is_active(const pxl8_transition* transition) {
return transition && transition->active;
}
bool pxl8_transition_is_complete(const pxl8_transition* transition) {
if (!transition) return true;
return transition->time >= transition->duration;
}
void pxl8_transition_render(const pxl8_transition* transition, pxl8_gfx* gfx) {
if (!transition || !gfx || !transition->active) return;
f32 progress = pxl8_transition_get_progress(transition);
i32 width = pxl8_gfx_get_width(gfx);
i32 height = pxl8_gfx_get_height(gfx);
switch (transition->type) {
case PXL8_TRANSITION_FADE: {
u8 alpha = (u8)(progress * 255.0f);
u32 fade_color = (transition->color & 0x00FFFFFF) | (alpha << 24);
for (i32 y = 0; y < height; y++) {
for (i32 x = 0; x < width; x++) {
u32 bg = pxl8_2d_get_pixel(gfx, x, y);
u32 r_bg = (bg >> 16) & 0xFF;
u32 g_bg = (bg >> 8) & 0xFF;
u32 b_bg = bg & 0xFF;
u32 r_fg = (fade_color >> 16) & 0xFF;
u32 g_fg = (fade_color >> 8) & 0xFF;
u32 b_fg = fade_color & 0xFF;
u32 r = (r_bg * (255 - alpha) + r_fg * alpha) / 255;
u32 g = (g_bg * (255 - alpha) + g_fg * alpha) / 255;
u32 b = (b_bg * (255 - alpha) + b_fg * alpha) / 255;
pxl8_2d_pixel(gfx, x, y, 0xFF000000 | (r << 16) | (g << 8) | b);
}
}
break;
}
case PXL8_TRANSITION_WIPE_LEFT: {
i32 wipe_x = (i32)(width * progress);
pxl8_2d_rect_fill(gfx, 0, 0, wipe_x, height, transition->color);
break;
}
case PXL8_TRANSITION_WIPE_RIGHT: {
i32 wipe_x = (i32)(width * (1.0f - progress));
pxl8_2d_rect_fill(gfx, wipe_x, 0, width - wipe_x, height, transition->color);
break;
}
case PXL8_TRANSITION_WIPE_UP: {
i32 wipe_y = (i32)(height * progress);
pxl8_2d_rect_fill(gfx, 0, 0, width, wipe_y, transition->color);
break;
}
case PXL8_TRANSITION_WIPE_DOWN: {
i32 wipe_y = (i32)(height * (1.0f - progress));
pxl8_2d_rect_fill(gfx, 0, wipe_y, width, height - wipe_y, transition->color);
break;
}
case PXL8_TRANSITION_CIRCLE_CLOSE: {
i32 center_x = width / 2;
i32 center_y = height / 2;
i32 max_radius = (i32)sqrtf((f32)(center_x * center_x + center_y * center_y));
i32 radius = (i32)(max_radius * (1.0f - progress));
for (i32 y = 0; y < height; y++) {
for (i32 x = 0; x < width; x++) {
i32 dx = x - center_x;
i32 dy = y - center_y;
i32 dist = (i32)sqrtf((f32)(dx * dx + dy * dy));
if (dist > radius) {
pxl8_2d_pixel(gfx, x, y, transition->color);
}
}
}
break;
}
case PXL8_TRANSITION_CIRCLE_OPEN: {
i32 center_x = width / 2;
i32 center_y = height / 2;
i32 max_radius = (i32)sqrtf((f32)(center_x * center_x + center_y * center_y));
i32 radius = (i32)(max_radius * progress);
for (i32 y = 0; y < height; y++) {
for (i32 x = 0; x < width; x++) {
i32 dx = x - center_x;
i32 dy = y - center_y;
i32 dist = (i32)sqrtf((f32)(dx * dx + dy * dy));
if (dist < radius) {
pxl8_2d_pixel(gfx, x, y, transition->color);
}
}
}
break;
}
case PXL8_TRANSITION_DISSOLVE: {
u32 seed = 12345;
for (i32 y = 0; y < height; y++) {
for (i32 x = 0; x < width; x++) {
seed = seed * 1103515245 + 12345;
f32 noise = (f32)((seed / 65536) % 1000) / 1000.0f;
if (noise < progress) {
pxl8_2d_pixel(gfx, x, y, transition->color);
}
}
}
break;
}
case PXL8_TRANSITION_PIXELATE: {
i32 max_block_size = 32;
i32 block_size = (i32)(max_block_size * progress);
if (block_size < 1) block_size = 1;
pxl8_pixel_mode mode = pxl8_gfx_get_pixel_mode(gfx);
bool has_fb = (mode == PXL8_PIXEL_HICOLOR)
? (pxl8_gfx_get_framebuffer_hicolor(gfx) != NULL)
: (pxl8_gfx_get_framebuffer_indexed(gfx) != NULL);
if (!has_fb) break;
for (i32 y = 0; y < height; y += block_size) {
for (i32 x = 0; x < width; x += block_size) {
u32 color_sum_r = 0, color_sum_g = 0, color_sum_b = 0;
i32 count = 0;
for (i32 by = 0; by < block_size && y + by < height; by++) {
for (i32 bx = 0; bx < block_size && x + bx < width; bx++) {
u32 color = pxl8_2d_get_pixel(gfx, x + bx, y + by);
color_sum_r += (color >> 16) & 0xFF;
color_sum_g += (color >> 8) & 0xFF;
color_sum_b += color & 0xFF;
count++;
}
}
if (count > 0) {
u32 avg_color = 0xFF000000 |
((color_sum_r / count) << 16) |
((color_sum_g / count) << 8) |
(color_sum_b / count);
for (i32 by = 0; by < block_size && y + by < height; by++) {
for (i32 bx = 0; bx < block_size && x + bx < width; bx++) {
pxl8_2d_pixel(gfx, x + bx, y + by, avg_color);
}
}
}
}
}
break;
}
}
}
void pxl8_transition_reset(pxl8_transition* transition) {
if (!transition) return;
transition->time = 0.0f;
transition->active = false;
}
void pxl8_transition_set_color(pxl8_transition* transition, u32 color) {
if (!transition) return;
transition->color = color;
}
void pxl8_transition_set_reverse(pxl8_transition* transition, bool reverse) {
if (!transition) return;
transition->reverse = reverse;
}
void pxl8_transition_start(pxl8_transition* transition) {
if (!transition) return;
transition->active = true;
transition->time = 0.0f;
}
void pxl8_transition_stop(pxl8_transition* transition) {
if (!transition) return;
transition->active = false;
}
void pxl8_transition_update(pxl8_transition* transition, f32 dt) {
if (!transition || !transition->active) return;
transition->time += dt;
if (transition->time >= transition->duration) {
transition->time = transition->duration;
transition->active = false;
if (transition->on_complete) {
transition->on_complete(transition->userdata);
}
}
}

53
src/gfx/pxl8_transition.h Normal file
View file

@ -0,0 +1,53 @@
#pragma once
#include "pxl8_gfx.h"
#include "pxl8_types.h"
typedef enum pxl8_transition_type {
PXL8_TRANSITION_FADE,
PXL8_TRANSITION_WIPE_LEFT,
PXL8_TRANSITION_WIPE_RIGHT,
PXL8_TRANSITION_WIPE_UP,
PXL8_TRANSITION_WIPE_DOWN,
PXL8_TRANSITION_CIRCLE_OPEN,
PXL8_TRANSITION_CIRCLE_CLOSE,
PXL8_TRANSITION_DISSOLVE,
PXL8_TRANSITION_PIXELATE
} pxl8_transition_type;
typedef struct pxl8_transition {
pxl8_transition_type type;
f32 duration;
f32 time;
bool active;
bool reverse;
u32 color;
void (*on_complete)(void* userdata);
void* userdata;
} pxl8_transition;
#ifdef __cplusplus
extern "C" {
#endif
pxl8_transition* pxl8_transition_create(pxl8_transition_type type, f32 duration);
void pxl8_transition_destroy(pxl8_transition* transition);
f32 pxl8_transition_get_progress(const pxl8_transition* transition);
bool pxl8_transition_is_active(const pxl8_transition* transition);
bool pxl8_transition_is_complete(const pxl8_transition* transition);
void pxl8_transition_set_color(pxl8_transition* transition, u32 color);
void pxl8_transition_set_reverse(pxl8_transition* transition, bool reverse);
void pxl8_transition_render(const pxl8_transition* transition, pxl8_gfx* gfx);
void pxl8_transition_reset(pxl8_transition* transition);
void pxl8_transition_start(pxl8_transition* transition);
void pxl8_transition_stop(pxl8_transition* transition);
void pxl8_transition_update(pxl8_transition* transition, f32 dt);
#ifdef __cplusplus
}
#endif