feat(effects): heat-haze sinusoidal al fons del títol (configurable a ui.yaml)
This commit is contained in:
1 parent
9233bd8d75
commit
1fc3ab8e31
9 files changed
+150
-3
No files matched your search
@@ -92,6 +92,7 @@ set(APP_SOURCES
|
|||||||
source/core/rendering/sprite/moving_sprite.cpp
|
source/core/rendering/sprite/moving_sprite.cpp
|
||||||
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/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
|
||||||
|
|||||||
@@ -113,3 +113,13 @@ title:
|
|||||||
copyright_color: 246
|
copyright_color: 246
|
||||||
blink_visible_ms: 800
|
blink_visible_ms: 800
|
||||||
blink_hidden_ms: 200
|
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
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#include "core/rendering/effects/heat_haze.hpp"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <numbers>
|
||||||
|
|
||||||
|
#include "core/rendering/surface.hpp"
|
||||||
|
|
||||||
|
namespace Effects {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr float TWO_PI = 2.0F * std::numbers::pi_v<float>;
|
||||||
|
}
|
||||||
|
|
||||||
|
HeatHaze::HeatHaze(Params params)
|
||||||
|
: params_(params) {}
|
||||||
|
|
||||||
|
void HeatHaze::tick(int delta_ms) {
|
||||||
|
const float DT = static_cast<float>(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<float>(src.getWidth());
|
||||||
|
for (int row = 0; row < H; ++row) {
|
||||||
|
const float ARG = (TWO_PI * static_cast<float>(row) / WL) + phase_;
|
||||||
|
const int DX = static_cast<int>(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<float>(row), W, 1.0F};
|
||||||
|
src.render(x + DX, y + row, &row_rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Effects
|
||||||
@@ -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
|
||||||
@@ -102,6 +102,12 @@ UiConfig::UiConfig()
|
|||||||
title_copyright_color_(Defaults::UI::Title::COPYRIGHT_COLOR),
|
title_copyright_color_(Defaults::UI::Title::COPYRIGHT_COLOR),
|
||||||
title_blink_visible_ms_(Defaults::UI::Title::BLINK_VISIBLE_MS),
|
title_blink_visible_ms_(Defaults::UI::Title::BLINK_VISIBLE_MS),
|
||||||
title_blink_hidden_ms_(Defaults::UI::Title::BLINK_HIDDEN_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_select_sound_(Defaults::UI::MENU_SELECT_SOUND),
|
||||||
menu_accept_sound_(Defaults::UI::MENU_ACCEPT_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},
|
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_);
|
readColorIndex(t, "copyright_color", title_copyright_color_);
|
||||||
readField(t, "blink_visible_ms", title_blink_visible_ms_);
|
readField(t, "blink_visible_ms", title_blink_visible_ms_);
|
||||||
readField(t, "blink_hidden_ms", title_blink_hidden_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")) {
|
if (yaml.contains("notifier")) {
|
||||||
|
|||||||
@@ -82,12 +82,20 @@ class UiConfig {
|
|||||||
[[nodiscard]] auto pauseExitMs() const -> int { return pause_exit_ms_; }
|
[[nodiscard]] auto pauseExitMs() const -> int { return pause_exit_ms_; }
|
||||||
|
|
||||||
// Escena TITLE (placeholder visual fins que tinguem artwork).
|
// 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 titleDurationMs() const -> int { return title_duration_ms_; }
|
||||||
[[nodiscard]] auto titleBgColor() const -> Uint8 { return title_bg_color_; }
|
[[nodiscard]] auto titleBgColor() const -> Uint8 { return title_bg_color_; }
|
||||||
[[nodiscard]] auto titleTextColor() const -> Uint8 { return title_text_color_; }
|
[[nodiscard]] auto titleTextColor() const -> Uint8 { return title_text_color_; }
|
||||||
[[nodiscard]] auto titleCopyrightColor() const -> Uint8 { return title_copyright_color_; }
|
[[nodiscard]] auto titleCopyrightColor() const -> Uint8 { return title_copyright_color_; }
|
||||||
[[nodiscard]] auto titleBlinkVisibleMs() const -> int { return title_blink_visible_ms_; }
|
[[nodiscard]] auto titleBlinkVisibleMs() const -> int { return title_blink_visible_ms_; }
|
||||||
[[nodiscard]] auto titleBlinkHiddenMs() const -> int { return title_blink_hidden_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
|
// Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa
|
||||||
// Easing::linear com a fallback.
|
// Easing::linear com a fallback.
|
||||||
@@ -159,6 +167,7 @@ class UiConfig {
|
|||||||
Uint8 title_copyright_color_;
|
Uint8 title_copyright_color_;
|
||||||
int title_blink_visible_ms_;
|
int title_blink_visible_ms_;
|
||||||
int title_blink_hidden_ms_;
|
int title_blink_hidden_ms_;
|
||||||
|
TitleHeatHaze title_heat_haze_;
|
||||||
|
|
||||||
std::string menu_select_sound_;
|
std::string menu_select_sound_;
|
||||||
std::string menu_accept_sound_;
|
std::string menu_accept_sound_;
|
||||||
|
|||||||
@@ -56,6 +56,14 @@ TitleScene::TitleScene(std::shared_ptr<Text> display_font, std::shared_ptr<Text>
|
|||||||
next_factory_(std::move(next_factory)),
|
next_factory_(std::move(next_factory)),
|
||||||
timer_ms_(UiConfig::get().titleDurationMs()) {
|
timer_ms_(UiConfig::get().titleDurationMs()) {
|
||||||
std::cout << "[scene] " << SCENE_NAME << " start\n";
|
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;
|
TitleScene::~TitleScene() = default;
|
||||||
@@ -69,6 +77,7 @@ void TitleScene::update(int delta_ms) {
|
|||||||
|
|
||||||
timer_ms_ -= delta_ms;
|
timer_ms_ -= delta_ms;
|
||||||
blink_elapsed_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) {
|
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);
|
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");
|
const auto& palette = Resource::Cache::get().getPalette("ui");
|
||||||
|
|
||||||
// Fons: la imatge si està disponible (cobreix tot el canvas amb la
|
// Fons: la imatge si està disponible (cobreix tot el canvas amb la
|
||||||
// seua paleta nadiua); en cas contrari, color pla de la paleta UI
|
// seua paleta nadiua), opcionalment amb heat-haze; en cas contrari,
|
||||||
// com a fallback defensiu.
|
// color pla de la paleta UI com a fallback defensiu.
|
||||||
if (background_) {
|
if (background_) {
|
||||||
background_->render(0, 0);
|
if (heat_haze_) {
|
||||||
|
heat_haze_->render(*background_, 0, 0);
|
||||||
|
} else {
|
||||||
|
background_->render(0, 0);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, palette[ui_config.titleBgColor()]);
|
Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, palette[ui_config.titleBgColor()]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include "core/rendering/effects/heat_haze.hpp"
|
||||||
#include "core/scene/scene.hpp"
|
#include "core/scene/scene.hpp"
|
||||||
|
|
||||||
class Surface;
|
class Surface;
|
||||||
@@ -30,4 +32,5 @@ class TitleScene : public Scene {
|
|||||||
SceneFactory next_factory_;
|
SceneFactory next_factory_;
|
||||||
int timer_ms_;
|
int timer_ms_;
|
||||||
int blink_elapsed_ms_{0};
|
int blink_elapsed_ms_{0};
|
||||||
|
std::optional<Effects::HeatHaze> heat_haze_;
|
||||||
};
|
};
|
||||||
@@ -258,6 +258,19 @@ namespace Defaults {
|
|||||||
constexpr Uint8 COPYRIGHT_COLOR = 246; // (218,218,255) off-white amb un mínim de fred.
|
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_VISIBLE_MS = 800;
|
||||||
constexpr int BLINK_HIDDEN_MS = 200;
|
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 Title
|
||||||
} // namespace UI
|
} // namespace UI
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user