i'll just say it's better, and i ripped out the frontend for now.
This commit is contained in:
1 parent
97946c613a
commit
51c3f6bacc
14 files changed
+1111
-1358
No files matched your search
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 222 KiB |
+28
-27
@@ -1,7 +1,6 @@
|
||||
// compositor.h
|
||||
#ifndef COMPOSITOR_H
|
||||
#define COMPOSITOR_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
@@ -26,7 +25,6 @@ enum easing_t {
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
|
||||
typedef struct compositor_t compositor_t;
|
||||
|
||||
// Initialize compositor with given dimensions
|
||||
@@ -37,33 +35,30 @@ void compositor_destroy(compositor_t* comp);
|
||||
|
||||
// Insert element, returns unique ID
|
||||
uint32_t compositor_insert(compositor_t* comp,
|
||||
const uint32_t* fb,
|
||||
uint32_t fb_width,
|
||||
uint32_t fb_height,
|
||||
int32_t x, int32_t y,
|
||||
int32_t z_order, float scale = 1.0f);
|
||||
const uint32_t* fb,
|
||||
uint32_t fb_width,
|
||||
uint32_t fb_height,
|
||||
int32_t x, int32_t y,
|
||||
int32_t z_order, float scale = 1.0f);
|
||||
|
||||
// Remove element by ID
|
||||
void compositor_remove(compositor_t* comp, uint32_t id);
|
||||
|
||||
// AIO Update element properties
|
||||
void compositor_update(compositor_t* comp, uint32_t id,
|
||||
int32_t x, int32_t y,
|
||||
int32_t z_order, float scale,
|
||||
bool visible); // holy clusterfuck
|
||||
int32_t x, int32_t y,
|
||||
int32_t z_order, float scale,
|
||||
bool visible);
|
||||
|
||||
// Individual setters
|
||||
void compositor_set_position(compositor_t* comp, uint32_t id,
|
||||
int32_t x, int32_t y);
|
||||
|
||||
int32_t x, int32_t y);
|
||||
void compositor_set_zorder(compositor_t* comp, uint32_t id,
|
||||
int32_t z_order);
|
||||
|
||||
int32_t z_order);
|
||||
void compositor_set_visible(compositor_t* comp, uint32_t id,
|
||||
bool visible);
|
||||
|
||||
bool visible);
|
||||
void compositor_set_scale(compositor_t* comp, uint32_t id,
|
||||
float scale);
|
||||
float scale);
|
||||
|
||||
// Advances all animations by a given time delta.
|
||||
void compositor_tick(compositor_t* comp, float delta_time_sec);
|
||||
@@ -71,20 +66,29 @@ void compositor_tick(compositor_t* comp, float delta_time_sec);
|
||||
// Instantly sets the element-wide alpha.
|
||||
void compositor_set_alpha(compositor_t* comp, uint32_t id, float alpha);
|
||||
|
||||
// Starts a "slide" animation on an element.
|
||||
// Animation functions with queue parameter
|
||||
void compositor_animate_move(compositor_t* comp, uint32_t id,
|
||||
int32_t to_x, int32_t to_y,
|
||||
float duration_sec, easing_t easing);
|
||||
float duration_sec, easing_t easing, bool queue = false);
|
||||
|
||||
// Starts a "fade" or "dissolve" animation on an element.
|
||||
void compositor_animate_fade(compositor_t* comp, uint32_t id,
|
||||
float to_alpha,
|
||||
float duration_sec, easing_t easing);
|
||||
float duration_sec, easing_t easing, bool queue = false);
|
||||
|
||||
// Starts a "scale" animation on an element.
|
||||
void compositor_animate_scale(compositor_t* comp, uint32_t id,
|
||||
float to_scale,
|
||||
float duration_sec, easing_t easing);
|
||||
float duration_sec, easing_t easing, bool queue = false);
|
||||
|
||||
// Immediately complete all animations
|
||||
void compositor_animation_complete(compositor_t* comp, uint32_t id);
|
||||
|
||||
void compositor_animation_complete_all(compositor_t* comp);
|
||||
|
||||
// Returns true if any element is animating or has queued animations.
|
||||
bool compositor_is_animating(compositor_t * comp);
|
||||
|
||||
// Returns true if the specified element is animating or has queued animations.
|
||||
bool compositor_is_element_animating(compositor_t* comp, uint32_t id);
|
||||
|
||||
// Compose all visible elements and return framebuffer pointer
|
||||
const uint32_t* compositor_compose(compositor_t* comp);
|
||||
@@ -93,9 +97,6 @@ void compositor_mark_content_dirty(compositor_t* comp, uint32_t id);
|
||||
void compositor_mark_all_dirty(compositor_t* comp);
|
||||
|
||||
// Returns the last error message set for this compositor instance.
|
||||
// Returns nullptr if no error has occurred.
|
||||
const char* compositor_get_last_error(compositor_t* comp);
|
||||
|
||||
}
|
||||
|
||||
#endif // COMPOSITOR_H
|
||||
#endif // COMPOSITOR_H
|
||||
+3
-2
@@ -5,10 +5,11 @@
|
||||
#include <stdint.h>
|
||||
|
||||
extern "C" {
|
||||
// Loads an image file as ARGB8888. Returns pointer to pixel data (free with image_free), sets width/height.
|
||||
// Loads an image file as ARGB8888. Returns pointer to pixel data, sets width/height.
|
||||
uint32_t* image_load(const char* filename, int* width, int* height);
|
||||
// Loads and resizes an image file to desired dimensions as ARGB8888. Returns pointer to pixel data (free with image_free).
|
||||
// Loads and resizes an image file to desired dimensions as ARGB8888. Returns pointer to pixel data.
|
||||
uint32_t* image_load_resized(const char* filename, int desired_width, int desired_height);
|
||||
// Frees image data.
|
||||
void image_free(uint32_t* data);
|
||||
}
|
||||
|
||||
|
||||
+11
-5
@@ -7,22 +7,28 @@
|
||||
struct text_t;
|
||||
|
||||
extern "C" {
|
||||
// initializes a new text renderer with given width and height
|
||||
text_t* text_init(int width, int height);
|
||||
// shares cache, prefer when using the same font to save memory
|
||||
text_t* text_init_shared(text_t* source, int width, int height);
|
||||
// destroys the text renderer and frees resources
|
||||
void text_destroy(text_t* text);
|
||||
// pushes new text buffer to be rendered, can be stacked
|
||||
void text_push(text_t* text,
|
||||
const char* str,
|
||||
float size,
|
||||
float speed,
|
||||
const char* font_path,
|
||||
bool outline);
|
||||
// finish current text immediately
|
||||
// load font and set as current font
|
||||
void text_set_font(text_t* text, const char* font_path, float size);
|
||||
// finish whole current text buffer immediately
|
||||
void text_show_all_current(text_t* text);
|
||||
// returns true if text is still animating
|
||||
bool text_is_animating(text_t* text);
|
||||
// advance, call every frame, returns false when done
|
||||
// advance, call every frame, returns true when it touched the framebuffer
|
||||
bool text_nextframe(text_t* text, float deltatime);
|
||||
uint32_t* text_getfb(text_t* text);
|
||||
// gets the framebuffer of the text renderer
|
||||
const uint32_t* text_getfb(text_t* text);
|
||||
// clears the text buffer and the frame buffer
|
||||
void text_clear(text_t* text);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,12 +35,19 @@ typedef struct window_t window_t;
|
||||
#define KEY_0 39
|
||||
|
||||
extern "C" {
|
||||
// creates a window with given width, height and title, title can be utf8
|
||||
window_t* window_create(int width, int height, const char* title);
|
||||
// destroys the window
|
||||
void window_destroy(window_t* s);
|
||||
// polls window events, returns true if the window should close
|
||||
bool window_pollevent(window_t* s);
|
||||
// blits the given framebuffer to the window. must be the same size as the window and ARGB8888 format
|
||||
void window_blit(window_t* s, const uint32_t* framebuffer);
|
||||
// returns true if the given key is currently held down
|
||||
bool window_is_key_down(window_t* s, int key_scancode);
|
||||
// returns true if the given mouse button is currently held down
|
||||
bool window_is_mouse_button_down(window_t* s, int button_index);
|
||||
// gets the current mouse position in window coordinates
|
||||
void window_get_mouse_position(window_t* s, int* x, int* y);
|
||||
}
|
||||
|
||||
|
||||
+467
-176
@@ -12,13 +12,14 @@
|
||||
// Define the tile size
|
||||
#define TILE_SIZE 64
|
||||
|
||||
struct property_anim_t {
|
||||
float progress_sec;
|
||||
float duration_sec;
|
||||
easing_t easing;
|
||||
};
|
||||
|
||||
struct animation_t {
|
||||
animation_type_t type = ANIM_NONE;
|
||||
easing_t easing = EASE_LINEAR;
|
||||
|
||||
// Time
|
||||
float progress_sec = 0.0f;
|
||||
float duration_sec = 0.0f;
|
||||
|
||||
// Start/End state
|
||||
int32_t start_x = 0, start_y = 0;
|
||||
@@ -30,8 +31,34 @@ struct animation_t {
|
||||
float start_alpha = 1.0f;
|
||||
float end_alpha = 1.0f;
|
||||
|
||||
// Combined animation (ANIM_MOVE_FADE)
|
||||
bool is_move_fade = false;
|
||||
// Per-property animation state
|
||||
property_anim_t position_anim;
|
||||
property_anim_t scale_anim;
|
||||
property_anim_t alpha_anim;
|
||||
|
||||
// Track which properties are being animated
|
||||
bool animating_position = false;
|
||||
bool animating_scale = false;
|
||||
bool animating_alpha = false;
|
||||
};
|
||||
|
||||
// Queued animation structure
|
||||
struct queued_animation_t {
|
||||
animation_type_t type;
|
||||
easing_t easing;
|
||||
float duration_sec;
|
||||
|
||||
// Target values
|
||||
int32_t to_x, to_y;
|
||||
float to_scale;
|
||||
float to_alpha;
|
||||
|
||||
bool is_move_fade;
|
||||
|
||||
// Which properties this animation affects
|
||||
bool affects_position;
|
||||
bool affects_scale;
|
||||
bool affects_alpha;
|
||||
};
|
||||
|
||||
struct element_t {
|
||||
@@ -46,6 +73,7 @@ struct element_t {
|
||||
uint32_t id; // Unique identifier
|
||||
std::vector<uint32_t> occupied_tiles; // List of occupied tiles
|
||||
animation_t anim;
|
||||
std::vector<queued_animation_t> anim_queue;
|
||||
};
|
||||
|
||||
struct compositor_t {
|
||||
@@ -268,83 +296,115 @@ extern "C" {
|
||||
void compositor_tick(compositor_t* comp, float delta_time_sec) {
|
||||
if (!comp) return;
|
||||
|
||||
// Loop over all elements in the map
|
||||
for (auto& pair : comp->elements_map) {
|
||||
element_t* elem = &pair.second;
|
||||
animation_t* anim = &elem->anim;
|
||||
|
||||
if (anim->type == ANIM_NONE) {
|
||||
continue; // Not animating
|
||||
// Check if we need to start a queued animation
|
||||
if (anim->type == ANIM_NONE && !elem->anim_queue.empty()) {
|
||||
queued_animation_t& next = elem->anim_queue.front();
|
||||
|
||||
anim->type = next.type;
|
||||
anim->animating_position = next.affects_position;
|
||||
anim->animating_scale = next.affects_scale;
|
||||
anim->animating_alpha = next.affects_alpha;
|
||||
|
||||
if (next.affects_position) {
|
||||
anim->start_x = elem->x;
|
||||
anim->start_y = elem->y;
|
||||
anim->end_x = next.to_x;
|
||||
anim->end_y = next.to_y;
|
||||
anim->position_anim.progress_sec = 0.0f;
|
||||
anim->position_anim.duration_sec = next.duration_sec;
|
||||
anim->position_anim.easing = next.easing;
|
||||
}
|
||||
if (next.affects_alpha) {
|
||||
anim->start_alpha = elem->alpha;
|
||||
anim->end_alpha = next.to_alpha;
|
||||
anim->alpha_anim.progress_sec = 0.0f;
|
||||
anim->alpha_anim.duration_sec = next.duration_sec;
|
||||
anim->alpha_anim.easing = next.easing;
|
||||
}
|
||||
if (next.affects_scale) {
|
||||
anim->start_scale = elem->scale;
|
||||
anim->end_scale = next.to_scale;
|
||||
anim->scale_anim.progress_sec = 0.0f;
|
||||
anim->scale_anim.duration_sec = next.duration_sec;
|
||||
anim->scale_anim.easing = next.easing;
|
||||
}
|
||||
|
||||
elem->anim_queue.erase(elem->anim_queue.begin());
|
||||
}
|
||||
|
||||
// --- 1. Update Progress ---
|
||||
anim->progress_sec += delta_time_sec;
|
||||
float t_raw = anim->progress_sec / anim->duration_sec;
|
||||
// Skip if nothing is animating
|
||||
if (anim->type == ANIM_NONE) continue;
|
||||
|
||||
// Check for animation end
|
||||
if (t_raw >= 1.0f) {
|
||||
t_raw = 1.0f; // Clamp to 1.0
|
||||
anim->type = ANIM_NONE; // Animation is done
|
||||
}
|
||||
|
||||
// --- 2. Apply Easing ---
|
||||
float t_eased = apply_easing(anim->easing, t_raw);
|
||||
|
||||
// --- 3. Update Properties ---
|
||||
|
||||
// Mark old position as dirty before we change it
|
||||
// We only need to do this *once* at the start of the tick
|
||||
mark_element_dirty(comp, elem);
|
||||
|
||||
bool position_changed = false;
|
||||
bool all_done = true;
|
||||
|
||||
// Animate Move
|
||||
if (anim->type == ANIM_MOVE || anim->is_move_fade) {
|
||||
elem->x = (int32_t)std::round(lerp(static_cast<float>(anim->start_x),
|
||||
static_cast<float>(anim->end_x), t_eased));
|
||||
elem->y = (int32_t)std::round(lerp(static_cast<float>(anim->start_y),
|
||||
static_cast<float>(anim->end_y), t_eased));
|
||||
// Update position animation with its own timing
|
||||
if (anim->animating_position) {
|
||||
anim->position_anim.progress_sec += delta_time_sec;
|
||||
float t_raw = anim->position_anim.progress_sec / anim->position_anim.duration_sec;
|
||||
|
||||
if (t_raw >= 1.0f) {
|
||||
elem->x = anim->end_x;
|
||||
elem->y = anim->end_y;
|
||||
anim->animating_position = false;
|
||||
} else {
|
||||
float t_eased = apply_easing(anim->position_anim.easing, t_raw);
|
||||
elem->x = (int32_t)std::round(lerp(static_cast<float>(anim->start_x),
|
||||
static_cast<float>(anim->end_x), t_eased));
|
||||
elem->y = (int32_t)std::round(lerp(static_cast<float>(anim->start_y),
|
||||
static_cast<float>(anim->end_y), t_eased));
|
||||
all_done = false;
|
||||
}
|
||||
position_changed = true;
|
||||
}
|
||||
|
||||
// Animate Fade
|
||||
if (anim->type == ANIM_FADE || anim->is_move_fade) {
|
||||
elem->alpha = lerp(anim->start_alpha, anim->end_alpha, t_eased);
|
||||
}
|
||||
|
||||
// Animate Scale
|
||||
if (anim->type == ANIM_SCALE) {
|
||||
elem->scale = lerp(anim->start_scale, anim->end_scale, t_eased);
|
||||
position_changed = true; // Scale change also affects position/hash
|
||||
}
|
||||
|
||||
// --- 4. Handle End-of-Animation State ---
|
||||
if (anim->type == ANIM_NONE) {
|
||||
// Animation just finished, snap to final values
|
||||
if (anim->is_move_fade) {
|
||||
elem->x = anim->end_x;
|
||||
elem->y = anim->end_y;
|
||||
// Update alpha animation with its own timing
|
||||
if (anim->animating_alpha) {
|
||||
anim->alpha_anim.progress_sec += delta_time_sec;
|
||||
float t_raw = anim->alpha_anim.progress_sec / anim->alpha_anim.duration_sec;
|
||||
|
||||
if (t_raw >= 1.0f) {
|
||||
elem->alpha = anim->end_alpha;
|
||||
anim->is_move_fade = false;
|
||||
} else if (anim->type == ANIM_MOVE) {
|
||||
elem->x = anim->end_x;
|
||||
elem->y = anim->end_y;
|
||||
} else if (anim->type == ANIM_FADE) {
|
||||
elem->alpha = anim->end_alpha;
|
||||
// If it faded out, set it to invisible to remove from render
|
||||
if (elem->alpha == 0.0f) elem->visible = false;
|
||||
} else if (anim->type == ANIM_SCALE) {
|
||||
elem->scale = anim->end_scale;
|
||||
anim->animating_alpha = false;
|
||||
} else {
|
||||
float t_eased = apply_easing(anim->alpha_anim.easing, t_raw);
|
||||
elem->alpha = lerp(anim->start_alpha, anim->end_alpha, t_eased);
|
||||
all_done = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Update scale animation with its own timing
|
||||
if (anim->animating_scale) {
|
||||
anim->scale_anim.progress_sec += delta_time_sec;
|
||||
float t_raw = anim->scale_anim.progress_sec / anim->scale_anim.duration_sec;
|
||||
|
||||
if (t_raw >= 1.0f) {
|
||||
elem->scale = anim->end_scale;
|
||||
anim->animating_scale = false;
|
||||
} else {
|
||||
float t_eased = apply_easing(anim->scale_anim.easing, t_raw);
|
||||
elem->scale = lerp(anim->start_scale, anim->end_scale, t_eased);
|
||||
all_done = false;
|
||||
}
|
||||
position_changed = true;
|
||||
}
|
||||
|
||||
// If all animations are done, mark as complete
|
||||
if (all_done) {
|
||||
anim->type = ANIM_NONE;
|
||||
}
|
||||
|
||||
// --- 5. Update Spatial Hash ---
|
||||
// If position or scale changed, the hash must be updated
|
||||
// Update spatial hash
|
||||
if (position_changed) {
|
||||
clear_spatial_hash(comp, elem->id);
|
||||
update_spatial_hash(comp, elem);
|
||||
clear_spatial_hash(comp, elem->id);
|
||||
update_spatial_hash(comp, elem);
|
||||
} else {
|
||||
// If only alpha changed, we just need to mark it dirty
|
||||
mark_element_dirty(comp, elem);
|
||||
}
|
||||
}
|
||||
@@ -621,10 +681,9 @@ void compositor_set_alpha(compositor_t* comp, uint32_t id, float alpha) {
|
||||
|
||||
// --- Animation ---
|
||||
|
||||
// Starts a "slide" animation on an element.
|
||||
void compositor_animate_move(compositor_t* comp, uint32_t id,
|
||||
int32_t to_x, int32_t to_y,
|
||||
float duration_sec, easing_t easing) {
|
||||
float duration_sec, easing_t easing, bool queue) {
|
||||
if (!comp) return;
|
||||
element_t* elem = find_element(comp, id);
|
||||
if (!elem) {
|
||||
@@ -632,50 +691,41 @@ void compositor_animate_move(compositor_t* comp, uint32_t id,
|
||||
return;
|
||||
}
|
||||
|
||||
elem->anim.type = ANIM_MOVE;
|
||||
elem->anim.easing = easing;
|
||||
elem->anim.progress_sec = 0.0f;
|
||||
elem->anim.duration_sec = duration_sec;
|
||||
if (queue) {
|
||||
queued_animation_t queued;
|
||||
queued.type = ANIM_MOVE;
|
||||
queued.easing = easing;
|
||||
queued.duration_sec = duration_sec;
|
||||
queued.to_x = to_x;
|
||||
queued.to_y = to_y;
|
||||
queued.is_move_fade = false;
|
||||
queued.affects_position = true;
|
||||
queued.affects_scale = false;
|
||||
queued.affects_alpha = false;
|
||||
elem->anim_queue.push_back(queued);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parallel execution with independent timing
|
||||
elem->anim.start_x = elem->x;
|
||||
elem->anim.start_y = elem->y;
|
||||
elem->anim.end_x = to_x;
|
||||
elem->anim.end_y = to_y;
|
||||
elem->anim.animating_position = true;
|
||||
|
||||
elem->anim.is_move_fade = false;
|
||||
// Set position-specific timing
|
||||
elem->anim.position_anim.progress_sec = 0.0f;
|
||||
elem->anim.position_anim.duration_sec = duration_sec;
|
||||
elem->anim.position_anim.easing = easing;
|
||||
|
||||
if (elem->anim.type == ANIM_NONE) {
|
||||
elem->anim.type = ANIM_MOVE;
|
||||
}
|
||||
}
|
||||
|
||||
// Starts a "fade" or "dissolve" animation on an element.
|
||||
void compositor_animate_fade(compositor_t* comp, uint32_t id,
|
||||
float to_alpha,
|
||||
float duration_sec, easing_t easing) {
|
||||
if (!comp) return;
|
||||
element_t* elem = find_element(comp, id);
|
||||
if (!elem) {
|
||||
set_error(comp, "Element not found for animate_fade.");
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're fading in from 0, we must make it visible first
|
||||
if (elem->alpha == 0.0f && to_alpha > 0.0f) {
|
||||
elem->visible = true;
|
||||
}
|
||||
|
||||
elem->anim.type = ANIM_FADE;
|
||||
elem->anim.easing = easing;
|
||||
elem->anim.progress_sec = 0.0f;
|
||||
elem->anim.duration_sec = duration_sec;
|
||||
|
||||
elem->anim.start_alpha = elem->alpha;
|
||||
elem->anim.end_alpha = std::max(0.0f, std::min(1.0f, to_alpha)); // Clamp 0-1
|
||||
|
||||
elem->anim.is_move_fade = false;
|
||||
}
|
||||
|
||||
// Starts a "scale" animation on an element.
|
||||
void compositor_animate_scale(compositor_t* comp, uint32_t id,
|
||||
float to_scale,
|
||||
float duration_sec, easing_t easing) {
|
||||
float duration_sec, easing_t easing, bool queue) {
|
||||
if (!comp) return;
|
||||
element_t* elem = find_element(comp, id);
|
||||
if (!elem) {
|
||||
@@ -683,35 +733,204 @@ void compositor_animate_scale(compositor_t* comp, uint32_t id,
|
||||
return;
|
||||
}
|
||||
|
||||
elem->anim.type = ANIM_SCALE;
|
||||
elem->anim.easing = easing;
|
||||
elem->anim.progress_sec = 0.0f;
|
||||
elem->anim.duration_sec = duration_sec;
|
||||
|
||||
elem->anim.start_scale = elem->scale;
|
||||
elem->anim.end_scale = to_scale;
|
||||
|
||||
elem->anim.is_move_fade = false;
|
||||
}
|
||||
|
||||
// Example for a combined animation
|
||||
void compositor_animate_move_fade(compositor_t* comp, uint32_t id,
|
||||
int32_t to_x, int32_t to_y, float to_alpha,
|
||||
float duration_sec, easing_t easing) {
|
||||
if (!comp) return;
|
||||
element_t* elem = find_element(comp, id);
|
||||
if (!elem) {
|
||||
set_error(comp, "Element not found for animate_move_fade.");
|
||||
if (queue) {
|
||||
queued_animation_t queued;
|
||||
queued.type = ANIM_SCALE;
|
||||
queued.easing = easing;
|
||||
queued.duration_sec = duration_sec;
|
||||
queued.to_scale = to_scale;
|
||||
queued.is_move_fade = false;
|
||||
queued.affects_position = false;
|
||||
queued.affects_scale = true;
|
||||
queued.affects_alpha = false;
|
||||
elem->anim_queue.push_back(queued);
|
||||
return;
|
||||
}
|
||||
|
||||
// Trigger both
|
||||
compositor_animate_move(comp, id, to_x, to_y, duration_sec, easing);
|
||||
compositor_animate_fade(comp, id, to_alpha, duration_sec, easing);
|
||||
// Parallel execution with independent timing
|
||||
elem->anim.start_scale = elem->scale;
|
||||
elem->anim.end_scale = to_scale;
|
||||
elem->anim.animating_scale = true;
|
||||
|
||||
// Mark as combined
|
||||
elem->anim.type = ANIM_MOVE_FADE; // Override type
|
||||
elem->anim.is_move_fade = true;
|
||||
// Set scale-specific timing
|
||||
elem->anim.scale_anim.progress_sec = 0.0f;
|
||||
elem->anim.scale_anim.duration_sec = duration_sec;
|
||||
elem->anim.scale_anim.easing = easing;
|
||||
|
||||
if (elem->anim.type == ANIM_NONE) {
|
||||
elem->anim.type = ANIM_SCALE;
|
||||
}
|
||||
}
|
||||
|
||||
void compositor_animate_fade(compositor_t* comp, uint32_t id,
|
||||
float to_alpha,
|
||||
float duration_sec, easing_t easing, bool queue) {
|
||||
if (!comp) return;
|
||||
element_t* elem = find_element(comp, id);
|
||||
if (!elem) {
|
||||
set_error(comp, "Element not found for animate_fade.");
|
||||
return;
|
||||
}
|
||||
|
||||
to_alpha = std::max(0.0f, std::min(1.0f, to_alpha));
|
||||
|
||||
if (queue) {
|
||||
queued_animation_t queued;
|
||||
queued.type = ANIM_FADE;
|
||||
queued.easing = easing;
|
||||
queued.duration_sec = duration_sec;
|
||||
queued.to_alpha = to_alpha;
|
||||
queued.is_move_fade = false;
|
||||
queued.affects_position = false;
|
||||
queued.affects_scale = false;
|
||||
queued.affects_alpha = true;
|
||||
elem->anim_queue.push_back(queued);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parallel execution with independent timing
|
||||
if (elem->alpha == 0.0f && to_alpha > 0.0f) {
|
||||
elem->visible = true;
|
||||
}
|
||||
|
||||
elem->anim.start_alpha = elem->alpha;
|
||||
elem->anim.end_alpha = to_alpha;
|
||||
elem->anim.animating_alpha = true;
|
||||
|
||||
// Set alpha-specific timing
|
||||
elem->anim.alpha_anim.progress_sec = 0.0f;
|
||||
elem->anim.alpha_anim.duration_sec = duration_sec;
|
||||
elem->anim.alpha_anim.easing = easing;
|
||||
|
||||
if (elem->anim.type == ANIM_NONE) {
|
||||
elem->anim.type = ANIM_FADE;
|
||||
}
|
||||
}
|
||||
|
||||
// Complete all animations on a specific element
|
||||
void compositor_animation_complete(compositor_t* comp, uint32_t id) {
|
||||
if (!comp) return;
|
||||
element_t* elem = find_element(comp, id);
|
||||
if (!elem) {
|
||||
set_error(comp, "Element not found for animation_complete.");
|
||||
return;
|
||||
}
|
||||
|
||||
mark_element_dirty(comp, elem);
|
||||
bool position_changed = false;
|
||||
|
||||
// Complete current animation
|
||||
if (elem->anim.type != ANIM_NONE) {
|
||||
if (elem->anim.animating_position) {
|
||||
elem->x = elem->anim.end_x;
|
||||
elem->y = elem->anim.end_y;
|
||||
position_changed = true;
|
||||
}
|
||||
if (elem->anim.animating_alpha) {
|
||||
elem->alpha = elem->anim.end_alpha;
|
||||
if (elem->alpha == 0.0f) elem->visible = false;
|
||||
}
|
||||
if (elem->anim.animating_scale) {
|
||||
elem->scale = elem->anim.end_scale;
|
||||
position_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Complete all queued animations
|
||||
for (const auto& queued : elem->anim_queue) {
|
||||
if (queued.affects_position) {
|
||||
elem->x = queued.to_x;
|
||||
elem->y = queued.to_y;
|
||||
position_changed = true;
|
||||
}
|
||||
if (queued.affects_alpha) {
|
||||
elem->alpha = queued.to_alpha;
|
||||
if (elem->alpha == 0.0f) elem->visible = false;
|
||||
}
|
||||
if (queued.affects_scale) {
|
||||
elem->scale = queued.to_scale;
|
||||
position_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear animations
|
||||
elem->anim.type = ANIM_NONE;
|
||||
elem->anim.animating_position = false;
|
||||
elem->anim.animating_scale = false;
|
||||
elem->anim.animating_alpha = false;
|
||||
elem->anim_queue.clear();
|
||||
|
||||
// Update spatial hash
|
||||
if (position_changed) {
|
||||
clear_spatial_hash(comp, elem->id);
|
||||
update_spatial_hash(comp, elem);
|
||||
}
|
||||
else {
|
||||
mark_element_dirty(comp, elem);
|
||||
}
|
||||
}
|
||||
|
||||
// Complete ALL animations on ALL elements
|
||||
void compositor_animation_complete_all(compositor_t* comp) {
|
||||
if (!comp) return;
|
||||
|
||||
// Iterate over all elements
|
||||
for (auto& pair : comp->elements_map) {
|
||||
element_t* elem = &pair.second;
|
||||
|
||||
mark_element_dirty(comp, elem);
|
||||
bool position_changed = false;
|
||||
|
||||
// Complete current animation
|
||||
if (elem->anim.type != ANIM_NONE) {
|
||||
if (elem->anim.animating_position) {
|
||||
elem->x = elem->anim.end_x;
|
||||
elem->y = elem->anim.end_y;
|
||||
position_changed = true;
|
||||
}
|
||||
if (elem->anim.animating_alpha) {
|
||||
elem->alpha = elem->anim.end_alpha;
|
||||
if (elem->alpha == 0.0f) elem->visible = false;
|
||||
}
|
||||
if (elem->anim.animating_scale) {
|
||||
elem->scale = elem->anim.end_scale;
|
||||
position_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Complete all queued animations
|
||||
for (const auto& queued : elem->anim_queue) {
|
||||
if (queued.affects_position) {
|
||||
elem->x = queued.to_x;
|
||||
elem->y = queued.to_y;
|
||||
position_changed = true;
|
||||
}
|
||||
if (queued.affects_alpha) {
|
||||
elem->alpha = queued.to_alpha;
|
||||
if (elem->alpha == 0.0f) elem->visible = false;
|
||||
}
|
||||
if (queued.affects_scale) {
|
||||
elem->scale = queued.to_scale;
|
||||
position_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear animations
|
||||
elem->anim.type = ANIM_NONE;
|
||||
elem->anim.animating_position = false;
|
||||
elem->anim.animating_scale = false;
|
||||
elem->anim.animating_alpha = false;
|
||||
elem->anim_queue.clear();
|
||||
|
||||
// Update spatial hash
|
||||
if (position_changed) {
|
||||
clear_spatial_hash(comp, elem->id);
|
||||
update_spatial_hash(comp, elem);
|
||||
}
|
||||
else {
|
||||
mark_element_dirty(comp, elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Dirty Marking ---
|
||||
@@ -739,6 +958,40 @@ void compositor_mark_content_dirty(compositor_t* comp, uint32_t id) {
|
||||
comp->last_error.clear();
|
||||
}
|
||||
|
||||
// Returns true if any element has an active animation or queued animations.
|
||||
bool compositor_is_animating(compositor_t* comp) {
|
||||
if (!comp) return false;
|
||||
for (const auto& pair : comp->elements_map) {
|
||||
const element_t& elem = pair.second;
|
||||
const animation_t& anim = elem.anim;
|
||||
|
||||
// Active animation (type set) or any per-property animating flags
|
||||
if (anim.type != ANIM_NONE) return true;
|
||||
if (anim.animating_position || anim.animating_scale || anim.animating_alpha) return true;
|
||||
|
||||
// Pending queued animations
|
||||
if (!elem.anim_queue.empty()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Returns true if the specified element is animating or has queued animations.
|
||||
bool compositor_is_element_animating(compositor_t* comp, uint32_t id) {
|
||||
if (!comp) return false;
|
||||
element_t* elem = find_element(comp, id);
|
||||
if (!elem) {
|
||||
set_error(comp, "Element with ID " + std::to_string(id) + " not found to check animation.");
|
||||
return false;
|
||||
}
|
||||
const animation_t& anim = elem->anim;
|
||||
// Active animation (type set) or any per-property animating flags
|
||||
if (anim.type != ANIM_NONE) return true;
|
||||
if (anim.animating_position || anim.animating_scale || anim.animating_alpha) return true;
|
||||
// Pending queued animations
|
||||
if (!elem->anim_queue.empty()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Rendering ---
|
||||
|
||||
// Renders all dirty tiles and returns a pointer to the main framebuffer.
|
||||
@@ -807,72 +1060,110 @@ const uint32_t* compositor_compose(compositor_t* comp) {
|
||||
// --- Blending Loop (with SIMD) ---
|
||||
int32_t x = blend_x0;
|
||||
|
||||
#if defined(__SSE2__)
|
||||
// --- SSE2 Path (Processes 4 pixels at a time) ---
|
||||
#if defined(__SSE2__) || (defined(_M_AMD64) || defined(_M_X64))
|
||||
// Pre-calculate fixed values for the SIMD loop
|
||||
const __m128i v_zero = _mm_setzero_si128();
|
||||
const __m128i v_one = _mm_set1_epi16(1);
|
||||
const __m128i v_255 = _mm_set1_epi16(255);
|
||||
const __m128i v_elem_a_u8 = _mm_set1_epi16((short)elem_a_u8);
|
||||
const __m128i v_opaque_mask = _mm_set1_epi32(0xFF000000); // For setting final alpha to 0xFF
|
||||
|
||||
// Calculate the end point for the 4-pixel chunks
|
||||
int32_t simd_end = blend_x0 + ((blend_x1 - blend_x0) / 4) * 4;
|
||||
// Determine the loop boundary for 4-pixel chunks
|
||||
int32_t sse_limit = blend_x1 - (blend_x1 - x) % 4;
|
||||
|
||||
// Pre-calculate SSE constants
|
||||
const __m128i zero = _mm_setzero_si128();
|
||||
const __m128i c255 = _mm_set1_epi16(255);
|
||||
// Mask to force output alpha to 255 (0xFFFF in the 16-bit unpacked world)
|
||||
const __m128i alpha_mask = _mm_set_epi16(0, 0, 0, 0xFFFF, 0, 0, 0, 0xFFFF);
|
||||
// Element alpha broadcasted to 4 16-bit words
|
||||
const __m128i elem_a_16 = _mm_set1_epi16(elem_a_u8);
|
||||
for (; x < sse_limit; x += 4) {
|
||||
// 1. Map destination x to source x (Need 4 values: x, x+1, x+2, x+3)
|
||||
// This assumes inv_scale is pre-calculated. We must do the 4 lookups.
|
||||
int32_t src_x0 = (int32_t)((x - elem_x0) * inv_scale);
|
||||
int32_t src_x1 = (int32_t)(((x + 1) - elem_x0) * inv_scale);
|
||||
int32_t src_x2 = (int32_t)(((x + 2) - elem_x0) * inv_scale);
|
||||
int32_t src_x3 = (int32_t)(((x + 3) - elem_x0) * inv_scale);
|
||||
|
||||
for (; x < simd_end; x += 4) {
|
||||
// 1. GATHER: Map 4 dest x-coords to 4 source x-coords
|
||||
int32_t src_x0 = (int32_t)((x + 0 - elem_x0) * inv_scale);
|
||||
int32_t src_x1 = (int32_t)((x + 1 - elem_x0) * inv_scale);
|
||||
int32_t src_x2 = (int32_t)((x + 2 - elem_x0) * inv_scale);
|
||||
int32_t src_x3 = (int32_t)((x + 3 - elem_x0) * inv_scale);
|
||||
// 2. Sample 4 source pixels (ARGB format)
|
||||
uint32_t src_pixels[4];
|
||||
src_pixels[0] = src_row[src_x0];
|
||||
src_pixels[1] = src_row[src_x1];
|
||||
src_pixels[2] = src_row[src_x2];
|
||||
src_pixels[3] = src_row[src_x3];
|
||||
|
||||
// 2. LOAD: Load 4 source pixels and 4 destination pixels
|
||||
__m128i src_px = _mm_set_epi32(src_row[src_x3], src_row[src_x2], src_row[src_x1], src_row[src_x0]);
|
||||
__m128i dst_px = _mm_loadu_si128((__m128i*)(dst_row + x));
|
||||
__m128i v_src = _mm_loadu_si128((const __m128i*)src_pixels);
|
||||
__m128i v_dst = _mm_loadu_si128((const __m128i*) & dst_row[x]);
|
||||
|
||||
// 3. UNPACK: Unpack 8-bit channels to 16-bit
|
||||
__m128i src_lo = _mm_unpacklo_epi8(src_px, zero); // Pixels 0, 1
|
||||
__m128i src_hi = _mm_unpackhi_epi8(src_px, zero); // Pixels 2, 3
|
||||
__m128i dst_lo = _mm_unpacklo_epi8(dst_px, zero);
|
||||
__m128i dst_hi = _mm_unpackhi_epi8(dst_px, zero);
|
||||
// --- Unpack 4 pixels from 8-bit (ARGB) to 16-bit (A, R, G, B components) ---
|
||||
// v_src_16bit_lo: A0 R0 G0 B0 | A1 R1 G1 B1 (packed 16-bit values, high bits zero)
|
||||
// v_src_16bit_hi: A2 R2 G2 B2 | A3 R3 G3 B3
|
||||
__m128i v_src_16bit_lo = _mm_unpacklo_epi8(v_src, v_zero);
|
||||
__m128i v_src_16bit_hi = _mm_unpackhi_epi8(v_src, v_zero);
|
||||
|
||||
// 4. GET ALPHA: Get pixel alpha (A) from [B,G,R,A]
|
||||
// [A0,A0,A0,A0, A1,A1,A1,A1]
|
||||
__m128i src_a_lo = _mm_unpacklo_epi64(_mm_shufflelo_epi16(src_lo, _MM_SHUFFLE(3, 3, 3, 3)), _mm_shufflehi_epi16(src_lo, _MM_SHUFFLE(3, 3, 3, 3)));
|
||||
// [A2,A2,A2,A2, A3,A3,A3,A3]
|
||||
__m128i src_a_hi = _mm_unpacklo_epi64(_mm_shufflelo_epi16(src_hi, _MM_SHUFFLE(3, 3, 3, 3)), _mm_shufflehi_epi16(src_hi, _MM_SHUFFLE(3, 3, 3, 3)));
|
||||
__m128i v_dst_16bit_lo = _mm_unpacklo_epi8(v_dst, v_zero);
|
||||
__m128i v_dst_16bit_hi = _mm_unpackhi_epi8(v_dst, v_zero);
|
||||
|
||||
// 5. MODULATE ALPHA: (pixel_alpha * elem_alpha) / 255
|
||||
src_a_lo = _mm_srli_epi16(_mm_mullo_epi16(src_a_lo, elem_a_16), 8);
|
||||
src_a_hi = _mm_srli_epi16(_mm_mullo_epi16(src_a_hi, elem_a_16), 8);
|
||||
// 3. Modulate alpha (ModAlpha = (SrcAlpha * ElemAlpha) / 255)
|
||||
// Get Src Alpha
|
||||
__m128i v_src_a_lo = _mm_shufflelo_epi16(v_src_16bit_lo, _MM_SHUFFLE(3, 3, 3, 3)); // A0 A0 A0 A0 | A1 A1 A1 A1
|
||||
v_src_a_lo = _mm_shufflehi_epi16(v_src_a_lo, _MM_SHUFFLE(3, 3, 3, 3));
|
||||
__m128i v_src_a_hi = _mm_shufflelo_epi16(v_src_16bit_hi, _MM_SHUFFLE(3, 3, 3, 3)); // A2 A2 A2 A2 | A3 A3 A3 A3
|
||||
v_src_a_hi = _mm_shufflehi_epi16(v_src_a_hi, _MM_SHUFFLE(3, 3, 3, 3));
|
||||
|
||||
// 6. BLEND: (src * src_a) + (dst * (255 - src_a))
|
||||
__m128i inv_a_lo = _mm_sub_epi16(c255, src_a_lo);
|
||||
__m128i inv_a_hi = _mm_sub_epi16(c255, src_a_hi);
|
||||
// ModAlpha = SrcAlpha * ElemAlpha
|
||||
__m128i v_mod_a_lo = _mm_mullo_epi16(v_src_a_lo, v_elem_a_u8);
|
||||
__m128i v_mod_a_hi = _mm_mullo_epi16(v_src_a_hi, v_elem_a_u8);
|
||||
|
||||
__m128i src_scaled_lo = _mm_mullo_epi16(src_lo, src_a_lo);
|
||||
__m128i src_scaled_hi = _mm_mullo_epi16(src_hi, src_a_hi);
|
||||
// ModAlpha = (SrcAlpha * ElemAlpha + 255/2) / 255 (Simple division approximation)
|
||||
// For / 255, we use a more accurate approximation or just accept the error.
|
||||
// The scalar path uses (x / 255), we'll approximate with (x + 128) / 256 for a faster result.
|
||||
// ModAlpha_approx = (v_mod_a + 128) >> 8
|
||||
__m128i v_mod_a_lo_div = _mm_add_epi16(v_mod_a_lo, _mm_set1_epi16(128));
|
||||
__m128i v_mod_a_hi_div = _mm_add_epi16(v_mod_a_hi, _mm_set1_epi16(128));
|
||||
__m128i v_mod_a_approx_lo = _mm_srli_epi16(v_mod_a_lo_div, 8); // Now contains ModAlpha_approx for 4 pixels (A0..A3)
|
||||
__m128i v_mod_a_approx_hi = _mm_srli_epi16(v_mod_a_hi_div, 8);
|
||||
|
||||
__m128i dst_scaled_lo = _mm_mullo_epi16(dst_lo, inv_a_lo);
|
||||
__m128i dst_scaled_hi = _mm_mullo_epi16(dst_hi, inv_a_hi);
|
||||
// Replicate ModAlpha_approx for blending (A0 R0 G0 B0, A1 R1 G1 B1)
|
||||
__m128i v_mod_a_lo_blend = _mm_unpacklo_epi16(v_mod_a_approx_lo, v_mod_a_approx_lo);
|
||||
v_mod_a_lo_blend = _mm_unpacklo_epi32(v_mod_a_lo_blend, v_mod_a_lo_blend); // ModAlpha_0 for R0 G0 B0
|
||||
__m128i v_mod_a_hi_blend = _mm_unpacklo_epi16(v_mod_a_approx_hi, v_mod_a_approx_hi);
|
||||
v_mod_a_hi_blend = _mm_unpacklo_epi32(v_mod_a_hi_blend, v_mod_a_hi_blend); // ModAlpha_2 for R2 G2 B2
|
||||
|
||||
__m128i sum_lo = _mm_add_epi16(src_scaled_lo, dst_scaled_lo);
|
||||
__m128i sum_hi = _mm_add_epi16(src_scaled_hi, dst_scaled_hi);
|
||||
// Also need the alpha for the second pixel in each 64-bit lane
|
||||
__m128i v_mod_a_lo_1 = _mm_unpackhi_epi16(v_mod_a_approx_lo, v_mod_a_approx_lo);
|
||||
v_mod_a_lo_blend = _mm_unpackhi_epi64(v_mod_a_lo_blend, v_mod_a_lo_1); // A0 for R0 G0 B0 | A1 for R1 G1 B1
|
||||
__m128i v_mod_a_hi_1 = _mm_unpackhi_epi16(v_mod_a_approx_hi, v_mod_a_approx_hi);
|
||||
v_mod_a_hi_blend = _mm_unpackhi_epi64(v_mod_a_hi_blend, v_mod_a_hi_1); // A2 for R2 G2 B2 | A3 for R3 G3 B3
|
||||
|
||||
// Divide by 255 (by shifting right 8)
|
||||
__m128i out_lo = _mm_srli_epi16(sum_lo, 8);
|
||||
__m128i out_hi = _mm_srli_epi16(sum_hi, 8);
|
||||
// ModAlpha_blend now contains: [A0 B0 G0 R0 | A1 B1 G1 R1] and [A2 B2 G2 R2 | A3 B3 G3 R3] (reversed order for convenience)
|
||||
// It's crucial: the alpha value is replicated to the R, G, B positions of its corresponding pixel.
|
||||
|
||||
// 7. SET ALPHA: Force output alpha to 255
|
||||
out_lo = _mm_or_si128(out_lo, alpha_mask);
|
||||
out_hi = _mm_or_si128(out_hi, alpha_mask);
|
||||
// Calculate InvAlpha = 255 - ModAlpha
|
||||
__m128i v_inv_a_lo_blend = _mm_sub_epi16(v_255, v_mod_a_lo_blend);
|
||||
__m128i v_inv_a_hi_blend = _mm_sub_epi16(v_255, v_mod_a_hi_blend);
|
||||
|
||||
// 8. PACK & STORE: Pack 16-bit channels back to 8-bit
|
||||
__m128i packed_out = _mm_packus_epi16(out_lo, out_hi);
|
||||
_mm_storeu_si128((__m128i*)(dst_row + x), packed_out);
|
||||
// 4. Blend: (Src * ModA + Dst * InvA) / 255
|
||||
// Src * ModA
|
||||
__m128i v_src_x_mod_a_lo = _mm_mullo_epi16(_mm_andnot_si128(v_opaque_mask, v_src_16bit_lo), v_mod_a_lo_blend);
|
||||
__m128i v_src_x_mod_a_hi = _mm_mullo_epi16(_mm_andnot_si128(v_opaque_mask, v_src_16bit_hi), v_mod_a_hi_blend);
|
||||
|
||||
// Dst * InvA
|
||||
__m128i v_dst_x_inv_a_lo = _mm_mullo_epi16(_mm_andnot_si128(v_opaque_mask, v_dst_16bit_lo), v_inv_a_lo_blend);
|
||||
__m128i v_dst_x_inv_a_hi = _mm_mullo_epi16(_mm_andnot_si128(v_opaque_mask, v_dst_16bit_hi), v_inv_a_hi_blend);
|
||||
|
||||
// Sum
|
||||
__m128i v_sum_lo = _mm_add_epi16(v_src_x_mod_a_lo, v_dst_x_inv_a_lo);
|
||||
__m128i v_sum_hi = _mm_add_epi16(v_src_x_mod_a_hi, v_dst_x_inv_a_hi);
|
||||
|
||||
// Divide by 255: (v_sum + 128) / 256 (Fast approx)
|
||||
__m128i v_out_16bit_lo = _mm_srli_epi16(_mm_add_epi16(v_sum_lo, _mm_set1_epi16(128)), 8);
|
||||
__m128i v_out_16bit_hi = _mm_srli_epi16(_mm_add_epi16(v_sum_hi, _mm_set1_epi16(128)), 8);
|
||||
|
||||
// Pack the results back to 8-bit, keeping the alpha from v_opaque_mask (0xFF)
|
||||
__m128i v_out_8bit_packed = _mm_packus_epi16(v_out_16bit_lo, v_out_16bit_hi);
|
||||
|
||||
// OR with the Opaque Alpha mask
|
||||
__m128i v_final_out = _mm_or_si128(v_out_8bit_packed, v_opaque_mask);
|
||||
|
||||
// Store the result
|
||||
_mm_storeu_si128((__m128i*) & dst_row[x], v_final_out);
|
||||
}
|
||||
#endif
|
||||
// --- Scalar Path (Processes remaining pixels 1 by 1) ---
|
||||
// This loop handles the "tail" on SSE2, or all pixels on ARM.
|
||||
for (; x < blend_x1; x++) {
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ uint32_t* image_load_resized(const char* filename, int desired_width, int desire
|
||||
// Allocate buffer for resized image
|
||||
unsigned char* resized = (unsigned char*)malloc(desired_width * desired_height * 4);
|
||||
if (!resized) {
|
||||
fprintf(stderr, "image.cpp 41: malloc failed!");
|
||||
fprintf(stderr, "image.cpp 43: malloc failed!");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
|
||||
+105
-760
@@ -1,8 +1,6 @@
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
#include <vector>
|
||||
#include <window.h>
|
||||
@@ -10,527 +8,15 @@
|
||||
#include <compositor.h>
|
||||
#include <image.h>
|
||||
|
||||
// ============================================================================
|
||||
// COMMAND SYSTEM
|
||||
// ============================================================================
|
||||
|
||||
enum CommandType {
|
||||
CMD_SHOW,
|
||||
CMD_HIDE,
|
||||
CMD_MOVE,
|
||||
CMD_CHANGE_SPRITE,
|
||||
CMD_SET_Z,
|
||||
CMD_SET_SCALE,
|
||||
CMD_SPEAK,
|
||||
CMD_CLEAR,
|
||||
CMD_WAIT,
|
||||
CMD_BACKGROUND,
|
||||
};
|
||||
|
||||
enum TransitionType {
|
||||
TRANS_NONE,
|
||||
TRANS_FADE,
|
||||
TRANS_SLIDE_LEFT,
|
||||
TRANS_SLIDE_RIGHT,
|
||||
};
|
||||
|
||||
struct CmdShow {
|
||||
const char* character;
|
||||
const char* sprite;
|
||||
int x, y, z;
|
||||
TransitionType transition;
|
||||
float duration_ms;
|
||||
};
|
||||
|
||||
struct CmdHide {
|
||||
const char* character;
|
||||
TransitionType transition;
|
||||
float duration_ms;
|
||||
};
|
||||
|
||||
struct CmdMove {
|
||||
const char* character;
|
||||
int x, y;
|
||||
TransitionType transition;
|
||||
float duration_ms;
|
||||
};
|
||||
|
||||
struct CmdChangeSprite {
|
||||
const char* character;
|
||||
const char* sprite;
|
||||
};
|
||||
|
||||
struct CmdSetZ {
|
||||
const char* character;
|
||||
int z;
|
||||
};
|
||||
|
||||
struct CmdSetScale {
|
||||
const char* character;
|
||||
float scale;
|
||||
TransitionType transition;
|
||||
float duration_ms;
|
||||
};
|
||||
|
||||
struct CmdSpeak {
|
||||
const char* speaker;
|
||||
const char* text;
|
||||
float text_speed;
|
||||
};
|
||||
|
||||
struct CmdWait {};
|
||||
struct CmdClear {};
|
||||
struct CmdBackground {
|
||||
const char* filename;
|
||||
TransitionType transition;
|
||||
float duration_ms;
|
||||
};
|
||||
|
||||
struct Command {
|
||||
CommandType type;
|
||||
bool blocking;
|
||||
|
||||
union {
|
||||
CmdShow show;
|
||||
CmdHide hide;
|
||||
CmdMove move;
|
||||
CmdChangeSprite change_sprite;
|
||||
CmdSetZ set_z;
|
||||
CmdSetScale set_scale;
|
||||
CmdSpeak speak;
|
||||
CmdWait wait;
|
||||
CmdClear clear;
|
||||
CmdBackground background;
|
||||
};
|
||||
};
|
||||
|
||||
struct Scene {
|
||||
const Command* commands;
|
||||
int total_commands;
|
||||
int current_command;
|
||||
|
||||
void reset() { current_command = 0; }
|
||||
bool has_next() const { return current_command < total_commands; }
|
||||
const Command* current() const {
|
||||
if (current_command < total_commands) return &commands[current_command];
|
||||
return nullptr;
|
||||
}
|
||||
void next() {
|
||||
if (current_command < total_commands) current_command++;
|
||||
}
|
||||
};
|
||||
|
||||
constexpr Command Show(const char* character, const char* sprite, int x, int y, int z,
|
||||
TransitionType trans = TRANS_NONE, float duration = 0.0f, bool blocking = true)
|
||||
{
|
||||
return Command{
|
||||
.type = CMD_SHOW,
|
||||
.blocking = blocking,
|
||||
.show = CmdShow{character, sprite, x, y, z, trans, duration}
|
||||
};
|
||||
}
|
||||
|
||||
constexpr Command Hide(const char* character, TransitionType trans = TRANS_NONE, float duration = 0.0f, bool blocking = true)
|
||||
{
|
||||
return Command{
|
||||
.type = CMD_HIDE,
|
||||
.blocking = blocking,
|
||||
.hide = CmdHide{character, trans, duration}
|
||||
};
|
||||
}
|
||||
|
||||
constexpr Command Speak(const char* speaker, const char* text, float text_speed = 30.0f, bool blocking = true)
|
||||
{
|
||||
return Command{
|
||||
.type = CMD_SPEAK,
|
||||
.blocking = blocking,
|
||||
.speak = CmdSpeak{speaker, text, text_speed}
|
||||
};
|
||||
}
|
||||
|
||||
constexpr Command Wait(bool blocking = true)
|
||||
{
|
||||
return Command{ .type = CMD_WAIT, .blocking = blocking, .wait = {} };
|
||||
}
|
||||
|
||||
constexpr Command Clear(bool blocking = true)
|
||||
{
|
||||
return Command{ .type = CMD_CLEAR, .blocking = blocking, .clear = {} };
|
||||
}
|
||||
|
||||
constexpr Command SetZ(const char* character, int z, bool blocking = false)
|
||||
{
|
||||
return Command{ .type = CMD_SET_Z, .blocking = blocking, .set_z = CmdSetZ{character, z} };
|
||||
}
|
||||
|
||||
constexpr Command SetScale(const char* character, float scale, TransitionType trans = TRANS_NONE, float duration = 0.0f, bool blocking = false)
|
||||
{
|
||||
return Command{ .type = CMD_SET_SCALE, .blocking = blocking, .set_scale = CmdSetScale{character, scale, trans, duration} };
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ANIMATION TRACKER
|
||||
// ============================================================================
|
||||
|
||||
struct ActiveAnimation {
|
||||
float remaining_time;
|
||||
|
||||
ActiveAnimation(float duration) : remaining_time(duration) {}
|
||||
};
|
||||
|
||||
class AnimationTracker {
|
||||
private:
|
||||
std::vector<ActiveAnimation> active_animations;
|
||||
|
||||
public:
|
||||
void add_animation(float duration_sec) {
|
||||
if (duration_sec > 0.0f) {
|
||||
active_animations.push_back(ActiveAnimation(duration_sec));
|
||||
}
|
||||
}
|
||||
|
||||
void update(float delta_time) {
|
||||
for (size_t i = 0; i < active_animations.size(); ) {
|
||||
active_animations[i].remaining_time -= delta_time;
|
||||
if (active_animations[i].remaining_time <= 0.0f) {
|
||||
// Animation finished, remove it
|
||||
active_animations.erase(active_animations.begin() + i);
|
||||
}
|
||||
else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool has_active_animations() const {
|
||||
return !active_animations.empty();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
active_animations.clear();
|
||||
}
|
||||
|
||||
int count() const {
|
||||
return (int)active_animations.size(); // it won't overflow.. right?
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// CHARACTER MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
struct SpriteElement {
|
||||
uint32_t* image_data;
|
||||
uint32_t element_id;
|
||||
bool visible;
|
||||
};
|
||||
|
||||
struct Character {
|
||||
std::string name;
|
||||
std::unordered_map<std::string, SpriteElement> sprites;
|
||||
int width, height;
|
||||
int current_x, current_y;
|
||||
int current_z;
|
||||
float current_scale;
|
||||
std::string current_sprite;
|
||||
};
|
||||
|
||||
class CharacterManager {
|
||||
private:
|
||||
std::unordered_map<std::string, Character> characters;
|
||||
compositor_t* compositor;
|
||||
int screen_width;
|
||||
|
||||
public:
|
||||
CharacterManager(compositor_t* comp, int screen_w)
|
||||
: compositor(comp), screen_width(screen_w) {
|
||||
}
|
||||
|
||||
void add_character(const char* name, int width, int height) {
|
||||
Character ch;
|
||||
ch.name = name;
|
||||
ch.width = width;
|
||||
ch.height = height;
|
||||
ch.current_x = 0;
|
||||
ch.current_y = 0;
|
||||
ch.current_z = 1;
|
||||
ch.current_scale = 1.0f;
|
||||
ch.current_sprite = "";
|
||||
characters[name] = ch;
|
||||
}
|
||||
|
||||
void load_sprite(const char* name, const char* emotion, const char* filename,
|
||||
int width, int height, int initial_z = 1) {
|
||||
auto it = characters.find(name);
|
||||
if (it == characters.end()) return;
|
||||
|
||||
uint32_t* img = image_load_resized(filename, width, height);
|
||||
if (!img) return;
|
||||
|
||||
SpriteElement elem;
|
||||
elem.image_data = img;
|
||||
elem.element_id = compositor_insert(compositor, img, width, height, 0, 0, initial_z);
|
||||
elem.visible = false;
|
||||
|
||||
compositor_set_alpha(compositor, elem.element_id, 0.0f);
|
||||
compositor_set_visible(compositor, elem.element_id, false);
|
||||
|
||||
it->second.sprites[emotion] = elem;
|
||||
}
|
||||
|
||||
Character* get_character(const char* name) {
|
||||
auto it = characters.find(name);
|
||||
if (it != characters.end()) return &it->second;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t get_active_element_id(Character* ch) {
|
||||
if (!ch || ch->current_sprite.empty()) return 0;
|
||||
auto it = ch->sprites.find(ch->current_sprite);
|
||||
if (it == ch->sprites.end()) return 0;
|
||||
return it->second.element_id;
|
||||
}
|
||||
|
||||
void show_character(const char* name, int x, int y, int z, const char* sprite_name,
|
||||
TransitionType transition, float duration_ms) {
|
||||
Character* ch = get_character(name);
|
||||
if (!ch) return;
|
||||
|
||||
for (auto& pair : ch->sprites) {
|
||||
if (pair.first != sprite_name && pair.second.visible) {
|
||||
compositor_set_visible(compositor, pair.second.element_id, false);
|
||||
compositor_set_alpha(compositor, pair.second.element_id, 0.0f);
|
||||
pair.second.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
auto it = ch->sprites.find(sprite_name);
|
||||
if (it == ch->sprites.end()) return;
|
||||
|
||||
SpriteElement& elem = it->second;
|
||||
float duration_sec = duration_ms / 1000.0f;
|
||||
|
||||
ch->current_x = x;
|
||||
ch->current_y = y;
|
||||
ch->current_z = z;
|
||||
ch->current_sprite = sprite_name;
|
||||
elem.visible = true;
|
||||
|
||||
compositor_set_zorder(compositor, elem.element_id, z);
|
||||
|
||||
if (transition == TRANS_FADE) {
|
||||
compositor_set_position(compositor, elem.element_id, x, y);
|
||||
compositor_set_visible(compositor, elem.element_id, true);
|
||||
compositor_animate_fade(compositor, elem.element_id, 1.0f, duration_sec, EASE_LINEAR);
|
||||
}
|
||||
else if (transition == TRANS_SLIDE_LEFT) {
|
||||
int start_x = -ch->width;
|
||||
compositor_set_position(compositor, elem.element_id, start_x, y);
|
||||
compositor_set_alpha(compositor, elem.element_id, 1.0f);
|
||||
compositor_set_visible(compositor, elem.element_id, true);
|
||||
compositor_animate_move(compositor, elem.element_id, x, y, duration_sec, EASE_OUT_CUBIC);
|
||||
}
|
||||
else if (transition == TRANS_SLIDE_RIGHT) {
|
||||
int start_x = screen_width;
|
||||
compositor_set_position(compositor, elem.element_id, start_x, y);
|
||||
compositor_set_alpha(compositor, elem.element_id, 1.0f);
|
||||
compositor_set_visible(compositor, elem.element_id, true);
|
||||
compositor_animate_move(compositor, elem.element_id, x, y, duration_sec, EASE_OUT_CUBIC);
|
||||
}
|
||||
else {
|
||||
compositor_set_position(compositor, elem.element_id, x, y);
|
||||
compositor_set_alpha(compositor, elem.element_id, 1.0f);
|
||||
compositor_set_visible(compositor, elem.element_id, true);
|
||||
}
|
||||
}
|
||||
|
||||
void hide_character(const char* name, TransitionType transition, float duration_ms) {
|
||||
Character* ch = get_character(name);
|
||||
if (!ch) return;
|
||||
|
||||
float duration_sec = duration_ms / 1000.0f;
|
||||
|
||||
for (auto& pair : ch->sprites) {
|
||||
if (pair.second.visible) {
|
||||
if (transition == TRANS_FADE) {
|
||||
compositor_animate_fade(compositor, pair.second.element_id, 0.0f, duration_sec, EASE_LINEAR);
|
||||
}
|
||||
else {
|
||||
compositor_set_visible(compositor, pair.second.element_id, false);
|
||||
compositor_set_alpha(compositor, pair.second.element_id, 0.0f);
|
||||
}
|
||||
pair.second.visible = false;
|
||||
}
|
||||
}
|
||||
ch->current_sprite = "";
|
||||
}
|
||||
|
||||
void move_character(const char* name, int x, int y, TransitionType transition, float duration_ms) {
|
||||
Character* ch = get_character(name);
|
||||
uint32_t id = get_active_element_id(ch);
|
||||
if (id == 0) return;
|
||||
|
||||
ch->current_x = x;
|
||||
ch->current_y = y;
|
||||
float duration_sec = duration_ms / 1000.0f;
|
||||
|
||||
if (transition == TRANS_SLIDE_LEFT || transition == TRANS_SLIDE_RIGHT) {
|
||||
compositor_animate_move(compositor, id, x, y, duration_sec, EASE_IN_OUT_QUAD);
|
||||
}
|
||||
else {
|
||||
compositor_set_position(compositor, id, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
void change_sprite(const char* name, const char* new_emotion) {
|
||||
Character* ch = get_character(name);
|
||||
if (!ch) return;
|
||||
|
||||
if (!ch->current_sprite.empty()) {
|
||||
auto old_it = ch->sprites.find(ch->current_sprite);
|
||||
if (old_it != ch->sprites.end()) {
|
||||
compositor_set_visible(compositor, old_it->second.element_id, false);
|
||||
compositor_set_alpha(compositor, old_it->second.element_id, 0.0f);
|
||||
old_it->second.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
auto new_it = ch->sprites.find(new_emotion);
|
||||
if (new_it != ch->sprites.end()) {
|
||||
compositor_set_position(compositor, new_it->second.element_id,
|
||||
ch->current_x, ch->current_y);
|
||||
compositor_set_zorder(compositor, new_it->second.element_id, ch->current_z);
|
||||
compositor_set_alpha(compositor, new_it->second.element_id, 1.0f);
|
||||
compositor_set_visible(compositor, new_it->second.element_id, true);
|
||||
new_it->second.visible = true;
|
||||
ch->current_sprite = new_emotion;
|
||||
}
|
||||
}
|
||||
|
||||
void set_z(const char* name, int z) {
|
||||
Character* ch = get_character(name);
|
||||
uint32_t id = get_active_element_id(ch);
|
||||
if (id == 0) return;
|
||||
|
||||
ch->current_z = z;
|
||||
compositor_set_zorder(compositor, id, z);
|
||||
}
|
||||
|
||||
void set_scale(const char* name, float scale, TransitionType transition, float duration_ms) {
|
||||
Character* ch = get_character(name);
|
||||
uint32_t id = get_active_element_id(ch);
|
||||
if (id == 0) return;
|
||||
|
||||
ch->current_scale = scale;
|
||||
float duration_sec = duration_ms / 1000.0f;
|
||||
|
||||
if (transition == TRANS_FADE) {
|
||||
compositor_animate_scale(compositor, id, scale, duration_sec, EASE_IN_OUT_QUAD);
|
||||
}
|
||||
else {
|
||||
compositor_set_scale(compositor, id, scale);
|
||||
}
|
||||
}
|
||||
|
||||
~CharacterManager() {
|
||||
for (auto& pair : characters) {
|
||||
for (auto& sprite : pair.second.sprites) {
|
||||
image_free(sprite.second.image_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// COMMAND EXECUTOR
|
||||
// ============================================================================
|
||||
|
||||
class CommandExecutor {
|
||||
private:
|
||||
CharacterManager* char_manager;
|
||||
compositor_t* compositor;
|
||||
text_t* name_text;
|
||||
text_t* dialogue_text;
|
||||
uint32_t name_id;
|
||||
uint32_t dialogue_id;
|
||||
|
||||
public:
|
||||
CommandExecutor(CharacterManager* cm, compositor_t* comp,
|
||||
text_t* nt, text_t* dt, uint32_t nid, uint32_t did)
|
||||
: char_manager(cm), compositor(comp), name_text(nt), dialogue_text(dt),
|
||||
name_id(nid), dialogue_id(did) {
|
||||
}
|
||||
|
||||
float execute(const Command* cmd) {
|
||||
switch (cmd->type) {
|
||||
case CMD_SHOW:
|
||||
char_manager->show_character(cmd->show.character, cmd->show.x, cmd->show.y,
|
||||
cmd->show.z, cmd->show.sprite,
|
||||
cmd->show.transition, cmd->show.duration_ms);
|
||||
return cmd->show.duration_ms / 1000.0f;
|
||||
|
||||
case CMD_HIDE:
|
||||
char_manager->hide_character(cmd->hide.character,
|
||||
cmd->hide.transition, cmd->hide.duration_ms);
|
||||
return cmd->hide.duration_ms / 1000.0f;
|
||||
|
||||
case CMD_MOVE:
|
||||
char_manager->move_character(cmd->move.character, cmd->move.x, cmd->move.y,
|
||||
cmd->move.transition, cmd->move.duration_ms);
|
||||
return cmd->move.duration_ms / 1000.0f;
|
||||
|
||||
case CMD_CHANGE_SPRITE:
|
||||
char_manager->change_sprite(cmd->change_sprite.character, cmd->change_sprite.sprite);
|
||||
break;
|
||||
|
||||
case CMD_SET_Z:
|
||||
char_manager->set_z(cmd->set_z.character, cmd->set_z.z);
|
||||
break;
|
||||
|
||||
case CMD_SET_SCALE:
|
||||
char_manager->set_scale(cmd->set_scale.character, cmd->set_scale.scale,
|
||||
cmd->set_scale.transition, cmd->set_scale.duration_ms);
|
||||
return cmd->set_scale.duration_ms / 1000.0f;
|
||||
|
||||
case CMD_SPEAK:
|
||||
if (cmd->speak.speaker) {
|
||||
text_push(name_text, cmd->speak.speaker, 34.0f, 0.0f, "assets/Vera.ttf", true);
|
||||
}
|
||||
if (cmd->speak.text) {
|
||||
text_push(dialogue_text, cmd->speak.text, 24.0f, cmd->speak.text_speed, "assets/Vera.ttf", true);
|
||||
}
|
||||
compositor_mark_content_dirty(compositor, name_id);
|
||||
compositor_mark_content_dirty(compositor, dialogue_id);
|
||||
break;
|
||||
|
||||
case CMD_CLEAR:
|
||||
text_clear(name_text);
|
||||
text_clear(dialogue_text);
|
||||
break;
|
||||
|
||||
case CMD_WAIT:
|
||||
case CMD_BACKGROUND:
|
||||
break;
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// MAIN PROGRAM
|
||||
// ============================================================================
|
||||
|
||||
enum AppState {
|
||||
STATE_IDLE,
|
||||
STATE_ANIMATING,
|
||||
STATE_WAITING_FOR_INPUT
|
||||
};
|
||||
|
||||
int main() {
|
||||
const int WIDTH = 1280;
|
||||
const int HEIGHT = 720;
|
||||
const int CHAR_WIDTH = 400;
|
||||
const int CHAR_HEIGHT = 600;
|
||||
const int CHAR_Y_OFFSET = 140;
|
||||
const int POS_Y = HEIGHT - CHAR_HEIGHT - CHAR_Y_OFFSET;
|
||||
|
||||
window_t* window = window_create(WIDTH, HEIGHT, "Visual Novel Engine");
|
||||
window_t* window = window_create(WIDTH, HEIGHT, u8"The Obwarzanek Narrative Engine");
|
||||
if (!window) {
|
||||
printf("Failed to create window\n");
|
||||
return 1;
|
||||
@@ -549,198 +35,56 @@ int main() {
|
||||
compositor_set_alpha(compositor, bg_id, 1.0f);
|
||||
}
|
||||
|
||||
const int CHAR_WIDTH = 400;
|
||||
const int CHAR_HEIGHT = 600;
|
||||
const int CHAR_Y_OFFSET = 140;
|
||||
const int POS_Y = HEIGHT - CHAR_HEIGHT - CHAR_Y_OFFSET;
|
||||
|
||||
CharacterManager char_manager(compositor, WIDTH);
|
||||
|
||||
char_manager.add_character("maksw", CHAR_WIDTH, CHAR_HEIGHT);
|
||||
char_manager.add_character("wikor", CHAR_WIDTH, CHAR_HEIGHT);
|
||||
|
||||
char_manager.load_sprite("maksw", "neutral", "assets/maksw.png", CHAR_WIDTH, CHAR_HEIGHT);
|
||||
char_manager.load_sprite("wikor", "neutral", "assets/wikor.png", CHAR_WIDTH, CHAR_HEIGHT);
|
||||
|
||||
uint32_t* textbox = image_load_resized("assets/textbox.png", WIDTH, 180);
|
||||
if (textbox) {
|
||||
uint32_t box_id = compositor_insert(compositor, textbox, WIDTH, 180, 0, HEIGHT - 180, 3);
|
||||
compositor_set_alpha(compositor, box_id, 1.0f);
|
||||
compositor_insert(compositor, textbox, WIDTH, 180, 0, HEIGHT - 180, 3);
|
||||
}
|
||||
|
||||
text_t* name_text = text_init(WIDTH, HEIGHT);
|
||||
uint32_t* maksw_happy = image_load_resized("assets/maksw.png", CHAR_WIDTH, CHAR_HEIGHT);
|
||||
uint32_t maksw_happy_id = compositor_insert(compositor, maksw_happy,
|
||||
CHAR_WIDTH, CHAR_HEIGHT, -600, POS_Y, 1);
|
||||
|
||||
uint32_t* maksw_neutral = image_load_resized("assets/maksw2.png", CHAR_WIDTH, CHAR_HEIGHT);
|
||||
uint32_t maksw_neutral_id = compositor_insert(compositor, maksw_neutral,
|
||||
CHAR_WIDTH, CHAR_HEIGHT, 60, POS_Y, 1);
|
||||
compositor_set_visible(compositor, maksw_neutral_id, false);
|
||||
|
||||
uint32_t* wikor = image_load_resized("assets/wikor.png", CHAR_WIDTH, CHAR_HEIGHT);
|
||||
uint32_t wikor_id = compositor_insert(compositor, wikor,
|
||||
CHAR_WIDTH, CHAR_HEIGHT, WIDTH - CHAR_WIDTH - 60, POS_Y, 2);
|
||||
|
||||
text_t* name_text = text_init(300, 100);
|
||||
text_set_font(name_text, "assets/Aller_Rg.ttf", 40.0f);
|
||||
uint32_t name_id = compositor_insert(compositor, text_getfb(name_text),
|
||||
WIDTH, HEIGHT, 60, HEIGHT - 170, 4);
|
||||
compositor_set_alpha(compositor, name_id, 1.0f);
|
||||
300, 100, 60, HEIGHT - 180, 4);
|
||||
|
||||
text_t* dialogue_text = text_init_shared(name_text, WIDTH - 140, HEIGHT);
|
||||
text_t* dialogue_text = text_init(WIDTH - 140, 160);
|
||||
text_set_font(dialogue_text, "assets/Vera.ttf", 30.0f);
|
||||
uint32_t dialogue_id = compositor_insert(compositor, text_getfb(dialogue_text),
|
||||
WIDTH - 140, HEIGHT, 60, HEIGHT - 130, 4);
|
||||
compositor_set_alpha(compositor, dialogue_id, 1.0f);
|
||||
WIDTH - 140, 160, 60, HEIGHT - 130, 4);
|
||||
|
||||
// --- Scene Script ---
|
||||
Command scene_commands[] = {
|
||||
// entrances
|
||||
Show("maksw", "neutral", 100, POS_Y, 2, TRANS_SLIDE_LEFT, 500.0f, false),
|
||||
Show("wikor", "neutral", 800, POS_Y, 1, TRANS_SLIDE_RIGHT, 500.0f, true),
|
||||
uint32_t* black = (uint32_t*)malloc(WIDTH * HEIGHT * sizeof(uint32_t));
|
||||
for (int i = 0; i < WIDTH * HEIGHT; i++) {
|
||||
black[i] = 0xFF000000;
|
||||
}
|
||||
uint32_t black_id = compositor_insert(compositor, black,
|
||||
WIDTH, HEIGHT, 0, 0, 5);
|
||||
compositor_set_alpha(compositor, black_id, 0.0f);
|
||||
compositor_set_visible(compositor, black_id, false);
|
||||
|
||||
// wilczur starts (maksw)
|
||||
SetScale("maksw", 1.05f, TRANS_FADE, 200.0f),
|
||||
SetZ("maksw", 3),
|
||||
SetScale("wikor", 1.0f, TRANS_FADE, 200.0f),
|
||||
SetZ("wikor", 1),
|
||||
|
||||
Speak(u8"wilczur", u8"Long day, huh? Feels like we ran a marathon we didn't sign up for."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
Speak(u8"wilczur", u8"I thought the report would close this morning. Guess not."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
// wiktor responds (wikor)
|
||||
SetScale("wikor", 1.05f, TRANS_FADE, 200.0f),
|
||||
SetZ("wikor", 3),
|
||||
SetScale("maksw", 1.0f, TRANS_FADE, 200.0f),
|
||||
SetZ("maksw", 1),
|
||||
|
||||
Speak(u8"admirał wiktorov", u8"You always underestimate bureaucracy's appetite for time."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
Speak(u8"admirał wiktorov", u8"It eats minutes like some folks eat sweets."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
// narrator (reset both scales)
|
||||
//SetScale("maksw", 1.0f, TRANS_FADE, 150.0f),
|
||||
//SetZ("maksw", 1),
|
||||
SetScale("wikor", 1.0f, TRANS_FADE, 150.0f),
|
||||
SetZ("wikor", 1),
|
||||
|
||||
Speak(u8"", u8"The lights in the corridor hummed, steady and indifferent."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
// wilczur again
|
||||
SetScale("maksw", 1.05f, TRANS_FADE, 200.0f),
|
||||
SetZ("maksw", 3),
|
||||
//SetScale("wikor", 1.0f, TRANS_FADE, 200.0f),
|
||||
//SetZ("wikor", 1),
|
||||
|
||||
Speak(u8"wilczur", u8"I keep telling myself 'five minutes' and then three episodes later it's midnight."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
Speak(u8"wilczur", u8"Maybe I'm cursed to wait in dim rooms forever."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
// wiktor
|
||||
SetScale("wikor", 1.05f, TRANS_FADE, 200.0f),
|
||||
SetZ("wikor", 3),
|
||||
SetScale("maksw", 1.0f, TRANS_FADE, 200.0f),
|
||||
SetZ("maksw", 1),
|
||||
|
||||
Speak(u8"admirał wiktorov", u8"Or blessed with patience you haven't earned yet."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
Speak(u8"admirał wiktorov", u8"Either way, keep the coffee coming and nobody gets dramatic."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
// narrator
|
||||
//SetScale("maksw", 1.0f, TRANS_FADE, 150.0f),
|
||||
//SetZ("maksw", 1),
|
||||
SetScale("wikor", 1.0f, TRANS_FADE, 150.0f),
|
||||
SetZ("wikor", 1),
|
||||
|
||||
Speak(u8"", u8"A faint orange band slid behind the clouds like a secret smile."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
// wilczur
|
||||
SetScale("maksw", 1.05f, TRANS_FADE, 200.0f),
|
||||
SetZ("maksw", 3),
|
||||
//SetScale("wikor", 1.0f, TRANS_FADE, 200.0f),
|
||||
//SetZ("wikor", 1),
|
||||
|
||||
Speak(u8"wilczur", u8"Did you see that? A flare, or the heavens showing off."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
Speak(u8"wilczur", u8"If it's a flare, somebody's throwing a party I wasn't invited to."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
// wiktor
|
||||
SetScale("wikor", 1.05f, TRANS_FADE, 200.0f),
|
||||
SetZ("wikor", 3),
|
||||
SetScale("maksw", 1.0f, TRANS_FADE, 200.0f),
|
||||
SetZ("maksw", 1),
|
||||
|
||||
Speak(u8"admirał wiktorov", u8"Not every bright thing is celebration. Could be dust, reflection."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
Speak(u8"admirał wiktorov", u8"But reflection's a good guess — keeps us calmer than panic does."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
// narrator
|
||||
//SetScale("maksw", 1.0f, TRANS_FADE, 150.0f),
|
||||
//SetZ("maksw", 1),
|
||||
SetScale("wikor", 1.0f, TRANS_FADE, 150.0f),
|
||||
SetZ("wikor", 1),
|
||||
|
||||
Speak(u8"", u8"The distant rumble that followed was more felt than heard."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
// final small exchange
|
||||
SetScale("maksw", 1.05f, TRANS_FADE, 200.0f),
|
||||
SetZ("maksw", 3),
|
||||
//SetScale("wikor", 1.0f, TRANS_FADE, 200.0f),
|
||||
//SetZ("wikor", 1),
|
||||
|
||||
Speak(u8"wilczur", u8"If the sky's putting on a show, at least the scenery's decent."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
Speak(u8"wilczur", u8"Make a note: next time, bring proper snacks."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
SetScale("wikor", 1.05f, TRANS_FADE, 200.0f),
|
||||
SetZ("wikor", 3),
|
||||
SetScale("maksw", 1.0f, TRANS_FADE, 200.0f),
|
||||
SetZ("maksw", 1),
|
||||
|
||||
Speak(u8"admirał wiktorov", u8"Noted. I'll smuggle provisions like a cunning admiral."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
Speak(u8"admirał wiktorov", u8"Till then, keep an eye on the horizon and your soda stash safe."),
|
||||
Wait(),
|
||||
Clear(),
|
||||
|
||||
// exits
|
||||
Hide("maksw", TRANS_FADE, 500.0f, false),
|
||||
Hide("wikor", TRANS_FADE, 500.0f, true),
|
||||
};
|
||||
|
||||
Scene scene = { scene_commands, sizeof(scene_commands) / sizeof(Command), 0 };
|
||||
CommandExecutor executor(&char_manager, compositor, name_text, dialogue_text,
|
||||
name_id, dialogue_id);
|
||||
compositor_animate_move(compositor, maksw_happy_id, 60, POS_Y, 1.0f, EASE_OUT_CUBIC);
|
||||
compositor_animate_scale(compositor, maksw_happy_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC, true);
|
||||
text_push(name_text, "maksw", 0.0f, true);
|
||||
text_push(dialogue_text,
|
||||
"Hey there! Welcome to the Obwarzanek Narrative Engine demo. "
|
||||
"This is a simple visual novel engine showcasing text rendering, "
|
||||
"image compositing, and basic animation features.",
|
||||
30.0f, true);
|
||||
|
||||
bool running = true;
|
||||
|
||||
AppState current_state = STATE_IDLE;
|
||||
AnimationTracker anim_tracker; // Track multiple animations
|
||||
|
||||
auto last_frame_time = std::chrono::high_resolution_clock::now();
|
||||
auto last_report_time = last_frame_time;
|
||||
auto last_report_time = last_frame_time;
|
||||
|
||||
int frame_count = 0;
|
||||
float frame_time_sum = 0.0f;
|
||||
@@ -777,91 +121,92 @@ int main() {
|
||||
last_report_time = current_time; // Mark the current time as the new report start
|
||||
}
|
||||
|
||||
// Update animation tracker
|
||||
anim_tracker.update(delta_time_sec);
|
||||
static bool mouse_was_down = false;
|
||||
bool mouse_is_down = window_is_mouse_button_down(window, MOUSE_BUTTON_LEFT);
|
||||
bool mouse_clicked = mouse_is_down && !mouse_was_down;
|
||||
mouse_was_down = mouse_is_down;
|
||||
|
||||
static bool mouse_was_down = false;
|
||||
bool mouse_is_down = window_is_mouse_button_down(window, MOUSE_BUTTON_LEFT);
|
||||
bool mouse_clicked = mouse_is_down && !mouse_was_down;
|
||||
mouse_was_down = mouse_is_down;
|
||||
static bool key_was_down = false;
|
||||
bool key_is_down = window_is_key_down(window, KEY_SPACE);
|
||||
bool key_pressed = key_is_down && !key_was_down;
|
||||
key_was_down = key_is_down;
|
||||
|
||||
static bool key_was_down = false;
|
||||
bool key_is_down = window_is_key_down(window, KEY_SPACE);
|
||||
bool key_pressed = key_is_down && !key_was_down;
|
||||
key_was_down = key_is_down;
|
||||
static unsigned int lines = 0;
|
||||
static bool dialog_finished = false;
|
||||
|
||||
bool advance_input = mouse_clicked || key_pressed;
|
||||
|
||||
// Check if animations finished
|
||||
if (current_state == STATE_ANIMATING) {
|
||||
if (!anim_tracker.has_active_animations()) {
|
||||
current_state = STATE_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
if (advance_input) {
|
||||
if (mouse_clicked || key_pressed) {
|
||||
if (text_is_animating(dialogue_text)) {
|
||||
text_show_all_current(dialogue_text);
|
||||
}
|
||||
else if (current_state == STATE_WAITING_FOR_INPUT) {
|
||||
current_state = STATE_IDLE;
|
||||
}
|
||||
else if (current_state == STATE_ANIMATING) {
|
||||
// Skip all remaining animations
|
||||
anim_tracker.clear();
|
||||
current_state = STATE_IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
// Process commands if idle
|
||||
while (current_state == STATE_IDLE) {
|
||||
if (!scene.has_next()) {
|
||||
running = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const Command* cmd = scene.current();
|
||||
float duration = executor.execute(cmd);
|
||||
|
||||
// NEW: Add animation to tracker
|
||||
if (duration > 0.0f) {
|
||||
anim_tracker.add_animation(duration);
|
||||
}
|
||||
|
||||
scene.next();
|
||||
|
||||
// Check blocking flag
|
||||
if (cmd->blocking) {
|
||||
if (cmd->type == CMD_WAIT) {
|
||||
current_state = STATE_WAITING_FOR_INPUT;
|
||||
else {
|
||||
if (lines == 0) {
|
||||
// line 2
|
||||
compositor_animation_complete_all(compositor);
|
||||
compositor_animate_scale(compositor, maksw_happy_id, 1.0f, 0.2f, EASE_IN_OUT_CUBIC);
|
||||
compositor_animate_scale(compositor, wikor_id, 1.1f, 0.3f, EASE_IN_OUT_CUBIC);
|
||||
text_clear(dialogue_text);
|
||||
text_clear(name_text);
|
||||
text_push(name_text, "wikor", 0.0f, true);
|
||||
text_push(dialogue_text, "Hello to you too, maksw..", 30.0f, true);
|
||||
lines++;
|
||||
}
|
||||
else if (lines == 1) {
|
||||
// line 3
|
||||
compositor_animation_complete_all(compositor);
|
||||
compositor_animate_scale(compositor, wikor_id, 1.0f, 0.2f, EASE_IN_OUT_CUBIC);
|
||||
compositor_set_visible(compositor, maksw_happy_id, false);
|
||||
compositor_set_visible(compositor, maksw_neutral_id, true);
|
||||
compositor_animate_scale(compositor, maksw_neutral_id, 1.05f, 0.3f, EASE_IN_OUT_CUBIC);
|
||||
text_clear(dialogue_text);
|
||||
text_clear(name_text);
|
||||
text_push(name_text, "maksw", 0.0f, true);
|
||||
text_push(dialogue_text, "why so.. serious?", 30.0f, true);
|
||||
lines++;
|
||||
}
|
||||
else if (lines == 2) {
|
||||
// fade out
|
||||
compositor_animation_complete_all(compositor);
|
||||
compositor_animate_scale(compositor, maksw_neutral_id, 1.0f, 0.2f, EASE_IN_OUT_CUBIC);
|
||||
compositor_set_visible(compositor, black_id, true);
|
||||
compositor_animate_fade(compositor, black_id, 1.0f, 1.5f, EASE_IN_OUT_CUBIC);
|
||||
dialog_finished = true;
|
||||
lines++;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
else if (anim_tracker.has_active_animations()) {
|
||||
current_state = STATE_ANIMATING;
|
||||
break;
|
||||
}
|
||||
// If blocking but no animations, continue to next command
|
||||
}
|
||||
// If non-blocking, loop continues immediately to next command
|
||||
}
|
||||
|
||||
compositor_tick(compositor, delta_time_sec);
|
||||
|
||||
text_nextframe(name_text, delta_time_sec);
|
||||
text_nextframe(dialogue_text, delta_time_sec);
|
||||
compositor_mark_content_dirty(compositor, name_id);
|
||||
compositor_mark_content_dirty(compositor, dialogue_id);
|
||||
if (dialog_finished) {
|
||||
compositor_mark_content_dirty(compositor, black_id);
|
||||
if (compositor_is_element_animating(compositor, black_id) == false) {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
if (text_nextframe(name_text, delta_time_sec))
|
||||
compositor_mark_content_dirty(compositor, name_id);
|
||||
if (text_nextframe(dialogue_text, delta_time_sec))
|
||||
compositor_mark_content_dirty(compositor, dialogue_id);
|
||||
|
||||
const uint32_t* fb = compositor_compose(compositor);
|
||||
window_blit(window, fb);
|
||||
}
|
||||
|
||||
if (maksw_happy) image_free(maksw_happy);
|
||||
if (maksw_neutral) image_free(maksw_neutral);
|
||||
if (wikor) image_free(wikor);
|
||||
|
||||
if (black) free(black);
|
||||
if (bg) image_free(bg);
|
||||
if (textbox) image_free(textbox);
|
||||
|
||||
text_destroy(name_text);
|
||||
text_destroy(dialogue_text);
|
||||
compositor_destroy(compositor);
|
||||
window_destroy(window);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+397
-307
@@ -1,6 +1,3 @@
|
||||
#include <text.h>
|
||||
|
||||
// C/C++ Standard Libraries
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -11,9 +8,17 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <algorithm>
|
||||
#include <iostream> // For std::clamp
|
||||
#include <iostream>
|
||||
#include <text.h>
|
||||
|
||||
// SSE2 intrinsics for x86/x64
|
||||
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)
|
||||
#define USE_SSE2 1
|
||||
#include <immintrin.h>
|
||||
#else
|
||||
#define USE_SSE2 0
|
||||
#endif
|
||||
|
||||
// Embed stb_truetype implementation
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
#include "stb_truetype.h"
|
||||
|
||||
@@ -157,6 +162,10 @@ struct text_t {
|
||||
shared_cache_t* cache;
|
||||
bool owns_cache; // True if this text_t owns the cache and should delete it
|
||||
|
||||
// Default font settings
|
||||
std::string default_font_path;
|
||||
float default_font_size = 16.0f;
|
||||
|
||||
// Rendering State (per-instance)
|
||||
std::vector<text_params_t> text_queue;
|
||||
int queue_index = 0;
|
||||
@@ -445,6 +454,61 @@ static uint32_t alpha_blend(uint32_t dest, uint32_t src) {
|
||||
return (out_a << 24) | (out_r << 16) | (out_g << 8) | out_b;
|
||||
}
|
||||
|
||||
#if USE_SSE2
|
||||
// SSE2-accelerated alpha blending for 4 pixels at once
|
||||
static inline void alpha_blend_sse2(uint32_t* dest, __m128i src_pixels, __m128i src_rgb_mask) {
|
||||
// Load 4 destination pixels
|
||||
__m128i dest_pixels = _mm_loadu_si128((__m128i*)dest);
|
||||
|
||||
// Extract alpha channels
|
||||
__m128i src_a = _mm_srli_epi32(src_pixels, 24);
|
||||
__m128i dest_a = _mm_srli_epi32(dest_pixels, 24);
|
||||
|
||||
// Check for trivial cases per pixel would be complex, so we just blend
|
||||
// For optimization, you could check if all alphas are 0 or 255
|
||||
|
||||
// Unpack to 16-bit for precision
|
||||
__m128i zero = _mm_setzero_si128();
|
||||
|
||||
// Extract RGBA components (low 8 pixels as 16-bit)
|
||||
__m128i src_lo = _mm_unpacklo_epi8(src_pixels, zero);
|
||||
__m128i src_hi = _mm_unpackhi_epi8(src_pixels, zero);
|
||||
__m128i dest_lo = _mm_unpacklo_epi8(dest_pixels, zero);
|
||||
__m128i dest_hi = _mm_unpackhi_epi8(dest_pixels, zero);
|
||||
|
||||
// Replicate alpha to all channels
|
||||
__m128i src_a_lo = _mm_shufflelo_epi16(src_lo, _MM_SHUFFLE(3, 3, 3, 3));
|
||||
src_a_lo = _mm_shufflehi_epi16(src_a_lo, _MM_SHUFFLE(3, 3, 3, 3));
|
||||
__m128i src_a_hi = _mm_shufflelo_epi16(src_hi, _MM_SHUFFLE(3, 3, 3, 3));
|
||||
src_a_hi = _mm_shufflehi_epi16(src_a_hi, _MM_SHUFFLE(3, 3, 3, 3));
|
||||
|
||||
// Compute: src * src_a
|
||||
__m128i src_mul_lo = _mm_mullo_epi16(src_lo, src_a_lo);
|
||||
__m128i src_mul_hi = _mm_mullo_epi16(src_hi, src_a_hi);
|
||||
|
||||
// Compute: 255 - src_a
|
||||
__m128i inv_src_a_lo = _mm_sub_epi16(_mm_set1_epi16(255), src_a_lo);
|
||||
__m128i inv_src_a_hi = _mm_sub_epi16(_mm_set1_epi16(255), src_a_hi);
|
||||
|
||||
// Compute: dest * (255 - src_a)
|
||||
__m128i dest_mul_lo = _mm_mullo_epi16(dest_lo, inv_src_a_lo);
|
||||
__m128i dest_mul_hi = _mm_mullo_epi16(dest_hi, inv_src_a_hi);
|
||||
|
||||
// Add and divide by 255 (approximate with >> 8)
|
||||
__m128i result_lo = _mm_add_epi16(src_mul_lo, dest_mul_lo);
|
||||
__m128i result_hi = _mm_add_epi16(src_mul_hi, dest_mul_hi);
|
||||
|
||||
result_lo = _mm_srli_epi16(result_lo, 8);
|
||||
result_hi = _mm_srli_epi16(result_hi, 8);
|
||||
|
||||
// Pack back to 8-bit
|
||||
__m128i result = _mm_packus_epi16(result_lo, result_hi);
|
||||
|
||||
// Store result
|
||||
_mm_storeu_si128((__m128i*)dest, result);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Draws a glyph using its cached SDF bitmap.
|
||||
static void draw_glyph(text_t* text,
|
||||
int x, int y,
|
||||
@@ -461,16 +525,14 @@ static void draw_glyph(text_t* text,
|
||||
int y1 = y + glyph->h;
|
||||
|
||||
// --- SDF Parameters ---
|
||||
// These must match the values used in get_glyph
|
||||
const float padding_percent = 0.15f;
|
||||
const float onedge_value = 128.0f; // 0-255 range midpoint
|
||||
const float onedge_value = 128.0f;
|
||||
float padding = std::max(1.0f, size * padding_percent);
|
||||
float pixel_dist_scale = onedge_value / padding;
|
||||
// ----------------------
|
||||
|
||||
// Calculate outline width and anti-alias smoothing
|
||||
float outline_width = std::max(1.0f, size * 0.05f);
|
||||
float aa_width = 0.75f; // Anti-aliasing width in pixels
|
||||
float aa_width = 0.75f;
|
||||
|
||||
// Clip to framebuffer
|
||||
int gx0 = (x0 < 0) ? -x0 : 0;
|
||||
@@ -481,371 +543,399 @@ static void draw_glyph(text_t* text,
|
||||
uint32_t color_rgb = color & 0x00FFFFFF;
|
||||
|
||||
for (int gy = gy0; gy < gy1; ++gy) {
|
||||
int fb_y = y0 + gy;
|
||||
uint32_t* dest_row = &text->framebuffer[fb_y * text->width + x0];
|
||||
|
||||
#if USE_SSE2
|
||||
// Process 4 pixels at a time with SSE2
|
||||
int gx = gx0;
|
||||
__m128i color_rgb_vec = _mm_set1_epi32(color_rgb);
|
||||
|
||||
for (; gx + 3 < gx1; gx += 4) {
|
||||
// Process 4 pixels
|
||||
uint8_t sdf_vals[4];
|
||||
float alphas[4];
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
sdf_vals[i] = glyph->data[gy * glyph->w + (gx + i)];
|
||||
float dist = (sdf_vals[i] - onedge_value) / pixel_dist_scale;
|
||||
|
||||
if (outline) {
|
||||
float edge_inner_0 = -aa_width;
|
||||
float edge_inner_1 = +aa_width;
|
||||
float mask_inner = smoothstep(edge_inner_0, edge_inner_1, dist);
|
||||
|
||||
float edge_outer_0 = -outline_width - aa_width;
|
||||
float edge_outer_1 = -outline_width + aa_width;
|
||||
float mask_outer = smoothstep(edge_outer_0, edge_outer_1, dist);
|
||||
|
||||
alphas[i] = std::clamp(mask_outer - mask_inner, 0.0f, 1.0f);
|
||||
} else {
|
||||
alphas[i] = smoothstep(-aa_width, aa_width, dist);
|
||||
}
|
||||
}
|
||||
|
||||
// Build source pixels
|
||||
uint32_t src_pixels[4];
|
||||
bool any_visible = false;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
uint32_t alpha_byte = (uint32_t)(alphas[i] * 255.0f);
|
||||
src_pixels[i] = (alpha_byte << 24) | color_rgb;
|
||||
if (alpha_byte > 0) any_visible = true;
|
||||
}
|
||||
|
||||
if (any_visible) {
|
||||
__m128i src_vec = _mm_loadu_si128((__m128i*)src_pixels);
|
||||
alpha_blend_sse2(&dest_row[gx], src_vec, color_rgb_vec);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle remaining pixels
|
||||
for (; gx < gx1; ++gx) {
|
||||
#else
|
||||
for (int gx = gx0; gx < gx1; ++gx) {
|
||||
|
||||
// 1. Get SDF value (0-255)
|
||||
#endif
|
||||
uint8_t sdf_raw = glyph->data[gy * glyph->w + gx];
|
||||
|
||||
// 2. Convert to signed pixel distance (Value > 0 is INSIDE, < 0 is OUTSIDE)
|
||||
float dist = (sdf_raw - onedge_value) / pixel_dist_scale;
|
||||
|
||||
float alpha = 0.0f;
|
||||
if (outline) {
|
||||
// Render the outline
|
||||
// 1. Inner mask (the fill)
|
||||
float edge_inner_0 = -aa_width;
|
||||
float edge_inner_1 = +aa_width;
|
||||
float mask_inner = smoothstep(edge_inner_0, edge_inner_1, dist);
|
||||
|
||||
// 2. Outer mask (the fill + outline)
|
||||
float edge_outer_0 = -outline_width - aa_width;
|
||||
float edge_outer_1 = -outline_width + aa_width;
|
||||
float mask_outer = smoothstep(edge_outer_0, edge_outer_1, dist);
|
||||
|
||||
alpha = std::clamp(mask_outer - mask_inner, 0.0f, 1.0f);
|
||||
|
||||
}
|
||||
else {
|
||||
// Render the fill
|
||||
// Use smoothstep for anti-aliasing around the 0-edge
|
||||
} else {
|
||||
alpha = smoothstep(-aa_width, aa_width, dist);
|
||||
}
|
||||
|
||||
if (alpha == 0.0f) continue;
|
||||
|
||||
int fb_x = x0 + gx;
|
||||
int fb_y = y0 + gy;
|
||||
|
||||
uint32_t* dest_pixel = &text->framebuffer[fb_y * text->width + fb_x];
|
||||
uint32_t src_pixel = ((uint32_t)(alpha * 255.0f) << 24) | color_rgb;
|
||||
uint32_t src_pixel = ((uint32_t)(alpha * 255.0f) << 24) | color_rgb;
|
||||
|
||||
*dest_pixel = alpha_blend(*dest_pixel, src_pixel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Public C-Style API ---
|
||||
|
||||
extern "C" {
|
||||
|
||||
// --- Lifecycle ---
|
||||
// --- Lifecycle ---
|
||||
|
||||
text_t* text_init(int width, int height) {
|
||||
if (width <= 0 || height <= 0) return nullptr;
|
||||
text_t* text_init(int width, int height) {
|
||||
if (width <= 0 || height <= 0) return nullptr;
|
||||
|
||||
text_t* text = new (std::nothrow) text_t;
|
||||
if (!text) return nullptr;
|
||||
text_t* text = new (std::nothrow) text_t;
|
||||
if (!text) return nullptr;
|
||||
|
||||
text->width = width;
|
||||
text->height = height;
|
||||
|
||||
text->framebuffer = new (std::nothrow) uint32_t[width * height];
|
||||
if (!text->framebuffer) {
|
||||
delete text;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Create new cache that this text_t owns
|
||||
text->cache = new (std::nothrow) shared_cache_t;
|
||||
if (!text->cache) {
|
||||
delete[] text->framebuffer;
|
||||
delete text;
|
||||
return nullptr;
|
||||
}
|
||||
text->owns_cache = true;
|
||||
|
||||
text->queue_index = 0;
|
||||
text->char_index = 0;
|
||||
text->char_progress = 0.0f;
|
||||
text->current_font = nullptr;
|
||||
text->last_line_height = 0.0f;
|
||||
|
||||
text->current_x = text->padding_x;
|
||||
text->current_y = text->padding_y;
|
||||
|
||||
text_clear(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
text_t* text_init_shared(text_t* source, int width, int height) {
|
||||
if (width <= 0 || height <= 0 || !source || !source->cache) return nullptr;
|
||||
|
||||
text_t* text = new (std::nothrow) text_t;
|
||||
if (!text) return nullptr;
|
||||
|
||||
text->width = width;
|
||||
text->height = height;
|
||||
|
||||
text->framebuffer = new (std::nothrow) uint32_t[width * height];
|
||||
if (!text->framebuffer) {
|
||||
delete text;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Share the cache from source
|
||||
text->cache = source->cache;
|
||||
text->cache->refcount++;
|
||||
text->owns_cache = false;
|
||||
|
||||
text->queue_index = 0;
|
||||
text->char_index = 0;
|
||||
text->char_progress = 0.0f;
|
||||
text->current_font = nullptr;
|
||||
text->last_line_height = 0.0f;
|
||||
|
||||
text->current_x = text->padding_x;
|
||||
text->current_y = text->padding_y;
|
||||
|
||||
text_clear(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
void text_destroy(text_t* text) {
|
||||
if (!text) return;
|
||||
|
||||
delete[] text->framebuffer;
|
||||
|
||||
// Handle cache cleanup with reference counting
|
||||
if (text->cache) {
|
||||
text->cache->refcount--;
|
||||
if (text->cache->refcount == 0) {
|
||||
delete text->cache;
|
||||
}
|
||||
}
|
||||
text->width = width;
|
||||
text->height = height;
|
||||
|
||||
text->framebuffer = new (std::nothrow) uint32_t[width * height];
|
||||
if (!text->framebuffer) {
|
||||
delete text;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// --- Text Queue & State ---
|
||||
// Create new cache that this text_t owns
|
||||
text->cache = new (std::nothrow) shared_cache_t;
|
||||
if (!text->cache) {
|
||||
delete[] text->framebuffer;
|
||||
delete text;
|
||||
return nullptr;
|
||||
}
|
||||
text->owns_cache = true;
|
||||
|
||||
// Adds a block of text to the rendering queue.
|
||||
void text_push(text_t* text,
|
||||
const char* str,
|
||||
float size,
|
||||
float speed,
|
||||
const char* font_path,
|
||||
bool outline)
|
||||
{
|
||||
if (!text || !str) return;
|
||||
text->queue_index = 0;
|
||||
text->char_index = 0;
|
||||
text->char_progress = 0.0f;
|
||||
text->current_font = nullptr;
|
||||
text->last_line_height = 0.0f;
|
||||
|
||||
text_params_t p;
|
||||
p.text = str;
|
||||
p.size = std::max(1.0f, size);
|
||||
p.speed = speed;
|
||||
p.outline = outline;
|
||||
p.font_path = (font_path ? font_path : "");
|
||||
p.lines_computed = false; // Mark for precomputation
|
||||
text->current_x = text->padding_x;
|
||||
text->current_y = text->padding_y;
|
||||
|
||||
text->text_queue.push_back(std::move(p));
|
||||
text_clear(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
text_t* text_init_shared(text_t* source, int width, int height) {
|
||||
if (width <= 0 || height <= 0 || !source || !source->cache) return nullptr;
|
||||
|
||||
text_t* text = new (std::nothrow) text_t;
|
||||
if (!text) return nullptr;
|
||||
|
||||
text->width = width;
|
||||
text->height = height;
|
||||
|
||||
text->framebuffer = new (std::nothrow) uint32_t[width * height];
|
||||
if (!text->framebuffer) {
|
||||
delete text;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Clears the framebuffer and resets the text queue and all state.
|
||||
void text_clear(text_t* text) {
|
||||
if (!text) return;
|
||||
memset(text->framebuffer, 0, text->width * text->height * sizeof(uint32_t));
|
||||
text->current_x = text->padding_x;
|
||||
text->current_y = text->padding_y;
|
||||
text->text_queue.clear();
|
||||
text->queue_index = 0;
|
||||
text->char_index = 0;
|
||||
text->char_progress = 0.0f;
|
||||
text->last_line_height = 0.0f;
|
||||
}
|
||||
// Share the cache from source
|
||||
text->cache = source->cache;
|
||||
text->cache->refcount++;
|
||||
text->owns_cache = false;
|
||||
|
||||
// Immediately sets the character index to the end of the current text block.
|
||||
void text_show_all_current(text_t* text) {
|
||||
if (!text || text->queue_index >= text->text_queue.size()) return;
|
||||
text->queue_index = 0;
|
||||
text->char_index = 0;
|
||||
text->char_progress = 0.0f;
|
||||
text->current_font = nullptr;
|
||||
text->last_line_height = 0.0f;
|
||||
|
||||
text_params_t* params = &text->text_queue[text->queue_index];
|
||||
text->current_x = text->padding_x;
|
||||
text->current_y = text->padding_y;
|
||||
|
||||
size_t text_len_cp = utf8_count_codepoints(params->text);
|
||||
text_clear(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
// Only update if it's not already complete
|
||||
if (text->char_index < (int)text_len_cp) {
|
||||
text->char_index = (int)text_len_cp;
|
||||
text->char_progress = static_cast<float>(text_len_cp);
|
||||
void text_destroy(text_t* text) {
|
||||
if (!text) return;
|
||||
|
||||
delete[] text->framebuffer;
|
||||
|
||||
// Handle cache cleanup with reference counting
|
||||
if (text->cache) {
|
||||
text->cache->refcount--;
|
||||
if (text->cache->refcount == 0) {
|
||||
delete text->cache;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Update & Animation ---
|
||||
delete text;
|
||||
}
|
||||
|
||||
// --- Font Configuration ---
|
||||
|
||||
// Sets the default font path and size to be used by subsequent text_push calls.
|
||||
// Pass NULL for font_path to keep the current default font.
|
||||
// Pass size <= 0 to keep the current default size.
|
||||
void text_set_font(text_t* text, const char* font_path, float size) {
|
||||
if (!text) return;
|
||||
|
||||
if (font_path != nullptr) {
|
||||
text->default_font_path = font_path;
|
||||
}
|
||||
|
||||
if (size > 0.0f) {
|
||||
text->default_font_size = size;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Text Queue & State ---
|
||||
|
||||
// Adds a block of text to the rendering queue.
|
||||
void text_push(text_t* text,
|
||||
const char* str,
|
||||
float speed,
|
||||
bool outline)
|
||||
{
|
||||
if (!text || !str) return;
|
||||
|
||||
text_params_t p;
|
||||
p.text = str;
|
||||
|
||||
p.size = text->default_font_size;
|
||||
|
||||
p.speed = speed;
|
||||
p.outline = outline;
|
||||
|
||||
p.font_path = text->default_font_path;
|
||||
|
||||
p.lines_computed = false; // Mark for precomputation
|
||||
|
||||
text->text_queue.push_back(std::move(p));
|
||||
}
|
||||
|
||||
// Clears the framebuffer and resets the text queue and all state.
|
||||
void text_clear(text_t* text) {
|
||||
if (!text) return;
|
||||
#if USE_SSE2
|
||||
// Clear framebuffer with SSE2
|
||||
size_t total_pixels = text->width * text->height;
|
||||
__m128i zero = _mm_setzero_si128();
|
||||
|
||||
size_t i = 0;
|
||||
for (; i + 3 < total_pixels; i += 4) {
|
||||
_mm_storeu_si128((__m128i*) & text->framebuffer[i], zero);
|
||||
}
|
||||
|
||||
// Clear remaining pixels
|
||||
for (; i < total_pixels; i++) {
|
||||
text->framebuffer[i] = 0;
|
||||
}
|
||||
#else
|
||||
memset(text->framebuffer, 0, text->width * text->height * sizeof(uint32_t));
|
||||
#endif
|
||||
text->current_x = text->padding_x;
|
||||
text->current_y = text->padding_y;
|
||||
text->text_queue.clear();
|
||||
text->queue_index = 0;
|
||||
text->char_index = 0;
|
||||
text->char_progress = 0.0f;
|
||||
text->last_line_height = 0.0f;
|
||||
}
|
||||
|
||||
// Immediately sets the character index to the end of the current text block.
|
||||
void text_show_all_current(text_t* text) {
|
||||
if (!text || text->queue_index >= text->text_queue.size()) return;
|
||||
|
||||
text_params_t* params = &text->text_queue[text->queue_index];
|
||||
|
||||
size_t text_len_cp = utf8_count_codepoints(params->text);
|
||||
|
||||
// Only update if it's not already complete
|
||||
if (text->char_index < (int)text_len_cp) {
|
||||
text->char_index = (int)text_len_cp;
|
||||
text->char_progress = static_cast<float>(text_len_cp);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Update & Animation ---
|
||||
|
||||
// Advances the text animation and renders new characters.
|
||||
// deltatime is the time elapsed since the last frame (in seconds).
|
||||
bool text_nextframe(text_t* text, float deltatime) {
|
||||
if (!text) return true;
|
||||
if (text->queue_index >= text->text_queue.size()) {
|
||||
return true; // Nothing to do
|
||||
bool text_nextframe(text_t* text, float deltatime) {
|
||||
if (!text) return false;
|
||||
if (text->queue_index >= text->text_queue.size()) {
|
||||
return false; // Nothing to do
|
||||
}
|
||||
|
||||
text_params_t* params = &text->text_queue[text->queue_index];
|
||||
|
||||
// Load font if needed
|
||||
if (!params->font_path.empty()) {
|
||||
cached_font_t* font = load_font(text, params->font_path);
|
||||
if (font) text->current_font = font;
|
||||
}
|
||||
|
||||
if (!text->current_font) {
|
||||
if (text->cache->font_cache.empty()) return false;
|
||||
text->current_font = &text->cache->font_cache.begin()->second;
|
||||
}
|
||||
|
||||
// Precompute layout
|
||||
precompute_line_breaks(text, params);
|
||||
|
||||
const stbtt_fontinfo* info = &text->current_font->info;
|
||||
float scale = stbtt_ScaleForPixelHeight(info, params->size);
|
||||
int ascent, descent, line_gap;
|
||||
stbtt_GetFontVMetrics(info, &ascent, &descent, &line_gap);
|
||||
float baseline = ascent * scale;
|
||||
|
||||
const size_t total_cp = utf8_count_codepoints(params->text);
|
||||
|
||||
bool did_draw = false;
|
||||
|
||||
// Update animation progress
|
||||
if (params->speed > 0.0f && text->char_index < (int)total_cp) {
|
||||
text->char_progress += params->speed * deltatime;
|
||||
int new_index = static_cast<int>(text->char_progress);
|
||||
if (new_index > text->char_index) {
|
||||
text->char_index = std::min(new_index, (int)total_cp);
|
||||
did_draw = true; // Will draw new characters
|
||||
}
|
||||
|
||||
text_params_t* params = &text->text_queue[text->queue_index];
|
||||
|
||||
// Load font
|
||||
if (!params->font_path.empty()) {
|
||||
cached_font_t* font = load_font(text, params->font_path);
|
||||
if (font) {
|
||||
text->current_font = font;
|
||||
}
|
||||
}
|
||||
|
||||
if (!text->current_font) {
|
||||
if (text->cache->font_cache.empty()) {
|
||||
fprintf(stderr, "Error: text_nextframe() called with no valid font loaded.\n");
|
||||
return false;
|
||||
}
|
||||
text->current_font = &text->cache->font_cache.begin()->second;
|
||||
}
|
||||
|
||||
// Calculate layout on first frame
|
||||
precompute_line_breaks(text, params);
|
||||
|
||||
const stbtt_fontinfo* info = &text->current_font->info;
|
||||
float scale = stbtt_ScaleForPixelHeight(info, params->size);
|
||||
int ascent, descent, line_gap;
|
||||
stbtt_GetFontVMetrics(info, &ascent, &descent, &line_gap);
|
||||
float baseline = ascent * scale;
|
||||
|
||||
// --- Update animation progress using deltatime ---
|
||||
size_t total_cp = utf8_count_codepoints(params->text);
|
||||
if (params->speed > 0.0f && text->char_index < (int)total_cp) {
|
||||
// The animation speed (params->speed) is now characters per second.
|
||||
// Multiply by deltatime to get the progress to advance this frame.
|
||||
text->char_progress += params->speed * deltatime;
|
||||
|
||||
// Only update char_index if it has advanced past the current index.
|
||||
// This prevents the animation from advancing multiple characters in a single frame
|
||||
// if the deltatime is large, but still allows continuous float progress.
|
||||
int new_char_index = static_cast<int>(text->char_progress);
|
||||
|
||||
// We only want to draw characters *up to* the new index, but we
|
||||
// still need to check if the new index is greater than the last frame's index.
|
||||
if (new_char_index > text->char_index) {
|
||||
text->char_index = new_char_index;
|
||||
}
|
||||
}
|
||||
else if (params->speed <= 0.0f) {
|
||||
}
|
||||
else if (params->speed <= 0.0f) {
|
||||
if (text->char_index < (int)total_cp) {
|
||||
text->char_index = (int)total_cp;
|
||||
did_draw = true; // Draw all instantly
|
||||
}
|
||||
// Cap char_index to the total number of codepoints
|
||||
text->char_index = (int)std::min((size_t)text->char_index, total_cp);
|
||||
}
|
||||
|
||||
// Only draw NEW characters
|
||||
size_t draw_start = text->last_rendered_char; // cp index
|
||||
size_t draw_end = text->char_index; // cp index
|
||||
// Draw only new characters
|
||||
size_t draw_start = text->last_rendered_char;
|
||||
size_t draw_end = text->char_index;
|
||||
|
||||
// Check if any new characters should be drawn
|
||||
if (draw_start < draw_end) {
|
||||
// Drawing loop - only for NEW characters
|
||||
for (const auto& line : params->lines) {
|
||||
// Skip lines that are completely rendered
|
||||
if (line.end_cp_index <= draw_start) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip lines that haven't started yet
|
||||
if (line.start_cp_index >= draw_end) {
|
||||
break;
|
||||
}
|
||||
|
||||
text->current_x = text->padding_x;
|
||||
text->current_y = line.y_offset;
|
||||
|
||||
// Calculate x position up to where we need to draw
|
||||
// Iterate through the line bytes and sum advances for cps before draw_start
|
||||
size_t byte_i = line.start_byte_index;
|
||||
size_t cp_i = line.start_cp_index;
|
||||
|
||||
while (byte_i < line.end_byte_index && cp_i < draw_start) {
|
||||
uint32_t cp = utf8_next(params->text, byte_i);
|
||||
const cached_glyph_t* glyph = get_glyph(text, cp, params->size);
|
||||
if (glyph) {
|
||||
text->current_x += glyph->advance;
|
||||
}
|
||||
++cp_i;
|
||||
}
|
||||
|
||||
// Draw only NEW characters on this line
|
||||
byte_i = line.start_byte_index;
|
||||
cp_i = line.start_cp_index;
|
||||
|
||||
// advance byte_i to the first drawable codepoint (max(line.start_cp_index, draw_start))
|
||||
while (byte_i < line.end_byte_index && cp_i < std::max(line.start_cp_index, draw_start)) {
|
||||
utf8_next(params->text, byte_i);
|
||||
++cp_i;
|
||||
}
|
||||
|
||||
// Now iterate and draw until cp reaches min(line.end_cp_index, draw_end)
|
||||
while (byte_i < line.end_byte_index && cp_i < std::min(line.end_cp_index, draw_end)) {
|
||||
uint32_t cp = utf8_next(params->text, byte_i);
|
||||
if (cp == (uint32_t)'\n') { ++cp_i; continue; }
|
||||
|
||||
const cached_glyph_t* glyph = get_glyph(text, cp, params->size);
|
||||
if (!glyph) {
|
||||
++cp_i;
|
||||
continue;
|
||||
}
|
||||
|
||||
int x_base = static_cast<int>(text->current_x + glyph->xoff);
|
||||
int y_base = static_cast<int>(text->current_y + baseline + glyph->yoff);
|
||||
|
||||
if (params->outline) {
|
||||
draw_glyph(text, x_base, y_base, params->color_outline, glyph, params->size, true);
|
||||
}
|
||||
draw_glyph(text, x_base, y_base, params->color_main, glyph, params->size, false);
|
||||
|
||||
text->current_x += glyph->advance;
|
||||
++cp_i;
|
||||
}
|
||||
}
|
||||
// Update tracking only if we drew something new
|
||||
text->last_rendered_char = (int)text->char_index;
|
||||
}
|
||||
|
||||
// Handle block transition
|
||||
if (text->char_index >= (int)total_cp) {
|
||||
text->queue_index++;
|
||||
text->char_index = 0;
|
||||
text->char_progress = 0.0f;
|
||||
text->last_rendered_char = 0; // Reset for next block
|
||||
// Only advance Y if there was a previous block rendered (to prevent double-spacing on first block)
|
||||
if (text->queue_index > 1) {
|
||||
text->current_y += text->last_line_height; // Advance y-pos
|
||||
}
|
||||
}
|
||||
|
||||
// Check if queue is finished
|
||||
if (text->queue_index >= text->text_queue.size()) {
|
||||
text->text_queue.clear();
|
||||
text->queue_index = 0;
|
||||
text->char_index = 0;
|
||||
text->char_progress = 0.0f;
|
||||
text->last_rendered_char = 0;
|
||||
if (draw_start < draw_end) {
|
||||
for (const auto& line : params->lines) {
|
||||
if (line.end_cp_index <= draw_start) continue;
|
||||
if (line.start_cp_index >= draw_end) break;
|
||||
|
||||
text->current_x = text->padding_x;
|
||||
text->current_y = text->padding_y;
|
||||
text->last_line_height = 0.0f;
|
||||
text->current_y = line.y_offset;
|
||||
|
||||
return true; // Finished
|
||||
// Advance current_x to draw_start
|
||||
size_t byte_i = line.start_byte_index;
|
||||
size_t cp_i = line.start_cp_index;
|
||||
while (byte_i < line.end_byte_index && cp_i < draw_start) {
|
||||
uint32_t cp = utf8_next(params->text, byte_i);
|
||||
const cached_glyph_t* glyph = get_glyph(text, cp, params->size);
|
||||
if (glyph) text->current_x += glyph->advance;
|
||||
++cp_i;
|
||||
}
|
||||
|
||||
// Draw new characters
|
||||
byte_i = line.start_byte_index;
|
||||
cp_i = line.start_cp_index;
|
||||
while (byte_i < line.end_byte_index && cp_i < std::max(line.start_cp_index, draw_start)) {
|
||||
utf8_next(params->text, byte_i);
|
||||
++cp_i;
|
||||
}
|
||||
|
||||
while (byte_i < line.end_byte_index && cp_i < std::min(line.end_cp_index, draw_end)) {
|
||||
uint32_t cp = utf8_next(params->text, byte_i);
|
||||
if (cp == (uint32_t)'\n') { ++cp_i; continue; }
|
||||
|
||||
const cached_glyph_t* glyph = get_glyph(text, cp, params->size);
|
||||
if (!glyph) { ++cp_i; continue; }
|
||||
|
||||
int x_base = static_cast<int>(text->current_x + glyph->xoff);
|
||||
int y_base = static_cast<int>(text->current_y + baseline + glyph->yoff);
|
||||
|
||||
if (params->outline) draw_glyph(text, x_base, y_base, params->color_outline, glyph, params->size, true);
|
||||
draw_glyph(text, x_base, y_base, params->color_main, glyph, params->size, false);
|
||||
|
||||
text->current_x += glyph->advance;
|
||||
++cp_i;
|
||||
}
|
||||
}
|
||||
|
||||
return false; // Not finished
|
||||
text->last_rendered_char = (int)text->char_index;
|
||||
did_draw = true;
|
||||
}
|
||||
|
||||
// Returns true if the currently running text block has not yet displayed all characters.
|
||||
bool text_is_animating(text_t* text) {
|
||||
if (!text) return false;
|
||||
if (text->queue_index >= text->text_queue.size()) return false;
|
||||
|
||||
const text_params_t* params = &text->text_queue[text->queue_index];
|
||||
size_t total_cp = utf8_count_codepoints(params->text);
|
||||
return text->char_index < (int)total_cp;
|
||||
// Handle block transition
|
||||
if (text->char_index >= (int)total_cp) {
|
||||
text->queue_index++;
|
||||
text->char_index = 0;
|
||||
text->char_progress = 0.0f;
|
||||
text->last_rendered_char = 0;
|
||||
if (text->queue_index > 1) text->current_y += text->last_line_height;
|
||||
}
|
||||
|
||||
// --- Accessors ---
|
||||
// Return true if something was drawn or queue finished
|
||||
if (did_draw || text->queue_index >= text->text_queue.size()) return true;
|
||||
|
||||
// Returns a pointer to the internal framebuffer.
|
||||
uint32_t* text_getfb(text_t* text) {
|
||||
return text ? text->framebuffer : nullptr;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
// Returns true if the currently running text block has not yet displayed all characters.
|
||||
bool text_is_animating(text_t* text) {
|
||||
if (!text) return false;
|
||||
if (text->queue_index >= text->text_queue.size()) return false;
|
||||
|
||||
const text_params_t* params = &text->text_queue[text->queue_index];
|
||||
size_t total_cp = utf8_count_codepoints(params->text);
|
||||
return text->char_index < (int)total_cp;
|
||||
}
|
||||
|
||||
// --- Accessors ---
|
||||
|
||||
// Returns a pointer to the internal framebuffer.
|
||||
const uint32_t* text_getfb(text_t* text) {
|
||||
return text ? text->framebuffer : nullptr;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
+84
-80
@@ -5,130 +5,134 @@
|
||||
#include <cstdio>
|
||||
|
||||
struct window_t {
|
||||
SDL_Window* window = nullptr;
|
||||
SDL_Renderer* renderer = nullptr;
|
||||
SDL_Texture* texture = nullptr;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
uint32_t last_tick_time;
|
||||
SDL_Window* window;
|
||||
SDL_Renderer* renderer;
|
||||
SDL_Texture* texture;
|
||||
int width;
|
||||
int height;
|
||||
|
||||
const uint8_t* keyboard_state = nullptr;
|
||||
int mouse_x = 0;
|
||||
int mouse_y = 0;
|
||||
uint32_t mouse_buttons = 0; // Bitmask for L/M/R/X1/X2 buttons
|
||||
const uint8_t* keyboard_state;
|
||||
int mouse_x;
|
||||
int mouse_y;
|
||||
uint32_t mouse_buttons;
|
||||
};
|
||||
|
||||
// Internal helper
|
||||
static void window_cleanup(window_t* s) {
|
||||
if (!s) return;
|
||||
if (s->texture) SDL_DestroyTexture(s->texture);
|
||||
if (s->renderer) SDL_DestroyRenderer(s->renderer);
|
||||
if (s->window) SDL_DestroyWindow(s->window);
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
window_t* window_create(int width, int height, const char* title) {
|
||||
if (SDL_Init(SDL_INIT_VIDEO) != 0) return nullptr;
|
||||
if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0) {
|
||||
std::fprintf(stderr, "SDL_InitSubSystem failed: %s\n", SDL_GetError());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
window_t* s = new window_t;
|
||||
window_t* s = new window_t{};
|
||||
if (!s) return nullptr;
|
||||
|
||||
s->window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, 0);
|
||||
if (!s->window) { delete s; return nullptr; }
|
||||
|
||||
s->renderer = SDL_CreateRenderer(s->window, -1, SDL_RENDERER_ACCELERATED);
|
||||
if (!s->renderer) { delete s; return nullptr; }
|
||||
|
||||
// Use ARGB8888 format for 32-bit framebuffer
|
||||
s->texture = SDL_CreateTexture(s->renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, width, height);
|
||||
if (!s->texture) { delete s; return nullptr; }
|
||||
|
||||
// --- Input Initialization ---
|
||||
// Get the pointer to the keyboard state array
|
||||
s->keyboard_state = SDL_GetKeyboardState(nullptr);
|
||||
|
||||
s->width = width;
|
||||
s->height = height;
|
||||
|
||||
s->window = SDL_CreateWindow(title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, width, height, 0);
|
||||
if (!s->window) {
|
||||
std::fprintf(stderr, "SDL_CreateWindow failed: %s\n", SDL_GetError());
|
||||
delete s;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
s->renderer = SDL_CreateRenderer(s->window, -1, SDL_RENDERER_ACCELERATED);
|
||||
if (!s->renderer) {
|
||||
std::fprintf(stderr, "SDL_CreateRenderer failed: %s\n", SDL_GetError());
|
||||
window_cleanup(s);
|
||||
delete s;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
s->texture = SDL_CreateTexture(s->renderer, SDL_PIXELFORMAT_ARGB8888,
|
||||
SDL_TEXTUREACCESS_STREAMING, width, height);
|
||||
if (!s->texture) {
|
||||
std::fprintf(stderr, "SDL_CreateTexture failed: %s\n", SDL_GetError());
|
||||
window_cleanup(s);
|
||||
delete s;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
s->keyboard_state = SDL_GetKeyboardState(nullptr);
|
||||
return s;
|
||||
}
|
||||
|
||||
void window_destroy(window_t* s) {
|
||||
if (s->texture) SDL_DestroyTexture(s->texture);
|
||||
if (s->renderer) SDL_DestroyRenderer(s->renderer);
|
||||
if (s->window) SDL_DestroyWindow(s->window);
|
||||
if (!s) return;
|
||||
|
||||
// Reset state pointers after destruction
|
||||
s->texture = nullptr;
|
||||
s->renderer = nullptr;
|
||||
s->window = nullptr;
|
||||
|
||||
SDL_Quit();
|
||||
window_cleanup(s);
|
||||
SDL_QuitSubSystem(SDL_INIT_VIDEO);
|
||||
delete s;
|
||||
}
|
||||
|
||||
bool window_pollevent(window_t* s) {
|
||||
SDL_Event event;
|
||||
bool quit_requested = false;
|
||||
|
||||
// Process all events in the queue
|
||||
while (SDL_PollEvent(&event)) {
|
||||
if (event.type == SDL_QUIT) {
|
||||
quit_requested = true;
|
||||
}
|
||||
SDL_Event ev;
|
||||
bool quit = false;
|
||||
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
if (ev.type == SDL_QUIT)
|
||||
quit = true;
|
||||
}
|
||||
|
||||
s->mouse_buttons = SDL_GetMouseState(&s->mouse_x, &s->mouse_y);
|
||||
|
||||
return quit_requested;
|
||||
return quit;
|
||||
}
|
||||
|
||||
// Checks if a key (identified by its Scancode) is currently held down.
|
||||
// SDL_SCANCODE_* constants should be used by the caller.
|
||||
bool window_is_key_down(window_t* s, int key_scancode) {
|
||||
// Check for valid scancode and that the state array is initialized
|
||||
if (key_scancode >= 0 && key_scancode < SDL_NUM_SCANCODES && s->keyboard_state) {
|
||||
return s->keyboard_state[key_scancode] == 1;
|
||||
bool window_is_key_down(window_t* s, int sc) {
|
||||
if (!s->keyboard_state) return false;
|
||||
if (sc < 0 || sc >= SDL_NUM_SCANCODES) return false;
|
||||
return s->keyboard_state[sc] != 0;
|
||||
}
|
||||
|
||||
bool window_is_mouse_button_down(window_t* s, int button) {
|
||||
switch (button) {
|
||||
case MOUSE_BUTTON_LEFT: return s->mouse_buttons & SDL_BUTTON_LMASK;
|
||||
case MOUSE_BUTTON_MIDDLE: return s->mouse_buttons & SDL_BUTTON_MMASK;
|
||||
case MOUSE_BUTTON_RIGHT: return s->mouse_buttons & SDL_BUTTON_RMASK;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Checks if a mouse button is currently held down.
|
||||
// button_index should be one of MOUSE_BUTTON_LEFT, MOUSE_BUTTON_MIDDLE, or MOUSE_BUTTON_RIGHT.
|
||||
bool window_is_mouse_button_down(window_t* s, int button_index) {
|
||||
if (button_index == MOUSE_BUTTON_LEFT) {
|
||||
return s->mouse_buttons & SDL_BUTTON_LMASK;
|
||||
}
|
||||
if (button_index == MOUSE_BUTTON_MIDDLE) {
|
||||
return s->mouse_buttons & SDL_BUTTON_MMASK;
|
||||
}
|
||||
if (button_index == MOUSE_BUTTON_RIGHT) {
|
||||
return s->mouse_buttons & SDL_BUTTON_RMASK;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Gets the current mouse cursor position relative to the window.
|
||||
void window_get_mouse_position(window_t* s, int* x, int* y) {
|
||||
if (x) *x = s->mouse_x;
|
||||
if (y) *y = s->mouse_y;
|
||||
}
|
||||
|
||||
void window_blit(window_t* s, const uint32_t* framebuffer) {
|
||||
if (!s->texture) return; // Prevent crash if texture wasn't created
|
||||
void window_blit(window_t* s, const uint32_t* fb) {
|
||||
if (!s->texture) return;
|
||||
|
||||
void* pixels;
|
||||
int pitch;
|
||||
|
||||
// Lock and update the texture
|
||||
|
||||
SDL_LockTexture(s->texture, nullptr, &pixels, &pitch);
|
||||
|
||||
// Copy the framebuffer. Use memcpy for the whole thing if pitch is what's expected (pitch == width * 4)
|
||||
// Note: The loop-based copy is safer if pitch != width * 4, but let's stick to the row-by-row copy for safety as in the original.
|
||||
for (int y = 0; y < s->height; ++y) {
|
||||
const uint32_t* src = framebuffer + y * s->width;
|
||||
// Calculate destination address for the start of the current row
|
||||
uint32_t* dst = (uint32_t*)((uint8_t*)pixels + y * pitch);
|
||||
// Copy one row (s->width * 4 bytes)
|
||||
memcpy(dst, src, s->width * sizeof(uint32_t));
|
||||
const int expected_pitch = s->width * 4;
|
||||
const size_t total = (size_t)s->width * (size_t)s->height * 4;
|
||||
|
||||
if (pitch == expected_pitch) {
|
||||
std::memcpy(pixels, fb, total);
|
||||
} else {
|
||||
for (int y = 0; y < s->height; ++y) {
|
||||
uint8_t* row = (uint8_t*)pixels + y * pitch;
|
||||
std::memcpy(row, fb + y * s->width, expected_pitch);
|
||||
}
|
||||
}
|
||||
|
||||
SDL_UnlockTexture(s->texture);
|
||||
|
||||
// Rendering
|
||||
SDL_RenderClear(s->renderer);
|
||||
SDL_RenderCopy(s->renderer, s->texture, nullptr, nullptr);
|
||||
SDL_RenderPresent(s->renderer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -73,18 +73,22 @@
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<OutDir>bin\$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
|
||||
<CodeAnalysisRuleSet>NativeRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<OutDir>bin\$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
|
||||
<CodeAnalysisRuleSet>NativeRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<OutDir>bin\$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
|
||||
<CodeAnalysisRuleSet>NativeRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<OutDir>bin\$(Platform)\$(Configuration)\</OutDir>
|
||||
<IntDir>obj\$(Platform)\$(Configuration)\</IntDir>
|
||||
<CodeAnalysisRuleSet>NativeRecommendedRules.ruleset</CodeAnalysisRuleSet>
|
||||
</PropertyGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
@@ -194,6 +198,7 @@
|
||||
<Image Include="assets\wikor.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Font Include="assets\Aller_Rg.ttf" />
|
||||
<Font Include="assets\Vera.ttf" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
|
||||
@@ -81,5 +81,8 @@
|
||||
<Font Include="assets\Vera.ttf">
|
||||
<Filter>Assets</Filter>
|
||||
</Font>
|
||||
<Font Include="assets\Aller_Rg.ttf">
|
||||
<Filter>Assets</Filter>
|
||||
</Font>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in new issue
Block a user