feat(logo): scanline reveal CRT amb glitch, bursts VHS i pulse de banda
This commit is contained in:
1 parent
243ced7498
commit
e32cff9dac
9 files changed
+440
-20
No files matched your search
@@ -0,0 +1,192 @@
|
||||
#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
|
||||
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <random>
|
||||
|
||||
class Surface;
|
||||
|
||||
namespace Effects {
|
||||
|
||||
// Revelat tipus "scanline CRT" d'una Surface. Una banda lluminosa
|
||||
// baixa des de dalt cap a baix de la imatge: les files damunt de la
|
||||
// banda es renderitzen normalment (ja revelades), les files sota
|
||||
// queden invisibles, i a la banda mateixa s'aplica un overlay
|
||||
// blanc semi-transparent. Al voltant de la banda les files reben un
|
||||
// jitter horitzontal aleatori (falloff lineal amb la distància) i
|
||||
// damunt la part ja revelada apareixen ocasionalment "bursts VHS" —
|
||||
// franges horitzontals desplaçades durant uns ms — per a un toc
|
||||
// demoscene/VHS deteriorat.
|
||||
//
|
||||
// Quan la banda surt per sota del logo i s'esgota `duration_ms`,
|
||||
// dispara un flash blanc breu i passa a `isFinished()=true`. El
|
||||
// caller decideix què fa amb el control de fase a partir d'aquí.
|
||||
//
|
||||
// Ús:
|
||||
// ScanReveal sr;
|
||||
// sr.begin({.duration_ms = 1500, ...});
|
||||
// // cada frame:
|
||||
// sr.tick(delta_ms);
|
||||
// sr.render(*logo, x, y);
|
||||
// if (sr.isFinished()) { ... canvi de fase ... }
|
||||
class ScanReveal {
|
||||
public:
|
||||
struct Params {
|
||||
int duration_ms;
|
||||
int band_height_px;
|
||||
int band_alpha;
|
||||
float band_pulse_hz;
|
||||
float band_pulse_depth;
|
||||
int glitch_amplitude_px;
|
||||
int glitch_band_radius_px;
|
||||
int vhs_burst_per_sec;
|
||||
int vhs_burst_duration_ms;
|
||||
int vhs_burst_height_px;
|
||||
int vhs_burst_amplitude_px;
|
||||
int flash_ms;
|
||||
};
|
||||
|
||||
ScanReveal();
|
||||
|
||||
void begin(const Params& params);
|
||||
void tick(int delta_ms);
|
||||
|
||||
// Renderitza un frame del revelat sobre la Surface `src`. El
|
||||
// caller s'encarrega de pintar negre per sota (les files no
|
||||
// revelades simplement no es dibuixen — queda el que hi haja al
|
||||
// canvas, normalment negre).
|
||||
void render(const Surface& src, int x, int y) const;
|
||||
|
||||
[[nodiscard]] auto isActive() const -> bool;
|
||||
[[nodiscard]] auto isFinished() const -> bool;
|
||||
|
||||
private:
|
||||
Params params_{};
|
||||
int elapsed_ms_{0};
|
||||
// Flash post-scan: arranca quan el scan toca fons; decreix amb
|
||||
// `delta_ms` fins a 0. Mentre `flash_active_` és true encara no
|
||||
// hem reportat finished (donant marge per pintar el flash sencer).
|
||||
int flash_remaining_ms_{0};
|
||||
bool flash_started_{false};
|
||||
|
||||
// Burst VHS actiu (com a màxim un alhora — simplifica i ja és
|
||||
// suficient per al ritme visual). y_norm ∈ [0,1] es resol en
|
||||
// píxels al `render()` perquè aquí no coneixem l'alçada del logo.
|
||||
int burst_remaining_ms_{0};
|
||||
float burst_y_norm_{0.0F};
|
||||
int burst_dx_{0};
|
||||
|
||||
// RNG per als bursts. Seed des de random_device a `begin()`
|
||||
// perquè cada execució sigui visualment diferent.
|
||||
std::minstd_rand rng_;
|
||||
};
|
||||
|
||||
} // namespace Effects
|
||||
Reference in new issue
Block a user