parallel animations, a different test dialogue, utf-8
This commit is contained in:
1 parent
1b31faa7ee
commit
475e57d85c
3 files changed
+577
-435
No files matched your search
+200
-158
@@ -1,9 +1,10 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <chrono> // <-- NEW: For delta-time calculation
|
#include <chrono>
|
||||||
|
#include <vector> // <-- NEW: For tracking multiple animations
|
||||||
#include <window.h>
|
#include <window.h>
|
||||||
#include <text.h>
|
#include <text.h>
|
||||||
#include <compositor.h>
|
#include <compositor.h>
|
||||||
@@ -14,52 +15,41 @@
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
enum CommandType {
|
enum CommandType {
|
||||||
CMD_SHOW, // Show character with specific sprite
|
CMD_SHOW,
|
||||||
CMD_HIDE, // Hide character (all sprites)
|
CMD_HIDE,
|
||||||
CMD_MOVE, // Move character to position
|
CMD_MOVE,
|
||||||
CMD_CHANGE_SPRITE, // Change character's sprite/expression
|
CMD_CHANGE_SPRITE,
|
||||||
CMD_SET_Z, // Change z-order
|
CMD_SET_Z,
|
||||||
CMD_SET_SCALE, // Change scale
|
CMD_SET_SCALE,
|
||||||
CMD_SPEAK, // Display dialogue
|
CMD_SPEAK,
|
||||||
CMD_WAIT, // Wait for click
|
CMD_WAIT,
|
||||||
CMD_BACKGROUND, // Change background
|
CMD_BACKGROUND,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum TransitionType {
|
enum TransitionType {
|
||||||
TRANS_NONE, // Instant snap
|
TRANS_NONE,
|
||||||
TRANS_FADE, // Fade in / Fade out
|
TRANS_FADE,
|
||||||
TRANS_SLIDE_LEFT, // Slide in from left
|
TRANS_SLIDE_LEFT,
|
||||||
TRANS_SLIDE_RIGHT, // Slide in from right
|
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 {
|
struct Command {
|
||||||
CommandType type;
|
CommandType type;
|
||||||
const char* character_name; // Which character (by name)
|
const char* character_name;
|
||||||
|
|
||||||
// Position/transform data
|
|
||||||
int x, y;
|
int x, y;
|
||||||
int z;
|
int z;
|
||||||
float scale;
|
float scale;
|
||||||
|
|
||||||
// Sprite/image data
|
|
||||||
const char* sprite_name;
|
const char* sprite_name;
|
||||||
|
|
||||||
// Dialogue data
|
|
||||||
const char* speaker;
|
const char* speaker;
|
||||||
const char* text;
|
const char* text;
|
||||||
|
|
||||||
// Animation/transition data
|
|
||||||
TransitionType transition;
|
TransitionType transition;
|
||||||
float duration_ms;
|
float duration_ms;
|
||||||
|
|
||||||
|
bool blocking; // NEW: If true, wait for this animation to complete before next command
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Scene {
|
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<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 active_animations.size();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CHARACTER MANAGEMENT
|
// CHARACTER MANAGEMENT
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
struct SpriteElement {
|
struct SpriteElement {
|
||||||
uint32_t* image_data;
|
uint32_t* image_data;
|
||||||
uint32_t element_id; // compositor element ID
|
uint32_t element_id;
|
||||||
bool visible;
|
bool visible;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Character {
|
struct Character {
|
||||||
std::string name;
|
std::string name;
|
||||||
std::unordered_map<std::string, SpriteElement> sprites; // emotion -> sprite
|
std::unordered_map<std::string, SpriteElement> sprites;
|
||||||
int width, height;
|
int width, height;
|
||||||
int current_x, current_y;
|
int current_x, current_y;
|
||||||
int current_z;
|
int current_z;
|
||||||
float current_scale;
|
float current_scale;
|
||||||
std::string current_sprite; // Which sprite is currently visible
|
std::string current_sprite;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CharacterManager {
|
class CharacterManager {
|
||||||
@@ -109,7 +146,6 @@ public:
|
|||||||
: compositor(comp), screen_width(screen_w) {
|
: compositor(comp), screen_width(screen_w) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register a character
|
|
||||||
void add_character(const char* name, int width, int height) {
|
void add_character(const char* name, int width, int height) {
|
||||||
Character ch;
|
Character ch;
|
||||||
ch.name = name;
|
ch.name = name;
|
||||||
@@ -123,7 +159,6 @@ public:
|
|||||||
characters[name] = ch;
|
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,
|
void load_sprite(const char* name, const char* emotion, const char* filename,
|
||||||
int width, int height, int initial_z = 1) {
|
int width, int height, int initial_z = 1) {
|
||||||
auto it = characters.find(name);
|
auto it = characters.find(name);
|
||||||
@@ -137,21 +172,18 @@ public:
|
|||||||
elem.element_id = compositor_insert(compositor, img, width, height, 0, 0, initial_z);
|
elem.element_id = compositor_insert(compositor, img, width, height, 0, 0, initial_z);
|
||||||
elem.visible = false;
|
elem.visible = false;
|
||||||
|
|
||||||
// NEW: Start fully faded out and invisible
|
|
||||||
compositor_set_alpha(compositor, elem.element_id, 0.0f);
|
compositor_set_alpha(compositor, elem.element_id, 0.0f);
|
||||||
compositor_set_visible(compositor, elem.element_id, false);
|
compositor_set_visible(compositor, elem.element_id, false);
|
||||||
|
|
||||||
it->second.sprites[emotion] = elem;
|
it->second.sprites[emotion] = elem;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get character by name
|
|
||||||
Character* get_character(const char* name) {
|
Character* get_character(const char* name) {
|
||||||
auto it = characters.find(name);
|
auto it = characters.find(name);
|
||||||
if (it != characters.end()) return &it->second;
|
if (it != characters.end()) return &it->second;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the element ID of the character's currently active sprite
|
|
||||||
uint32_t get_active_element_id(Character* ch) {
|
uint32_t get_active_element_id(Character* ch) {
|
||||||
if (!ch || ch->current_sprite.empty()) return 0;
|
if (!ch || ch->current_sprite.empty()) return 0;
|
||||||
auto it = ch->sprites.find(ch->current_sprite);
|
auto it = ch->sprites.find(ch->current_sprite);
|
||||||
@@ -159,13 +191,11 @@ public:
|
|||||||
return it->second.element_id;
|
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,
|
void show_character(const char* name, int x, int y, int z, const char* sprite_name,
|
||||||
TransitionType transition, float duration_ms) {
|
TransitionType transition, float duration_ms) {
|
||||||
Character* ch = get_character(name);
|
Character* ch = get_character(name);
|
||||||
if (!ch) return;
|
if (!ch) return;
|
||||||
|
|
||||||
// Hide all other sprites for this character first
|
|
||||||
for (auto& pair : ch->sprites) {
|
for (auto& pair : ch->sprites) {
|
||||||
if (pair.first != sprite_name && pair.second.visible) {
|
if (pair.first != sprite_name && pair.second.visible) {
|
||||||
compositor_set_visible(compositor, pair.second.element_id, false);
|
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);
|
auto it = ch->sprites.find(sprite_name);
|
||||||
if (it == ch->sprites.end()) return;
|
if (it == ch->sprites.end()) return;
|
||||||
|
|
||||||
SpriteElement& elem = it->second;
|
SpriteElement& elem = it->second;
|
||||||
float duration_sec = duration_ms / 1000.0f;
|
float duration_sec = duration_ms / 1000.0f;
|
||||||
|
|
||||||
// Update state
|
|
||||||
ch->current_x = x;
|
ch->current_x = x;
|
||||||
ch->current_y = y;
|
ch->current_y = y;
|
||||||
ch->current_z = z;
|
ch->current_z = z;
|
||||||
ch->current_sprite = sprite_name;
|
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);
|
compositor_set_zorder(compositor, elem.element_id, z);
|
||||||
|
|
||||||
// --- NEW: Apply Transition ---
|
|
||||||
if (transition == TRANS_FADE) {
|
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_set_visible(compositor, elem.element_id, true);
|
||||||
compositor_animate_fade(compositor, elem.element_id, 1.0f, duration_sec, EASE_LINEAR);
|
compositor_animate_fade(compositor, elem.element_id, 1.0f, duration_sec, EASE_LINEAR);
|
||||||
}
|
}
|
||||||
else if (transition == TRANS_SLIDE_LEFT) {
|
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_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_set_visible(compositor, elem.element_id, true);
|
||||||
compositor_animate_move(compositor, elem.element_id, x, y, duration_sec, EASE_OUT_CUBIC);
|
compositor_animate_move(compositor, elem.element_id, x, y, duration_sec, EASE_OUT_CUBIC);
|
||||||
}
|
}
|
||||||
else if (transition == TRANS_SLIDE_RIGHT) {
|
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_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_set_visible(compositor, elem.element_id, true);
|
||||||
compositor_animate_move(compositor, elem.element_id, x, y, duration_sec, EASE_OUT_CUBIC);
|
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_position(compositor, elem.element_id, x, y);
|
||||||
compositor_set_alpha(compositor, elem.element_id, 1.0f);
|
compositor_set_alpha(compositor, elem.element_id, 1.0f);
|
||||||
compositor_set_visible(compositor, elem.element_id, true);
|
compositor_set_visible(compositor, elem.element_id, true);
|
||||||
@@ -224,14 +250,12 @@ public:
|
|||||||
|
|
||||||
float duration_sec = duration_ms / 1000.0f;
|
float duration_sec = duration_ms / 1000.0f;
|
||||||
|
|
||||||
// Hide all visible sprites for this character
|
|
||||||
for (auto& pair : ch->sprites) {
|
for (auto& pair : ch->sprites) {
|
||||||
if (pair.second.visible) {
|
if (pair.second.visible) {
|
||||||
if (transition == TRANS_FADE) {
|
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);
|
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_visible(compositor, pair.second.element_id, false);
|
||||||
compositor_set_alpha(compositor, pair.second.element_id, 0.0f);
|
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) {
|
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);
|
Character* ch = get_character(name);
|
||||||
if (!ch) return;
|
if (!ch) return;
|
||||||
|
|
||||||
// Hide current sprite
|
|
||||||
if (!ch->current_sprite.empty()) {
|
if (!ch->current_sprite.empty()) {
|
||||||
auto old_it = ch->sprites.find(ch->current_sprite);
|
auto old_it = ch->sprites.find(ch->current_sprite);
|
||||||
if (old_it != ch->sprites.end()) {
|
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);
|
auto new_it = ch->sprites.find(new_emotion);
|
||||||
if (new_it != ch->sprites.end()) {
|
if (new_it != ch->sprites.end()) {
|
||||||
compositor_set_position(compositor, new_it->second.element_id,
|
compositor_set_position(compositor, new_it->second.element_id,
|
||||||
@@ -304,7 +324,7 @@ public:
|
|||||||
ch->current_scale = scale;
|
ch->current_scale = scale;
|
||||||
float duration_sec = duration_ms / 1000.0f;
|
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);
|
compositor_animate_scale(compositor, id, scale, duration_sec, EASE_IN_OUT_QUAD);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -312,7 +332,6 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
~CharacterManager() {
|
~CharacterManager() {
|
||||||
for (auto& pair : characters) {
|
for (auto& pair : characters) {
|
||||||
for (auto& sprite : pair.second.sprites) {
|
for (auto& sprite : pair.second.sprites) {
|
||||||
@@ -342,37 +361,36 @@ public:
|
|||||||
name_id(nid), dialogue_id(did) {
|
name_id(nid), dialogue_id(did) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NEW: Execute a command and return the duration of any triggered animation
|
|
||||||
float execute(const Command* cmd) {
|
float execute(const Command* cmd) {
|
||||||
switch (cmd->type) {
|
switch (cmd->type) {
|
||||||
case CMD_SHOW:
|
case CMD_SHOW:
|
||||||
char_manager->show_character(cmd->character_name, cmd->x, cmd->y,
|
char_manager->show_character(cmd->character_name, cmd->x, cmd->y,
|
||||||
cmd->z, cmd->sprite_name,
|
cmd->z, cmd->sprite_name,
|
||||||
cmd->transition, cmd->duration_ms);
|
cmd->transition, cmd->duration_ms);
|
||||||
return cmd->duration_ms / 1000.0f; // Return duration
|
return cmd->duration_ms / 1000.0f;
|
||||||
|
|
||||||
case CMD_HIDE:
|
case CMD_HIDE:
|
||||||
char_manager->hide_character(cmd->character_name,
|
char_manager->hide_character(cmd->character_name,
|
||||||
cmd->transition, cmd->duration_ms);
|
cmd->transition, cmd->duration_ms);
|
||||||
return cmd->duration_ms / 1000.0f; // Return duration
|
return cmd->duration_ms / 1000.0f;
|
||||||
|
|
||||||
case CMD_MOVE:
|
case CMD_MOVE:
|
||||||
char_manager->move_character(cmd->character_name, cmd->x, cmd->y,
|
char_manager->move_character(cmd->character_name, cmd->x, cmd->y,
|
||||||
cmd->transition, cmd->duration_ms);
|
cmd->transition, cmd->duration_ms);
|
||||||
return cmd->duration_ms / 1000.0f; // Return duration
|
return cmd->duration_ms / 1000.0f;
|
||||||
|
|
||||||
case CMD_CHANGE_SPRITE:
|
case CMD_CHANGE_SPRITE:
|
||||||
char_manager->change_sprite(cmd->character_name, cmd->sprite_name);
|
char_manager->change_sprite(cmd->character_name, cmd->sprite_name);
|
||||||
break; // Instant
|
break;
|
||||||
|
|
||||||
case CMD_SET_Z:
|
case CMD_SET_Z:
|
||||||
char_manager->set_z(cmd->character_name, cmd->z);
|
char_manager->set_z(cmd->character_name, cmd->z);
|
||||||
break; // Instant
|
break;
|
||||||
|
|
||||||
case CMD_SET_SCALE:
|
case CMD_SET_SCALE:
|
||||||
char_manager->set_scale(cmd->character_name, cmd->scale,
|
char_manager->set_scale(cmd->character_name, cmd->scale,
|
||||||
cmd->transition, cmd->duration_ms);
|
cmd->transition, cmd->duration_ms);
|
||||||
return cmd->duration_ms / 1000.0f; // Return duration
|
return cmd->duration_ms / 1000.0f;
|
||||||
|
|
||||||
case CMD_SPEAK:
|
case CMD_SPEAK:
|
||||||
text_clear(name_text);
|
text_clear(name_text);
|
||||||
@@ -385,13 +403,13 @@ public:
|
|||||||
}
|
}
|
||||||
compositor_mark_content_dirty(compositor, name_id);
|
compositor_mark_content_dirty(compositor, name_id);
|
||||||
compositor_mark_content_dirty(compositor, dialogue_id);
|
compositor_mark_content_dirty(compositor, dialogue_id);
|
||||||
break; // Instant
|
break;
|
||||||
|
|
||||||
case CMD_WAIT:
|
case CMD_WAIT:
|
||||||
case CMD_BACKGROUND:
|
case CMD_BACKGROUND:
|
||||||
break; // No-op
|
break;
|
||||||
}
|
}
|
||||||
return 0.0f; // Default: no duration
|
return 0.0f;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -399,11 +417,10 @@ public:
|
|||||||
// MAIN PROGRAM
|
// MAIN PROGRAM
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
// NEW: Main application state
|
|
||||||
enum AppState {
|
enum AppState {
|
||||||
STATE_IDLE, // Ready to process next command
|
STATE_IDLE,
|
||||||
STATE_ANIMATING, // Waiting for an animation to finish
|
STATE_ANIMATING,
|
||||||
STATE_WAITING_FOR_INPUT // Waiting for user click (after CMD_WAIT)
|
STATE_WAITING_FOR_INPUT
|
||||||
};
|
};
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
@@ -423,14 +440,12 @@ int main() {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Background ---
|
|
||||||
uint32_t* bg = image_load_resized("assets/background.png", WIDTH, HEIGHT);
|
uint32_t* bg = image_load_resized("assets/background.png", WIDTH, HEIGHT);
|
||||||
if (bg) {
|
if (bg) {
|
||||||
uint32_t bg_id = compositor_insert(compositor, bg, WIDTH, HEIGHT, 0, 0, 0);
|
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_WIDTH = 400;
|
||||||
const int CHAR_HEIGHT = 600;
|
const int CHAR_HEIGHT = 600;
|
||||||
const int CHAR_Y_OFFSET = 140;
|
const int CHAR_Y_OFFSET = 140;
|
||||||
@@ -438,111 +453,147 @@ int main() {
|
|||||||
|
|
||||||
CharacterManager char_manager(compositor, WIDTH);
|
CharacterManager char_manager(compositor, WIDTH);
|
||||||
|
|
||||||
// Register characters
|
|
||||||
char_manager.add_character("maksw", CHAR_WIDTH, CHAR_HEIGHT);
|
char_manager.add_character("maksw", CHAR_WIDTH, CHAR_HEIGHT);
|
||||||
char_manager.add_character("wikor", 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("maksw", "neutral", "assets/maksw.png", CHAR_WIDTH, CHAR_HEIGHT);
|
||||||
char_manager.load_sprite("wikor", "neutral", "assets/wikor.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);
|
uint32_t* textbox = image_load_resized("assets/textbox.png", WIDTH, 180);
|
||||||
if (textbox) {
|
if (textbox) {
|
||||||
uint32_t box_id = compositor_insert(compositor, textbox, WIDTH, 180, 0, HEIGHT - 180, 3);
|
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);
|
text_t* name_text = text_init(WIDTH, HEIGHT);
|
||||||
uint32_t name_id = compositor_insert(compositor, text_getfb(name_text),
|
uint32_t name_id = compositor_insert(compositor, text_getfb(name_text),
|
||||||
WIDTH, HEIGHT, 60, HEIGHT - 170, 4);
|
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),
|
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);
|
compositor_set_alpha(compositor, dialogue_id, 1.0f);
|
||||||
|
|
||||||
// --- Scene Script (UPDATED with transitions) ---
|
// --- Scene Script ---
|
||||||
Command scene_commands[] = {
|
Command scene_commands[] = {
|
||||||
// Show both characters
|
// Show both characters IN PARALLEL
|
||||||
{CMD_SHOW, "maksw", 100, POS_Y, 2, 1.0f, "neutral", nullptr, nullptr, TRANS_SLIDE_LEFT, 500.0f},
|
{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},
|
{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)
|
// Maksw speaks
|
||||||
{CMD_SET_Z, "maksw", 0, 0, 3, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0},
|
{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},
|
{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",
|
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw",
|
||||||
"Hey, Wikor, nice weather today, huh?", 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},
|
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
|
||||||
|
|
||||||
// Wikor responds
|
// Wikor responds
|
||||||
{CMD_SET_SCALE, "maksw", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 100.0f},
|
{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},
|
{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},
|
{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},
|
{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, "Wikor",
|
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, u8"admirał wiktorov",
|
||||||
"Yeah, it's surprisingly sunny for November.", TRANS_NONE, 0},
|
"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},
|
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
|
||||||
|
|
||||||
// Maksw speaks again
|
// Maksw replies
|
||||||
{CMD_SET_SCALE, "wikor", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 100.0f},
|
{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},
|
{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},
|
{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},
|
{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",
|
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw",
|
||||||
"We should get back to debugging soon though.", 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},
|
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
|
||||||
|
|
||||||
// Wikor final line
|
// Wikor gets smug
|
||||||
{CMD_SET_SCALE, "maksw", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 100.0f},
|
{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},
|
{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},
|
{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},
|
{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, "Wikor",
|
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, u8"admirał wiktorov",
|
||||||
"You're right, that compositor won't fix itself.", TRANS_NONE, 0},
|
"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},
|
{CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true},
|
||||||
|
|
||||||
// Fade out
|
// Maksw teasingly replies
|
||||||
{CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, ""},
|
{CMD_SET_SCALE, "wikor", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 100.0f, false},
|
||||||
{CMD_HIDE, "maksw", 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_FADE, 1000.0f},
|
{CMD_SET_Z, "wikor", 0, 0, 1, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, false},
|
||||||
{CMD_HIDE, "wikor", 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_FADE, 1000.0f},
|
{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 };
|
Scene scene = { scene_commands, sizeof(scene_commands) / sizeof(Command), 0 };
|
||||||
CommandExecutor executor(&char_manager, compositor, name_text, dialogue_text,
|
CommandExecutor executor(&char_manager, compositor, name_text, dialogue_text,
|
||||||
name_id, dialogue_id);
|
name_id, dialogue_id);
|
||||||
|
|
||||||
// --- Main Loop ---
|
|
||||||
bool running = true;
|
bool running = true;
|
||||||
|
|
||||||
// NEW: State machine variables
|
AppState current_state = STATE_IDLE;
|
||||||
AppState current_state = STATE_IDLE; // Start in IDLE to process first commands
|
AnimationTracker anim_tracker; // NEW: Track multiple animations
|
||||||
float animation_timer = 0.0f;
|
|
||||||
|
|
||||||
// NEW: Delta-time variables
|
|
||||||
auto last_frame_time = std::chrono::high_resolution_clock::now();
|
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) {
|
while (running) {
|
||||||
if (window_pollevent(window)) running = false;
|
if (window_pollevent(window)) running = false;
|
||||||
|
|
||||||
// --- 1. Calculate Delta Time ---
|
|
||||||
auto current_time = std::chrono::high_resolution_clock::now();
|
auto current_time = std::chrono::high_resolution_clock::now();
|
||||||
std::chrono::duration<float> delta_duration = current_time - last_frame_time;
|
std::chrono::duration<float> delta_duration = current_time - last_frame_time;
|
||||||
float delta_time_sec = delta_duration.count();
|
float delta_time_sec = delta_duration.count();
|
||||||
last_frame_time = current_time;
|
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);
|
compositor_tick(compositor, delta_time_sec);
|
||||||
|
|
||||||
// Update text animation
|
|
||||||
text_nextframe(name_text);
|
text_nextframe(name_text);
|
||||||
text_nextframe(dialogue_text);
|
text_nextframe(dialogue_text);
|
||||||
compositor_mark_content_dirty(compositor, name_id);
|
compositor_mark_content_dirty(compositor, name_id);
|
||||||
compositor_mark_content_dirty(compositor, dialogue_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;
|
static bool mouse_was_down = false;
|
||||||
bool mouse_is_down = window_is_mouse_button_down(window, MOUSE_BUTTON_LEFT);
|
bool mouse_is_down = window_is_mouse_button_down(window, MOUSE_BUTTON_LEFT);
|
||||||
bool mouse_clicked = mouse_is_down && !mouse_was_down;
|
bool mouse_clicked = mouse_is_down && !mouse_was_down;
|
||||||
@@ -555,39 +606,28 @@ int main() {
|
|||||||
|
|
||||||
bool advance_input = mouse_clicked || key_pressed;
|
bool advance_input = mouse_clicked || key_pressed;
|
||||||
|
|
||||||
// --- 4. Main State Machine ---
|
// Check if animations finished
|
||||||
|
|
||||||
// Check if animation finished
|
|
||||||
if (current_state == STATE_ANIMATING) {
|
if (current_state == STATE_ANIMATING) {
|
||||||
animation_timer -= delta_time_sec;
|
if (!anim_tracker.has_active_animations()) {
|
||||||
if (animation_timer <= 0.0f) {
|
|
||||||
current_state = STATE_IDLE;
|
current_state = STATE_IDLE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle user input
|
|
||||||
if (advance_input) {
|
if (advance_input) {
|
||||||
if (text_is_animating(dialogue_text)) {
|
if (text_is_animating(dialogue_text)) {
|
||||||
// Fast-forward text
|
|
||||||
text_show_all_current(dialogue_text);
|
text_show_all_current(dialogue_text);
|
||||||
}
|
}
|
||||||
else if (current_state == STATE_WAITING_FOR_INPUT) {
|
else if (current_state == STATE_WAITING_FOR_INPUT) {
|
||||||
// Advance from a WAIT command
|
|
||||||
current_state = STATE_IDLE;
|
current_state = STATE_IDLE;
|
||||||
}
|
}
|
||||||
else if (current_state == STATE_ANIMATING) {
|
else if (current_state == STATE_ANIMATING) {
|
||||||
// User wants to skip animation
|
// Skip all remaining animations
|
||||||
animation_timer = 0.0f;
|
anim_tracker.clear();
|
||||||
current_state = STATE_IDLE;
|
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
|
// 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) {
|
while (current_state == STATE_IDLE) {
|
||||||
if (!scene.has_next()) {
|
if (!scene.has_next()) {
|
||||||
running = false;
|
running = false;
|
||||||
@@ -596,31 +636,33 @@ int main() {
|
|||||||
|
|
||||||
const Command* cmd = scene.current();
|
const Command* cmd = scene.current();
|
||||||
float duration = executor.execute(cmd);
|
float duration = executor.execute(cmd);
|
||||||
|
|
||||||
|
// NEW: Add animation to tracker
|
||||||
|
if (duration > 0.0f) {
|
||||||
|
anim_tracker.add_animation(duration);
|
||||||
|
}
|
||||||
|
|
||||||
scene.next();
|
scene.next();
|
||||||
|
|
||||||
if (duration > 0.0f) {
|
// NEW: Check blocking flag
|
||||||
// This was an animated command
|
if (cmd->blocking) {
|
||||||
current_state = STATE_ANIMATING;
|
if (cmd->type == CMD_WAIT) {
|
||||||
animation_timer = duration;
|
|
||||||
}
|
|
||||||
else if (cmd->type == CMD_WAIT) {
|
|
||||||
// This was a wait command
|
|
||||||
current_state = STATE_WAITING_FOR_INPUT;
|
current_state = STATE_WAITING_FOR_INPUT;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
// else: Instant command, loop continues to next command
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 5. Render Frame ---
|
|
||||||
const uint32_t* fb = compositor_compose(compositor);
|
const uint32_t* fb = compositor_compose(compositor);
|
||||||
window_blit(window, fb);
|
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 (bg) image_free(bg);
|
||||||
if (textbox) image_free(textbox);
|
if (textbox) image_free(textbox);
|
||||||
text_destroy(name_text);
|
text_destroy(name_text);
|
||||||
|
|||||||
+373
-277
@@ -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 <text.h>
|
#include <text.h>
|
||||||
|
|
||||||
// C/C++ Standard Libraries
|
// C/C++ Standard Libraries
|
||||||
@@ -20,42 +11,102 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <iostream> // Included for std::clamp in smoothstep
|
#include <iostream> // For std::clamp
|
||||||
|
|
||||||
// Embed stb_truetype implementation
|
// Embed stb_truetype implementation
|
||||||
#define STB_TRUETYPE_IMPLEMENTATION
|
#define STB_TRUETYPE_IMPLEMENTATION
|
||||||
#include "stb_truetype.h"
|
#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<const unsigned char*>(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 ---
|
// --- Internal Data Structures ---
|
||||||
|
|
||||||
/**
|
// Holds the raw file data and stbtt_fontinfo for a cached font.
|
||||||
* @brief Holds the raw file data and stbtt_fontinfo for a cached font.
|
|
||||||
*/
|
|
||||||
struct cached_font_t {
|
struct cached_font_t {
|
||||||
std::vector<uint8_t> file_data;
|
std::vector<uint8_t> file_data;
|
||||||
stbtt_fontinfo info;
|
stbtt_fontinfo info;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Uniquely identifies a specific glyph variant for caching.
|
||||||
* @brief Uniquely identifies a specific glyph variant for caching.
|
// Now uses SDF, so the key includes the size and Unicode codepoint.
|
||||||
* Now uses SDF, so the key includes the size.
|
|
||||||
*/
|
|
||||||
struct glyph_key_t {
|
struct glyph_key_t {
|
||||||
const cached_font_t* font;
|
const cached_font_t* font;
|
||||||
char c;
|
uint32_t codepoint;
|
||||||
float size;
|
float size;
|
||||||
|
|
||||||
bool operator<(const glyph_key_t& other) const {
|
bool operator<(const glyph_key_t& other) const {
|
||||||
if (font != other.font) return font < other.font;
|
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;
|
if (size != other.size) return size < other.size;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Holds the rendered SDF bitmap and metrics for a cached glyph.
|
||||||
* @brief Holds the rendered SDF bitmap and metrics for a cached glyph.
|
|
||||||
*/
|
|
||||||
struct cached_glyph_t {
|
struct cached_glyph_t {
|
||||||
std::unique_ptr<uint8_t[]> data; // This now holds SDF data
|
std::unique_ptr<uint8_t[]> data; // This now holds SDF data
|
||||||
int w, h;
|
int w, h;
|
||||||
@@ -64,18 +115,17 @@ struct cached_glyph_t {
|
|||||||
int advance;
|
int advance;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Stores the start and end indices for a precomputed line of text.
|
||||||
* @brief 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 {
|
struct line_break_t {
|
||||||
size_t start_index; // Index of the first char in the line
|
size_t start_byte_index; // byte index of first char in the line
|
||||||
size_t end_index; // Index *after* the last 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
|
float y_offset; // The y-coordinate to start drawing this line
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Holds the parameters for a single `text_push` call.
|
||||||
* @brief Holds the parameters for a single `text_push` call.
|
|
||||||
*/
|
|
||||||
struct text_params_t {
|
struct text_params_t {
|
||||||
std::string text;
|
std::string text;
|
||||||
float size;
|
float size;
|
||||||
@@ -90,18 +140,14 @@ struct text_params_t {
|
|||||||
mutable bool lines_computed = false;
|
mutable bool lines_computed = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// Shared cache structure that can be referenced by multiple text objects.
|
||||||
* @brief Shared cache structure that can be referenced by multiple text objects.
|
|
||||||
*/
|
|
||||||
struct shared_cache_t {
|
struct shared_cache_t {
|
||||||
std::map<std::string, cached_font_t> font_cache;
|
std::map<std::string, cached_font_t> font_cache;
|
||||||
std::map<glyph_key_t, cached_glyph_t> glyph_cache;
|
std::map<glyph_key_t, cached_glyph_t> glyph_cache;
|
||||||
int refcount = 1; // Reference counting for proper cleanup
|
int refcount = 1; // Reference counting for proper cleanup
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
// The main internal context structure for the text renderer.
|
||||||
* @brief The main internal context structure for the text renderer.
|
|
||||||
*/
|
|
||||||
struct text_t {
|
struct text_t {
|
||||||
uint32_t* framebuffer;
|
uint32_t* framebuffer;
|
||||||
int width;
|
int width;
|
||||||
@@ -114,7 +160,7 @@ struct text_t {
|
|||||||
// Rendering State (per-instance)
|
// Rendering State (per-instance)
|
||||||
std::vector<text_params_t> text_queue;
|
std::vector<text_params_t> text_queue;
|
||||||
int queue_index = 0;
|
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
|
float char_progress = 0.0f; // Cumulative progress for animation
|
||||||
|
|
||||||
cached_font_t* current_font = nullptr;
|
cached_font_t* current_font = nullptr;
|
||||||
@@ -125,135 +171,15 @@ struct text_t {
|
|||||||
const float padding_x = 10.0f;
|
const float padding_x = 10.0f;
|
||||||
const float padding_y = 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) {
|
// --- Caching & Font Loading ---
|
||||||
// Use std::clamp (from <iostream> or <algorithm> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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) {
|
static cached_font_t* load_font(text_t* text, const std::string& font_path) {
|
||||||
// Check cache (now in shared_cache_t)
|
// Check cache (now in shared_cache_t)
|
||||||
auto it = text->cache->font_cache.find(font_path);
|
auto it = text->cache->font_cache.find(font_path);
|
||||||
@@ -300,14 +226,12 @@ static cached_font_t* load_font(text_t* text, const std::string& font_path) {
|
|||||||
return &map_it->second;
|
return &map_it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Gets a cached SDF glyph, generating it if it doesn't exist.
|
||||||
* @brief 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)
|
||||||
*/
|
|
||||||
static const cached_glyph_t* get_glyph(text_t* text, char c, float size)
|
|
||||||
{
|
{
|
||||||
if (!text->current_font) return nullptr;
|
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)
|
// Check cache (now in shared_cache_t)
|
||||||
auto it = text->cache->glyph_cache.find(key);
|
auto it = text->cache->glyph_cache.find(key);
|
||||||
@@ -321,21 +245,19 @@ static const cached_glyph_t* get_glyph(text_t* text, char c, float size)
|
|||||||
float scale = stbtt_ScaleForPixelHeight(info, size);
|
float scale = stbtt_ScaleForPixelHeight(info, size);
|
||||||
|
|
||||||
// Get advance metric
|
// Get advance metric
|
||||||
stbtt_GetCodepointHMetrics(info, c, &new_glyph.advance, nullptr);
|
int adv;
|
||||||
new_glyph.advance = static_cast<int>(new_glyph.advance * scale);
|
stbtt_GetCodepointHMetrics(info, (int)codepoint, &adv, nullptr);
|
||||||
|
new_glyph.advance = static_cast<int>(adv * scale);
|
||||||
|
|
||||||
// SDF parameters
|
// SDF parameters
|
||||||
const float padding_percent = 0.15f;
|
const float padding_percent = 0.15f;
|
||||||
// FIX: Changed from 180 to 128 for a balanced distance range
|
const unsigned char onedge_value = 128; // 0-255 range midpoint
|
||||||
const unsigned char onedge_value = 128;
|
const int padding = std::max(1, (int)(size * padding_percent)); // Padding in pixels
|
||||||
// Padding in pixels, at least 1
|
float pixel_dist_scale = (float)onedge_value / (float)padding; // 'distance units' per pixel
|
||||||
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;
|
|
||||||
|
|
||||||
// Generate the SDF bitmap
|
// Generate the SDF bitmap
|
||||||
unsigned char* bitmap = stbtt_GetCodepointSDF(
|
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
|
&new_glyph.w, &new_glyph.h, &new_glyph.xoff, &new_glyph.yoff
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -343,7 +265,8 @@ static const cached_glyph_t* get_glyph(text_t* text, char c, float size)
|
|||||||
// Glyph has no bitmap (e.g., space)
|
// Glyph has no bitmap (e.g., space)
|
||||||
new_glyph.w = new_glyph.h = 0;
|
new_glyph.w = new_glyph.h = 0;
|
||||||
new_glyph.data = nullptr;
|
new_glyph.data = nullptr;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
// Copy the SDF bitmap into our managed cache
|
// Copy the SDF bitmap into our managed cache
|
||||||
new_glyph.data = std::make_unique<uint8_t[]>(new_glyph.w * new_glyph.h);
|
new_glyph.data = std::make_unique<uint8_t[]>(new_glyph.w * new_glyph.h);
|
||||||
memcpy(new_glyph.data.get(), bitmap, new_glyph.w * new_glyph.h);
|
memcpy(new_glyph.data.get(), bitmap, new_glyph.w * new_glyph.h);
|
||||||
@@ -356,11 +279,9 @@ static const cached_glyph_t* get_glyph(text_t* text, char c, float size)
|
|||||||
return &map_it->second;
|
return &map_it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Text Layout ---
|
||||||
|
|
||||||
/**
|
// Precomputes line breaks for a text block.
|
||||||
* @brief Precomputes line breaks for a text block.
|
|
||||||
* This is marked 'static' and defined before it's used in text_nextframe.
|
|
||||||
*/
|
|
||||||
static void precompute_line_breaks(text_t* text, const text_params_t* params) {
|
static void precompute_line_breaks(text_t* text, const text_params_t* params) {
|
||||||
if (params->lines_computed) return;
|
if (params->lines_computed) return;
|
||||||
|
|
||||||
@@ -373,7 +294,8 @@ static void precompute_line_breaks(text_t* text, const text_params_t* params) {
|
|||||||
if (params->font_path.empty()) {
|
if (params->font_path.empty()) {
|
||||||
if (text->cache->font_cache.empty()) return; // No default font
|
if (text->cache->font_cache.empty()) return; // No default font
|
||||||
font = &text->cache->font_cache.begin()->second;
|
font = &text->cache->font_cache.begin()->second;
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
font = load_font(text, params->font_path);
|
font = load_font(text, params->font_path);
|
||||||
}
|
}
|
||||||
if (!font) {
|
if (!font) {
|
||||||
@@ -391,75 +313,224 @@ static void precompute_line_breaks(text_t* text, const text_params_t* params) {
|
|||||||
|
|
||||||
float x = text->padding_x;
|
float x = text->padding_x;
|
||||||
float y = text->current_y; // Start from the current render position
|
float y = text->current_y; // Start from the current render position
|
||||||
size_t line_start_index = 0;
|
size_t line_start_byte = 0;
|
||||||
size_t last_word_break_index = 0; // Index *after* the space
|
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 x_at_last_word_break = text->padding_x;
|
||||||
float max_width = text->width - text->padding_x;
|
float max_width = text->width - text->padding_x;
|
||||||
|
|
||||||
for (size_t i = 0; i < params->text.length(); ++i) {
|
// We'll iterate the UTF-8 string and maintain both byte index and cp index
|
||||||
char c = params->text[i];
|
const std::string& s = params->text;
|
||||||
|
size_t byte_i = 0;
|
||||||
|
size_t cp_index = 0;
|
||||||
|
|
||||||
if (c == '\n') {
|
while (byte_i < s.size()) {
|
||||||
params->lines.push_back({line_start_index, i, y});
|
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;
|
y += line_height;
|
||||||
x = text->padding_x;
|
x = text->padding_x;
|
||||||
line_start_index = i + 1;
|
line_start_byte = byte_i;
|
||||||
last_word_break_index = i + 1;
|
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;
|
x_at_last_word_break = x;
|
||||||
|
++cp_index;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store last word break position
|
// store last word break pos
|
||||||
if (c == ' ' || c == '\t') {
|
if (cp == (uint32_t)' ' || cp == (uint32_t)'\t') {
|
||||||
last_word_break_index = i + 1;
|
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;
|
int advance = glyph ? glyph->advance : 0;
|
||||||
|
|
||||||
// Check for wrap *before* adding the character
|
// check wrap before adding the char
|
||||||
if (x + advance > max_width && line_start_index < i) {
|
if (x + advance > max_width && line_start_byte < prev_byte) {
|
||||||
// --- Time to wrap ---
|
// wrap
|
||||||
if (last_word_break_index > line_start_index) {
|
if (last_word_break_byte > line_start_byte) {
|
||||||
// Wrap at the last word break
|
// wrap at last word break (exclude trailing space)
|
||||||
params->lines.push_back({line_start_index, last_word_break_index - 1, y}); // -1 to exclude space
|
// end_byte = last_word_break_byte - bytes of space? last_word_break_byte already points after the space
|
||||||
i = last_word_break_index - 1; // Re-process from the space
|
// we want to exclude final space visually: line end byte = last_word_break_byte - size_of_space_byte_sequence
|
||||||
line_start_index = last_word_break_index;
|
// But simpler: set end_byte to last_word_break_byte - 1 byte may not be correct for multi-byte,
|
||||||
} else {
|
// so we set end_cp = last_word_break_cp - 1 and then compute end_byte by walking from line_start_byte.
|
||||||
// Force wrap in the middle of a word
|
// But we already tracked last_word_break_byte and last_word_break_cp as byte AFTER the space; so we set
|
||||||
params->lines.push_back({line_start_index, i, y});
|
// 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.
|
||||||
line_start_index = i;
|
params->lines.push_back({ line_start_byte, last_word_break_byte, line_start_cp, last_word_break_cp, y });
|
||||||
i--; // Re-process current char on new line
|
// 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;
|
||||||
|
}
|
||||||
|
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
|
// reset for new line
|
||||||
y += line_height;
|
y += line_height;
|
||||||
x = text->padding_x;
|
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;
|
x_at_last_word_break = x;
|
||||||
} else {
|
continue; // loop will continue, not advancing cp_index here (we set it appropriately)
|
||||||
// No wrap, just advance
|
}
|
||||||
|
else {
|
||||||
|
// no wrap, just advance
|
||||||
x += advance;
|
x += advance;
|
||||||
if (last_word_break_index == i + 1) {
|
++cp_index;
|
||||||
x_at_last_word_break = x; // Update x pos after the space
|
// 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
|
// Add the final line if any text remains
|
||||||
if (line_start_index < params->text.length()) {
|
if (line_start_byte < s.size()) {
|
||||||
params->lines.push_back({line_start_index, params->text.length(), y});
|
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
|
text->last_line_height = line_height; // Store for next block's y_offset
|
||||||
params->lines_computed = true;
|
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" {
|
extern "C" {
|
||||||
|
|
||||||
|
// --- Lifecycle ---
|
||||||
|
|
||||||
text_t* text_init(int width, int height) {
|
text_t* text_init(int width, int height) {
|
||||||
if (width <= 0 || height <= 0) return nullptr;
|
if (width <= 0 || height <= 0) return nullptr;
|
||||||
|
|
||||||
@@ -546,6 +617,9 @@ void text_destroy(text_t* text) {
|
|||||||
delete text;
|
delete text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Text Queue & State ---
|
||||||
|
|
||||||
|
// Adds a block of text to the rendering queue.
|
||||||
void text_push(text_t* text,
|
void text_push(text_t* text,
|
||||||
const char* str,
|
const char* str,
|
||||||
float size,
|
float size,
|
||||||
@@ -566,37 +640,7 @@ void text_push(text_t* text,
|
|||||||
text->text_queue.push_back(std::move(p));
|
text->text_queue.push_back(std::move(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Clears the framebuffer and resets the text queue and all state.
|
||||||
* @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<float>(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) {
|
void text_clear(text_t* text) {
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
memset(text->framebuffer, 0, text->width * text->height * sizeof(uint32_t));
|
memset(text->framebuffer, 0, text->width * text->height * sizeof(uint32_t));
|
||||||
@@ -609,14 +653,28 @@ void text_clear(text_t* text) {
|
|||||||
text->last_line_height = 0.0f;
|
text->last_line_height = 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t* text_getfb(text_t* text) {
|
// Immediately sets the character index to the end of the current text block.
|
||||||
return text ? text->framebuffer : nullptr;
|
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.
|
||||||
bool text_nextframe(text_t* text) {
|
bool text_nextframe(text_t* text) {
|
||||||
if (!text) return true;
|
if (!text) return true;
|
||||||
if (text->queue_index >= text->text_queue.size()) {
|
if (text->queue_index >= text->text_queue.size()) {
|
||||||
return true;
|
return true; // Nothing to do
|
||||||
}
|
}
|
||||||
|
|
||||||
text_params_t* params = &text->text_queue[text->queue_index];
|
text_params_t* params = &text->text_queue[text->queue_index];
|
||||||
@@ -637,6 +695,7 @@ bool text_nextframe(text_t* text) {
|
|||||||
text->current_font = &text->cache->font_cache.begin()->second;
|
text->current_font = &text->cache->font_cache.begin()->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate layout on first frame
|
||||||
precompute_line_breaks(text, params);
|
precompute_line_breaks(text, params);
|
||||||
|
|
||||||
const stbtt_fontinfo* info = &text->current_font->info;
|
const stbtt_fontinfo* info = &text->current_font->info;
|
||||||
@@ -646,27 +705,29 @@ bool text_nextframe(text_t* text) {
|
|||||||
float baseline = ascent * scale;
|
float baseline = ascent * scale;
|
||||||
|
|
||||||
// Update animation progress
|
// Update animation progress
|
||||||
if (params->speed > 0.0f && text->char_index < params->text.length()) {
|
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_progress += params->speed;
|
||||||
text->char_index = static_cast<int>(text->char_progress);
|
text->char_index = static_cast<int>(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());
|
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);
|
||||||
|
|
||||||
// CRITICAL FIX: Only draw NEW characters
|
// Only draw NEW characters
|
||||||
size_t draw_start = text->last_rendered_char;
|
size_t draw_start = text->last_rendered_char; // cp index
|
||||||
size_t draw_end = text->char_index;
|
size_t draw_end = text->char_index; // cp index
|
||||||
|
|
||||||
// Drawing loop - only for NEW characters
|
// Drawing loop - only for NEW characters
|
||||||
for (const auto& line : params->lines) {
|
for (const auto& line : params->lines) {
|
||||||
// Skip lines that are completely rendered
|
// Skip lines that are completely rendered
|
||||||
if (line.end_index <= draw_start) {
|
if (line.end_cp_index <= draw_start) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip lines that haven't started yet
|
// Skip lines that haven't started yet
|
||||||
if (line.start_index >= draw_end) {
|
if (line.start_cp_index >= draw_end) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,24 +735,41 @@ bool text_nextframe(text_t* text) {
|
|||||||
text->current_y = line.y_offset;
|
text->current_y = line.y_offset;
|
||||||
|
|
||||||
// Calculate x position up to where we need to draw
|
// 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) {
|
// Iterate through the line bytes and sum advances for cps before draw_start
|
||||||
char c = params->text[i];
|
size_t byte_i = line.start_byte_index;
|
||||||
if (c == '\n') continue;
|
size_t cp_i = line.start_cp_index;
|
||||||
const cached_glyph_t* glyph = get_glyph(text, c, params->size);
|
|
||||||
|
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) {
|
if (glyph) {
|
||||||
text->current_x += glyph->advance;
|
text->current_x += glyph->advance;
|
||||||
}
|
}
|
||||||
|
++cp_i;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw only NEW characters on this line
|
// Draw only NEW characters on this line
|
||||||
for (size_t i = std::max(line.start_index, draw_start);
|
byte_i = line.start_byte_index;
|
||||||
i < std::min(line.end_index, draw_end); ++i) {
|
cp_i = line.start_cp_index;
|
||||||
|
|
||||||
char c = params->text[i];
|
// advance byte_i to the first drawable codepoint (max(line.start_cp_index, draw_start))
|
||||||
if (c == '\n') continue;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
const cached_glyph_t* glyph = get_glyph(text, c, params->size);
|
// Now iterate and draw until cp reaches min(line.end_cp_index, draw_end)
|
||||||
if (!glyph) continue;
|
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<int>(text->current_x + glyph->xoff);
|
int x_base = static_cast<int>(text->current_x + glyph->xoff);
|
||||||
int y_base = static_cast<int>(text->current_y + baseline + glyph->yoff);
|
int y_base = static_cast<int>(text->current_y + baseline + glyph->yoff);
|
||||||
@@ -702,37 +780,55 @@ bool text_nextframe(text_t* text) {
|
|||||||
draw_glyph(text, x_base, y_base, params->color_main, glyph, params->size, false);
|
draw_glyph(text, x_base, y_base, params->color_main, glyph, params->size, false);
|
||||||
|
|
||||||
text->current_x += glyph->advance;
|
text->current_x += glyph->advance;
|
||||||
|
++cp_i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update tracking
|
// Update tracking
|
||||||
text->last_rendered_char = text->char_index;
|
text->last_rendered_char = (int)text->char_index;
|
||||||
|
|
||||||
// Handle block transition
|
// Handle block transition
|
||||||
if (text->char_index >= params->text.length()) {
|
if (text->char_index >= (int)utf8_count_codepoints(params->text)) {
|
||||||
text->queue_index++;
|
text->queue_index++;
|
||||||
text->char_index = 0;
|
text->char_index = 0;
|
||||||
text->char_progress = 0.0f;
|
text->char_progress = 0.0f;
|
||||||
text->last_rendered_char = 0; // ADD THIS - reset for next block
|
text->last_rendered_char = 0; // Reset for next block
|
||||||
|
text->current_y += text->last_line_height; // Advance y-pos
|
||||||
text->current_y += text->last_line_height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if queue is finished
|
||||||
if (text->queue_index >= text->text_queue.size()) {
|
if (text->queue_index >= text->text_queue.size()) {
|
||||||
text->text_queue.clear();
|
text->text_queue.clear();
|
||||||
text->queue_index = 0;
|
text->queue_index = 0;
|
||||||
text->char_index = 0;
|
text->char_index = 0;
|
||||||
text->char_progress = 0.0f;
|
text->char_progress = 0.0f;
|
||||||
text->last_rendered_char = 0; // ADD THIS
|
text->last_rendered_char = 0;
|
||||||
|
|
||||||
text->current_x = text->padding_x;
|
text->current_x = text->padding_x;
|
||||||
text->current_y = text->padding_y;
|
text->current_y = text->padding_y;
|
||||||
text->last_line_height = 0.0f;
|
text->last_line_height = 0.0f;
|
||||||
|
|
||||||
return true;
|
return true; // Finished
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
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"
|
} // extern "C"
|
||||||
@@ -97,6 +97,7 @@
|
|||||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||||
<EnableEnhancedInstructionSet>AdvancedVectorExtensions2</EnableEnhancedInstructionSet>
|
<EnableEnhancedInstructionSet>AdvancedVectorExtensions2</EnableEnhancedInstructionSet>
|
||||||
|
<AdditionalOptions>/Zc:char8_t- %(AdditionalOptions)</AdditionalOptions>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<SubSystem>Console</SubSystem>
|
<SubSystem>Console</SubSystem>
|
||||||
@@ -118,6 +119,7 @@
|
|||||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||||
<EnableEnhancedInstructionSet>AdvancedVectorExtensions2</EnableEnhancedInstructionSet>
|
<EnableEnhancedInstructionSet>AdvancedVectorExtensions2</EnableEnhancedInstructionSet>
|
||||||
|
<AdditionalOptions>/Zc:char8_t- %(AdditionalOptions)</AdditionalOptions>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<SubSystem>Console</SubSystem>
|
<SubSystem>Console</SubSystem>
|
||||||
@@ -137,6 +139,7 @@
|
|||||||
<AdditionalIncludeDirectories>include</AdditionalIncludeDirectories>
|
<AdditionalIncludeDirectories>include</AdditionalIncludeDirectories>
|
||||||
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
<MultiProcessorCompilation>true</MultiProcessorCompilation>
|
||||||
<EnableEnhancedInstructionSet>AdvancedVectorExtensions2</EnableEnhancedInstructionSet>
|
<EnableEnhancedInstructionSet>AdvancedVectorExtensions2</EnableEnhancedInstructionSet>
|
||||||
|
<AdditionalOptions>/Zc:char8_t- %(AdditionalOptions)</AdditionalOptions>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<SubSystem>Console</SubSystem>
|
<SubSystem>Console</SubSystem>
|
||||||
@@ -158,6 +161,7 @@
|
|||||||
<LanguageStandard>stdcpp20</LanguageStandard>
|
<LanguageStandard>stdcpp20</LanguageStandard>
|
||||||
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
<RuntimeLibrary>MultiThreadedDebug</RuntimeLibrary>
|
||||||
<EnableEnhancedInstructionSet>AdvancedVectorExtensions2</EnableEnhancedInstructionSet>
|
<EnableEnhancedInstructionSet>AdvancedVectorExtensions2</EnableEnhancedInstructionSet>
|
||||||
|
<AdditionalOptions>/Zc:char8_t- %(AdditionalOptions)</AdditionalOptions>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
<Link>
|
<Link>
|
||||||
<SubSystem>Console</SubSystem>
|
<SubSystem>Console</SubSystem>
|
||||||
|
|||||||
Reference in new issue
Block a user