feat(effects): heat-haze sinusoidal al fons del títol (configurable a ui.yaml)

This commit is contained in:
JailDesigner committed 2026-05-06 11:48:29 +02:00
1 parent 9233bd8d75
commit 1fc3ab8e31
9 files changed
+150 -3

No files matched your search

@@ -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
+13
View File
@@ -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")) {
+9
View File
@@ -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_;