diff --git a/include/text.h b/include/text.h index 058750f..a45cfda 100644 --- a/include/text.h +++ b/include/text.h @@ -21,7 +21,7 @@ void text_push(text_t* text, void text_show_all_current(text_t* text); bool text_is_animating(text_t* text); // advance, call every frame, returns false when done -bool text_nextframe(text_t* text); +bool text_nextframe(text_t* text, float deltatime); uint32_t* text_getfb(text_t* text); void text_clear(text_t* text); } diff --git a/src/main.cpp b/src/main.cpp index 5a5feef..8e3d213 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,7 +4,7 @@ #include #include #include -#include // <-- NEW: For tracking multiple animations +#include #include #include #include @@ -22,6 +22,7 @@ enum CommandType { CMD_SET_Z, CMD_SET_SCALE, CMD_SPEAK, + CMD_CLEAR, CMD_WAIT, CMD_BACKGROUND, }; @@ -33,23 +34,74 @@ enum TransitionType { TRANS_SLIDE_RIGHT, }; -struct Command { - CommandType type; - const char* character_name; - - int x, y; - int z; - float scale; - - const char* sprite_name; - - const char* speaker; - const char* text; - +struct CmdShow { + const char* character; + const char* sprite; + int x, y, z; TransitionType transition; float duration_ms; +}; - bool blocking; // NEW: If true, wait for this animation to complete before next command +struct CmdHide { + const char* character; + TransitionType transition; + float duration_ms; +}; + +struct CmdMove { + const char* character; + int x, y; + TransitionType transition; + float duration_ms; +}; + +struct CmdChangeSprite { + const char* character; + const char* sprite; +}; + +struct CmdSetZ { + const char* character; + int z; +}; + +struct CmdSetScale { + const char* character; + float scale; + TransitionType transition; + float duration_ms; +}; + +struct CmdSpeak { + const char* speaker; + const char* text; + float text_speed; +}; + +struct CmdWait {}; +struct CmdClear {}; +struct CmdBackground { + const char* filename; + TransitionType transition; + float duration_ms; +}; + +struct Command { + CommandType type; + bool blocking; + + union { + CmdShow show; + CmdHide hide; + CmdMove move; + CmdChangeSprite change_sprite; + CmdSetZ set_z; + CmdSetScale set_scale; + CmdSpeak speak; + CmdWait wait; + CmdClear clear; + CmdBackground background; + }; }; struct Scene { @@ -364,47 +416,50 @@ public: 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; + char_manager->show_character(cmd->show.character, cmd->show.x, cmd->show.y, + cmd->show.z, cmd->show.sprite, + cmd->show.transition, cmd->show.duration_ms); + return cmd->show.duration_ms / 1000.0f; case CMD_HIDE: - char_manager->hide_character(cmd->character_name, - cmd->transition, cmd->duration_ms); - return cmd->duration_ms / 1000.0f; + char_manager->hide_character(cmd->hide.character, + cmd->hide.transition, cmd->hide.duration_ms); + return cmd->hide.duration_ms / 1000.0f; case CMD_MOVE: - char_manager->move_character(cmd->character_name, cmd->x, cmd->y, - cmd->transition, cmd->duration_ms); - return cmd->duration_ms / 1000.0f; + char_manager->move_character(cmd->move.character, cmd->move.x, cmd->move.y, + cmd->move.transition, cmd->move.duration_ms); + return cmd->move.duration_ms / 1000.0f; case CMD_CHANGE_SPRITE: - char_manager->change_sprite(cmd->character_name, cmd->sprite_name); + char_manager->change_sprite(cmd->change_sprite.character, cmd->change_sprite.sprite); break; case CMD_SET_Z: - char_manager->set_z(cmd->character_name, cmd->z); + char_manager->set_z(cmd->set_z.character, cmd->set_z.z); 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; + char_manager->set_scale(cmd->set_scale.character, cmd->set_scale.scale, + cmd->set_scale.transition, cmd->set_scale.duration_ms); + return cmd->set_scale.duration_ms / 1000.0f; case CMD_SPEAK: - text_clear(name_text); - text_clear(dialogue_text); - if (cmd->speaker) { - text_push(name_text, cmd->speaker, 34.0f, 0.0f, "assets/Vera.ttf", true); + if (cmd->speak.speaker) { + text_push(name_text, cmd->speak.speaker, 34.0f, 0.0f, "assets/Vera.ttf", true); } - if (cmd->text) { - text_push(dialogue_text, cmd->text, 24.0f, 2.0f, "assets/Vera.ttf", true); + if (cmd->speak.text) { + text_push(dialogue_text, cmd->speak.text, 24.0f, cmd->speak.text_speed, "assets/Vera.ttf", true); } compositor_mark_content_dirty(compositor, name_id); compositor_mark_content_dirty(compositor, dialogue_id); break; + case CMD_CLEAR: + text_clear(name_text); + text_clear(dialogue_text); + break; + case CMD_WAIT: case CMD_BACKGROUND: break; @@ -477,71 +532,29 @@ int main() { // --- Scene Script --- Command scene_commands[] = { - {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}, + {.type = CMD_SHOW, .blocking = false, .show = {.character = "maksw", .sprite = "neutral", .x = 100, .y = POS_Y, .z = 2, .transition = TRANS_SLIDE_LEFT, .duration_ms = 500.0f } }, + {.type = CMD_SHOW, .blocking = true, .show = {.character = "wikor", .sprite = "neutral", .x = 800, .y = POS_Y, .z = 1, .transition = TRANS_SLIDE_RIGHT, .duration_ms = 500.0f } }, - // Maksw starts talking - {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw", - "Wow, it's been a really long week, hasn't it? The sky looks different today.", TRANS_NONE, 0, true}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, + {.type = CMD_SPEAK, .blocking = true, .speak = {.speaker = "Maksw", .text = "Wow, it's been a really long week, hasn't it? The sky looks different today.", .text_speed = 30.0f } }, + {.type = CMD_SPEAK, .blocking = true, .speak = {.speaker = "", .text = "\nline 2", .text_speed = 0.0f } }, + {.type = CMD_WAIT, .blocking = true, .wait = {} }, + {.type = CMD_CLEAR, .blocking = true, .clear = {} }, - {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw", - "I mean, just look at how the clouds are stretching all the way to the horizon. Feels almost cinematic.", TRANS_NONE, 0, true}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, + {.type = CMD_SPEAK, .blocking = true, .speak = {.speaker = "Maksw", .text = "I mean, just look at how the clouds are stretching all the way to the horizon. Feels almost cinematic.", .text_speed = 30.0f } }, + {.type = CMD_WAIT, .blocking = true, .wait = {} }, + {.type = CMD_CLEAR, .blocking = true, .clear = {} }, - {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw", - "And the air, it smells like rain... or maybe just damp leaves? Either way, kinda refreshing.", TRANS_NONE, 0, true}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, + {.type = CMD_SET_SCALE, .blocking = false, .set_scale = {.character = "maksw", .scale = 1.0f, .transition = TRANS_FADE, .duration_ms = 200.0f } }, + {.type = CMD_SET_Z, .blocking = false, .set_z = {.character = "maksw", .z = 1 } }, + {.type = CMD_SET_Z, .blocking = false, .set_z = {.character = "wikor", .z = 3 } }, + {.type = CMD_SET_SCALE, .blocking = false, .set_scale = {.character = "wikor", .scale = 1.05f, .transition = TRANS_FADE, .duration_ms = 200.0f } }, - // Wikor responds - {CMD_SET_SCALE, "maksw", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 200.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", - "I see what you mean. It's almost like the clouds are in slow motion today. Peaceful, but a bit eerie too.", TRANS_NONE, 0, true}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, + {.type = CMD_SPEAK, .blocking = true, .speak = {.speaker = u8"admirał wiktorov", .text = "I see what you mean. It's almost like the clouds are in slow motion today. Peaceful, but a bit eerie too.", .text_speed = 30.0f } }, + {.type = CMD_WAIT, .blocking = true, .wait = {} }, + {.type = CMD_CLEAR, .blocking = true, .clear = {} }, - {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, u8"admirał wiktorov", - "I almost feel like grabbing a cup of tea and just watching the sky for hours. But I have a feeling that won't happen.", TRANS_NONE, 0, true}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, - - {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, u8"admirał wiktorov", - "Also, did you notice that weird bird earlier? It looked like it was practicing aerobatics or something.", TRANS_NONE, 0, true}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, - - // Maksw continues - {CMD_SET_SCALE, "wikor", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 200.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", - "Yeah, that bird was definitely showing off. Maybe it's training for some secret bird Olympics.", TRANS_NONE, 0, true}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, - - {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw", - "Honestly, between the weather, the clouds, and the bird, it feels like the whole world hit the pause button for a moment.", TRANS_NONE, 0, true}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, - - {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw", - "But then reality hits. Bugs to fix, emails to answer, the usual chaos. Can't just float in a dream forever.", TRANS_NONE, 0, true}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, - - {CMD_SPEAK, nullptr, 0, 0, 0, 0, nullptr, "Maksw", - "Still, I like moments like this. Makes me appreciate the little things more. You know?", TRANS_NONE, 0, true}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, - - // Wikor closes it - {CMD_SET_SCALE, "maksw", 0, 0, 0, 1.0f, nullptr, nullptr, nullptr, TRANS_FADE, 200.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", - "Absolutely. Alright, back to work. But next time the sky looks like this, we should take a proper break.", TRANS_NONE, 0, true}, - {CMD_WAIT, nullptr, 0, 0, 0, 0, nullptr, nullptr, nullptr, TRANS_NONE, 0, true}, - - {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}, + {.type = CMD_HIDE, .blocking = false, .hide = {.character = "maksw", .transition = TRANS_FADE, .duration_ms = 500.0f } }, + {.type = CMD_HIDE, .blocking = true, .hide = {.character = "wikor", .transition = TRANS_FADE, .duration_ms = 500.0f } }, }; Scene scene = { scene_commands, sizeof(scene_commands) / sizeof(Command), 0 }; @@ -551,7 +564,7 @@ int main() { bool running = true; AppState current_state = STATE_IDLE; - AnimationTracker anim_tracker; // NEW: Track multiple animations + AnimationTracker anim_tracker; // Track multiple animations auto last_frame_time = std::chrono::high_resolution_clock::now(); @@ -580,13 +593,6 @@ int main() { frame_time_sum = 0.0f; } - compositor_tick(compositor, delta_time_sec); - - text_nextframe(name_text); - text_nextframe(dialogue_text); - compositor_mark_content_dirty(compositor, name_id); - compositor_mark_content_dirty(compositor, dialogue_id); - // Update animation tracker anim_tracker.update(delta_time_sec); @@ -640,7 +646,7 @@ int main() { scene.next(); - // NEW: Check blocking flag + // Check blocking flag if (cmd->blocking) { if (cmd->type == CMD_WAIT) { current_state = STATE_WAITING_FOR_INPUT; @@ -655,6 +661,13 @@ int main() { // If non-blocking, loop continues immediately to next command } + compositor_tick(compositor, delta_time_sec); + + text_nextframe(name_text, delta_time_sec); + text_nextframe(dialogue_text, delta_time_sec); + compositor_mark_content_dirty(compositor, name_id); + compositor_mark_content_dirty(compositor, dialogue_id); + const uint32_t* fb = compositor_compose(compositor); window_blit(window, fb); } diff --git a/src/text.cpp b/src/text.cpp index 8156c3e..ce2d5b3 100644 --- a/src/text.cpp +++ b/src/text.cpp @@ -670,8 +670,9 @@ extern "C" { // --- Update & Animation --- - // Advances the text animation and renders new characters. - bool text_nextframe(text_t* text) { +// Advances the text animation and renders new characters. +// deltatime is the time elapsed since the last frame (in seconds). + bool text_nextframe(text_t* text, float deltatime) { if (!text) return true; if (text->queue_index >= text->text_queue.size()) { return true; // Nothing to do @@ -704,94 +705,112 @@ extern "C" { stbtt_GetFontVMetrics(info, &ascent, &descent, &line_gap); float baseline = ascent * scale; - // Update animation progress + // --- Update animation progress using deltatime --- size_t total_cp = utf8_count_codepoints(params->text); if (params->speed > 0.0f && text->char_index < (int)total_cp) { - text->char_progress += params->speed; - text->char_index = static_cast(text->char_progress); + // The animation speed (params->speed) is now characters per second. + // Multiply by deltatime to get the progress to advance this frame. + text->char_progress += params->speed * deltatime; + + // Only update char_index if it has advanced past the current index. + // This prevents the animation from advancing multiple characters in a single frame + // if the deltatime is large, but still allows continuous float progress. + int new_char_index = static_cast(text->char_progress); + + // We only want to draw characters *up to* the new index, but we + // still need to check if the new index is greater than the last frame's index. + if (new_char_index > text->char_index) { + text->char_index = new_char_index; + } } else if (params->speed <= 0.0f) { text->char_index = (int)total_cp; } + // Cap char_index to the total number of codepoints text->char_index = (int)std::min((size_t)text->char_index, total_cp); // Only draw NEW characters size_t draw_start = text->last_rendered_char; // cp index - size_t draw_end = text->char_index; // cp index + 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) { - uint32_t cp = utf8_next(params->text, byte_i); - const cached_glyph_t* glyph = get_glyph(text, cp, params->size); - if (glyph) { - text->current_x += glyph->advance; - } - ++cp_i; - } - - // Draw only NEW characters on this line - byte_i = line.start_byte_index; - cp_i = line.start_cp_index; - - // advance byte_i to the first drawable codepoint (max(line.start_cp_index, draw_start)) - while (byte_i < line.end_byte_index && cp_i < std::max(line.start_cp_index, draw_start)) { - utf8_next(params->text, byte_i); - ++cp_i; - } - - // Now iterate and draw until cp reaches min(line.end_cp_index, draw_end) - while (byte_i < line.end_byte_index && cp_i < std::min(line.end_cp_index, draw_end)) { - uint32_t cp = utf8_next(params->text, byte_i); - if (cp == (uint32_t)'\n') { ++cp_i; continue; } - - const cached_glyph_t* glyph = get_glyph(text, cp, params->size); - if (!glyph) { - ++cp_i; + // Check if any new characters should be drawn + if (draw_start < draw_end) { + // Drawing loop - only for NEW characters + for (const auto& line : params->lines) { + // Skip lines that are completely rendered + if (line.end_cp_index <= draw_start) { continue; } - 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); + // Skip lines that haven't started yet + if (line.start_cp_index >= draw_end) { + break; } - draw_glyph(text, x_base, y_base, params->color_main, glyph, params->size, false); - text->current_x += glyph->advance; - ++cp_i; + text->current_x = text->padding_x; + text->current_y = line.y_offset; + + // Calculate x position up to where we need to draw + // Iterate through the line bytes and sum advances for cps before draw_start + size_t byte_i = line.start_byte_index; + size_t cp_i = line.start_cp_index; + + while (byte_i < line.end_byte_index && cp_i < draw_start) { + uint32_t cp = utf8_next(params->text, byte_i); + const cached_glyph_t* glyph = get_glyph(text, cp, params->size); + if (glyph) { + text->current_x += glyph->advance; + } + ++cp_i; + } + + // Draw only NEW characters on this line + byte_i = line.start_byte_index; + cp_i = line.start_cp_index; + + // advance byte_i to the first drawable codepoint (max(line.start_cp_index, draw_start)) + while (byte_i < line.end_byte_index && cp_i < std::max(line.start_cp_index, draw_start)) { + utf8_next(params->text, byte_i); + ++cp_i; + } + + // Now iterate and draw until cp reaches min(line.end_cp_index, draw_end) + while (byte_i < line.end_byte_index && cp_i < std::min(line.end_cp_index, draw_end)) { + uint32_t cp = utf8_next(params->text, byte_i); + if (cp == (uint32_t)'\n') { ++cp_i; continue; } + + const cached_glyph_t* glyph = get_glyph(text, cp, params->size); + if (!glyph) { + ++cp_i; + continue; + } + + int x_base = static_cast(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 only if we drew something new + text->last_rendered_char = (int)text->char_index; } - // Update tracking - text->last_rendered_char = (int)text->char_index; - // Handle block transition - if (text->char_index >= (int)utf8_count_codepoints(params->text)) { + if (text->char_index >= (int)total_cp) { text->queue_index++; text->char_index = 0; text->char_progress = 0.0f; text->last_rendered_char = 0; // Reset for next block - text->current_y += text->last_line_height; // Advance y-pos + // Only advance Y if there was a previous block rendered (to prevent double-spacing on first block) + if (text->queue_index > 1) { + text->current_y += text->last_line_height; // Advance y-pos + } } // Check if queue is finished