From 1fc3ab8e3177c67adb77751c37861f60ab497bca Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 6 May 2026 11:48:29 +0200 Subject: [PATCH] =?UTF-8?q?feat(effects):=20heat-haze=20sinusoidal=20al=20?= =?UTF-8?q?fons=20del=20t=C3=ADtol=20(configurable=20a=20ui.yaml)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 1 + data/ui.yaml | 10 +++++ source/core/rendering/effects/heat_haze.cpp | 48 +++++++++++++++++++++ source/core/rendering/effects/heat_haze.hpp | 37 ++++++++++++++++ source/core/ui/ui_config.cpp | 13 ++++++ source/core/ui/ui_config.hpp | 9 ++++ source/game/scenes/title_scene.cpp | 19 ++++++-- source/game/scenes/title_scene.hpp | 3 ++ source/utils/defaults.hpp | 13 ++++++ 9 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 source/core/rendering/effects/heat_haze.cpp create mode 100644 source/core/rendering/effects/heat_haze.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d7cf101..cc6f284 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -92,6 +92,7 @@ set(APP_SOURCES source/core/rendering/sprite/moving_sprite.cpp 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/sdl3gpu/sdl3gpu_shader.cpp source/core/rendering/sdl3gpu/viewport.cpp source/core/rendering/sdl3gpu/pipelines/crtpi_pipeline.cpp diff --git a/data/ui.yaml b/data/ui.yaml index f1024ff..5f9e12a 100644 --- a/data/ui.yaml +++ b/data/ui.yaml @@ -113,3 +113,13 @@ title: copyright_color: 246 blink_visible_ms: 800 blink_hidden_ms: 200 + # 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. + # `speed_hz`: velocitat amb què avança la fase (cicles per segon). + # Si `enabled` és false, el fons es pinta sense distorsió. + heat_haze: + enabled: true + amplitude_px: 1.5 + wavelength_px: 32.0 + speed_hz: 0.6 diff --git a/source/core/rendering/effects/heat_haze.cpp b/source/core/rendering/effects/heat_haze.cpp new file mode 100644 index 0000000..93dc09c --- /dev/null +++ b/source/core/rendering/effects/heat_haze.cpp @@ -0,0 +1,48 @@ +#include "core/rendering/effects/heat_haze.hpp" + +#include + +#include +#include + +#include "core/rendering/surface.hpp" + +namespace Effects { + + namespace { + constexpr float TWO_PI = 2.0F * std::numbers::pi_v; + } + + HeatHaze::HeatHaze(Params params) + : params_(params) {} + + void HeatHaze::tick(int delta_ms) { + const float DT = static_cast(delta_ms) / 1000.0F; + phase_ = std::fmod(phase_ + (TWO_PI * params_.speed_hz * DT), TWO_PI); + } + + void HeatHaze::render(const Surface& src, int x, int y) const { + // Pas 1 — blit pla de tota la Surface al destí. És la base que + // tapa els marges que les files desplaçades deixarien al + // descobert, evitant haver de demanar al caller un pre-fill. Per + // a fons amb gradient continu (cels, sorres) la costura amb el + // pas 2 no es nota; per a fons d'alt contrast cal moderar + // l'amplitud. + src.render(x, y); + + // Pas 2 — re-blit fila a fila amb desplaçament horitzontal sin. + // Wavelength <=0 dóna divisió absurda: cau a 1 px com a salvaguarda; + // a la pràctica el caller passa un valor >0 vingut d'ui.yaml. + const float WL = (params_.wavelength_px > 0.0F) ? params_.wavelength_px : 1.0F; + const int H = src.getHeight(); + const auto W = static_cast(src.getWidth()); + for (int row = 0; row < H; ++row) { + const float ARG = (TWO_PI * static_cast(row) / WL) + phase_; + const int DX = static_cast(std::lround(params_.amplitude_px * std::sin(ARG))); + if (DX == 0) { continue; } // El pas 1 ja ha pintat aquesta fila on toca. + SDL_FRect row_rect{0.0F, static_cast(row), W, 1.0F}; + src.render(x + DX, y + row, &row_rect); + } + } + +} // namespace Effects diff --git a/source/core/rendering/effects/heat_haze.hpp b/source/core/rendering/effects/heat_haze.hpp new file mode 100644 index 0000000..893e4c7 --- /dev/null +++ b/source/core/rendering/effects/heat_haze.hpp @@ -0,0 +1,37 @@ +#pragma once + +class Surface; + +namespace Effects { + + // Distorsió sinusoidal horitzontal de cada fila: dx(y) = + // amplitude · sin(2π·y/wavelength + phase). La phase avança al + // ritme marcat per `speed_hz` (cicles per segon). Es pinta sobre + // qualsevol Surface — habitualment un fons de canvas — i manté el + // seu propi estat temporal, així dues instàncies poden tenir + // paràmetres i fases independents. + // + // `render()` fa dos passos: primer un blit pla de tota la Surface + // (perquè els marges no quedin descoberts pel desplaçament) i + // després el re-blit fila a fila amb el sin. La costura és + // invisible en fons amb gradient continu i pràcticament invisible + // en altres si l'amplitud és moderada. + class HeatHaze { + public: + struct Params { + float amplitude_px; + float wavelength_px; + float speed_hz; + }; + + explicit HeatHaze(Params params); + + void tick(int delta_ms); + void render(const Surface& src, int x, int y) const; + + private: + Params params_; + float phase_{0.0F}; + }; + +} // namespace Effects diff --git a/source/core/ui/ui_config.cpp b/source/core/ui/ui_config.cpp index 2c39ecf..09e44c9 100644 --- a/source/core/ui/ui_config.cpp +++ b/source/core/ui/ui_config.cpp @@ -102,6 +102,12 @@ UiConfig::UiConfig() title_copyright_color_(Defaults::UI::Title::COPYRIGHT_COLOR), title_blink_visible_ms_(Defaults::UI::Title::BLINK_VISIBLE_MS), title_blink_hidden_ms_(Defaults::UI::Title::BLINK_HIDDEN_MS), + title_heat_haze_{ + .enabled = Defaults::UI::Title::HeatHaze::ENABLED, + .amplitude_px = Defaults::UI::Title::HeatHaze::AMPLITUDE_PX, + .wavelength_px = Defaults::UI::Title::HeatHaze::WAVELENGTH_PX, + .speed_hz = Defaults::UI::Title::HeatHaze::SPEED_HZ, + }, menu_select_sound_(Defaults::UI::MENU_SELECT_SOUND), menu_accept_sound_(Defaults::UI::MENU_ACCEPT_SOUND), hud_font_{.name = Defaults::UI::Font::HUD_NAME, .gif = Defaults::UI::Font::HUD_GIF, .fnt = Defaults::UI::Font::HUD_FNT, .kerning = Defaults::UI::Font::HUD_KERNING}, @@ -218,6 +224,13 @@ void UiConfig::load(const std::string& file_path) { readColorIndex(t, "copyright_color", title_copyright_color_); readField(t, "blink_visible_ms", title_blink_visible_ms_); readField(t, "blink_hidden_ms", title_blink_hidden_ms_); + if (t.contains("heat_haze")) { + const auto& hh = t["heat_haze"]; + readField(hh, "enabled", title_heat_haze_.enabled); + readField(hh, "amplitude_px", title_heat_haze_.amplitude_px); + readField(hh, "wavelength_px", title_heat_haze_.wavelength_px); + readField(hh, "speed_hz", title_heat_haze_.speed_hz); + } } if (yaml.contains("notifier")) { diff --git a/source/core/ui/ui_config.hpp b/source/core/ui/ui_config.hpp index c1abac0..e9df296 100644 --- a/source/core/ui/ui_config.hpp +++ b/source/core/ui/ui_config.hpp @@ -82,12 +82,20 @@ class UiConfig { [[nodiscard]] auto pauseExitMs() const -> int { return pause_exit_ms_; } // Escena TITLE (placeholder visual fins que tinguem artwork). + struct TitleHeatHaze { + bool enabled; + float amplitude_px; + float wavelength_px; + float speed_hz; + }; + [[nodiscard]] auto titleDurationMs() const -> int { return title_duration_ms_; } [[nodiscard]] auto titleBgColor() const -> Uint8 { return title_bg_color_; } [[nodiscard]] auto titleTextColor() const -> Uint8 { return title_text_color_; } [[nodiscard]] auto titleCopyrightColor() const -> Uint8 { return title_copyright_color_; } [[nodiscard]] auto titleBlinkVisibleMs() const -> int { return title_blink_visible_ms_; } [[nodiscard]] auto titleBlinkHiddenMs() const -> int { return title_blink_hidden_ms_; } + [[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; } // Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa // Easing::linear com a fallback. @@ -159,6 +167,7 @@ class UiConfig { Uint8 title_copyright_color_; int title_blink_visible_ms_; int title_blink_hidden_ms_; + TitleHeatHaze title_heat_haze_; std::string menu_select_sound_; std::string menu_accept_sound_; diff --git a/source/game/scenes/title_scene.cpp b/source/game/scenes/title_scene.cpp index 012545b..cfe1d2e 100644 --- a/source/game/scenes/title_scene.cpp +++ b/source/game/scenes/title_scene.cpp @@ -56,6 +56,14 @@ TitleScene::TitleScene(std::shared_ptr display_font, std::shared_ptr next_factory_(std::move(next_factory)), timer_ms_(UiConfig::get().titleDurationMs()) { std::cout << "[scene] " << SCENE_NAME << " start\n"; + const auto& hh = UiConfig::get().titleHeatHaze(); + if (hh.enabled) { + heat_haze_.emplace(Effects::HeatHaze::Params{ + .amplitude_px = hh.amplitude_px, + .wavelength_px = hh.wavelength_px, + .speed_hz = hh.speed_hz, + }); + } } TitleScene::~TitleScene() = default; @@ -69,6 +77,7 @@ void TitleScene::update(int delta_ms) { timer_ms_ -= delta_ms; blink_elapsed_ms_ += delta_ms; + if (heat_haze_) { heat_haze_->tick(delta_ms); } const bool SKIPPED = std::ranges::any_of(EventBuffer::events, [](const SDL_Event& ev) { return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode); @@ -87,10 +96,14 @@ void TitleScene::render() { const auto& palette = Resource::Cache::get().getPalette("ui"); // Fons: la imatge si està disponible (cobreix tot el canvas amb la - // seua paleta nadiua); en cas contrari, color pla de la paleta UI - // com a fallback defensiu. + // seua paleta nadiua), opcionalment amb heat-haze; en cas contrari, + // color pla de la paleta UI com a fallback defensiu. if (background_) { - background_->render(0, 0); + if (heat_haze_) { + heat_haze_->render(*background_, 0, 0); + } else { + background_->render(0, 0); + } } else { Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, palette[ui_config.titleBgColor()]); } diff --git a/source/game/scenes/title_scene.hpp b/source/game/scenes/title_scene.hpp index 4040a84..062dcff 100644 --- a/source/game/scenes/title_scene.hpp +++ b/source/game/scenes/title_scene.hpp @@ -2,7 +2,9 @@ #include #include +#include +#include "core/rendering/effects/heat_haze.hpp" #include "core/scene/scene.hpp" class Surface; @@ -30,4 +32,5 @@ class TitleScene : public Scene { SceneFactory next_factory_; int timer_ms_; int blink_elapsed_ms_{0}; + std::optional heat_haze_; }; diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 9ce4d79..0ef540c 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -258,6 +258,19 @@ namespace Defaults { constexpr Uint8 COPYRIGHT_COLOR = 246; // (218,218,255) off-white amb un mínim de fred. constexpr int BLINK_VISIBLE_MS = 800; constexpr int BLINK_HIDDEN_MS = 200; + + // 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 + // vertical entre dos pics; `speed_hz` la velocitat amb què + // avança la fase (cicles/segon). Si `enabled` és false el + // fons es pinta sense distorsió. + namespace HeatHaze { + constexpr bool ENABLED = true; + constexpr float AMPLITUDE_PX = 1.5F; + constexpr float WAVELENGTH_PX = 32.0F; + constexpr float SPEED_HZ = 0.6F; + } // namespace HeatHaze } // namespace Title } // namespace UI