#include "core/rendering/effects/scan_reveal.hpp" #include #include #include #include #include #include #include "core/rendering/screen.hpp" #include "core/rendering/surface.hpp" namespace Effects { namespace { // Hash determinista per a (row, time_bucket): retorna float a // [-1, 1]. Usat per generar el jitter horitzontal de la banda // sense necessitat d'un RNG d'estat (cada frame el resultat // canvia perquè el time_bucket canvia, però dins d'un mateix // bucket dues files diferents donen valors distints). auto rowJitter(int row, int time_bucket) -> float { const auto SEED = static_cast((row * 73856093) ^ (time_bucket * 19349663)); // Mix barato (xorshift) per descorrelacionar bits. std::uint32_t x = SEED; x ^= x << 13; x ^= x >> 17; x ^= x << 5; // Normalitzem a [-1, 1]. const float NORM = static_cast(x) / static_cast(UINT32_MAX); return (NORM * 2.0F) - 1.0F; } // Bucket de temps per al jitter: agrupant en blocs de ~50ms el // jitter "salta" 20 cops per segon — visualment "viu" sense // ser caos pur a 60fps. constexpr int JITTER_TIME_BUCKET_MS = 50; } // namespace ScanReveal::ScanReveal() : rng_(std::random_device{}()) {} void ScanReveal::begin(const Params& params) { params_ = params; elapsed_ms_ = 0; flash_remaining_ms_ = 0; flash_started_ = false; burst_remaining_ms_ = 0; burst_y_norm_ = 0.0F; burst_dx_ = 0; } void ScanReveal::tick(int delta_ms) { if (params_.duration_ms <= 0) { return; } // 1) Avanç del scan (clampejat a duration_ms). elapsed_ms_ = std::min(elapsed_ms_ + delta_ms, params_.duration_ms); // 2) Tick del burst VHS actiu, si n'hi ha. if (burst_remaining_ms_ > 0) { burst_remaining_ms_ = std::max(0, burst_remaining_ms_ - delta_ms); } // 3) Spawn d'un nou burst (només durant el scan, no al flash). // Probabilitat per frame ≈ (per_sec/1000) · delta_ms. if (burst_remaining_ms_ == 0 && elapsed_ms_ < params_.duration_ms && params_.vhs_burst_per_sec > 0 && params_.vhs_burst_duration_ms > 0) { const float P = (static_cast(params_.vhs_burst_per_sec) / 1000.0F) * static_cast(delta_ms); std::uniform_real_distribution dist01(0.0F, 1.0F); if (dist01(rng_) < P) { burst_remaining_ms_ = params_.vhs_burst_duration_ms; burst_y_norm_ = dist01(rng_); std::uniform_int_distribution dist_sign(0, 1); const int SIGN = (dist_sign(rng_) == 0) ? 1 : -1; burst_dx_ = SIGN * params_.vhs_burst_amplitude_px; } } // 4) Dispara el flash quan el scan acaba (una sola vegada). if (!flash_started_ && elapsed_ms_ >= params_.duration_ms && params_.flash_ms > 0) { flash_started_ = true; flash_remaining_ms_ = params_.flash_ms; } // Decrement del flash mentre estiga actiu. if (flash_remaining_ms_ > 0) { flash_remaining_ms_ = std::max(0, flash_remaining_ms_ - delta_ms); } } auto ScanReveal::isActive() const -> bool { return params_.duration_ms > 0 && !isFinished(); } auto ScanReveal::isFinished() const -> bool { if (params_.duration_ms <= 0) { return true; } // Acabat = scan al final + flash exhaurit (o sense flash). return elapsed_ms_ >= params_.duration_ms && flash_remaining_ms_ <= 0; } void ScanReveal::render(const Surface& src, int x, int y) const { if (params_.duration_ms <= 0) { // Sense efecte: render pla. src.render(x, y); return; } const int H = src.getHeight(); const auto W_F = static_cast(src.getWidth()); const float T = static_cast(elapsed_ms_) / static_cast(params_.duration_ms); // Banda: comença a y=-band_height i acaba a y=H. Així al t=0 cap // fila és visible i al t=1 totes ho són. const float SWEEP_F = -static_cast(params_.band_height_px) + (T * static_cast(H + params_.band_height_px)); const int BAND_TOP = static_cast(std::floor(SWEEP_F)); const int BAND_BOT = BAND_TOP + params_.band_height_px; const int TIME_BUCKET = elapsed_ms_ / JITTER_TIME_BUCKET_MS; const int BURST_Y_PX = static_cast(burst_y_norm_ * static_cast(H)); const bool BURST_ACTIVE = burst_remaining_ms_ > 0; // Render fila a fila (només files revelades). for (int row = 0; row <= BAND_BOT && row < H; ++row) { int dx = 0; // Jitter de banda: amplitud màxima a la banda, falloff // lineal cap als marges del radi de glitch. const int DIST_TO_BAND = std::abs(row - BAND_TOP); if (params_.glitch_band_radius_px > 0 && DIST_TO_BAND <= params_.glitch_band_radius_px && params_.glitch_amplitude_px > 0) { const float FALLOFF = 1.0F - (static_cast(DIST_TO_BAND) / static_cast(params_.glitch_band_radius_px)); const float JIT = rowJitter(row, TIME_BUCKET); dx += static_cast(std::lround( FALLOFF * static_cast(params_.glitch_amplitude_px) * JIT)); } // Burst VHS: només damunt la part ja revelada (row < BAND_TOP) // perquè per sota encara no hi ha imatge "estable" que poder // distorsionar visualment. if (BURST_ACTIVE && row < BAND_TOP && row >= BURST_Y_PX && row < BURST_Y_PX + params_.vhs_burst_height_px) { dx += burst_dx_; } SDL_FRect row_rect{0.0F, static_cast(row), W_F, 1.0F}; src.render(x + dx, y + row, &row_rect); } auto& screen = Screen::get(); // Capa lluminosa de la banda. Verticalment només cobreix les // files del logo (és on viu el scan), però horitzontalment // travessa tot el canvas com un feix CRT, així sobre fons negre // no es veuen les costures del rectangle del Surface. L'alpha // base es modula amb un pulse sinusoidal a `band_pulse_hz` per // simular el ritme de refresc d'un CRT. if (params_.band_alpha > 0 && params_.band_height_px > 0) { const int VISIBLE_TOP = std::clamp(BAND_TOP, 0, H); const int VISIBLE_BOT = std::clamp(BAND_BOT, 0, H); const int VISIBLE_H = VISIBLE_BOT - VISIBLE_TOP; if (VISIBLE_H > 0) { // Pulse: fracció ∈ [0,1] avançada a la freqüència donada, // mapejada a un multiplicador ∈ [1-depth, 1] sobre l'alpha // base. Amb depth=0 el pulse és inactiu (multiplicador 1). const float PULSE_DEPTH = std::clamp(params_.band_pulse_depth, 0.0F, 1.0F); const float TWO_PI = 2.0F * std::numbers::pi_v; const float PHASE = TWO_PI * params_.band_pulse_hz * (static_cast(elapsed_ms_) / 1000.0F); const float WAVE = 0.5F + (0.5F * std::sin(PHASE)); // 0..1 const float MULT = (1.0F - PULSE_DEPTH) + (PULSE_DEPTH * WAVE); const float ALPHA_F = static_cast(params_.band_alpha) * MULT; const auto ALPHA = static_cast( std::clamp(static_cast(std::round(ALPHA_F)), 0, 255)); if (ALPHA > 0) { screen.blendRect(0, y + VISIBLE_TOP, screen.getCanvasWidth(), VISIBLE_H, 0xFFFFFFFFU, ALPHA); } } } // Flash final: capa blanca damunt de TOT el canvas (no només // del rectangle del logo) — així el flash banya l'escena // sencera com un CRT i no es veuen les costures del Surface // sobre fons negre. Alpha proporcional a `flash_remaining_ms_ // / flash_ms` (decreix suau cap a 0 al final del flash). if (flash_remaining_ms_ > 0 && params_.flash_ms > 0) { const float FLASH_T = static_cast(flash_remaining_ms_) / static_cast(params_.flash_ms); const auto ALPHA = static_cast( std::round(255.0F * std::clamp(FLASH_T, 0.0F, 1.0F))); if (ALPHA > 0) { screen.blendRect(0, 0, screen.getCanvasWidth(), screen.getCanvasHeight(), 0xFFFFFFFFU, ALPHA); } } } } // namespace Effects