feat(transitions): mosaic dissolve fade-out al final de la fase STARTING (configurable a ui.yaml)
This commit is contained in:
1 parent
ca64115bfe
commit
0618452224
9 files changed
+173
-8
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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_;
|
||||||
};
|
};
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user