From 5ac570df0f3417363f05c7167052b0525cbfc39e Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 5 Apr 2026 01:15:45 +0200 Subject: [PATCH] animacions al menu --- source/core/rendering/menu.cpp | 182 +++++++++++++++++++++++---------- source/core/rendering/text.cpp | 55 ++++++++++ source/core/rendering/text.hpp | 2 + 3 files changed, 183 insertions(+), 56 deletions(-) diff --git a/source/core/rendering/menu.cpp b/source/core/rendering/menu.cpp index f6b7925..041c378 100644 --- a/source/core/rendering/menu.cpp +++ b/source/core/rendering/menu.cpp @@ -67,6 +67,29 @@ namespace Menu { static Uint32 last_ticks_{0}; static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar + // --- Transició entre pàgines --- + static constexpr float TRANSITION_SPEED = 5.5F; // ~180 ms + static Page transition_outgoing_{"", {}, 0}; + static bool transition_active_{false}; + static float transition_progress_{1.0F}; + static int transition_dir_{+1}; // +1 endavant, -1 enrere + + // Helpers per triggerar transicions + static void pushPage(Page newPage) { + transition_outgoing_ = stack_.back(); + stack_.push_back(std::move(newPage)); + transition_active_ = true; + transition_progress_ = 0.0F; + transition_dir_ = +1; + } + static void popPage() { + transition_outgoing_ = stack_.back(); + stack_.pop_back(); + transition_active_ = true; + transition_progress_ = 0.0F; + transition_dir_ = -1; + } + // --- Helpers --- static std::string yesNo(bool b) { return b ? "SI" : "NO"; } @@ -80,9 +103,9 @@ namespace Menu { static Page buildRoot() { Page p{"OPCIONS", {}, 0}; - p.items.push_back({"VIDEO", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildVideo()); }, nullptr}); - p.items.push_back({"AUDIO", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildAudio()); }, nullptr}); - p.items.push_back({"CONTROLS", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildControls()); }, nullptr}); + p.items.push_back({"VIDEO", ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildVideo()); }, nullptr}); + p.items.push_back({"AUDIO", ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr}); + p.items.push_back({"CONTROLS", ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr}); return p; } @@ -122,10 +145,7 @@ 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}); + 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; } @@ -264,6 +284,8 @@ namespace Menu { stack_.clear(); open_anim_ = 0.0F; capturing_ = nullptr; + transition_active_ = false; + transition_progress_ = 1.0F; } auto isCapturing() -> bool { @@ -287,8 +309,10 @@ namespace Menu { if (page.items.empty()) { // Pàgina buida — només backspace surt if (sc == SDL_SCANCODE_BACKSPACE) { - stack_.pop_back(); - if (stack_.empty()) close(); + if (stack_.size() > 1) + popPage(); + else + close(); } return; } @@ -323,61 +347,43 @@ namespace Menu { } break; case SDL_SCANCODE_BACKSPACE: - stack_.pop_back(); - if (stack_.empty()) close(); + if (stack_.size() > 1) + popPage(); + else + close(); break; default: break; } } - void render(Uint32* pixel_data) { - if (!isOpen() || !font_ || !pixel_data) return; - - // Actualitza animació d'obertura - Uint32 now = SDL_GetTicks(); - float dt = static_cast(now - last_ticks_) / 1000.0F; - last_ticks_ = now; - if (open_anim_ < 1.0F) { - open_anim_ += OPEN_SPEED * dt; - if (open_anim_ > 1.0F) open_anim_ = 1.0F; - } - - const Page& page = stack_.back(); - const int target_h = boxHeight(page); - - float eased = Easing::outQuad(open_anim_); - - // Caixa creix verticalment des del centre - int box_h = static_cast(target_h * eased); - if (box_h < 2) box_h = 2; - int box_x = (SCREEN_W - BOX_W) / 2; - int box_y = (SCREEN_H - box_h) / 2; - - // Fons semi-transparent (alpha escalat per l'animació) - Uint8 alpha = static_cast(BG_ALPHA * eased); - blendRect(pixel_data, box_x, box_y, BOX_W, box_h, BG_COLOR, alpha); - - // Vora - drawBorder(pixel_data, box_x, box_y, BOX_W, box_h, BORDER_COLOR); - - // El contingut només apareix quan la caixa és prou gran - if (open_anim_ < 0.9F) return; - + // Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip. + // box_x/box_y són les coordenades de la caixa (per calcular posicions relatives); + // clip_x_min/clip_x_max limiten on es dibuixa text i la línia separadora. + static void renderPageContent(Uint32* pixel_data, const Page& page, int box_x, int box_y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) { // Títol int title_w = font_->width(page.title); - font_->draw(pixel_data, box_x + (BOX_W - title_w) / 2, box_y + TITLE_PAD_Y, page.title, TITLE_COLOR); + int title_x = box_x + (BOX_W - title_w) / 2 + x_offset; + font_->drawClipped(pixel_data, title_x, box_y + TITLE_PAD_Y, page.title, TITLE_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); - // Línia sota el títol + // Línia sota el títol (també lliscada) — clippada manualment int title_line_y = box_y + TITLE_PAD_Y + font_->charHeight() + 2; - fillRect(pixel_data, box_x + 4, title_line_y, BOX_W - 8, 1, BORDER_COLOR); + if (title_line_y >= clip_y_min && title_line_y < clip_y_max) { + int line_x = box_x + 4 + x_offset; + int line_w = BOX_W - 8; + int line_start = line_x < clip_x_min ? clip_x_min : line_x; + int line_end = (line_x + line_w) > clip_x_max ? clip_x_max : (line_x + line_w); + if (line_end > line_start) { + fillRect(pixel_data, line_start, title_line_y, line_end - line_start, 1, BORDER_COLOR); + } + } - // Items + // Items o placeholder buit int items_y = title_line_y + 4; if (page.items.empty()) { const char* empty_text = "(BUIT)"; int ew = font_->width(empty_text); - font_->draw(pixel_data, box_x + (BOX_W - ew) / 2, items_y + 2, empty_text, EMPTY_COLOR); + font_->drawClipped(pixel_data, box_x + (BOX_W - ew) / 2 + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); return; } @@ -387,33 +393,97 @@ namespace Menu { const Item& item = page.items[i]; if (selected) { - font_->draw(pixel_data, box_x + 4, y, ">", CURSOR_COLOR); + font_->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); } Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR; - font_->draw(pixel_data, box_x + ITEM_PAD_X, y, item.label, label_color); + font_->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max); - // Valor (dreta) — només per items no-submenu if (item.kind == ItemKind::Submenu) { - // Fletxa indicant que entra a un submenú const char* arrow = ">>"; int aw = font_->width(arrow); Uint32 ac = selected ? CURSOR_COLOR : VALUE_COLOR; - font_->draw(pixel_data, box_x + BOX_W - ITEM_PAD_X - aw, y, arrow, ac); + font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - aw + x_offset, y, arrow, ac, clip_x_min, clip_x_max, clip_y_min, clip_y_max); } else if (item.kind == ItemKind::KeyBind) { bool this_capturing = (capturing_ == item.scancode); const char* text = this_capturing ? "" : (item.scancode ? SDL_GetScancodeName(*item.scancode) : ""); if (!text || !*text) text = "---"; int tw = font_->width(text); Uint32 tc = this_capturing ? 0xFF00FFFF : (selected ? CURSOR_COLOR : VALUE_COLOR); - font_->draw(pixel_data, box_x + BOX_W - ITEM_PAD_X - tw, y, text, tc); + font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - tw + x_offset, y, text, tc, clip_x_min, clip_x_max, clip_y_min, clip_y_max); } else if (item.getValue) { std::string value = item.getValue(); int value_w = font_->width(value.c_str()); Uint32 value_color = selected ? CURSOR_COLOR : VALUE_COLOR; - font_->draw(pixel_data, box_x + BOX_W - ITEM_PAD_X - value_w, y, value.c_str(), value_color); + font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - value_w + x_offset, y, value.c_str(), value_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max); } } } + void render(Uint32* pixel_data) { + if (!isOpen() || !font_ || !pixel_data) return; + + // Delta time + Uint32 now = SDL_GetTicks(); + float dt = static_cast(now - last_ticks_) / 1000.0F; + last_ticks_ = now; + if (open_anim_ < 1.0F) { + open_anim_ += OPEN_SPEED * dt; + if (open_anim_ > 1.0F) open_anim_ = 1.0F; + } + + // Avança transició + if (transition_active_) { + transition_progress_ += TRANSITION_SPEED * dt; + if (transition_progress_ >= 1.0F) { + transition_progress_ = 1.0F; + transition_active_ = false; + } + } + + const Page& page = stack_.back(); + const int current_h = boxHeight(page); + + float eased = Easing::outQuad(open_anim_); + + // Calcula alçada (amb transició si escau) + int target_h = current_h; + if (transition_active_) { + int outgoing_h = boxHeight(transition_outgoing_); + float tp = Easing::outQuad(transition_progress_); + target_h = Easing::lerpInt(outgoing_h, current_h, tp); + } + + // Caixa creix verticalment durant l'obertura + int box_h = static_cast(target_h * eased); + if (box_h < 2) box_h = 2; + int box_x = (SCREEN_W - BOX_W) / 2; + int box_y = (SCREEN_H - box_h) / 2; + + // Fons semi-transparent (alpha escalat per l'animació d'obertura) + Uint8 alpha = static_cast(BG_ALPHA * eased); + blendRect(pixel_data, box_x, box_y, BOX_W, box_h, BG_COLOR, alpha); + + // El contingut només apareix quan la caixa és prou gran + if (open_anim_ >= 0.9F) { + int clip_x_min = box_x + 1; + int clip_x_max = box_x + BOX_W - 1; + int clip_y_min = box_y + 1; + int clip_y_max = box_y + box_h - 1; + + if (transition_active_) { + float tp = Easing::outQuad(transition_progress_); + int out_offset = static_cast(-transition_dir_ * BOX_W * tp); + int new_offset = static_cast(transition_dir_ * BOX_W * (1.0F - tp)); + renderPageContent(pixel_data, transition_outgoing_, box_x, box_y, out_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + renderPageContent(pixel_data, page, box_x, box_y, new_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + } else { + renderPageContent(pixel_data, page, box_x, box_y, 0, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + } + } + + // Vora per damunt de tot + drawBorder(pixel_data, box_x, box_y, BOX_W, box_h, BORDER_COLOR); + } + } // namespace Menu diff --git a/source/core/rendering/text.cpp b/source/core/rendering/text.cpp index 3826935..eb12e72 100644 --- a/source/core/rendering/text.cpp +++ b/source/core/rendering/text.cpp @@ -211,6 +211,61 @@ void Text::drawCentered(Uint32* pixel_data, int y, const char* text, Uint32 colo draw(pixel_data, x, y, text, color); } +void Text::drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const { + if (!bitmap_ || !pixel_data) return; + + // Descart ràpid si el glifo sencer cau fora verticalment + if (y + box_height_ <= clip_y_min || y >= clip_y_max) return; + + const char* ptr = text; + int cursor_x = x; + + while (*ptr) { + uint32_t cp = nextCodepoint(ptr); + if (cp == 0) break; + + auto it = glyphs_.find(cp); + if (it == glyphs_.end()) { + it = glyphs_.find('?'); + if (it == glyphs_.end()) { + cursor_x += box_width_; + continue; + } + } + + const auto& glyph = it->second; + + // Si el glifo està completament fora del clip horitzontal, salta + if (cursor_x + glyph.w <= clip_x_min || cursor_x >= clip_x_max) { + cursor_x += glyph.w + 1; + continue; + } + + for (int gy = 0; gy < box_height_; gy++) { + int dst_y = y + gy; + if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) continue; + if (dst_y < clip_y_min || dst_y >= clip_y_max) continue; + + for (int gx = 0; gx < glyph.w; gx++) { + int dst_x = cursor_x + gx; + if (dst_x < 0 || dst_x >= SCREEN_WIDTH) continue; + if (dst_x < clip_x_min || dst_x >= clip_x_max) continue; + + int src_x = glyph.x + gx; + int src_y = glyph.y + gy; + if (src_x >= bitmap_width_ || src_y >= bitmap_height_) continue; + + Uint8 pixel = bitmap_[src_x + src_y * bitmap_width_]; + if (pixel != 0) { + pixel_data[dst_x + dst_y * SCREEN_WIDTH] = color; + } + } + } + + cursor_x += glyph.w + 1; + } +} + auto Text::width(const char* text) const -> int { const char* ptr = text; int w = 0; diff --git a/source/core/rendering/text.hpp b/source/core/rendering/text.hpp index 8cd5007..8920c10 100644 --- a/source/core/rendering/text.hpp +++ b/source/core/rendering/text.hpp @@ -14,6 +14,8 @@ class Text { // Pinta texto sobre un buffer ARGB de 320x200 void draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const; void drawCentered(Uint32* pixel_data, int y, const char* text, Uint32 color) const; + // Com draw, però clippat a [clip_x_min, clip_x_max) × [clip_y_min, clip_y_max) + void drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const; // Calcula ancho en píxeles d'un text [[nodiscard]] auto width(const char* text) const -> int;