193 lines
8.6 KiB
C++
193 lines
8.6 KiB
C++
#include "core/rendering/effects/scan_reveal.hpp"
|
|
|
|
#include <SDL3/SDL.h>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <numbers>
|
|
#include <random>
|
|
|
|
#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<std::uint32_t>((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<float>(x) / static_cast<float>(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<float>(params_.vhs_burst_per_sec) / 1000.0F) * static_cast<float>(delta_ms);
|
|
std::uniform_real_distribution<float> 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<int> 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<float>(src.getWidth());
|
|
const float T = static_cast<float>(elapsed_ms_) / static_cast<float>(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<float>(params_.band_height_px) + (T * static_cast<float>(H + params_.band_height_px));
|
|
const int BAND_TOP = static_cast<int>(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<int>(burst_y_norm_ * static_cast<float>(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<float>(DIST_TO_BAND) / static_cast<float>(params_.glitch_band_radius_px));
|
|
const float JIT = rowJitter(row, TIME_BUCKET);
|
|
dx += static_cast<int>(std::lround(
|
|
FALLOFF * static_cast<float>(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<float>(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<float>;
|
|
const float PHASE = TWO_PI * params_.band_pulse_hz * (static_cast<float>(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<float>(params_.band_alpha) * MULT;
|
|
const auto ALPHA = static_cast<std::uint8_t>(
|
|
std::clamp(static_cast<int>(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<float>(flash_remaining_ms_) / static_cast<float>(params_.flash_ms);
|
|
const auto ALPHA = static_cast<std::uint8_t>(
|
|
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
|