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_parser.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/viewport.cpp
|
||||
source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp
|
||||
|
||||
@@ -120,6 +120,12 @@ title:
|
||||
# finestra, el comptador es reseteja i s'arrenca amb BOTH.
|
||||
starting_ms: 3000
|
||||
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).
|
||||
# `amplitude_px`: desplaçament màxim de cada fila en píxels.
|
||||
# `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_starting_ms_(Defaults::UI::Title::STARTING_MS),
|
||||
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_{
|
||||
.enabled = Defaults::UI::Title::HeatHaze::ENABLED,
|
||||
.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, "starting_ms", title_starting_ms_);
|
||||
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")) {
|
||||
const auto& hh = t["heat_haze"];
|
||||
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 titleStartingMs() const -> int { return title_starting_ms_; }
|
||||
[[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_; }
|
||||
|
||||
// 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_starting_ms_;
|
||||
float title_starting_blink_speedup_;
|
||||
int title_fade_out_ms_;
|
||||
int title_fade_out_max_block_;
|
||||
TitleHeatHaze title_heat_haze_;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
fade_out_.emplace(Transitions::Mosaic::Params{
|
||||
.max_block_size = UiConfig::get().titleFadeOutMaxBlock(),
|
||||
});
|
||||
}
|
||||
|
||||
TitleScene::~TitleScene() = default;
|
||||
@@ -119,25 +122,38 @@ void TitleScene::update(int delta_ms) {
|
||||
} else if (P2_START) {
|
||||
enterStarting(DebugConfig::StartingPlayers::P2);
|
||||
} else if (timer_ms_ <= 0) {
|
||||
// Attract: ningú ha premut. Saltem a la següent escena
|
||||
// amb BOTH per defecte. Demà, quan tinguem una DemoScene,
|
||||
// ací anirà la transició a demo en lloc de a intro.
|
||||
advanceTo(DebugConfig::StartingPlayers::BOTH);
|
||||
// Attract: ningú ha premut. Entrem a STARTING amb BOTH
|
||||
// perquè el fade-out de transició també hi compti.
|
||||
// Demà, quan tinguem una DemoScene, ací anirà la
|
||||
// transició a demo en lloc de a intro.
|
||||
enterStarting(DebugConfig::StartingPlayers::BOTH);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Phase::STARTING: {
|
||||
starting_timer_ms_ -= delta_ms;
|
||||
// Si l'altre jugador prem START dins de la finestra, pugem
|
||||
// a BOTH i resetegem el comptador. Pulsacions repetides del
|
||||
// mateix jugador ja "starting" no fan res.
|
||||
// Si l'altre jugador prem START dins de la finestra (i el
|
||||
// fade encara no ha arrencat), pugem a BOTH i resetegem el
|
||||
// comptador. Una vegada el fade ja està actiu, ignorem
|
||||
// pulsacions per no saltar visualment la transició.
|
||||
const bool OTHER_PRESSED =
|
||||
(pending_players_ == DebugConfig::StartingPlayers::P1 && P2_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;
|
||||
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) {
|
||||
advanceTo(pending_players_);
|
||||
}
|
||||
@@ -245,6 +261,14 @@ void TitleScene::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();
|
||||
Screen::get().present();
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <optional>
|
||||
|
||||
#include "core/rendering/effects/heat_haze.hpp"
|
||||
#include "core/rendering/transitions/mosaic.hpp"
|
||||
#include "core/scene/scene.hpp"
|
||||
#include "game/debug_config.hpp"
|
||||
|
||||
@@ -51,4 +52,5 @@ class TitleScene : public Scene {
|
||||
Phase phase_{Phase::IDLE};
|
||||
int starting_timer_ms_{0};
|
||||
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 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
|
||||
// (efecte de calor del desert). `amplitude_px` és el
|
||||
// desplaçament màxim per fila; `wavelength_px` la distància
|
||||
|
||||
Reference in new issue
Block a user