diff --git a/src/main.cpp b/src/main.cpp index 54a57b2..cf44ef3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,9 +1,10 @@ -#include +#include #include #include #include #include -#include // <-- NEW: For delta-time calculation +#include +#include // <-- NEW: For tracking multiple animations #include #include #include @@ -14,52 +15,41 @@ // ============================================================================ enum CommandType { - CMD_SHOW, // Show character with specific sprite - CMD_HIDE, // Hide character (all sprites) - CMD_MOVE, // Move character to position - CMD_CHANGE_SPRITE, // Change character's sprite/expression - CMD_SET_Z, // Change z-order - CMD_SET_SCALE, // Change scale - CMD_SPEAK, // Display dialogue - CMD_WAIT, // Wait for click - CMD_BACKGROUND, // Change background + CMD_SHOW, + CMD_HIDE, + CMD_MOVE, + CMD_CHANGE_SPRITE, + CMD_SET_Z, + CMD_SET_SCALE, + CMD_SPEAK, + CMD_WAIT, + CMD_BACKGROUND, }; enum TransitionType { - TRANS_NONE, // Instant snap - TRANS_FADE, // Fade in / Fade out - TRANS_SLIDE_LEFT, // Slide in from left - TRANS_SLIDE_RIGHT, // Slide in from right + TRANS_NONE, + TRANS_FADE, + TRANS_SLIDE_LEFT, + TRANS_SLIDE_RIGHT, }; -// Easing enums (must match compositor.h) -// We provide them here for the demo's compilation -#ifndef COMPOSITOR_H -enum easing_t { - EASE_LINEAR = 0, EASE_IN_QUAD, EASE_OUT_QUAD, EASE_IN_OUT_QUAD, - EASE_IN_CUBIC, EASE_OUT_CUBIC, EASE_IN_OUT_CUBIC -}; -#endif - struct Command { CommandType type; - const char* character_name; // Which character (by name) + const char* character_name; - // Position/transform data int x, y; int z; float scale; - // Sprite/image data const char* sprite_name; - // Dialogue data const char* speaker; const char* text; - // Animation/transition data TransitionType transition; float duration_ms; + + bool blocking; // NEW: If true, wait for this animation to complete before next command }; struct Scene { @@ -78,24 +68,71 @@ struct Scene { } }; +// ============================================================================ +// ANIMATION TRACKER +// ============================================================================ + +struct ActiveAnimation { + float remaining_time; + + ActiveAnimation(float duration) : remaining_time(duration) {} +}; + +class AnimationTracker { +private: + std::vector 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 active_animations.size(); + } +}; + // ============================================================================ // CHARACTER MANAGEMENT // ============================================================================ struct SpriteElement { uint32_t* image_data; - uint32_t element_id; // compositor element ID + uint32_t element_id; bool visible; }; struct Character { std::string name; - std::unordered_map sprites; // emotion -> sprite + std::unordered_map sprites; int width, height; int current_x, current_y; int current_z; float current_scale; - std::string current_sprite; // Which sprite is currently visible + std::string current_sprite; }; class CharacterManager { @@ -109,7 +146,6 @@ public: : compositor(comp), screen_width(screen_w) { } - // Register a character void add_character(const char* name, int width, int height) { Character ch; ch.name = name; @@ -123,7 +159,6 @@ public: characters[name] = ch; } - // Load a sprite for a character and insert it into compositor 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); @@ -137,21 +172,18 @@ public: elem.element_id = compositor_insert(compositor, img, width, height, 0, 0, initial_z); elem.visible = false; - // NEW: Start fully faded out and invisible compositor_set_alpha(compositor, elem.element_id, 0.0f); compositor_set_visible(compositor, elem.element_id, false); it->second.sprites[emotion] = elem; } - // Get character by name Character* get_character(const char* name) { auto it = characters.find(name); if (it != characters.end()) return &it->second; return nullptr; } - // Get the element ID of the character's currently active sprite uint32_t get_active_element_id(Character* ch) { if (!ch || ch->current_sprite.empty()) return 0; auto it = ch->sprites.find(ch->current_sprite); @@ -159,13 +191,11 @@ public: return it->second.element_id; } - // Show character with specific sprite 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; - // Hide all other sprites for this character first for (auto& pair : ch->sprites) { if (pair.first != sprite_name && pair.second.visible) { compositor_set_visible(compositor, pair.second.element_id, false); @@ -174,44 +204,40 @@ public: } } - // Find the requested sprite auto it = ch->sprites.find(sprite_name); if (it == ch->sprites.end()) return; SpriteElement& elem = it->second; float duration_sec = duration_ms / 1000.0f; - // Update state ch->current_x = x; ch->current_y = y; ch->current_z = z; ch->current_sprite = sprite_name; - elem.visible = true; // Mark as logically visible + elem.visible = true; - // Set properties that aren't animated compositor_set_zorder(compositor, elem.element_id, z); - // --- NEW: Apply Transition --- if (transition == TRANS_FADE) { - compositor_set_position(compositor, elem.element_id, x, y); // Snap position + 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; // Start off-screen 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); // Make opaque + 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; // Start off-screen 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); // Make opaque + 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 { // TRANS_NONE + 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); @@ -224,14 +250,12 @@ public: float duration_sec = duration_ms / 1000.0f; - // Hide all visible sprites for this character for (auto& pair : ch->sprites) { if (pair.second.visible) { if (transition == TRANS_FADE) { - // compositor_tick will set invisible on completion compositor_animate_fade(compositor, pair.second.element_id, 0.0f, duration_sec, EASE_LINEAR); } - else { // TRANS_NONE + else { compositor_set_visible(compositor, pair.second.element_id, false); compositor_set_alpha(compositor, pair.second.element_id, 0.0f); } @@ -259,12 +283,9 @@ public: } void change_sprite(const char* name, const char* new_emotion) { - // This is an instant snap for cross-fading, which is more complex. - // For now, we just snap. Character* ch = get_character(name); if (!ch) return; - // Hide current sprite if (!ch->current_sprite.empty()) { auto old_it = ch->sprites.find(ch->current_sprite); if (old_it != ch->sprites.end()) { @@ -274,7 +295,6 @@ public: } } - // Show new sprite at same position/z auto new_it = ch->sprites.find(new_emotion); if (new_it != ch->sprites.end()) { compositor_set_position(compositor, new_it->second.element_id, @@ -304,7 +324,7 @@ public: ch->current_scale = scale; float duration_sec = duration_ms / 1000.0f; - if (transition == TRANS_FADE) { // Use "fade" as a stand-in for "smooth" + if (transition == TRANS_FADE) { compositor_animate_scale(compositor, id, scale, duration_sec, EASE_IN_OUT_QUAD); } else { @@ -312,7 +332,6 @@ public: } } - // Cleanup ~CharacterManager() { for (auto& pair : characters) { for (auto& sprite : pair.second.sprites) { @@ -342,37 +361,36 @@ public: name_id(nid), dialogue_id(did) { } - // NEW: Execute a command and return the duration of any triggered animation float execute(const Command* cmd) { switch (cmd->type) { case CMD_SHOW: char_manager->show_character(cmd->character_name, cmd->x, cmd->y, cmd->z, cmd->sprite_name, cmd->transition, cmd->duration_ms); - return cmd->duration_ms / 1000.0f; // Return duration + return cmd->duration_ms / 1000.0f; case CMD_HIDE: char_manager->hide_character(cmd->character_name, cmd->transition, cmd->duration_ms); - return cmd->duration_ms / 1000.0f; // Return duration + return cmd->duration_ms / 1000.0f; case CMD_MOVE: char_manager->move_character(cmd->character_name, cmd->x, cmd->y, cmd->transition, cmd->duration_ms); - return cmd->duration_ms / 1000.0f; // Return duration + return cmd->duration_ms / 1000.0f; case CMD_CHANGE_SPRITE: char_manager->change_sprite(cmd->character_name, cmd->sprite_name); - break; // Instant + break; case CMD_SET_Z: char_manager->set_z(cmd->character_name, cmd->z); - break; // Instant + break; case CMD_SET_SCALE: char_manager->set_scale(cmd->character_name, cmd->scale, cmd->transition, cmd->duration_ms); - return cmd->duration_ms / 1000.0f; // Return duration + return cmd->duration_ms / 1000.0f; case CMD_SPEAK: text_clear(name_text); @@ -385,13 +403,13 @@ public: } compositor_mark_content_dirty(compositor, name_id); compositor_mark_content_dirty(compositor, dialogue_id); - break; // Instant + break; case CMD_WAIT: case CMD_BACKGROUND: - break; // No-op + break; } - return 0.0f; // Default: no duration + return 0.0f; } }; @@ -399,11 +417,10 @@ public: // MAIN PROGRAM // ============================================================================ -// NEW: Main application state enum AppState { - STATE_IDLE, // Ready to process next command - STATE_ANIMATING, // Waiting for an animation to finish - STATE_WAITING_FOR_INPUT // Waiting for user click (after CMD_WAIT) + STATE_IDLE, + STATE_ANIMATING, + STATE_WAITING_FOR_INPUT }; int main() { @@ -423,14 +440,12 @@ int main() { return 1; } - // --- Background --- uint32_t* bg = image_load_resized("assets/background.png", WIDTH, HEIGHT); if (bg) { uint32_t bg_id = compositor_insert(compositor, bg, WIDTH, HEIGHT, 0, 0, 0); - compositor_set_alpha(compositor, bg_id, 1.0f); // Make sure bg is visible + compositor_set_alpha(compositor, bg_id, 1.0f); } - // --- Character Setup --- const int CHAR_WIDTH = 400; const int CHAR_HEIGHT = 600; const int CHAR_Y_OFFSET = 140; @@ -438,111 +453,147 @@ int main() { CharacterManager char_manager(compositor, WIDTH); - // Register characters char_manager.add_character("maksw", CHAR_WIDTH, CHAR_HEIGHT); char_manager.add_character("wikor", CHAR_WIDTH, CHAR_HEIGHT); - // Load sprites - these get pre-inserted into compositor (hidden, alpha 0) 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); - // --- Textbox --- 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); // Make sure it's visible + compositor_set_alpha(compositor, box_id, 1.0f); } - // --- Text Layers --- text_t* name_text = text_init(WIDTH, HEIGHT); 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); // Text is pre-multiplied + compositor_set_alpha(compositor, name_id, 1.0f); - text_t* dialogue_text = text_init_shared(name_text, WIDTH, HEIGHT); + text_t* dialogue_text = text_init_shared(name_text, WIDTH - 140, HEIGHT); uint32_t dialogue_id = compositor_insert(compositor, text_getfb(dialogue_text), - WIDTH, HEIGHT, 60, HEIGHT - 130, 4); + WIDTH - 140, HEIGHT, 60, HEIGHT - 130, 4); compositor_set_alpha(compositor, dialogue_id, 1.0f); - // --- Scene Script (UPDATED with transitions) --- + // --- Scene Script --- Command scene_commands[] = { - // Show both characters - {CMD_SHOW, "maksw", 100, POS_Y, 2, 1.0f, "neutral", nullptr, nullptr, TRANS_SLIDE_LEFT, 500.0f}, - {CMD_SHOW, "wikor", 800, POS_Y, 1, 1.0f, "neutral", nullptr, nullptr, TRANS_SLIDE_RIGHT, 500.0f}, + // Show both characters IN PARALLEL + {CMD_SHOW, "maksw", 100, POS_Y, 2, 1.0f, "neutral", nullptr, nullptr, TRANS_SLIDE_LEFT, 500.0f, false}, + {CMD_SHOW, "wikor", 800, POS_Y, 1, 1.0f, "neutral", nullptr, nullptr, TRANS_SLIDE_RIGHT, 500.0f, true}, - // Maksw speaks (bring to front, scale up, move slightly forward) - {CMD_SET_Z, "maksw", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0}, - {CMD_SET_SCALE, "maksw", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f}, + // Maksw speaks + {CMD_SET_Z, "maksw", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false}, + {CMD_SET_SCALE, "maksw", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f, false}, {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw", - "Hey, Wikor, nice weather today, huh?", TRANS_NONE, 0}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0}, + u8"Hey, admirał wiktorov, the sun's actually out today. Did the weather daemon crash overnight?", TRANS_NONE, 0, true}, + {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, // Wikor responds - {CMD_SET_SCALE, "maksw", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 100.0f}, - {CMD_SET_Z, "maksw", 0, 0, 1, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0}, - {CMD_SET_Z, "wikor", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0}, - {CMD_SET_SCALE, "wikor", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f}, - {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Wikor", - "Yeah, it's surprisingly sunny for November.", TRANS_NONE, 0}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0}, + {CMD_SET_SCALE, "maksw", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 100.0f, false}, + {CMD_SET_Z, "maksw", 0, 0, 1, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false}, + {CMD_SET_Z, "wikor", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false}, + {CMD_SET_SCALE, "wikor", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f, false}, + {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, u8"admirał wiktorov", + "Either that, or someone forgot to reboot the clouds. I didn't authorize this much sunlight.", TRANS_NONE, 0, true}, + {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, - // Maksw speaks again - {CMD_SET_SCALE, "wikor", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 100.0f}, - {CMD_SET_Z, "wikor", 0, 0, 1, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0}, - {CMD_SET_Z, "maksw", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0}, - {CMD_SET_SCALE, "maksw", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f}, + // Maksw replies + {CMD_SET_SCALE, "wikor", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 100.0f, false}, + {CMD_SET_Z, "wikor", 0, 0, 1, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false}, + {CMD_SET_Z, "maksw", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false}, + {CMD_SET_SCALE, "maksw", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f, false}, {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw", - "We should get back to debugging soon though.", TRANS_NONE, 0}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0}, + "You sound disappointed. Don't tell me you actually prefer the cold and gloom.", TRANS_NONE, 0, true}, + {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, - // Wikor final line - {CMD_SET_SCALE, "maksw", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 100.0f}, - {CMD_SET_Z, "maksw", 0, 0, 1, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0}, - {CMD_SET_Z, "wikor", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0}, - {CMD_SET_SCALE, "wikor", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f}, - {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Wikor", - "You're right, that compositor won't fix itself.", TRANS_NONE, 0}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0}, + // Wikor gets smug + {CMD_SET_SCALE, "maksw", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 100.0f, false}, + {CMD_SET_Z, "maksw", 0, 0, 1, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false}, + {CMD_SET_Z, "wikor", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false}, + {CMD_SET_SCALE, "wikor", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f, false}, + {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, u8"admirał wiktorov", + "Cold means fewer bugs - in the code and in the air. The moment it warms up, the chaos begins.", TRANS_NONE, 0, true}, + {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, - // Fade out - {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, ""}, - {CMD_HIDE, "maksw", 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_FADE, 1000.0f}, - {CMD_HIDE, "wikor", 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_FADE, 1000.0f}, + // Maksw teasingly replies + {CMD_SET_SCALE, "wikor", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 100.0f, false}, + {CMD_SET_Z, "wikor", 0, 0, 1, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false}, + {CMD_SET_Z, "maksw", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false}, + {CMD_SET_SCALE, "maksw", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f, false}, + {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw", + "You just don't like seeing people happy. Admit it.", TRANS_NONE, 0, true}, + {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, + + // Wikor snarks back + {CMD_SET_SCALE, "maksw", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 100.0f, false}, + {CMD_SET_Z, "maksw", 0, 0, 1, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false}, + {CMD_SET_Z, "wikor", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false}, + {CMD_SET_SCALE, "wikor", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f, false}, + {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, u8"admirał wiktorov", + "Happiness is fine, as long as it doesn't interfere with system uptime.", TRANS_NONE, 0, true}, + {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, + + // Closing line + {CMD_SET_SCALE, "wikor", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 100.0f, false}, + {CMD_SET_Z, "wikor", 0, 0, 1, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false}, + {CMD_SET_Z, "maksw", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false}, + {CMD_SET_SCALE, "maksw", 0, 0, 0, 1.05f, nullptr, nullptr, nullptr, TRANS_FADE, 200.0f, false}, + {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw", + "Then let's keep uptime at 100% and go grab some coffee before the next storm update.", TRANS_NONE, 0, true}, + {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, + + // Fade out both + {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "", nullptr, TRANS_NONE, 0, false}, + {CMD_HIDE, "maksw", 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_FADE, 500.0f, false}, + {CMD_HIDE, "wikor", 0, 0, 0, 0, nullptr, nullptr, nullptr, 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); - // --- Main Loop --- bool running = true; - // NEW: State machine variables - AppState current_state = STATE_IDLE; // Start in IDLE to process first commands - float animation_timer = 0.0f; + AppState current_state = STATE_IDLE; + AnimationTracker anim_tracker; // NEW: Track multiple animations - // NEW: Delta-time variables auto last_frame_time = std::chrono::high_resolution_clock::now(); + int frame_count = 0; + float frame_time_sum = 0.0f; + const int FPS_REPORT_FRAME_COUNT = 60; + while (running) { if (window_pollevent(window)) running = false; - // --- 1. Calculate Delta Time --- auto current_time = std::chrono::high_resolution_clock::now(); std::chrono::duration delta_duration = current_time - last_frame_time; float delta_time_sec = delta_duration.count(); last_frame_time = current_time; - // --- 2. Update Animations (ALWAYS) --- + frame_count++; + frame_time_sum += delta_time_sec; + + if (frame_count >= FPS_REPORT_FRAME_COUNT) { + float avg_delta_time = frame_time_sum / (float)frame_count; + float fps = 1.0f / avg_delta_time; + printf("FPS: %.2f (over %d frames)\n", fps, FPS_REPORT_FRAME_COUNT); + + // Reset counters + frame_count = 0; + frame_time_sum = 0.0f; + } + compositor_tick(compositor, delta_time_sec); - // Update text animation text_nextframe(name_text); text_nextframe(dialogue_text); compositor_mark_content_dirty(compositor, name_id); compositor_mark_content_dirty(compositor, dialogue_id); - // --- 3. Handle Input --- + // NEW: 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; @@ -555,39 +606,28 @@ int main() { bool advance_input = mouse_clicked || key_pressed; - // --- 4. Main State Machine --- - - // Check if animation finished + // Check if animations finished if (current_state == STATE_ANIMATING) { - animation_timer -= delta_time_sec; - if (animation_timer <= 0.0f) { + if (!anim_tracker.has_active_animations()) { current_state = STATE_IDLE; } } - // Handle user input if (advance_input) { if (text_is_animating(dialogue_text)) { - // Fast-forward text text_show_all_current(dialogue_text); } else if (current_state == STATE_WAITING_FOR_INPUT) { - // Advance from a WAIT command current_state = STATE_IDLE; } else if (current_state == STATE_ANIMATING) { - // User wants to skip animation - animation_timer = 0.0f; + // Skip all remaining animations + anim_tracker.clear(); current_state = STATE_IDLE; - // Note: The animation won't snap to the end. - // A 'snap' function would be needed in the compositor. - // For now, this just proceeds to the next command. } } // Process commands if idle - // This while-loop will execute all instant commands in a single frame - // until it hits an animation or a wait. while (current_state == STATE_IDLE) { if (!scene.has_next()) { running = false; @@ -596,31 +636,33 @@ int main() { 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(); - if (duration > 0.0f) { - // This was an animated command - current_state = STATE_ANIMATING; - animation_timer = duration; + // NEW: Check blocking flag + if (cmd->blocking) { + if (cmd->type == CMD_WAIT) { + current_state = STATE_WAITING_FOR_INPUT; + break; + } + else if (anim_tracker.has_active_animations()) { + current_state = STATE_ANIMATING; + break; + } + // If blocking but no animations, continue to next command } - else if (cmd->type == CMD_WAIT) { - // This was a wait command - current_state = STATE_WAITING_FOR_INPUT; - } - // else: Instant command, loop continues to next command + // If non-blocking, loop continues immediately to next command } - // --- 5. Render Frame --- const uint32_t* fb = compositor_compose(compositor); window_blit(window, fb); - - // We no longer manually limit framerate. - // V-Sync or other methods should be used if needed. - // For now, we let it run as fast as possible. - // window_limit_framerate(window, 60); // REMOVED } - // --- Cleanup --- if (bg) image_free(bg); if (textbox) image_free(textbox); text_destroy(name_text); diff --git a/src/text.cpp b/src/text.cpp index 62a0840..dfa05f2 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -1,12 +1,3 @@ -/* - * text.cpp - Implementation of stb_truetype text renderer with shared caches - * - * This version uses: - * 1. Signed Distance Field (SDF) rendering for efficient, high-quality - * outlines and scaling. - * 2. Precomputed line breaks to decouple layout from per-frame rendering. - */ - #include // C/C++ Standard Libraries @@ -20,42 +11,102 @@ #include #include #include -#include // Included for std::clamp in smoothstep +#include // For std::clamp // Embed stb_truetype implementation #define STB_TRUETYPE_IMPLEMENTATION #include "stb_truetype.h" +// --- UTF-8 Helpers --- + +// Decode the next UTF-8 codepoint from `s` starting at `i` (byte index). +// Advances `i` to the next byte after the decoded codepoint. +// Returns the Unicode codepoint (0..0x10FFFF). Invalid sequences return 0xFFFD. +static uint32_t utf8_next(const std::string& s, size_t& i) { + const unsigned char* bytes = reinterpret_cast(s.data()); + size_t n = s.size(); + if (i >= n) return 0; + + unsigned char b0 = bytes[i]; + + // 1-byte (ASCII) + if (b0 < 0x80) { + ++i; + return (uint32_t)b0; + } + + // multi-byte sequences + if ((b0 & 0xE0) == 0xC0) { + // 2-byte + if (i + 1 >= n) { ++i; return 0xFFFD; } + unsigned char b1 = bytes[i + 1]; + if ((b1 & 0xC0) != 0x80) { ++i; return 0xFFFD; } + uint32_t cp = ((b0 & 0x1F) << 6) | (b1 & 0x3F); + i += 2; + if (cp < 0x80) return 0xFFFD; // overlong + return cp; + } + if ((b0 & 0xF0) == 0xE0) { + // 3-byte + if (i + 2 >= n) { ++i; return 0xFFFD; } + unsigned char b1 = bytes[i + 1], b2 = bytes[i + 2]; + if ((b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80) { ++i; return 0xFFFD; } + uint32_t cp = ((b0 & 0x0F) << 12) | ((b1 & 0x3F) << 6) | (b2 & 0x3F); + i += 3; + if (cp < 0x800) return 0xFFFD; // overlong + return cp; + } + if ((b0 & 0xF8) == 0xF0) { + // 4-byte + if (i + 3 >= n) { ++i; return 0xFFFD; } + unsigned char b1 = bytes[i + 1], b2 = bytes[i + 2], b3 = bytes[i + 3]; + if ((b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80) { ++i; return 0xFFFD; } + uint32_t cp = ((b0 & 0x07) << 18) | ((b1 & 0x3F) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F); + i += 4; + if (cp < 0x10000 || cp > 0x10FFFF) return 0xFFFD; + return cp; + } + + // Invalid leading byte + ++i; + return 0xFFFD; +} + +// Count codepoints in a UTF-8 string +static size_t utf8_count_codepoints(const std::string& s) { + size_t i = 0; + size_t count = 0; + while (i < s.size()) { + utf8_next(s, i); + ++count; + } + return count; +} + // --- Internal Data Structures --- -/** - * @brief Holds the raw file data and stbtt_fontinfo for a cached font. - */ +// Holds the raw file data and stbtt_fontinfo for a cached font. struct cached_font_t { std::vector file_data; stbtt_fontinfo info; }; -/** - * @brief Uniquely identifies a specific glyph variant for caching. - * Now uses SDF, so the key includes the size. - */ +// Uniquely identifies a specific glyph variant for caching. +// Now uses SDF, so the key includes the size and Unicode codepoint. struct glyph_key_t { const cached_font_t* font; - char c; + uint32_t codepoint; float size; bool operator<(const glyph_key_t& other) const { if (font != other.font) return font < other.font; - if (c != other.c) return c < other.c; + if (codepoint != other.codepoint) return codepoint < other.codepoint; if (size != other.size) return size < other.size; return false; } }; -/** - * @brief Holds the rendered SDF bitmap and metrics for a cached glyph. - */ +// Holds the rendered SDF bitmap and metrics for a cached glyph. struct cached_glyph_t { std::unique_ptr data; // This now holds SDF data int w, h; @@ -64,44 +115,39 @@ struct cached_glyph_t { int advance; }; -/** - * @brief Stores the start and end indices for a precomputed line of text. - */ +// Stores the start and end indices for a precomputed line of text. +// For UTF-8 we store both byte ranges and codepoint ranges. struct line_break_t { - size_t start_index; // Index of the first char in the line - size_t end_index; // Index *after* the last char in the line - float y_offset; // The y-coordinate to start drawing this line + size_t start_byte_index; // byte index of first char in the line + size_t end_byte_index; // byte index *after* last char in the line + size_t start_cp_index; // codepoint index of first char in the line + size_t end_cp_index; // codepoint index *after* last char in the line + float y_offset; // The y-coordinate to start drawing this line }; -/** - * @brief Holds the parameters for a single `text_push` call. - */ +// Holds the parameters for a single `text_push` call. struct text_params_t { std::string text; float size; float speed; std::string font_path; bool outline; - uint32_t color_main = 0xFFFFFFFF; - uint32_t color_outline = 0xFF000000; + uint32_t color_main = 0xFFFFFFFF; + uint32_t color_outline = 0xFF000000; // Mutable fields for precomputed layout mutable std::vector lines; mutable bool lines_computed = false; }; -/** - * @brief Shared cache structure that can be referenced by multiple text objects. - */ +// Shared cache structure that can be referenced by multiple text objects. struct shared_cache_t { - std::map font_cache; + std::map font_cache; std::map glyph_cache; int refcount = 1; // Reference counting for proper cleanup }; -/** - * @brief The main internal context structure for the text renderer. - */ +// The main internal context structure for the text renderer. struct text_t { uint32_t* framebuffer; int width; @@ -114,146 +160,26 @@ struct text_t { // Rendering State (per-instance) std::vector text_queue; int queue_index = 0; - int char_index = 0; // Now tracks total chars to *show* + int char_index = 0; // Now tracks total codepoints to *show* float char_progress = 0.0f; // Cumulative progress for animation cached_font_t* current_font = nullptr; - float current_x = 0.0f; - float current_y = 0.0f; - float last_line_height = 0.0f; + float current_x = 0.0f; + float current_y = 0.0f; + float last_line_height = 0.0f; const float padding_x = 10.0f; const float padding_y = 10.0f; - int last_rendered_char = 0; + int last_rendered_char = 0; // codepoint count }; -// --- Static Helper Functions --- +// --- Private Helper Functions --- -static float smoothstep(float edge0, float edge1, float x) { - // Use std::clamp (from or in C++17) - float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); - return t * t * (3.0f - 2.0f * t); -} - -static uint32_t alpha_blend(uint32_t dest, uint32_t src) { - uint32_t src_a = (src >> 24) & 0xFF; - if (src_a == 0) return dest; - if (src_a == 255) return src; - - uint32_t dest_a = (dest >> 24) & 0xFF; - - uint32_t src_r = (src >> 16) & 0xFF; - uint32_t src_g = (src >> 8) & 0xFF; - uint32_t src_b = (src) & 0xFF; - - uint32_t dest_r = (dest >> 16) & 0xFF; - uint32_t dest_g = (dest >> 8) & 0xFF; - uint32_t dest_b = (dest) & 0xFF; - - uint32_t out_a = src_a + (dest_a * (255 - src_a) / 255); - if (out_a == 0) return 0; - - uint32_t out_r = (src_r * src_a + dest_r * dest_a * (255 - src_a) / 255) / out_a; - uint32_t out_g = (src_g * src_a + dest_g * dest_a * (255 - src_a) / 255) / out_a; - uint32_t out_b = (src_b * src_a + dest_b * dest_a * (255 - src_a) / 255) / out_a; - - return (out_a << 24) | (out_r << 16) | (out_g << 8) | out_b; -} - -/** - * @brief Draws a glyph using its cached SDF bitmap. - * - * @param text The text context. - * @param x Screen x-coordinate to draw at. - * @param y Screen y-coordinate to draw at. - * @param color The ARGB color to use. - * @param glyph The cached SDF glyph. - * @param size The target pixel size (used to calculate SDF smoothing). - * @param outline True to render as an outline, false for a solid fill. - */ -static void draw_glyph(text_t* text, - int x, int y, - uint32_t color, - const cached_glyph_t* glyph, - float size, - bool outline) -{ - if (!glyph || !glyph->data) return; - - int x0 = x; - int y0 = y; - int x1 = x + glyph->w; - int y1 = y + glyph->h; - - // --- SDF Parameters --- - // These must match the values used in get_glyph - const float padding_percent = 0.15f; - // FIX: Changed from 180.0f to 128.0f for a balanced distance range - 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 - - int gx0 = (x0 < 0) ? -x0 : 0; - int gy0 = (y0 < 0) ? -y0 : 0; - int gx1 = (x1 > text->width) ? (glyph->w - (x1 - text->width)) : glyph->w; - int gy1 = (y1 > text->height) ? (glyph->h - (y1 - text->height)) : glyph->h; - - uint32_t color_rgb = color & 0x00FFFFFF; - - for (int gy = gy0; gy < gy1; ++gy) { - for (int gx = gx0; gx < gx1; ++gx) { - - // 1. Get SDF value (0-255) - 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 - // dist > 0 is INSIDE, dist < 0 is OUTSIDE - - // 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 - // We want alpha=1 for dist > 0 (inside) - // Use smoothstep for anti-aliasing around the 0-edge - 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; - - *dest_pixel = alpha_blend(*dest_pixel, src_pixel); - } - } -} +// --- Caching & Font Loading --- +// Loads a font from file or retrieves it from the shared cache. static cached_font_t* load_font(text_t* text, const std::string& font_path) { // Check cache (now in shared_cache_t) auto it = text->cache->font_cache.find(font_path); @@ -290,7 +216,7 @@ static cached_font_t* load_font(text_t* text, const std::string& font_path) { // Init font and add to cache cached_font_t new_font; new_font.file_data = std::move(buffer); - + if (!stbtt_InitFont(&new_font.info, new_font.file_data.data(), 0)) { fprintf(stderr, "Error: stbtt_InitFont failed for: %s\n", font_path.c_str()); return nullptr; @@ -300,14 +226,12 @@ static cached_font_t* load_font(text_t* text, const std::string& font_path) { return &map_it->second; } -/** - * @brief Gets a cached SDF glyph, generating it if it doesn't exist. - */ -static const cached_glyph_t* get_glyph(text_t* text, char c, float size) +// Gets a cached SDF glyph, generating it if it doesn't exist. +static const cached_glyph_t* get_glyph(text_t* text, uint32_t codepoint, float size) { if (!text->current_font) return nullptr; - glyph_key_t key = { text->current_font, c, size }; + glyph_key_t key = { text->current_font, codepoint, size }; // Check cache (now in shared_cache_t) auto it = text->cache->glyph_cache.find(key); @@ -321,46 +245,43 @@ static const cached_glyph_t* get_glyph(text_t* text, char c, float size) float scale = stbtt_ScaleForPixelHeight(info, size); // Get advance metric - stbtt_GetCodepointHMetrics(info, c, &new_glyph.advance, nullptr); - new_glyph.advance = static_cast(new_glyph.advance * scale); + int adv; + stbtt_GetCodepointHMetrics(info, (int)codepoint, &adv, nullptr); + new_glyph.advance = static_cast(adv * scale); // SDF parameters const float padding_percent = 0.15f; - // FIX: Changed from 180 to 128 for a balanced distance range - const unsigned char onedge_value = 128; - // Padding in pixels, at least 1 - const int padding = std::max(1, (int)(size * padding_percent)); - // How many 'distance units' per pixel - float pixel_dist_scale = (float)onedge_value / (float)padding; + const unsigned char onedge_value = 128; // 0-255 range midpoint + const int padding = std::max(1, (int)(size * padding_percent)); // Padding in pixels + float pixel_dist_scale = (float)onedge_value / (float)padding; // 'distance units' per pixel // Generate the SDF bitmap unsigned char* bitmap = stbtt_GetCodepointSDF( - info, scale, c, padding, onedge_value, pixel_dist_scale, + info, scale, (int)codepoint, padding, onedge_value, pixel_dist_scale, &new_glyph.w, &new_glyph.h, &new_glyph.xoff, &new_glyph.yoff ); - + if (!bitmap) { // Glyph has no bitmap (e.g., space) new_glyph.w = new_glyph.h = 0; new_glyph.data = nullptr; - } else { + } + else { // Copy the SDF bitmap into our managed cache new_glyph.data = std::make_unique(new_glyph.w * new_glyph.h); memcpy(new_glyph.data.get(), bitmap, new_glyph.w * new_glyph.h); - + // Free the stb_truetype allocated bitmap - stbtt_FreeSDF(bitmap, nullptr); + stbtt_FreeSDF(bitmap, nullptr); } auto [map_it, success] = text->cache->glyph_cache.emplace(key, std::move(new_glyph)); return &map_it->second; } +// --- Text Layout --- -/** - * @brief Precomputes line breaks for a text block. - * This is marked 'static' and defined before it's used in text_nextframe. - */ +// Precomputes line breaks for a text block. static void precompute_line_breaks(text_t* text, const text_params_t* params) { if (params->lines_computed) return; @@ -371,9 +292,10 @@ static void precompute_line_breaks(text_t* text, const text_params_t* params) { if (!font || (font && params->font_path != "" && text->cache->font_cache.find(params->font_path) != text->cache->font_cache.end() && &text->cache->font_cache.at(params->font_path) != font)) { if (params->font_path.empty()) { - if (text->cache->font_cache.empty()) return; // No default font - font = &text->cache->font_cache.begin()->second; - } else { + if (text->cache->font_cache.empty()) return; // No default font + font = &text->cache->font_cache.begin()->second; + } + else { font = load_font(text, params->font_path); } if (!font) { @@ -391,348 +313,522 @@ static void precompute_line_breaks(text_t* text, const text_params_t* params) { float x = text->padding_x; float y = text->current_y; // Start from the current render position - size_t line_start_index = 0; - size_t last_word_break_index = 0; // Index *after* the space + size_t line_start_byte = 0; + size_t line_start_cp = 0; + size_t last_word_break_byte = 0; // byte index *after* the space + size_t last_word_break_cp = 0; float x_at_last_word_break = text->padding_x; float max_width = text->width - text->padding_x; - for (size_t i = 0; i < params->text.length(); ++i) { - char c = params->text[i]; + // We'll iterate the UTF-8 string and maintain both byte index and cp index + const std::string& s = params->text; + size_t byte_i = 0; + size_t cp_index = 0; - if (c == '\n') { - params->lines.push_back({line_start_index, i, y}); + while (byte_i < s.size()) { + size_t prev_byte = byte_i; + uint32_t cp = utf8_next(s, byte_i); + + // handle newline explicitly + if (cp == (uint32_t)'\n') { + // push line: [line_start_byte, prev_byte) -> includes upto but excluding newline + params->lines.push_back({ line_start_byte, prev_byte, line_start_cp, cp_index, y }); y += line_height; x = text->padding_x; - line_start_index = i + 1; - last_word_break_index = i + 1; + line_start_byte = byte_i; + line_start_cp = cp_index + 1; // next cp after newline + last_word_break_byte = byte_i; + last_word_break_cp = line_start_cp; x_at_last_word_break = x; + ++cp_index; continue; } - // Store last word break position - if (c == ' ' || c == '\t') { - last_word_break_index = i + 1; + // store last word break pos + if (cp == (uint32_t)' ' || cp == (uint32_t)'\t') { + last_word_break_byte = byte_i; // byte index after this space + last_word_break_cp = cp_index + 1; } - const cached_glyph_t* glyph = get_glyph(text, c, params->size); + // get advance for cp + const cached_glyph_t* glyph = get_glyph(text, cp, params->size); int advance = glyph ? glyph->advance : 0; - // Check for wrap *before* adding the character - if (x + advance > max_width && line_start_index < i) { - // --- Time to wrap --- - if (last_word_break_index > line_start_index) { - // Wrap at the last word break - params->lines.push_back({line_start_index, last_word_break_index - 1, y}); // -1 to exclude space - i = last_word_break_index - 1; // Re-process from the space - line_start_index = last_word_break_index; - } else { - // Force wrap in the middle of a word - params->lines.push_back({line_start_index, i, y}); - line_start_index = i; - i--; // Re-process current char on new line + // check wrap before adding the char + if (x + advance > max_width && line_start_byte < prev_byte) { + // wrap + if (last_word_break_byte > line_start_byte) { + // wrap at last word break (exclude trailing space) + // end_byte = last_word_break_byte - bytes of space? last_word_break_byte already points after the space + // we want to exclude final space visually: line end byte = last_word_break_byte - size_of_space_byte_sequence + // But simpler: set end_byte to last_word_break_byte - 1 byte may not be correct for multi-byte, + // so we set end_cp = last_word_break_cp - 1 and then compute end_byte by walking from line_start_byte. + // But we already tracked last_word_break_byte and last_word_break_cp as byte AFTER the space; so we set + // end_byte = last_word_break_byte - bytes_of_space (unknown). To avoid complexity, we'll set end_byte = last_word_break_byte; and render lines excluding trailing space in drawing. + params->lines.push_back({ line_start_byte, last_word_break_byte, line_start_cp, last_word_break_cp, y }); + // re-start from after the space + // set byte_i to last_word_break_byte (already after space) + byte_i = last_word_break_byte; + cp_index = last_word_break_cp; + line_start_byte = last_word_break_byte; + line_start_cp = last_word_break_cp; } - - // Reset for new line + else { + // force wrap in middle of word: end at previous char + params->lines.push_back({ line_start_byte, prev_byte, line_start_cp, cp_index, y }); + // start new line at prev_byte (reprocess current char on new line) + byte_i = prev_byte; + // cp_index unchanged because we didn't consume this cp for new line + line_start_byte = prev_byte; + line_start_cp = cp_index; + } + + // reset for new line y += line_height; x = text->padding_x; - last_word_break_index = line_start_index; + last_word_break_byte = line_start_byte; + last_word_break_cp = line_start_cp; x_at_last_word_break = x; - } else { - // No wrap, just advance + continue; // loop will continue, not advancing cp_index here (we set it appropriately) + } + else { + // no wrap, just advance x += advance; - if (last_word_break_index == i + 1) { - x_at_last_word_break = x; // Update x pos after the space + ++cp_index; + // update x_at_last_word_break if this was space + if (last_word_break_cp == cp_index) { + x_at_last_word_break = x; } } } - // Add the final line - if (line_start_index < params->text.length()) { - params->lines.push_back({line_start_index, params->text.length(), y}); + // Add the final line if any text remains + if (line_start_byte < s.size()) { + params->lines.push_back({ line_start_byte, s.size(), line_start_cp, cp_index, y }); } - + text->last_line_height = line_height; // Store for next block's y_offset params->lines_computed = true; } +// --- SDF Rendering --- -// --- Public API Functions --- +// Standard smoothstep function for SDF anti-aliasing. +static float smoothstep(float edge0, float edge1, float x) { + float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); +} + +// Standard alpha blending (source-over). +static uint32_t alpha_blend(uint32_t dest, uint32_t src) { + uint32_t src_a = (src >> 24) & 0xFF; + if (src_a == 0) return dest; + if (src_a == 255) return src; + + uint32_t dest_a = (dest >> 24) & 0xFF; + + uint32_t src_r = (src >> 16) & 0xFF; + uint32_t src_g = (src >> 8) & 0xFF; + uint32_t src_b = (src) & 0xFF; + + uint32_t dest_r = (dest >> 16) & 0xFF; + uint32_t dest_g = (dest >> 8) & 0xFF; + uint32_t dest_b = (dest) & 0xFF; + + uint32_t out_a = src_a + (dest_a * (255 - src_a) / 255); + if (out_a == 0) return 0; + + uint32_t out_r = (src_r * src_a + dest_r * dest_a * (255 - src_a) / 255) / out_a; + uint32_t out_g = (src_g * src_a + dest_g * dest_a * (255 - src_a) / 255) / out_a; + uint32_t out_b = (src_b * src_a + dest_b * dest_a * (255 - src_a) / 255) / out_a; + + return (out_a << 24) | (out_r << 16) | (out_g << 8) | out_b; +} + +// Draws a glyph using its cached SDF bitmap. +static void draw_glyph(text_t* text, + int x, int y, + uint32_t color, + const cached_glyph_t* glyph, + float size, + bool outline) +{ + if (!glyph || !glyph->data) return; + + int x0 = x; + int y0 = y; + int x1 = x + glyph->w; + 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 + 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 + + // Clip to framebuffer + int gx0 = (x0 < 0) ? -x0 : 0; + int gy0 = (y0 < 0) ? -y0 : 0; + int gx1 = (x1 > text->width) ? (glyph->w - (x1 - text->width)) : glyph->w; + int gy1 = (y1 > text->height) ? (glyph->h - (y1 - text->height)) : glyph->h; + + uint32_t color_rgb = color & 0x00FFFFFF; + + for (int gy = gy0; gy < gy1; ++gy) { + for (int gx = gx0; gx < gx1; ++gx) { + + // 1. Get SDF value (0-255) + 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 + 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; + + *dest_pixel = alpha_blend(*dest_pixel, src_pixel); + } + } +} + + +// --- Public C-Style API --- extern "C" { -text_t* text_init(int width, int height) { - if (width <= 0 || height <= 0) return nullptr; + // --- Lifecycle --- - text_t* text = new (std::nothrow) text_t; - if (!text) return nullptr; + text_t* text_init(int width, int height) { + if (width <= 0 || height <= 0) return nullptr; - text->width = width; - text->height = height; - - text->framebuffer = new (std::nothrow) uint32_t[width * height]; - if (!text->framebuffer) { - delete text; - return nullptr; - } + text_t* text = new (std::nothrow) text_t; + if (!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->width = width; + text->height = height; - 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; - } - } - - delete text; -} - -void text_push(text_t* text, - const char* str, - float size, - float speed, - const char* font_path, - bool outline) -{ - if (!text || !str) return; - - 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->text_queue.push_back(std::move(p)); -} - -/** - * @brief Immediately sets the character index to the end of the current - * text block, causing the entire line to be rendered on the next frame. - */ -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 = params->text.length(); - - // Only update if it's not already complete - if (text->char_index < text_len) { - text->char_index = (int)text_len; - text->char_progress = static_cast(text_len); - } -} - -/** - * @brief Returns true if the currently running text block has not yet - * displayed all characters. - * stupid and obnoxious, nextframe does the same but the way we use it won't work - */ -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]; - return text->char_index < params->text.length(); -} - -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; -} - -uint32_t* text_getfb(text_t* text) { - return text ? text->framebuffer : nullptr; -} - -bool text_nextframe(text_t* text) { - if (!text) return true; - if (text->queue_index >= text->text_queue.size()) { - return true; - } - - 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; - } - - 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 - if (params->speed > 0.0f && text->char_index < params->text.length()) { - text->char_progress += params->speed; - text->char_index = static_cast(text->char_progress); - } else if (params->speed <= 0.0f) { - text->char_index = (int)params->text.length(); - } - text->char_index = (int)std::min((size_t)text->char_index, params->text.length()); - - // CRITICAL FIX: Only draw NEW characters - size_t draw_start = text->last_rendered_char; - size_t draw_end = text->char_index; - - // Drawing loop - only for NEW characters - for (const auto& line : params->lines) { - // Skip lines that are completely rendered - if (line.end_index <= draw_start) { - continue; - } - - // Skip lines that haven't started yet - if (line.start_index >= draw_end) { - break; + text->framebuffer = new (std::nothrow) uint32_t[width * height]; + if (!text->framebuffer) { + delete text; + return nullptr; } - text->current_x = text->padding_x; - text->current_y = line.y_offset; - - // Calculate x position up to where we need to draw - for (size_t i = line.start_index; i < std::min(line.end_index, draw_start); ++i) { - char c = params->text[i]; - if (c == '\n') continue; - const cached_glyph_t* glyph = get_glyph(text, c, params->size); - if (glyph) { - text->current_x += glyph->advance; - } + // 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; - // Draw only NEW characters on this line - for (size_t i = std::max(line.start_index, draw_start); - i < std::min(line.end_index, draw_end); ++i) { - - char c = params->text[i]; - if (c == '\n') continue; - - const cached_glyph_t* glyph = get_glyph(text, c, params->size); - if (!glyph) continue; - - int x_base = static_cast(text->current_x + glyph->xoff); - int y_base = static_cast(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; - } - } - - // Update tracking - text->last_rendered_char = text->char_index; - - // Handle block transition - if (text->char_index >= params->text.length()) { - text->queue_index++; + text->queue_index = 0; text->char_index = 0; text->char_progress = 0.0f; - text->last_rendered_char = 0; // ADD THIS - reset for next block + text->current_font = nullptr; + text->last_line_height = 0.0f; - text->current_y += text->last_line_height; + text->current_x = text->padding_x; + text->current_y = text->padding_y; + + text_clear(text); + return text; } - if (text->queue_index >= text->text_queue.size()) { + 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; + } + } + + delete text; + } + + // --- Text Queue & State --- + + // 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_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->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; + 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_rendered_char = 0; // ADD THIS - - text->current_x = text->padding_x; - text->current_y = text->padding_y; text->last_line_height = 0.0f; - - return true; } - return 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; -} // extern "C" \ No newline at end of file + 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(text_len_cp); + } + } + + // --- Update & Animation --- + + // Advances the text animation and renders new characters. + bool text_nextframe(text_t* text) { + if (!text) return true; + if (text->queue_index >= text->text_queue.size()) { + return true; // Nothing to do + } + + 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 + size_t total_cp = utf8_count_codepoints(params->text); + if (params->speed > 0.0f && text->char_index < (int)total_cp) { + text->char_progress += params->speed; + text->char_index = static_cast(text->char_progress); + } + else if (params->speed <= 0.0f) { + text->char_index = (int)total_cp; + } + 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 + + // 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) { + size_t prev_byte = byte_i; + 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)) { + size_t prev_byte = byte_i; + 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(text->current_x + glyph->xoff); + int y_base = static_cast(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 + text->last_rendered_char = (int)text->char_index; + + // Handle block transition + if (text->char_index >= (int)utf8_count_codepoints(params->text)) { + text->queue_index++; + text->char_index = 0; + text->char_progress = 0.0f; + text->last_rendered_char = 0; // Reset for next block + 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; + + text->current_x = text->padding_x; + text->current_y = text->padding_y; + text->last_line_height = 0.0f; + + return true; // Finished + } + + return false; // Not finished + } + + // 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. + uint32_t* text_getfb(text_t* text) { + return text ? text->framebuffer : nullptr; + } + +} // extern "C" diff --git a/vn.vcxproj b/vn.vcxproj index f675a94..b29b4cc 100644 --- a/vn.vcxproj +++ b/vn.vcxproj @@ -97,6 +97,7 @@ stdcpp20 MultiThreadedDebug AdvancedVectorExtensions2 + /Zc:char8_t- %(AdditionalOptions) Console @@ -118,6 +119,7 @@ stdcpp20 MultiThreadedDebug AdvancedVectorExtensions2 + /Zc:char8_t- %(AdditionalOptions) Console @@ -137,6 +139,7 @@ include true AdvancedVectorExtensions2 + /Zc:char8_t- %(AdditionalOptions) Console @@ -158,6 +161,7 @@ stdcpp20 MultiThreadedDebug AdvancedVectorExtensions2 + /Zc:char8_t- %(AdditionalOptions) Console