feat(intro/outro): panel translúcid sota el rètol amb easing del text

This commit is contained in:
JailDesigner committed 2026-04-26 18:41:17 +02:00
1 parent 0752df0f62
commit 8243e95ef1
4 files changed
+88 -6

No files matched your search

+13 -2
View File
@@ -8,13 +8,14 @@ room_intro:
fade_in_ms: 500 # negre opac → món visible fade_in_ms: 500 # negre opac → món visible
text_delay_ms: 500 # espera amb el món net abans de sortir el text text_delay_ms: 500 # espera amb el món net abans de sortir el text
text_enter_ms: 400 # text esquerra → centre text_enter_ms: 400 # text esquerra → centre
text_hold_ms: 1500 # text quiet al centre text_hold_ms: 2000 # text quiet al centre
text_exit_ms: 400 # text centre → fora per l'esquerra text_exit_ms: 400 # text centre → fora per l'esquerra
line_gap_px: -4 # px extra entre les dues línies (piràmide / sala) line_gap_px: -4 # px extra entre les dues línies (piràmide / sala)
player_spawn_delay_ms: 2400 # ms des de l'inici d'INTRO al spawn aeri.
room_outro: room_outro:
text_enter_ms: 400 text_enter_ms: 400
text_hold_ms: 1500 text_hold_ms: 2000
text_exit_ms: 400 text_exit_ms: 400
fade_out_ms: 500 # món visible → negre opac fade_out_ms: 500 # món visible → negre opac
line_gap_px: -4 # px extra entre les dues línies (fase / completada) line_gap_px: -4 # px extra entre les dues línies (fase / completada)
@@ -25,3 +26,13 @@ text:
# `data/ui.yaml`). El text entra suau i surt accelerant. # `data/ui.yaml`). El text entra suau i surt accelerant.
enter_curve: outCubic enter_curve: outCubic
exit_curve: inCubic exit_curve: inCubic
# Panel translúcid pintat sota el rètol de transició (INTRO i OUTRO).
# Va centrat verticalment al centre del playfield i ocupa tota
# l'amplada del canvas. Creix d'altura amb la mateixa enter_curve del
# text i decreix amb l'exit_curve. `color` són tres canals RGB (0..255);
# `alpha` és la transparència del blend (0 = invisible, 255 = opac).
text_panel:
color: [36, 182, 170]
alpha: 128
height_px: 32
+44
View File
@@ -640,6 +640,10 @@ void GameScene::renderRoomFade() {
Screen::get().blendRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, 0x00000000U, ALPHA); Screen::get().blendRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, 0x00000000U, ALPHA);
} }
// TEMPORAL — al pròxim commit s'extrauen compute_x, compute_panel_height,
// draw_panel i draw_line a mètodes privats; aquest NOLINT és només per
// "guardar feina" mentre encara estem fent ajustos visuals.
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
void GameScene::renderRoomText() { void GameScene::renderRoomText() {
// Text de la transició de sala. Font dedicada (`room_text_font_`, // Text de la transició de sala. Font dedicada (`room_text_font_`,
// CaesarDressing per defecte) — la pausa té la seua i no es barregen. // CaesarDressing per defecte) — la pausa té la seua i no es barregen.
@@ -699,6 +703,41 @@ void GameScene::renderRoomText() {
return GameCanvas::CENTER_X; return GameCanvas::CENTER_X;
}; };
// Panel translúcid sota el rètol: l'altura s'anima de 0 a
// `text_panel_height_px` durant l'enter (amb la mateixa
// text_enter_curve que els textos), es manté plena durant el hold
// i decreix durant l'exit. Cada estat (INTRO, OUTRO) li passa la
// tripleta de fases (enter, hold, exit) i les seues durades.
auto compute_panel_height = [&](int p_enter, int p_hold, int p_exit, int d_enter, int d_exit) -> int {
const int MAX = transition_config_.text_panel_height_px;
if (transition_.phase == p_enter) {
const float T = (d_enter > 0)
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(d_enter), 0.0F, 1.0F)
: 1.0F;
const auto CURVE = (transition_config_.text_enter_curve != nullptr) ? transition_config_.text_enter_curve : &Easing::linear;
return Easing::lerpInt(0, MAX, CURVE(T));
}
if (transition_.phase == p_hold) { return MAX; }
if (transition_.phase == p_exit) {
const float T = (d_exit > 0)
? std::clamp(static_cast<float>(transition_.elapsed_ms) / static_cast<float>(d_exit), 0.0F, 1.0F)
: 1.0F;
const auto CURVE = (transition_config_.text_exit_curve != nullptr) ? transition_config_.text_exit_curve : &Easing::linear;
return Easing::lerpInt(MAX, 0, CURVE(T));
}
return 0;
};
auto draw_panel = [&](int panel_height) {
if (panel_height <= 0 || transition_config_.text_panel_alpha == 0) { return; }
Screen::get().blendRect(0,
PLAY_CENTER_Y - (panel_height / 2),
GameCanvas::WIDTH,
panel_height,
transition_config_.text_panel_color,
transition_config_.text_panel_alpha);
};
auto draw_line = [&](const std::string& text, int x, int y) { auto draw_line = [&](const std::string& text, int x, int y) {
room_text_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE4_FLAG, room_text_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE4_FLAG,
x, x,
@@ -711,6 +750,9 @@ void GameScene::renderRoomText() {
}; };
if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase >= 2 && transition_.phase <= 4) { if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase >= 2 && transition_.phase <= 4) {
// Panel primer (queda sota el text gràcies a l'ordre de blits).
draw_panel(compute_panel_height(2, 3, 4, transition_config_.intro_text_enter_ms, transition_config_.intro_text_exit_ms));
const auto& mgr = Levels::LevelManager::get(); const auto& mgr = Levels::LevelManager::get();
const std::string LINE_PYRAMID = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid()); const std::string LINE_PYRAMID = RoomFsm::formatIntroPyramidLine(mgr.currentPyramid());
const std::string LINE_ROOM = RoomFsm::formatIntroRoomLine(mgr.currentRoom()); const std::string LINE_ROOM = RoomFsm::formatIntroRoomLine(mgr.currentRoom());
@@ -735,6 +777,8 @@ void GameScene::renderRoomText() {
draw_line(LINE_PYRAMID, X_PYRAMID, Y_PYRAMID); draw_line(LINE_PYRAMID, X_PYRAMID, Y_PYRAMID);
draw_line(LINE_ROOM, X_ROOM, Y_ROOM); draw_line(LINE_ROOM, X_ROOM, Y_ROOM);
} else if (state_ == RoomFsm::State::ROOM_OUTRO && transition_.phase >= 0 && transition_.phase <= 2) { } else if (state_ == RoomFsm::State::ROOM_OUTRO && transition_.phase >= 0 && transition_.phase <= 2) {
draw_panel(compute_panel_height(0, 1, 2, transition_config_.outro_text_enter_ms, transition_config_.outro_text_exit_ms));
// Mateix patró que l'INTRO: dues línies en direccions oposades. // Mateix patró que l'INTRO: dues línies en direccions oposades.
// Línia 1 ("Fase") per l'esquerra, línia 2 ("Completada") per // Línia 1 ("Fase") per l'esquerra, línia 2 ("Completada") per
// la dreta; surten cap al costat oposat del que han entrat. // la dreta; surten cap al costat oposat del que han entrat.
+13
View File
@@ -63,6 +63,19 @@ namespace RoomFsm {
cfg.text_exit_curve = UiConfig::curveByName(n["exit_curve"].get_value<std::string>()); cfg.text_exit_curve = UiConfig::curveByName(n["exit_curve"].get_value<std::string>());
} }
} }
if (yaml.contains("text_panel")) {
const auto& n = yaml["text_panel"];
if (n.contains("color") && n["color"].is_sequence() && n["color"].size() == 3) {
const auto R = n["color"][0].get_value<std::uint32_t>() & 0xFFU;
const auto G = n["color"][1].get_value<std::uint32_t>() & 0xFFU;
const auto B = n["color"][2].get_value<std::uint32_t>() & 0xFFU;
cfg.text_panel_color = (R << 16) | (G << 8) | B;
}
if (n.contains("alpha")) {
cfg.text_panel_alpha = static_cast<std::uint8_t>(n["alpha"].get_value<std::uint32_t>() & 0xFFU);
}
cfg.text_panel_height_px = readChildInt(n, "height_px", cfg.text_panel_height_px);
}
std::cout << "RoomFsm: timings carregats des de " << yaml_path std::cout << "RoomFsm: timings carregats des de " << yaml_path
<< " (intro fade " << cfg.intro_fade_in_ms << " (intro fade " << cfg.intro_fade_in_ms
+18 -4
View File
@@ -48,10 +48,13 @@ namespace RoomFsm {
int outro_black_hold_ms{0}; int outro_black_hold_ms{0};
// Mil·lisegons des de l'inici de ROOM_INTRO en què cau la // Mil·lisegons des de l'inici de ROOM_INTRO en què cau la
// cinemàtica d'spawn aeri dels jugadors actius. Independent del // cinemàtica d'spawn aeri dels jugadors actius. Específic per a
// fade-in perquè el mateix valor servirà per al respawn post-mort // l'entrada de sala — el respawn post-mort tindrà el seu propi
// (que no té fade que el sincronitze). 500 ms = típicament el // timing quan toqui dissenyar-lo. Un valor habitual: la suma de
// moment en què el fade-in acaba i el text encara no ha entrat. // fade_in + text_delay + text_enter + text_hold, perquè els
// players caiguin just quan el rètol comença a sortir. El
// fallback compilat (500 ms) deixa el spawn al final del fade-in
// si el YAML no defineix res — només pensat com a degradació.
int player_spawn_delay_ms{500}; int player_spawn_delay_ms{500};
// Píxels extra entre les dues línies del text d'INTRO (piràmide // Píxels extra entre les dues línies del text d'INTRO (piràmide
@@ -68,6 +71,17 @@ namespace RoomFsm {
UiConfig::CurveFn text_enter_curve{nullptr}; UiConfig::CurveFn text_enter_curve{nullptr};
UiConfig::CurveFn text_exit_curve{nullptr}; UiConfig::CurveFn text_exit_curve{nullptr};
// Panel translúcid sota el rètol de transició (INTRO i OUTRO).
// Centrat verticalment al centre del playfield, ocupa tota
// l'amplada del canvas. Creix d'altura 0 → height_px durant la
// fase d'enter del text (amb `text_enter_curve`) i decreix
// durant la fase d'exit (amb `text_exit_curve`). 0 d'alpha o
// 0 d'alçada el deixa invisible. Color RGB (sense alpha — la
// transparència viu al camp `alpha`).
std::uint32_t text_panel_color{0x000000U};
std::uint8_t text_panel_alpha{128};
int text_panel_height_px{40};
}; };
// Llegeix la configuració des del YAML. Si el fitxer manca o falten // Llegeix la configuració des del YAML. Si el fitxer manca o falten