#include "core/rendering/render_info.hpp" #include #include #include #include #include #include "core/rendering/hud_constants.hpp" #include "core/rendering/screen.hpp" #include "core/rendering/text.hpp" #include "core/resources/resource_cache.hpp" #include "utils/defines.hpp" #include "utils/easing.hpp" namespace { constexpr int TEXT_SIZE = Rendering::Hud::FONT_SIZE_PX; constexpr int PADDING_V = (RenderInfo::HEIGHT - TEXT_SIZE) / 2; // Llindar per saltar el dibuixat d'un segment quasi totalment transparent. // Evita una fileta d'un sol píxel quan acaba de començar a aparèixer. constexpr float VISIBLE_EPSILON = 0.01F; // Desplaçament en píxels de la ombra respecte del text principal. constexpr int SHADOW_OFFSET_PX = 1; // Índex de cada segment dins de segments_. Si canvia SEGMENT_COUNT o // l'ordre, actualitzar ací. constexpr int FPS_SEGMENT = 0; constexpr int DRIVER_SEGMENT = 1; constexpr int SHADER_SEGMENT = 2; constexpr int TIME_SEGMENT = 3; auto prettify(std::string s) -> std::string { std::ranges::replace(s, '-', ' '); std::ranges::transform(s, s.begin(), [](unsigned char c) { return std::tolower(c); }); return s; } auto formatTime(int elapsed_ms) -> std::string { const int TOTAL_CS = elapsed_ms / 10; const int MIN = TOTAL_CS / (60 * 100); const int SEC = (TOTAL_CS / 100) % 60; const int CENTI = TOTAL_CS % 100; std::array buf{}; std::snprintf(buf.data(), buf.size(), "%d:%02d.%02d", MIN, SEC, CENTI); return std::string{buf.data()}; } } // namespace auto RenderInfo::get() -> RenderInfo& { static RenderInfo instance_; return instance_; } RenderInfo::~RenderInfo() = default; void RenderInfo::init(const Frame& initial) { auto font_surface = Resource::Cache::get().getSurface("8bithud.gif"); text_ = std::make_unique(font_surface, "data/font/8bithud.fnt"); current_ = initial; // Restaura l'estat guardat: si l'overlay estava engegat, l'activem ja // plenament visible (sense animació d'entrada inicial). refreshSegments // és qui marca mono_digits al segment de temps; si no s'engega ara, // la primera crida a update() també ho farà abans de qualsevol render. if (initial.target_position != Defaults::RenderInfoPosition::OFF) { visible_pos_ = initial.target_position; info_anim_ = 1.0F; refreshSegments(); for (auto& seg : segments_) { seg.anim = seg.visible ? 1.0F : 0.0F; } } } void RenderInfo::update(int delta_ms, const Frame& frame) { current_ = frame; elapsed_ms_ += delta_ms; const float DT = static_cast(delta_ms) / 1000.0F; // Transició global de posició. Si la posició desitjada coincideix amb la // visible, entrem (anim→1). Si no, primer eixim (anim→0) del lloc actual; // quan arriba a 0 commutem visible_pos_ i deixem que el proper frame // dispari l'entrada. Excepció: de OFF a TOP/BOTTOM commutem en sec. const auto DESIRED = current_.target_position; if (DESIRED == visible_pos_) { // Si el destí és OFF, no cal animar cap a un lloc — la barra ja ha // d'estar fora. Sense aquest guard, en arribar de BOTTOM→OFF la // anim tornaria a pujar cap a 1 i el render (branca BOTTOM per // defecte) la tornaria a pintar abaix. if (visible_pos_ == Defaults::RenderInfoPosition::OFF) { info_anim_ = 0.0F; } else if (info_anim_ < 1.0F) { info_anim_ = std::min(1.0F, info_anim_ + (SLIDE_SPEED * DT)); } } else if (visible_pos_ == Defaults::RenderInfoPosition::OFF) { visible_pos_ = DESIRED; info_anim_ = 0.0F; } else { info_anim_ -= SLIDE_SPEED * DT; if (info_anim_ <= 0.0F) { info_anim_ = 0.0F; visible_pos_ = DESIRED; } } // Decidim què ha d'eixir cada segment i fem tendir la seua anim. refreshSegments(); for (auto& seg : segments_) { const float TARGET = seg.visible ? 1.0F : 0.0F; if (seg.anim < TARGET) { seg.anim = std::min(TARGET, seg.anim + (SEG_SPEED * DT)); } else if (seg.anim > TARGET) { seg.anim = std::max(TARGET, seg.anim - (SEG_SPEED * DT)); } } } void RenderInfo::refreshSegments() { // FPS (sempre visible mentre l'overlay apareix) segments_[FPS_SEGMENT].text = std::to_string(Screen::get().getLastFPS()) + " fps"; segments_[FPS_SEGMENT].visible = true; segments_[FPS_SEGMENT].mono_digits = false; // Driver GPU (sempre visible) const auto& driver = Screen::get().getGPUDriver(); segments_[DRIVER_SEGMENT].text = std::string(" | ") + (driver.empty() ? std::string("sdl") : driver); segments_[DRIVER_SEGMENT].visible = true; segments_[DRIVER_SEGMENT].mono_digits = false; // Shader + preset (només amb shader activat). El caller ha triat el // preset actiu segons el shader; ací només el formatem. if (current_.shader_enabled) { segments_[SHADER_SEGMENT].text = " | " + current_.shader_name + " " + prettify(current_.shader_preset); } segments_[SHADER_SEGMENT].visible = current_.shader_enabled; segments_[SHADER_SEGMENT].mono_digits = false; // Temps de joc (togglejable i persistent) segments_[TIME_SEGMENT].text = " | " + formatTime(elapsed_ms_); segments_[TIME_SEGMENT].visible = current_.show_time; segments_[TIME_SEGMENT].mono_digits = true; // Els tres primers segments baixem a minúscules per coherència estètica // amb la resta de la línia; el de temps manté dígits i puntuació. segments_[FPS_SEGMENT].text = prettify(segments_[FPS_SEGMENT].text); segments_[DRIVER_SEGMENT].text = prettify(segments_[DRIVER_SEGMENT].text); segments_[SHADER_SEGMENT].text = prettify(segments_[SHADER_SEGMENT].text); } auto RenderInfo::segmentWidth(const Segment& seg, int digit_cell_w) const -> int { if (text_ == nullptr || seg.text.empty()) { return 0; } return seg.mono_digits ? text_->lengthMonoDigits(seg.text, digit_cell_w) : text_->length(seg.text); } void RenderInfo::render() { if (text_ == nullptr) { return; } if (visible_pos_ == Defaults::RenderInfoPosition::OFF && info_anim_ <= 0.0F) { return; } const int DIGIT_CELL = text_->getCharacterSize() - 1; // Amplada total considerant animació individual: cada segment contribueix // menys quan està apareixent o desapareixent, per a que el conjunt // s'expandeixca/es contraga suaument. float total_w = 0.0F; for (const auto& seg : segments_) { if (seg.anim <= 0.0F || seg.text.empty()) { continue; } const int W = segmentWidth(seg, DIGIT_CELL); total_w += static_cast(W) * Easing::outQuad(seg.anim); } if (total_w <= 0.0F) { return; } // Y final/inicial segons posició. El lliscament va de (fora) a (dins) // interpolat amb outQuad de la anim global. const int CH = TEXT_SIZE; int final_y = 0; int start_y = 0; if (visible_pos_ == Defaults::RenderInfoPosition::TOP) { final_y = PADDING_V; start_y = -CH - 1; } else { final_y = GameCanvas::HEIGHT - CH - PADDING_V - 1; start_y = GameCanvas::HEIGHT; } const int Y = start_y + static_cast(static_cast(final_y - start_y) * Easing::outQuad(info_anim_)); // Centrat horitzontal sobre l'amplada animada. float cur_x = (static_cast(GameCanvas::WIDTH) - total_w) / 2.0F; for (const auto& seg : segments_) { if (seg.anim <= VISIBLE_EPSILON || seg.text.empty()) { continue; } const int XI = static_cast(cur_x); const int SEG_W = segmentWidth(seg, DIGIT_CELL); if (seg.mono_digits) { text_->writeMonoDigits(XI + SHADOW_OFFSET_PX, Y + SHADOW_OFFSET_PX, seg.text, current_.shadow_color, DIGIT_CELL); text_->writeMonoDigits(XI, Y, seg.text, current_.text_color, DIGIT_CELL); } else { text_->writeColored(XI + SHADOW_OFFSET_PX, Y + SHADOW_OFFSET_PX, seg.text, current_.shadow_color); text_->writeColored(XI, Y, seg.text, current_.text_color); } cur_x += static_cast(SEG_W) * Easing::outQuad(seg.anim); } }