animacions al menu

This commit is contained in:
2026-04-05 01:15:45 +02:00
parent 4238ae1bc4
commit 5ac570df0f
3 changed files with 183 additions and 56 deletions

View File

@@ -67,6 +67,29 @@ namespace Menu {
static Uint32 last_ticks_{0}; static Uint32 last_ticks_{0};
static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar 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 --- // --- Helpers ---
static std::string yesNo(bool b) { return b ? "SI" : "NO"; } static std::string yesNo(bool b) { return b ? "SI" : "NO"; }
@@ -80,9 +103,9 @@ namespace Menu {
static Page buildRoot() { static Page buildRoot() {
Page p{"OPCIONS", {}, 0}; Page p{"OPCIONS", {}, 0};
p.items.push_back({"VIDEO", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildVideo()); }, nullptr}); p.items.push_back({"VIDEO", ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildVideo()); }, nullptr});
p.items.push_back({"AUDIO", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildAudio()); }, nullptr}); p.items.push_back({"AUDIO", ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr});
p.items.push_back({"CONTROLS", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildControls()); }, nullptr}); p.items.push_back({"CONTROLS", ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr});
return p; return p;
} }
@@ -122,10 +145,7 @@ namespace Menu {
} }
return std::string("OFF"); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr}); return std::string("OFF"); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr});
p.items.push_back({"HORA", ItemKind::Toggle, 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 onOff(Options::render_info.show_time); },
[](int) { Options::render_info.show_time = !Options::render_info.show_time; },
nullptr});
return p; return p;
} }
@@ -264,6 +284,8 @@ namespace Menu {
stack_.clear(); stack_.clear();
open_anim_ = 0.0F; open_anim_ = 0.0F;
capturing_ = nullptr; capturing_ = nullptr;
transition_active_ = false;
transition_progress_ = 1.0F;
} }
auto isCapturing() -> bool { auto isCapturing() -> bool {
@@ -287,8 +309,10 @@ namespace Menu {
if (page.items.empty()) { if (page.items.empty()) {
// Pàgina buida — només backspace surt // Pàgina buida — només backspace surt
if (sc == SDL_SCANCODE_BACKSPACE) { if (sc == SDL_SCANCODE_BACKSPACE) {
stack_.pop_back(); if (stack_.size() > 1)
if (stack_.empty()) close(); popPage();
else
close();
} }
return; return;
} }
@@ -323,61 +347,43 @@ namespace Menu {
} }
break; break;
case SDL_SCANCODE_BACKSPACE: case SDL_SCANCODE_BACKSPACE:
stack_.pop_back(); if (stack_.size() > 1)
if (stack_.empty()) close(); popPage();
else
close();
break; break;
default: default:
break; break;
} }
} }
void render(Uint32* pixel_data) { // Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip.
if (!isOpen() || !font_ || !pixel_data) return; // 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.
// Actualitza animació d'obertura 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) {
Uint32 now = SDL_GetTicks();
float dt = static_cast<float>(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<int>(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<Uint8>(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;
// Títol // Títol
int title_w = font_->width(page.title); 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; 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; int items_y = title_line_y + 4;
if (page.items.empty()) { if (page.items.empty()) {
const char* empty_text = "(BUIT)"; const char* empty_text = "(BUIT)";
int ew = font_->width(empty_text); 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; return;
} }
@@ -387,33 +393,97 @@ namespace Menu {
const Item& item = page.items[i]; const Item& item = page.items[i];
if (selected) { 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; 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) { if (item.kind == ItemKind::Submenu) {
// Fletxa indicant que entra a un submenú
const char* arrow = ">>"; const char* arrow = ">>";
int aw = font_->width(arrow); int aw = font_->width(arrow);
Uint32 ac = selected ? CURSOR_COLOR : VALUE_COLOR; 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) { } else if (item.kind == ItemKind::KeyBind) {
bool this_capturing = (capturing_ == item.scancode); bool this_capturing = (capturing_ == item.scancode);
const char* text = this_capturing ? "<PREM TECLA>" : (item.scancode ? SDL_GetScancodeName(*item.scancode) : ""); const char* text = this_capturing ? "<PREM TECLA>" : (item.scancode ? SDL_GetScancodeName(*item.scancode) : "");
if (!text || !*text) text = "---"; if (!text || !*text) text = "---";
int tw = font_->width(text); int tw = font_->width(text);
Uint32 tc = this_capturing ? 0xFF00FFFF : (selected ? CURSOR_COLOR : VALUE_COLOR); 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) { } else if (item.getValue) {
std::string value = item.getValue(); std::string value = item.getValue();
int value_w = font_->width(value.c_str()); int value_w = font_->width(value.c_str());
Uint32 value_color = selected ? CURSOR_COLOR : VALUE_COLOR; 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<float>(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<int>(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<Uint8>(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<int>(-transition_dir_ * BOX_W * tp);
int new_offset = static_cast<int>(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 } // namespace Menu

View File

@@ -211,6 +211,61 @@ void Text::drawCentered(Uint32* pixel_data, int y, const char* text, Uint32 colo
draw(pixel_data, x, y, text, color); 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 { auto Text::width(const char* text) const -> int {
const char* ptr = text; const char* ptr = text;
int w = 0; int w = 0;

View File

@@ -14,6 +14,8 @@ class Text {
// Pinta texto sobre un buffer ARGB de 320x200 // Pinta texto sobre un buffer ARGB de 320x200
void draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const; 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; 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 // Calcula ancho en píxeles d'un text
[[nodiscard]] auto width(const char* text) const -> int; [[nodiscard]] auto width(const char* text) const -> int;