feat(transitions): mosaic dissolve fade-out al final de la fase STARTING (configurable a ui.yaml)

This commit is contained in:
JailDesigner committed 2026-05-06 13:54:09 +02:00
1 parent ca64115bfe
commit 0618452224
9 files changed
+173 -8

No files matched your search

+1
View File
@@ -93,6 +93,7 @@ set(APP_SOURCES
source/core/rendering/sprite/animated_sprite.cpp source/core/rendering/sprite/animated_sprite.cpp
source/core/rendering/sprite/animated_sprite_parser.cpp source/core/rendering/sprite/animated_sprite_parser.cpp
source/core/rendering/effects/heat_haze.cpp source/core/rendering/effects/heat_haze.cpp
source/core/rendering/transitions/mosaic.cpp
source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp source/core/rendering/sdl3gpu/sdl3gpu_shader.cpp
source/core/rendering/sdl3gpu/viewport.cpp source/core/rendering/sdl3gpu/viewport.cpp
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
+6
View File
@@ -120,6 +120,12 @@ title:
# finestra, el comptador es reseteja i s'arrenca amb BOTH. # finestra, el comptador es reseteja i s'arrenca amb BOTH.
starting_ms: 3000 starting_ms: 3000
starting_blink_speedup: 2.0 starting_blink_speedup: 2.0
# Fade-out estil "mosaic dissolve" durant l'última part de STARTING.
# `fade_out_ms` ha de ser <= starting_ms (els últims X ms del STARTING
# es fan amb el dissolve actiu fins a quedar negre). `fade_out_max_block`
# és la mida màxima de bloc en píxels al final del fade.
fade_out_ms: 800
fade_out_max_block: 32
# Distorsió sinusoidal del fons (efecte de calor del desert). # Distorsió sinusoidal del fons (efecte de calor del desert).
# `amplitude_px`: desplaçament màxim de cada fila en píxels. # `amplitude_px`: desplaçament màxim de cada fila en píxels.
# `wavelength_px`: distància vertical entre dos pics de l'ona. # `wavelength_px`: distància vertical entre dos pics de l'ona.
@@ -0,0 +1,71 @@
#include "core/rendering/transitions/mosaic.hpp"
#include <algorithm>
#include <cmath>
#include <cstdint>
#include "core/rendering/screen.hpp"
namespace Transitions {
Mosaic::Mosaic(Params params)
: params_(params) {}
void Mosaic::begin(int duration_ms) {
duration_ms_ = duration_ms;
elapsed_ms_ = 0;
}
void Mosaic::tick(int delta_ms) {
if (duration_ms_ <= 0) { return; }
elapsed_ms_ = std::min(elapsed_ms_ + delta_ms, duration_ms_);
}
auto Mosaic::progress() const -> float {
if (duration_ms_ <= 0) { return 0.0F; }
return std::clamp(static_cast<float>(elapsed_ms_) / static_cast<float>(duration_ms_), 0.0F, 1.0F);
}
void Mosaic::render() const {
if (duration_ms_ <= 0) { return; }
const float P = progress();
const int MAX_BLOCK = std::max(1, params_.max_block_size);
// Blocs de mida creixent al llarg del fade. A p=0 → 1 px (sense
// efecte visible); a p=1 → max_block. Es fa cap-amunt amb ceil
// perquè el primer increment arribi aviat (el salt visual de
// 1→2 ja es nota).
const int BLOCK = std::clamp(static_cast<int>(std::ceil(static_cast<float>(MAX_BLOCK) * P)), 1, MAX_BLOCK);
auto& screen = Screen::get();
std::uint32_t* canvas = screen.getCanvasArgb();
const int W = screen.getCanvasWidth();
const int H = screen.getCanvasHeight();
if (BLOCK > 1) {
// Sample-and-fill in-place. Per a cada bloc, mostregem el
// píxel central (les vores poden caure fora del canvas en
// blocs de la última columna/fila → clamp).
for (int by = 0; by < H; by += BLOCK) {
const int Y_END = std::min(by + BLOCK, H);
const int SY = std::min(by + (BLOCK / 2), H - 1);
for (int bx = 0; bx < W; bx += BLOCK) {
const int X_END = std::min(bx + BLOCK, W);
const int SX = std::min(bx + (BLOCK / 2), W - 1);
const std::uint32_t COLOR = canvas[(SY * W) + SX];
for (int yy = by; yy < Y_END; ++yy) {
std::uint32_t* row = canvas + (static_cast<std::ptrdiff_t>(yy) * W);
std::fill(row + bx, row + X_END, COLOR);
}
}
}
}
// Fade-to-black quadràtic per damunt: la mascara fosca arriba
// tard (a p=0.5 només 25% d'alpha) perquè es vegin els blocs.
const auto ALPHA = static_cast<std::uint8_t>(std::round(255.0F * P * P));
if (ALPHA > 0) {
screen.blendRect(0, 0, W, H, 0x00000000U, ALPHA);
}
}
} // namespace Transitions
@@ -0,0 +1,45 @@
#pragma once
namespace Transitions {
// Fade-out estil "mosaic dissolve": al llarg del temps els píxels del
// canvas es reagrupen en blocs cada cop més grans (1×1 → max_block_size×
// max_block_size). Cada bloc es pinta amb el píxel central del què
// tenia abans, donant aquell aspecte chunky d'arcade. Al damunt
// s'aplica un fade-to-black quadràtic (alpha = p² × 255) perquè el
// resultat acabi en negre opac.
//
// La classe NO sap res del Screen abans de `render()`. El caller hi
// crida després de pintar la seva escena: l'efecte llegeix els
// píxels actuals del canvas, els rebina i escriu pixels modificats
// al mateix canvas. És, per tant, un post-procés in-place.
class Mosaic {
public:
struct Params {
int max_block_size;
};
explicit Mosaic(Params params);
// Arrenca un fade nou. Si ja n'hi havia un actiu, el reemplaça.
// `duration_ms <= 0` deixa l'efecte inactiu.
void begin(int duration_ms);
void tick(int delta_ms);
[[nodiscard]] auto isActive() const -> bool { return duration_ms_ > 0 && elapsed_ms_ < duration_ms_; }
[[nodiscard]] auto isFinished() const -> bool { return duration_ms_ > 0 && elapsed_ms_ >= duration_ms_; }
[[nodiscard]] auto progress() const -> float;
// Aplica el mosaic + alpha negre sobre el canvas actual de Screen.
// Cridar després del render normal de l'escena, abans de
// `Screen::present()`.
void render() const;
private:
Params params_;
int duration_ms_{0};
int elapsed_ms_{0};
};
} // namespace Transitions
+4
View File
@@ -104,6 +104,8 @@ UiConfig::UiConfig()
title_blink_hidden_ms_(Defaults::UI::Title::BLINK_HIDDEN_MS), title_blink_hidden_ms_(Defaults::UI::Title::BLINK_HIDDEN_MS),
title_starting_ms_(Defaults::UI::Title::STARTING_MS), title_starting_ms_(Defaults::UI::Title::STARTING_MS),
title_starting_blink_speedup_(Defaults::UI::Title::STARTING_BLINK_SPEEDUP), title_starting_blink_speedup_(Defaults::UI::Title::STARTING_BLINK_SPEEDUP),
title_fade_out_ms_(Defaults::UI::Title::FADE_OUT_MS),
title_fade_out_max_block_(Defaults::UI::Title::FADE_OUT_MAX_BLOCK),
title_heat_haze_{ title_heat_haze_{
.enabled = Defaults::UI::Title::HeatHaze::ENABLED, .enabled = Defaults::UI::Title::HeatHaze::ENABLED,
.amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX, .amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX,
@@ -228,6 +230,8 @@ void UiConfig::load(const std::string& file_path) {
readField(t, "blink_hidden_ms", title_blink_hidden_ms_); readField(t, "blink_hidden_ms", title_blink_hidden_ms_);
readField(t, "starting_ms", title_starting_ms_); readField(t, "starting_ms", title_starting_ms_);
readField(t, "starting_blink_speedup", title_starting_blink_speedup_); readField(t, "starting_blink_speedup", title_starting_blink_speedup_);
readField(t, "fade_out_ms", title_fade_out_ms_);
readField(t, "fade_out_max_block", title_fade_out_max_block_);
if (t.contains("heat_haze")) { if (t.contains("heat_haze")) {
const auto& hh = t["heat_haze"]; const auto& hh = t["heat_haze"];
readField(hh, "enabled", title_heat_haze_.enabled); readField(hh, "enabled", title_heat_haze_.enabled);
+4
View File
@@ -97,6 +97,8 @@ class UiConfig {
[[nodiscard]] auto titleBlinkHiddenMs() const -> int { return title_blink_hidden_ms_; } [[nodiscard]] auto titleBlinkHiddenMs() const -> int { return title_blink_hidden_ms_; }
[[nodiscard]] auto titleStartingMs() const -> int { return title_starting_ms_; } [[nodiscard]] auto titleStartingMs() const -> int { return title_starting_ms_; }
[[nodiscard]] auto titleStartingBlinkSpeedup() const -> float { return title_starting_blink_speedup_; } [[nodiscard]] auto titleStartingBlinkSpeedup() const -> float { return title_starting_blink_speedup_; }
[[nodiscard]] auto titleFadeOutMs() const -> int { return title_fade_out_ms_; }
[[nodiscard]] auto titleFadeOutMaxBlock() const -> int { return title_fade_out_max_block_; }
[[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; } [[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; }
// Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa // Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa
@@ -171,6 +173,8 @@ class UiConfig {
int title_blink_hidden_ms_; int title_blink_hidden_ms_;
int title_starting_ms_; int title_starting_ms_;
float title_starting_blink_speedup_; float title_starting_blink_speedup_;
int title_fade_out_ms_;
int title_fade_out_max_block_;
TitleHeatHaze title_heat_haze_; TitleHeatHaze title_heat_haze_;
std::string menu_select_sound_; std::string menu_select_sound_;
+32 -8
View File
@@ -87,6 +87,9 @@ TitleScene::TitleScene(std::shared_ptr<Text> display_font, std::shared_ptr<Text>
.speed_hz = hh.speed_hz, .speed_hz = hh.speed_hz,
}); });
} }
fade_out_.emplace(Transitions::Mosaic::Params{
.max_block_size = UiConfig::get().titleFadeOutMaxBlock(),
});
} }
TitleScene::~TitleScene() = default; TitleScene::~TitleScene() = default;
@@ -119,25 +122,38 @@ void TitleScene::update(int delta_ms) {
} else if (P2_START) { } else if (P2_START) {
enterStarting(DebugConfig::StartingPlayers::P2); enterStarting(DebugConfig::StartingPlayers::P2);
} else if (timer_ms_ <= 0) { } else if (timer_ms_ <= 0) {
// Attract: ningú ha premut. Saltem a la següent escena // Attract: ningú ha premut. Entrem a STARTING amb BOTH
// amb BOTH per defecte. Demà, quan tinguem una DemoScene, // perquè el fade-out de transició també hi compti.
// ací anirà la transició a demo en lloc de a intro. // Demà, quan tinguem una DemoScene, ací anirà la
advanceTo(DebugConfig::StartingPlayers::BOTH); // transició a demo en lloc de a intro.
enterStarting(DebugConfig::StartingPlayers::BOTH);
} }
break; break;
} }
case Phase::STARTING: { case Phase::STARTING: {
starting_timer_ms_ -= delta_ms; starting_timer_ms_ -= delta_ms;
// Si l'altre jugador prem START dins de la finestra, pugem // Si l'altre jugador prem START dins de la finestra (i el
// a BOTH i resetegem el comptador. Pulsacions repetides del // fade encara no ha arrencat), pugem a BOTH i resetegem el
// mateix jugador ja "starting" no fan res. // comptador. Una vegada el fade ja està actiu, ignorem
// pulsacions per no saltar visualment la transició.
const bool OTHER_PRESSED = const bool OTHER_PRESSED =
(pending_players_ == DebugConfig::StartingPlayers::P1 && P2_START) || (pending_players_ == DebugConfig::StartingPlayers::P1 && P2_START) ||
(pending_players_ == DebugConfig::StartingPlayers::P2 && P1_START); (pending_players_ == DebugConfig::StartingPlayers::P2 && P1_START);
if (OTHER_PRESSED) { const bool FADE_LOCKED = fade_out_ && (fade_out_->isActive() || fade_out_->isFinished());
if (OTHER_PRESSED && !FADE_LOCKED) {
pending_players_ = DebugConfig::StartingPlayers::BOTH; pending_players_ = DebugConfig::StartingPlayers::BOTH;
starting_timer_ms_ = UiConfig::get().titleStartingMs(); starting_timer_ms_ = UiConfig::get().titleStartingMs();
} }
// Arrenca el fade quan al comptador li queden <= fade_out_ms,
// i només una vegada (ni en repeticions ni si l'altre jugador
// ha resetejat el timer).
const int FADE_MS = UiConfig::get().titleFadeOutMs();
if (fade_out_ && !fade_out_->isActive() && !fade_out_->isFinished() && starting_timer_ms_ <= FADE_MS && FADE_MS > 0) {
fade_out_->begin(FADE_MS);
}
if (fade_out_ && fade_out_->isActive()) { fade_out_->tick(delta_ms); }
if (starting_timer_ms_ <= 0) { if (starting_timer_ms_ <= 0) {
advanceTo(pending_players_); advanceTo(pending_players_);
} }
@@ -245,6 +261,14 @@ void TitleScene::render() {
} }
Notifier::get().render(); Notifier::get().render();
// El fade-out cobreix l'escena i el Notifier (forma part de la
// transició), però NO el RenderInfo (overlay de debug, sempre
// visible).
if (fade_out_ && (fade_out_->isActive() || fade_out_->isFinished())) {
fade_out_->render();
}
RenderInfo::get().render(); RenderInfo::get().render();
Screen::get().present(); Screen::get().present();
} }
+2
View File
@@ -6,6 +6,7 @@
#include <optional> #include <optional>
#include "core/rendering/effects/heat_haze.hpp" #include "core/rendering/effects/heat_haze.hpp"
#include "core/rendering/transitions/mosaic.hpp"
#include "core/scene/scene.hpp" #include "core/scene/scene.hpp"
#include "game/debug_config.hpp" #include "game/debug_config.hpp"
@@ -51,4 +52,5 @@ class TitleScene : public Scene {
Phase phase_{Phase::IDLE}; Phase phase_{Phase::IDLE};
int starting_timer_ms_{0}; int starting_timer_ms_{0};
DebugConfig::StartingPlayers pending_players_{DebugConfig::StartingPlayers::BOTH}; DebugConfig::StartingPlayers pending_players_{DebugConfig::StartingPlayers::BOTH};
std::optional<Transitions::Mosaic> fade_out_;
}; };
+8
View File
@@ -266,6 +266,14 @@ namespace Defaults {
constexpr int STARTING_MS = 3000; constexpr int STARTING_MS = 3000;
constexpr float STARTING_BLINK_SPEEDUP = 2.0F; constexpr float STARTING_BLINK_SPEEDUP = 2.0F;
// Fade-out estil mosaic dissolve dins l'última part de la
// fase STARTING. `FADE_OUT_MS` ha de ser <= STARTING_MS;
// si és més gran, comença ja a t=0 i acaba abans del salt.
// `FADE_OUT_MAX_BLOCK` és la mida màxima de bloc en píxels
// (32 al canvas de 320×240 dóna 10×8 blocs al final).
constexpr int FADE_OUT_MS = 800;
constexpr int FADE_OUT_MAX_BLOCK = 32;
// Distorsió sinusoidal horitzontal de l'imatge de fons // Distorsió sinusoidal horitzontal de l'imatge de fons
// (efecte de calor del desert). `amplitude_px` és el // (efecte de calor del desert). `amplitude_px` és el
// desplaçament màxim per fila; `wavelength_px` la distància // desplaçament màxim per fila; `wavelength_px` la distància