From f10671d9c041e6848481166b808a20c35b36de66 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Thu, 7 May 2026 16:44:34 +0200 Subject: [PATCH] feat(logo): timeline lineal de 7 fases amb glitch tail i durades al YAML --- source/core/rendering/effects/scan_reveal.cpp | 39 ++++++- source/core/rendering/effects/scan_reveal.hpp | 17 ++- source/core/ui/ui_config.cpp | 17 +++ source/core/ui/ui_config.hpp | 19 +++- source/game/scenes/logo_scene.cpp | 104 +++++++++++------- source/game/scenes/logo_scene.hpp | 49 +++++---- source/utils/defaults.hpp | 35 +++++- 7 files changed, 204 insertions(+), 76 deletions(-) diff --git a/source/core/rendering/effects/scan_reveal.cpp b/source/core/rendering/effects/scan_reveal.cpp index 756ea65..851e648 100644 --- a/source/core/rendering/effects/scan_reveal.cpp +++ b/source/core/rendering/effects/scan_reveal.cpp @@ -50,6 +50,8 @@ namespace Effects { burst_remaining_ms_ = 0; burst_y_norm_ = 0.0F; burst_dx_ = 0; + tail_started_ = false; + tail_remaining_ms_ = 0; } void ScanReveal::tick(int delta_ms) { @@ -63,9 +65,13 @@ namespace Effects { 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). + // 3) Spawn d'un nou burst. Permès durant el scan i durant el + // glitch tail post-flash; bloquejat exactament durant el + // flash perquè aquell és un punt visual de tancament net. // 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 bool TAIL_ACTIVE = tail_started_ && tail_remaining_ms_ > 0; + const bool SCAN_ONGOING = elapsed_ms_ < params_.duration_ms; + if (burst_remaining_ms_ == 0 && (SCAN_ONGOING || TAIL_ACTIVE) && 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) { @@ -86,6 +92,17 @@ namespace Effects { if (flash_remaining_ms_ > 0) { flash_remaining_ms_ = std::max(0, flash_remaining_ms_ - delta_ms); } + + // 5) Glitch tail: arranca un sol cop quan el scan ha acabat i el + // flash s'ha esgotat. Si no hi havia flash configurat, també + // arranca tot just acabar el scan. + if (!tail_started_ && elapsed_ms_ >= params_.duration_ms && flash_remaining_ms_ <= 0 && params_.tail_glitch_ms > 0) { + tail_started_ = true; + tail_remaining_ms_ = params_.tail_glitch_ms; + } + if (tail_remaining_ms_ > 0) { + tail_remaining_ms_ = std::max(0, tail_remaining_ms_ - delta_ms); + } } auto ScanReveal::isActive() const -> bool { @@ -94,8 +111,12 @@ namespace Effects { 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; + // Acabat = scan al final + flash exhaurit + glitch tail + // exhaurit (o sense tail configurat). + const bool SCAN_DONE = elapsed_ms_ >= params_.duration_ms; + const bool FLASH_DONE = flash_remaining_ms_ <= 0; + const bool TAIL_DONE = (params_.tail_glitch_ms <= 0) || (tail_started_ && tail_remaining_ms_ <= 0); + return SCAN_DONE && FLASH_DONE && TAIL_DONE; } void ScanReveal::render(const Surface& src, int x, int y) const { @@ -118,15 +139,21 @@ namespace Effects { 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; + // Durant el glitch tail post-flash, BAND_TOP queda just damunt + // del peu del logo, fet que (amb el codi de jitter que segueix) + // generaria jitter residual a les últimes files. Aquí el + // suprimim explícitament: el tail només viu de bursts VHS. + const bool TAIL_ACTIVE = tail_started_ && tail_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. + // lineal cap als marges del radi de glitch. Inactiu durant + // el glitch tail (no hi ha banda). 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) { + if (!TAIL_ACTIVE && 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( diff --git a/source/core/rendering/effects/scan_reveal.hpp b/source/core/rendering/effects/scan_reveal.hpp index 50cff26..0836007 100644 --- a/source/core/rendering/effects/scan_reveal.hpp +++ b/source/core/rendering/effects/scan_reveal.hpp @@ -18,8 +18,11 @@ namespace Effects { // 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í. + // dispara un flash blanc breu. Si `tail_glitch_ms > 0`, després del + // flash el logo queda totalment visible mentre els bursts VHS + // continuen saltant durant aquest temps (glitch tail). Quan el tail + // s'exhaureix `isFinished()=true`. El caller decideix què fa amb el + // control de fase a partir d'aquí. // // Ús: // ScanReveal sr; @@ -43,6 +46,10 @@ namespace Effects { int vhs_burst_height_px; int vhs_burst_amplitude_px; int flash_ms; + // Glitch tail post-flash: el logo queda visible sencer i els + // bursts VHS continuen saltant durant aquest temps. 0 ho + // desactiva (efecte acaba al flash). + int tail_glitch_ms; }; ScanReveal(); @@ -75,6 +82,12 @@ namespace Effects { float burst_y_norm_{0.0F}; int burst_dx_{0}; + // Glitch tail post-flash: arranca quan el flash s'esgota i + // permet que els bursts continuen saltant amb el logo ja del tot + // visible. `isFinished()` espera al final d'aquest tail. + bool tail_started_{false}; + int tail_remaining_ms_{0}; + // RNG per als bursts. Seed des de random_device a `begin()` // perquè cada execució sigui visualment diferent. std::minstd_rand rng_; diff --git a/source/core/ui/ui_config.cpp b/source/core/ui/ui_config.cpp index 5adfe66..051e03f 100644 --- a/source/core/ui/ui_config.cpp +++ b/source/core/ui/ui_config.cpp @@ -131,6 +131,7 @@ UiConfig::UiConfig() }, logo_scan_{ .duration_ms = Defaults::UI::Logo::Scan::DURATION_MS, + .tail_glitch_ms = Defaults::UI::Logo::Scan::TAIL_GLITCH_MS, .band_height_px = Defaults::UI::Logo::Scan::BAND_HEIGHT_PX, .band_alpha = Defaults::UI::Logo::Scan::BAND_ALPHA, .band_pulse_hz = Defaults::UI::Logo::Scan::BAND_PULSE_HZ, @@ -143,6 +144,13 @@ UiConfig::UiConfig() .vhs_burst_amplitude_px = Defaults::UI::Logo::Scan::VHS_BURST_AMPLITUDE_PX, .flash_ms = Defaults::UI::Logo::Scan::FLASH_MS, }, + logo_timeline_{ + .black_intro_ms = Defaults::UI::Logo::Timeline::BLACK_INTRO_MS, + .hold_silent_ms = Defaults::UI::Logo::Timeline::HOLD_SILENT_MS, + .hold_with_music_ms = Defaults::UI::Logo::Timeline::HOLD_WITH_MUSIC_MS, + .fade_out_ms = Defaults::UI::Logo::Timeline::FADE_OUT_MS, + .black_outro_ms = Defaults::UI::Logo::Timeline::BLACK_OUTRO_MS, + }, menu_select_sound_(Defaults::UI::MENU_SELECT_SOUND), menu_accept_sound_(Defaults::UI::MENU_ACCEPT_SOUND), player_start_sound_(Defaults::UI::PLAYER_START_SOUND), @@ -301,6 +309,7 @@ void UiConfig::load(const std::string& file_path) { if (l.contains("scan")) { const auto& s = l["scan"]; readField(s, "duration_ms", logo_scan_.duration_ms); + readField(s, "tail_glitch_ms", logo_scan_.tail_glitch_ms); readField(s, "band_height_px", logo_scan_.band_height_px); readField(s, "band_alpha", logo_scan_.band_alpha); readField(s, "band_pulse_hz", logo_scan_.band_pulse_hz); @@ -313,6 +322,14 @@ void UiConfig::load(const std::string& file_path) { readField(s, "vhs_burst_amplitude_px", logo_scan_.vhs_burst_amplitude_px); readField(s, "flash_ms", logo_scan_.flash_ms); } + if (l.contains("timeline")) { + const auto& tl = l["timeline"]; + readField(tl, "black_intro_ms", logo_timeline_.black_intro_ms); + readField(tl, "hold_silent_ms", logo_timeline_.hold_silent_ms); + readField(tl, "hold_with_music_ms", logo_timeline_.hold_with_music_ms); + readField(tl, "fade_out_ms", logo_timeline_.fade_out_ms); + readField(tl, "black_outro_ms", logo_timeline_.black_outro_ms); + } } if (yaml.contains("notifier")) { diff --git a/source/core/ui/ui_config.hpp b/source/core/ui/ui_config.hpp index a8978d2..efe61ed 100644 --- a/source/core/ui/ui_config.hpp +++ b/source/core/ui/ui_config.hpp @@ -124,9 +124,12 @@ class UiConfig { [[nodiscard]] auto titleHeatHaze() const -> const TitleHeatHaze& { return title_heat_haze_; } // Escena LOGO — paràmetres del scanline reveal del logo de l'editora. - // Tot s'aplica només a la fase SCAN (substitueix l'antic FADE_IN). + // `duration_ms` és la durada de la línia descendent; `tail_glitch_ms` + // és el temps post-flash en què el logo queda totalment visible + // però els bursts VHS encara hi salten (glitch tail). struct LogoScan { int duration_ms; + int tail_glitch_ms; int band_height_px; int band_alpha; float band_pulse_hz; @@ -142,6 +145,19 @@ class UiConfig { [[nodiscard]] auto logoScan() const -> const LogoScan& { return logo_scan_; } + // Timeline lineal de la LogoScene. Cada camp és la durada (ms) d'una + // fase. La SCAN no apareix aquí perquè la marca el propi + // `LogoScan` (duration_ms + tail_glitch_ms + flash_ms). + struct LogoTimeline { + int black_intro_ms; + int hold_silent_ms; + int hold_with_music_ms; + int fade_out_ms; + int black_outro_ms; + }; + + [[nodiscard]] auto logoTimeline() const -> const LogoTimeline& { return logo_timeline_; } + // Corbes d'easing triades al YAML. Si el nom no és conegut, s'usa // Easing::linear com a fallback. [[nodiscard]] auto pauseEnterCurve() const -> CurveFn { return pause_enter_curve_; } @@ -229,6 +245,7 @@ class UiConfig { TitleIntro title_intro_; TitleHeatHaze title_heat_haze_; LogoScan logo_scan_; + LogoTimeline logo_timeline_; std::string menu_select_sound_; std::string menu_accept_sound_; diff --git a/source/game/scenes/logo_scene.cpp b/source/game/scenes/logo_scene.cpp index f6c147d..e809925 100644 --- a/source/game/scenes/logo_scene.cpp +++ b/source/game/scenes/logo_scene.cpp @@ -38,6 +38,21 @@ LogoScene::LogoScene(std::shared_ptr logo, bool reset) audio->stopAllSounds(); } } + // Arrenquem en BLACK_INTRO: ni so ni efecte. El glitch sound i el + // begin() de scan_ es disparen quan entrem a SCAN. +} + +LogoScene::~LogoScene() = default; + +auto LogoScene::wantsSkip() -> bool { + return std::ranges::any_of(EventBuffer::events, [](const SDL_Event& ev) { + return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode); + }); +} + +void LogoScene::enterScan() { + phase_ = Phase::SCAN; + phase_elapsed_ms_ = 0; // So del scanline reveal: una sola pulsació al començament de la // fase SCAN, en grup INTERFACE perquè no es vegi afectat pel volum // de sons del joc i quedi sempre audible. @@ -60,26 +75,36 @@ LogoScene::LogoScene(std::shared_ptr logo, bool reset) .vhs_burst_height_px = s.vhs_burst_height_px, .vhs_burst_amplitude_px = s.vhs_burst_amplitude_px, .flash_ms = s.flash_ms, + .tail_glitch_ms = s.tail_glitch_ms, }); } -LogoScene::~LogoScene() = default; - -auto LogoScene::wantsSkip() -> bool { - return std::ranges::any_of(EventBuffer::events, [](const SDL_Event& ev) { - return ev.type == SDL_EVENT_KEY_DOWN && !Placeholder::isReservedScancode(ev.key.scancode); - }); -} - -void LogoScene::enterMiddle() { - phase_ = Phase::MIDDLE; +void LogoScene::enterHoldSilent() { + phase_ = Phase::HOLD_SILENT; phase_elapsed_ms_ = 0; } +void LogoScene::enterHoldMusic() { + phase_ = Phase::HOLD_MUSIC; + phase_elapsed_ms_ = 0; + // Música del títol al començament d'aquesta fase. La crida és + // idempotent (Audio detecta same-track + same-loop i no reinicia), + // així que TitleScene podrà fer el seu propi playMusic sense + // interrompre la pista durant el FADE_OUT i el BLACK_OUTRO. + if (auto* audio = Audio::get(); audio != nullptr) { + audio->playMusic("title.ogg", -1); + } +} + void LogoScene::enterFadeOut() { phase_ = Phase::FADE_OUT; phase_elapsed_ms_ = 0; - fade_.begin(FADE_OUT_MS, Transitions::FadeToBlack::Direction::FADE_OUT); + fade_.begin(UiConfig::get().logoTimeline().fade_out_ms, Transitions::FadeToBlack::Direction::FADE_OUT); +} + +void LogoScene::enterBlackOutro() { + phase_ = Phase::BLACK_OUTRO; + phase_elapsed_ms_ = 0; } void LogoScene::enterDone() { @@ -103,32 +128,32 @@ void LogoScene::update(int delta_ms) { if (phase_ != Phase::DONE) { phase_elapsed_ms_ += delta_ms; - // Tick dels efectes actius. `scan_` només fa feina visible - // durant Phase::SCAN; `fade_` només durant Phase::FADE_OUT. Tots - // dos són no-op si el seu duration_ms intern està a 0. + // Tick dels efectes actius. Tots dos són no-op si el seu + // duration_ms intern està a 0; només cal cridar-los si la fase + // corresponent està viva o just acabada. scan_.tick(delta_ms); fade_.tick(delta_ms); } + const auto& tl = UiConfig::get().logoTimeline(); switch (phase_) { - case Phase::SCAN: - if (scan_.isFinished()) { enterMiddle(); } + case Phase::BLACK_INTRO: + if (phase_elapsed_ms_ >= tl.black_intro_ms) { enterScan(); } break; - case Phase::MIDDLE: - // Música del títol al cor de la fase central. La crida és - // idempotent (Audio detecta same-track + same-loop i no - // reinicia), així que TitleScene podrà fer el seu propi - // playMusic sense interrompre la pista. - if (!music_started_ && phase_elapsed_ms_ >= MUSIC_TRIGGER_MS) { - music_started_ = true; - if (auto* audio = Audio::get(); audio != nullptr) { - audio->playMusic("title.ogg", -1); - } - } - if (phase_elapsed_ms_ >= MIDDLE_MS) { enterFadeOut(); } + case Phase::SCAN: + if (scan_.isFinished()) { enterHoldSilent(); } + break; + case Phase::HOLD_SILENT: + if (phase_elapsed_ms_ >= tl.hold_silent_ms) { enterHoldMusic(); } + break; + case Phase::HOLD_MUSIC: + if (phase_elapsed_ms_ >= tl.hold_with_music_ms) { enterFadeOut(); } break; case Phase::FADE_OUT: - if (phase_elapsed_ms_ >= FADE_OUT_MS) { enterDone(); } + if (phase_elapsed_ms_ >= tl.fade_out_ms) { enterBlackOutro(); } + break; + case Phase::BLACK_OUTRO: + if (phase_elapsed_ms_ >= tl.black_outro_ms) { enterDone(); } break; case Phase::DONE: break; @@ -142,25 +167,28 @@ void LogoScene::render() { constexpr std::uint32_t BLACK_ARGB = 0xFF000000U; Screen::get().fillRect(0, 0, GameCanvas::WIDTH, GameCanvas::HEIGHT, BLACK_ARGB); - if (logo_) { + // BLACK_INTRO i BLACK_OUTRO no pinten logo: només el negre del + // fillRect d'aquí dalt. La resta de fases sí. + const bool SHOW_LOGO = phase_ != Phase::BLACK_INTRO && phase_ != Phase::BLACK_OUTRO; + if (logo_ && SHOW_LOGO) { const int X = (GameCanvas::WIDTH - logo_->getWidth()) / 2; const int Y = (GameCanvas::HEIGHT - logo_->getHeight()) / 2; if (phase_ == Phase::SCAN) { - // Revelat CRT: la pròpia ScanReveal s'encarrega del blit - // fila a fila, de la banda lluminosa, dels glitches i del - // flash final. + // Revelat CRT + glitch tail: la pròpia ScanReveal s'encarrega + // del blit fila a fila, de la banda lluminosa, dels glitches, + // del flash final i dels bursts del tail. scan_.render(*logo_, X, Y); } else { - // MIDDLE i FADE_OUT: render normal, el logo ja està a - // pantalla complet i estable. + // HOLD_SILENT, HOLD_MUSIC, FADE_OUT: render normal, el logo + // ja està a pantalla complet i estable. logo_->render(X, Y); } } Notifier::get().render(); - // Capa de fosa: només FADE_OUT pinta negre semitransparent (el - // FADE_IN s'ha substituït per `scan_`). Va per damunt del logo i - // del Notifier, però per sota de RenderInfo (overlay de debug). + // Capa de fosa: només pinta durant FADE_OUT (alpha creixent). En + // BLACK_OUTRO ja deixem la pantalla negra del fillRect, així que no + // cal ni que `fade_` segueixi viu. fade_.render(); RenderInfo::get().render(); Screen::get().present(); diff --git a/source/game/scenes/logo_scene.hpp b/source/game/scenes/logo_scene.hpp index 6c407be..380e7f4 100644 --- a/source/game/scenes/logo_scene.hpp +++ b/source/game/scenes/logo_scene.hpp @@ -9,14 +9,22 @@ class Surface; -// Escena del logotip de l'editora. FSM amb tres fases visibles: -// `SCAN` — revelat tipus scanline CRT (durada YAML, ~1500 ms) -// `MIDDLE` — logo estable (`MIDDLE_MS`, 5 s) -// `FADE_OUT` — fade lineal a negre (`FADE_OUT_MS`, 500 ms) -// I una terminal `DONE` que dispara la transició cap a TITLE. A 2,5 s -// dins de `MIDDLE` arrenca la música del títol perquè entri al cor de -// l'escena i continue cap a TitleScene sense talls (Audio ja és -// idempotent quan es demana la mateixa pista que ja sona). +// Escena del logotip de l'editora. Timeline lineal en 7 fases visibles +// (durades configurables des de `logo.timeline.*` al YAML, excepte SCAN +// que la marca `logo.scan.duration_ms` + `tail_glitch_ms` + `flash_ms`): +// +// `BLACK_INTRO` — negre pur abans d'arrencar res. +// `SCAN` — revelat scanline CRT amb glitch tail. So +// `logoGlitchSound` es dispara just al començar +// aquesta fase (no al constructor). +// `HOLD_SILENT` — logo estable, en silenci, marge entre el final +// del glitch i l'arrencada de la música. +// `HOLD_MUSIC` — logo estable amb la música del títol sonant. +// `FADE_OUT` — fade lineal a negre. +// `BLACK_OUTRO` — negre amb la música encara sonant. TitleScene +// continuarà la pista de manera idempotent (Audio +// detecta same-track + same-loop i no reinicia). +// `DONE` — terminal; dispara la transició cap a TITLE. // // Skip: qualsevol tecla no reservada salta directament a la transició // (sense fade-out) — el TitleScene té el seu propi `intro.fade_in_ms`, @@ -27,14 +35,6 @@ class Surface; // silenciós encara que veníssem del joc en marxa. class LogoScene : public Scene { public: - static constexpr int MIDDLE_MS = 5000; - static constexpr int FADE_OUT_MS = 500; - // Offset dins de MIDDLE en què comença a sonar la pista del títol; - // triat perquè quedi clarament al cor de la fase central (logo - // estable) i prou abans del fade-out per donar continuïtat sonora a - // TitleScene. - static constexpr int MUSIC_TRIGGER_MS = 2500; - LogoScene(std::shared_ptr logo, bool reset); ~LogoScene() override; @@ -42,22 +42,25 @@ class LogoScene : public Scene { void render() override; private: - enum class Phase : std::uint8_t { SCAN, - MIDDLE, + enum class Phase : std::uint8_t { BLACK_INTRO, + SCAN, + HOLD_SILENT, + HOLD_MUSIC, FADE_OUT, + BLACK_OUTRO, DONE }; - void enterMiddle(); + void enterScan(); + void enterHoldSilent(); + void enterHoldMusic(); void enterFadeOut(); + void enterBlackOutro(); void enterDone(); [[nodiscard]] static auto wantsSkip() -> bool; std::shared_ptr logo_; - Phase phase_{Phase::SCAN}; + Phase phase_{Phase::BLACK_INTRO}; int phase_elapsed_ms_{0}; - bool music_started_{false}; - // Capa de fosa per al FADE_OUT cap a TITLE. El FADE_IN inicial s'ha - // substituït per `scan_` (revelat CRT amb glitch). Transitions::FadeToBlack fade_; Effects::ScanReveal scan_; }; diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 46ad21a..d58e7aa 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -360,14 +360,37 @@ namespace Defaults { } // namespace HeatHaze } // namespace Title - // Escena LOGO (logotip de l'editora). El revelat del logo a la - // fase SCAN imita un escaneig CRT amb una banda lluminosa que - // baixa de dalt a baix, glitch al voltant de la banda i bursts - // VHS ocasionals damunt la part ja revelada. Quan la banda toca - // el peu del logo, un flash blanc breu remata l'efecte i s'entra - // a MIDDLE (logo estable). + // Escena LOGO (logotip de l'editora). La timeline és lineal i + // composta per fases independents amb durades configurables des + // del YAML (`logo.timeline.*`). L'efecte CRT de revelat (scan + // line + glitch tail) ho gestiona `Effects::ScanReveal` i les + // seves constants viuen al sub-namespace `Scan` d'aquí baix. + namespace Logo::Timeline { + // Negre inicial abans que comence el revelat. Permet entrar + // a l'escena des d'una transició/fade extern sense talls. + constexpr int BLACK_INTRO_MS = 2000; + // Silenci amb logo estable abans d'arrencar la música del + // títol — espai per "respirar" entre el glitch i la pista. + constexpr int HOLD_SILENT_MS = 1000; + // Logo estable amb música del títol sonant. Quan acaba, + // comença el fade-out. + constexpr int HOLD_WITH_MUSIC_MS = 2500; + // Fade lineal a negre. + constexpr int FADE_OUT_MS = 500; + // Negre final amb música encara sonant — la TitleScene + // continuarà la pista de manera idempotent. + constexpr int BLACK_OUTRO_MS = 1000; + } // namespace Logo::Timeline + + // Constants del revelat CRT pròpiament dit. La banda lluminosa + // baixa de dalt a baix amb glitch al seu voltant i bursts VHS + // ocasionals damunt la part ja revelada. Quan la banda toca el + // peu del logo, un flash blanc remata l'efecte; després un + // "tail" manté el logo visible amb bursts durant `TAIL_GLITCH_MS` + // abans que `isFinished()` retorne true. namespace Logo::Scan { constexpr int DURATION_MS = 1500; + constexpr int TAIL_GLITCH_MS = 1500; constexpr int BAND_HEIGHT_PX = 4; constexpr int BAND_ALPHA = 200; // Pulse de brillantor sinusoidal sobre l'alpha base de la