diff --git a/CMakeLists.txt b/CMakeLists.txt index 520bd27..975433c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,6 +51,7 @@ set(APP_SOURCES source/game/sprite.cpp # Utils + source/utils/easing.cpp source/utils/utils.cpp # Main diff --git a/source/core/rendering/menu.cpp b/source/core/rendering/menu.cpp index f447d30..f6b7925 100644 --- a/source/core/rendering/menu.cpp +++ b/source/core/rendering/menu.cpp @@ -10,6 +10,7 @@ #include "core/rendering/screen.hpp" #include "core/rendering/text.hpp" #include "game/options.hpp" +#include "utils/easing.hpp" namespace Menu { @@ -121,6 +122,11 @@ namespace Menu { } return std::string("OFF"); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr}); + p.items.push_back({"HORA", ItemKind::Toggle, + [] { return onOff(Options::render_info.show_time); }, + [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, + nullptr}); + return p; } @@ -340,9 +346,7 @@ namespace Menu { const Page& page = stack_.back(); const int target_h = boxHeight(page); - // Ease-out quadratic: f(t) = 1 - (1-t)^2 - float t = open_anim_; - float eased = 1.0F - (1.0F - t) * (1.0F - t); + float eased = Easing::outQuad(open_anim_); // Caixa creix verticalment des del centre int box_h = static_cast(target_h * eased); diff --git a/source/core/rendering/overlay.cpp b/source/core/rendering/overlay.cpp index 187b570..36ff3bb 100644 --- a/source/core/rendering/overlay.cpp +++ b/source/core/rendering/overlay.cpp @@ -9,6 +9,7 @@ #include "core/rendering/text.hpp" #include "core/system/director.hpp" #include "game/options.hpp" +#include "utils/easing.hpp" namespace Overlay { @@ -50,7 +51,19 @@ namespace Overlay { static Uint32 last_ticks_ = 0; // --- Render info --- - static std::string render_info_text_; + static Options::RenderInfoPosition info_visible_pos_ = Options::RenderInfoPosition::OFF; + static float info_anim_ = 0.0F; // 0 = fora de pantalla, 1 = posició final + static constexpr float INFO_SLIDE_SPEED = 5.0F; + + // Segments del render info — cadascú amb la seva pròpia visibilitat animada + static constexpr int INFO_SEGMENT_COUNT = 4; + static constexpr float SEG_SPEED = 6.0F; // ~165 ms per aparèixer/desaparèixer + struct InfoSegment { + std::string text; + float anim{0.0F}; + bool visible{false}; + }; + static InfoSegment info_segments_[INFO_SEGMENT_COUNT]; // --- Doble ESC per a eixir --- static bool esc_waiting_ = false; @@ -129,17 +142,79 @@ namespace Overlay { font_->draw(pixel_data, box_x + NOTIF_PADDING_H, box_y + NOTIF_PADDING_V, notif.message.c_str(), NOTIF_TEXT_COLOR); } - // Render info (FPS, driver, shader) — centrat, posició configurable - if (Options::render_info.position != Options::RenderInfoPosition::OFF && !render_info_text_.empty()) { - int info_w = font_->width(render_info_text_.c_str()); - int info_x = (SCREEN_W - info_w) / 2; - int info_y = (Options::render_info.position == Options::RenderInfoPosition::TOP) - ? 1 - : SCREEN_H - font_->charHeight() - 1; - // Ombra (1px desplaçat) - font_->draw(pixel_data, info_x + 1, info_y + 1, render_info_text_.c_str(), Options::render_info.shadow_color); - // Text - font_->draw(pixel_data, info_x, info_y, render_info_text_.c_str(), Options::render_info.text_color); + // Render info (FPS, driver, shader) — animat amb slide vertical + // State machine: visible_pos s'actualitza cap a desired quan anim arriba a 0 + { + const auto desired = Options::render_info.position; + if (desired == info_visible_pos_) { + // Mateix lloc: entra fins a 1 + if (info_anim_ < 1.0F) { + info_anim_ += INFO_SLIDE_SPEED * dt; + if (info_anim_ > 1.0F) info_anim_ = 1.0F; + } + } else { + // Canvi: si visible_pos està OFF, commuta directament + if (info_visible_pos_ == Options::RenderInfoPosition::OFF) { + info_visible_pos_ = desired; + info_anim_ = 0.0F; + } else { + // Ix del lloc actual + info_anim_ -= INFO_SLIDE_SPEED * dt; + if (info_anim_ <= 0.0F) { + info_anim_ = 0.0F; + info_visible_pos_ = desired; + } + } + } + + // Actualitza animacions individuals dels segments + for (auto& seg : info_segments_) { + float target = seg.visible ? 1.0F : 0.0F; + if (seg.anim < target) { + seg.anim += SEG_SPEED * dt; + if (seg.anim > target) seg.anim = target; + } else if (seg.anim > target) { + seg.anim -= SEG_SPEED * dt; + if (seg.anim < target) seg.anim = target; + } + } + + // Render si hi ha alguna cosa visible + if (info_visible_pos_ != Options::RenderInfoPosition::OFF && info_anim_ > 0.0F) { + // Calcula amplada total interpolant cada segment per la seva anim + float total_w = 0.0F; + for (auto& seg : info_segments_) { + if (seg.anim > 0.0F && !seg.text.empty()) { + total_w += font_->width(seg.text.c_str()) * Easing::outQuad(seg.anim); + } + } + if (total_w > 0.0F) { + // Slide vertical (ease-out quadratic) — igual que abans + float eased_y = Easing::outQuad(info_anim_); + int ch = font_->charHeight(); + int final_y; + int start_y; + if (info_visible_pos_ == Options::RenderInfoPosition::TOP) { + final_y = 1; + start_y = -ch - 1; + } else { + final_y = SCREEN_H - ch - 1; + start_y = SCREEN_H; + } + int info_y = start_y + static_cast((final_y - start_y) * eased_y); + + // Dibuixa cada segment en la seva posició x acumulada + float cur_x = (SCREEN_W - total_w) / 2.0F; + for (auto& seg : info_segments_) { + if (seg.anim > 0.01F && !seg.text.empty()) { + int xi = static_cast(cur_x); + font_->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color); + font_->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color); + cur_x += font_->width(seg.text.c_str()) * Easing::outQuad(seg.anim); + } + } + } + } } // Elimina les acabades @@ -192,8 +267,16 @@ namespace Overlay { Options::render_info.position = static_cast(pos); } - void setRenderInfoText(const char* text) { - render_info_text_ = text; + void setRenderInfoSegments(const char* s0, const char* s1, const char* s2, const char* s3) { + const char* segs[INFO_SEGMENT_COUNT] = {s0, s1, s2, s3}; + for (int i = 0; i < INFO_SEGMENT_COUNT; i++) { + if (segs[i] != nullptr && *segs[i] != '\0') { + info_segments_[i].text = segs[i]; + info_segments_[i].visible = true; + } else { + info_segments_[i].visible = false; + } + } } auto isEscConsumed() -> bool { diff --git a/source/core/rendering/overlay.hpp b/source/core/rendering/overlay.hpp index 3d01fae..e407208 100644 --- a/source/core/rendering/overlay.hpp +++ b/source/core/rendering/overlay.hpp @@ -15,7 +15,9 @@ namespace Overlay { // Activa/desactiva la info de renderitzat (FPS, driver, shader, preset) void toggleRenderInfo(); void cycleRenderInfo(int dir); // dir=+1 avant, -1 endarrere - void setRenderInfoText(const char* text); + // Configura els segments del render info. Cada segment (nullptr o string buit + // per amagar) apareix/desapareix amb animació; el conjunt es centra dinàmicament. + void setRenderInfoSegments(const char* s0, const char* s1, const char* s2, const char* s3); // Gestió d'eixida amb doble ESC // Retorna true si l'ESC ha sigut consumit (no s'ha de passar al joc) diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index 5fa9dc0..d55696b 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -329,25 +329,34 @@ auto Screen::getActiveShaderName() const -> const char* { void Screen::updateRenderInfo() { static const Uint32 start_ticks = SDL_GetTicks(); std::string driver = gpu_driver_.empty() ? "sdl" : toLower(gpu_driver_); - std::string info = std::to_string(fps_.last_value) + " fps - " + driver; + // Segment 0: FPS + driver (sempre visible) + std::string fps_driver = std::to_string(fps_.last_value) + " fps - " + driver; + + // Segment 1: shader + preset (només si shaders actius) + std::string shader_seg; if (Options::video.shader_enabled) { - std::string shader_name = toLower(getActiveShaderName()); - std::string preset_name = toLower(getCurrentPresetName()); - info += " - " + shader_name + " " + preset_name; - if (Options::video.supersampling) info += " (ss)"; + shader_seg = " - " + toLower(getActiveShaderName()) + " " + toLower(getCurrentPresetName()); } - // Temps de joc: m:ss.cc - Uint32 elapsed = SDL_GetTicks() - start_ticks; - int minutes = elapsed / 60000; - int seconds = (elapsed / 1000) % 60; - int centis = (elapsed / 10) % 100; - char time_buf[32]; - snprintf(time_buf, sizeof(time_buf), " - %d:%02d.%02d", minutes, seconds, centis); - info += time_buf; + // Segment 2: supersampling indicator + const char* ss_seg = (Options::video.shader_enabled && Options::video.supersampling) ? " (ss)" : nullptr; - Overlay::setRenderInfoText(info.c_str()); + // Segment 3: hora (només si show_time) + char time_buf[32] = {0}; + if (Options::render_info.show_time) { + Uint32 elapsed = SDL_GetTicks() - start_ticks; + int minutes = elapsed / 60000; + int seconds = (elapsed / 1000) % 60; + int centis = (elapsed / 10) % 100; + snprintf(time_buf, sizeof(time_buf), " - %d:%02d.%02d", minutes, seconds, centis); + } + + Overlay::setRenderInfoSegments( + fps_driver.c_str(), + shader_seg.empty() ? nullptr : shader_seg.c_str(), + ss_seg, + time_buf[0] ? time_buf : nullptr); } void Screen::adjustWindowSize() { diff --git a/source/game/options.cpp b/source/game/options.cpp index 0b3023b..173369b 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -94,6 +94,8 @@ namespace Options { else render_info.position = RenderInfoPosition::OFF; } + if (node.contains("show_time")) + render_info.show_time = node["show_time"].get_value(); if (node.contains("text_color")) render_info.text_color = static_cast(node["text_color"].get_value()); if (node.contains("shadow_color")) @@ -238,6 +240,7 @@ namespace Options { pos = "bottom"; file << " position: " << pos << " # off/top/bottom\n"; } + file << " show_time: " << (render_info.show_time ? "true" : "false") << "\n"; file << " text_color: " << render_info.text_color << "\n"; file << " shadow_color: " << render_info.shadow_color << "\n"; file << "\n"; diff --git a/source/game/options.hpp b/source/game/options.hpp index 6b732cb..6d65576 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -56,6 +56,7 @@ namespace Options { // Opcions del render info struct RenderInfo { RenderInfoPosition position{RenderInfoPosition::OFF}; + bool show_time{true}; Uint32 text_color{0xFF00D7FF}; // Groc daurat (ABGR) Uint32 shadow_color{0xFF005A6B}; // Ombra daurada fosca (ABGR) }; diff --git a/source/utils/easing.cpp b/source/utils/easing.cpp new file mode 100644 index 0000000..4309bcf --- /dev/null +++ b/source/utils/easing.cpp @@ -0,0 +1,24 @@ +#include "utils/easing.hpp" + +namespace Easing { + + auto linear(float t) -> float { return t; } + + auto outQuad(float t) -> float { return 1.0F - (1.0F - t) * (1.0F - t); } + auto inQuad(float t) -> float { return t * t; } + auto inOutQuad(float t) -> float { + return t < 0.5F ? 2.0F * t * t : 1.0F - (-2.0F * t + 2.0F) * (-2.0F * t + 2.0F) / 2.0F; + } + + auto outCubic(float t) -> float { + const float inv = 1.0F - t; + return 1.0F - inv * inv * inv; + } + auto inCubic(float t) -> float { return t * t * t; } + + auto lerp(float a, float b, float t) -> float { return a + (b - a) * t; } + auto lerpInt(int a, int b, float t) -> int { + return a + static_cast((b - a) * t); + } + +} // namespace Easing diff --git a/source/utils/easing.hpp b/source/utils/easing.hpp new file mode 100644 index 0000000..d93e99d --- /dev/null +++ b/source/utils/easing.hpp @@ -0,0 +1,22 @@ +#pragma once + +// Funcions de suavitzat per animacions. Totes accepten t en el rang [0, 1] +// i retornen un valor en [0, 1] (amb overshoot possible en algunes variants). +namespace Easing { + + auto linear(float t) -> float; + + // Quadratic + auto outQuad(float t) -> float; + auto inQuad(float t) -> float; + auto inOutQuad(float t) -> float; + + // Cubic + auto outCubic(float t) -> float; + auto inCubic(float t) -> float; + + // Interpolacions (aplicar després d'un easing al paràmetre t) + auto lerp(float a, float b, float t) -> float; + auto lerpInt(int a, int b, float t) -> int; + +} // namespace Easing