refactor(bloque1): reorganitzar fitxers als subsistemes correctes

- app_logo.hpp/cpp i logo_scaler.hpp/cpp moguts a source/ui/
- spatial_grid.hpp/cpp mogut a source/boids_mgr/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-19 00:41:50 +01:00
parent 2d1f4195dc
commit 6409b61bd5
6 changed files with 0 additions and 0 deletions

687
source/ui/app_logo.cpp Normal file
View File

@@ -0,0 +1,687 @@
#include "app_logo.hpp"
#include <SDL3/SDL_render.h> // for SDL_DestroyTexture, SDL_RenderGeometry, SDL_SetTextureAlphaMod
#include <cmath> // for powf, sinf, cosf
#include <cstdlib> // for free()
#include <iostream> // for std::cout
#include "logo_scaler.hpp" // for LogoScaler
#include "defines.hpp" // for APPLOGO_HEIGHT_PERCENT, getResourcesDirectory
// ============================================================================
// Destructor - Liberar las 4 texturas SDL
// ============================================================================
AppLogo::~AppLogo() {
if (logo1_base_texture_) {
SDL_DestroyTexture(logo1_base_texture_);
logo1_base_texture_ = nullptr;
}
if (logo1_native_texture_) {
SDL_DestroyTexture(logo1_native_texture_);
logo1_native_texture_ = nullptr;
}
if (logo2_base_texture_) {
SDL_DestroyTexture(logo2_base_texture_);
logo2_base_texture_ = nullptr;
}
if (logo2_native_texture_) {
SDL_DestroyTexture(logo2_native_texture_);
logo2_native_texture_ = nullptr;
}
}
// ============================================================================
// Inicialización - Pre-escalar logos a 2 resoluciones (base y nativa)
// ============================================================================
bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_height) {
renderer_ = renderer;
base_screen_width_ = screen_width;
base_screen_height_ = screen_height;
screen_width_ = screen_width;
screen_height_ = screen_height;
std::string resources_dir = getResourcesDirectory();
// ========================================================================
// 1. Detectar resolución nativa del monitor
// ========================================================================
if (!LogoScaler::detectNativeResolution(native_screen_width_, native_screen_height_)) {
std::cout << "No se pudo detectar resolución nativa, usando solo base" << std::endl;
// Fallback: usar resolución base como nativa
native_screen_width_ = screen_width;
native_screen_height_ = screen_height;
}
// ========================================================================
// 2. Calcular alturas finales para ambas resoluciones
// ========================================================================
int logo_base_target_height = static_cast<int>(base_screen_height_ * APPLOGO_HEIGHT_PERCENT);
int logo_native_target_height = static_cast<int>(native_screen_height_ * APPLOGO_HEIGHT_PERCENT);
std::cout << "Pre-escalando logos:" << std::endl;
std::cout << " Base: " << base_screen_width_ << "x" << base_screen_height_
<< " (altura logo: " << logo_base_target_height << "px)" << std::endl;
std::cout << " Nativa: " << native_screen_width_ << "x" << native_screen_height_
<< " (altura logo: " << logo_native_target_height << "px)" << std::endl;
// ========================================================================
// 3. Cargar y escalar LOGO1 (data/logo/logo.png) a 2 versiones
// ========================================================================
std::string logo1_path = resources_dir + "/data/logo/logo.png";
// 3a. Versión BASE de logo1
unsigned char* logo1_base_data = LogoScaler::loadAndScale(
logo1_path,
0, // width calculado automáticamente por aspect ratio
logo_base_target_height,
logo1_base_width_,
logo1_base_height_
);
if (logo1_base_data == nullptr) {
std::cout << "Error: No se pudo escalar logo1 (base)" << std::endl;
return false;
}
logo1_base_texture_ = LogoScaler::createTextureFromBuffer(
renderer, logo1_base_data, logo1_base_width_, logo1_base_height_
);
free(logo1_base_data); // Liberar buffer temporal
if (logo1_base_texture_ == nullptr) {
std::cout << "Error: No se pudo crear textura logo1 (base)" << std::endl;
return false;
}
// Habilitar alpha blending
SDL_SetTextureBlendMode(logo1_base_texture_, SDL_BLENDMODE_BLEND);
// 3b. Versión NATIVA de logo1
unsigned char* logo1_native_data = LogoScaler::loadAndScale(
logo1_path,
0, // width calculado automáticamente
logo_native_target_height,
logo1_native_width_,
logo1_native_height_
);
if (logo1_native_data == nullptr) {
std::cout << "Error: No se pudo escalar logo1 (nativa)" << std::endl;
return false;
}
logo1_native_texture_ = LogoScaler::createTextureFromBuffer(
renderer, logo1_native_data, logo1_native_width_, logo1_native_height_
);
free(logo1_native_data);
if (logo1_native_texture_ == nullptr) {
std::cout << "Error: No se pudo crear textura logo1 (nativa)" << std::endl;
return false;
}
SDL_SetTextureBlendMode(logo1_native_texture_, SDL_BLENDMODE_BLEND);
// ========================================================================
// 4. Cargar y escalar LOGO2 (data/logo/logo2.png) a 2 versiones
// ========================================================================
std::string logo2_path = resources_dir + "/data/logo/logo2.png";
// 4a. Versión BASE de logo2
unsigned char* logo2_base_data = LogoScaler::loadAndScale(
logo2_path,
0,
logo_base_target_height,
logo2_base_width_,
logo2_base_height_
);
if (logo2_base_data == nullptr) {
std::cout << "Error: No se pudo escalar logo2 (base)" << std::endl;
return false;
}
logo2_base_texture_ = LogoScaler::createTextureFromBuffer(
renderer, logo2_base_data, logo2_base_width_, logo2_base_height_
);
free(logo2_base_data);
if (logo2_base_texture_ == nullptr) {
std::cout << "Error: No se pudo crear textura logo2 (base)" << std::endl;
return false;
}
SDL_SetTextureBlendMode(logo2_base_texture_, SDL_BLENDMODE_BLEND);
// 4b. Versión NATIVA de logo2
unsigned char* logo2_native_data = LogoScaler::loadAndScale(
logo2_path,
0,
logo_native_target_height,
logo2_native_width_,
logo2_native_height_
);
if (logo2_native_data == nullptr) {
std::cout << "Error: No se pudo escalar logo2 (nativa)" << std::endl;
return false;
}
logo2_native_texture_ = LogoScaler::createTextureFromBuffer(
renderer, logo2_native_data, logo2_native_width_, logo2_native_height_
);
free(logo2_native_data);
if (logo2_native_texture_ == nullptr) {
std::cout << "Error: No se pudo crear textura logo2 (nativa)" << std::endl;
return false;
}
SDL_SetTextureBlendMode(logo2_native_texture_, SDL_BLENDMODE_BLEND);
// ========================================================================
// 5. Inicialmente usar texturas BASE (la resolución de inicio)
// ========================================================================
logo1_current_texture_ = logo1_base_texture_;
logo1_current_width_ = logo1_base_width_;
logo1_current_height_ = logo1_base_height_;
logo2_current_texture_ = logo2_base_texture_;
logo2_current_width_ = logo2_base_width_;
logo2_current_height_ = logo2_base_height_;
std::cout << "Logos pre-escalados exitosamente (4 texturas creadas)" << std::endl;
return true;
}
void AppLogo::update(float delta_time, AppMode current_mode) {
// Si estamos en SANDBOX, resetear y no hacer nada (logo desactivado)
if (current_mode == AppMode::SANDBOX) {
state_ = AppLogoState::HIDDEN;
timer_ = 0.0f;
logo1_alpha_ = 0;
logo2_alpha_ = 0;
return;
}
// Máquina de estados con fade in/out
timer_ += delta_time;
switch (state_) {
case AppLogoState::HIDDEN:
// Esperando el intervalo de espera
if (timer_ >= APPLOGO_DISPLAY_INTERVAL) {
state_ = AppLogoState::FADE_IN;
timer_ = 0.0f;
logo1_alpha_ = 0;
logo2_alpha_ = 0;
// Elegir UNA animación aleatoria (misma para ambos logos, misma entrada y salida)
current_animation_ = getRandomAnimation();
}
break;
case AppLogoState::FADE_IN:
// Fade in: alpha de 0 a 255, con Logo 2 retrasado 0.25s
{
// Calcular progreso de cada logo (Logo 2 con retraso)
float fade_progress_logo1 = timer_ / APPLOGO_ANIMATION_DURATION;
float fade_progress_logo2 = std::max(0.0f, (timer_ - APPLOGO_LOGO2_DELAY) / APPLOGO_ANIMATION_DURATION);
// Verificar si fade in completado (cuando logo2 también termina)
if (fade_progress_logo2 >= 1.0f) {
// Fade in completado para ambos logos
state_ = AppLogoState::VISIBLE;
timer_ = 0.0f;
logo1_alpha_ = 255;
logo2_alpha_ = 255;
// Resetear variables de ambos logos
logo1_scale_ = 1.0f;
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
logo1_rotation_ = 0.0f;
logo2_scale_ = 1.0f;
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
logo2_rotation_ = 0.0f;
} else {
// Interpolar alpha con retraso de forma LINEAL (sin easing)
logo1_alpha_ = static_cast<int>(std::min(1.0f, fade_progress_logo1) * 255.0f);
logo2_alpha_ = static_cast<int>(std::min(1.0f, fade_progress_logo2) * 255.0f);
// ================================================================
// Aplicar MISMA animación (current_animation_) a ambos logos
// con sus respectivos progresos
// ================================================================
switch (current_animation_) {
case AppLogoAnimationType::ZOOM_ONLY:
logo1_scale_ = 1.2f - (std::min(1.0f, fade_progress_logo1) * 0.2f);
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
logo1_rotation_ = 0.0f;
logo2_scale_ = 1.2f - (std::min(1.0f, fade_progress_logo2) * 0.2f);
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
logo2_rotation_ = 0.0f;
break;
case AppLogoAnimationType::ELASTIC_STICK:
{
float prog1 = std::min(1.0f, fade_progress_logo1);
float elastic_t1 = easeOutElastic(prog1);
logo1_scale_ = 1.2f - (elastic_t1 * 0.2f);
float squash_t1 = easeOutBack(prog1);
logo1_squash_y_ = 0.6f + (squash_t1 * 0.4f);
logo1_stretch_x_ = 1.0f + (1.0f - logo1_squash_y_) * 0.5f;
logo1_rotation_ = 0.0f;
float prog2 = std::min(1.0f, fade_progress_logo2);
float elastic_t2 = easeOutElastic(prog2);
logo2_scale_ = 1.2f - (elastic_t2 * 0.2f);
float squash_t2 = easeOutBack(prog2);
logo2_squash_y_ = 0.6f + (squash_t2 * 0.4f);
logo2_stretch_x_ = 1.0f + (1.0f - logo2_squash_y_) * 0.5f;
logo2_rotation_ = 0.0f;
}
break;
case AppLogoAnimationType::ROTATE_SPIRAL:
{
float prog1 = std::min(1.0f, fade_progress_logo1);
float ease_t1 = easeInOutQuad(prog1);
logo1_scale_ = 0.3f + (ease_t1 * 0.7f);
logo1_rotation_ = (1.0f - prog1) * 6.28f;
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
float prog2 = std::min(1.0f, fade_progress_logo2);
float ease_t2 = easeInOutQuad(prog2);
logo2_scale_ = 0.3f + (ease_t2 * 0.7f);
logo2_rotation_ = (1.0f - prog2) * 6.28f;
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
}
break;
case AppLogoAnimationType::BOUNCE_SQUASH:
{
float prog1 = std::min(1.0f, fade_progress_logo1);
float bounce_t1 = easeOutBounce(prog1);
logo1_scale_ = 1.0f;
float squash_amount1 = (1.0f - bounce_t1) * 0.3f;
logo1_squash_y_ = 1.0f - squash_amount1;
logo1_stretch_x_ = 1.0f + squash_amount1 * 0.5f;
logo1_rotation_ = 0.0f;
float prog2 = std::min(1.0f, fade_progress_logo2);
float bounce_t2 = easeOutBounce(prog2);
logo2_scale_ = 1.0f;
float squash_amount2 = (1.0f - bounce_t2) * 0.3f;
logo2_squash_y_ = 1.0f - squash_amount2;
logo2_stretch_x_ = 1.0f + squash_amount2 * 0.5f;
logo2_rotation_ = 0.0f;
}
break;
}
}
}
break;
case AppLogoState::VISIBLE:
// Logo completamente visible, esperando duración
if (timer_ >= APPLOGO_DISPLAY_DURATION) {
state_ = AppLogoState::FADE_OUT;
timer_ = 0.0f;
logo1_alpha_ = 255;
logo2_alpha_ = 255;
// NO elegir nueva animación - reutilizar current_animation_ (simetría entrada/salida)
}
break;
case AppLogoState::FADE_OUT:
// Fade out: alpha de 255 a 0, con Logo 2 retrasado 0.25s (misma animación que entrada)
{
// Calcular progreso de cada logo (Logo 2 con retraso)
float fade_progress_logo1 = timer_ / APPLOGO_ANIMATION_DURATION;
float fade_progress_logo2 = std::max(0.0f, (timer_ - APPLOGO_LOGO2_DELAY) / APPLOGO_ANIMATION_DURATION);
// Verificar si fade out completado (cuando logo2 también termina)
if (fade_progress_logo2 >= 1.0f) {
// Fade out completado, volver a HIDDEN
state_ = AppLogoState::HIDDEN;
timer_ = 0.0f;
logo1_alpha_ = 0;
logo2_alpha_ = 0;
// Resetear variables de ambos logos
logo1_scale_ = 1.0f;
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
logo1_rotation_ = 0.0f;
logo2_scale_ = 1.0f;
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
logo2_rotation_ = 0.0f;
} else {
// Interpolar alpha con retraso de forma LINEAL (255 → 0, sin easing)
logo1_alpha_ = static_cast<int>((1.0f - std::min(1.0f, fade_progress_logo1)) * 255.0f);
logo2_alpha_ = static_cast<int>((1.0f - std::min(1.0f, fade_progress_logo2)) * 255.0f);
// ================================================================
// Aplicar MISMA animación (current_animation_) de forma invertida
// ================================================================
switch (current_animation_) {
case AppLogoAnimationType::ZOOM_ONLY:
logo1_scale_ = 1.0f + (std::min(1.0f, fade_progress_logo1) * 0.2f);
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
logo1_rotation_ = 0.0f;
logo2_scale_ = 1.0f + (std::min(1.0f, fade_progress_logo2) * 0.2f);
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
logo2_rotation_ = 0.0f;
break;
case AppLogoAnimationType::ELASTIC_STICK:
{
float prog1 = std::min(1.0f, fade_progress_logo1);
logo1_scale_ = 1.0f + (prog1 * prog1 * 0.2f);
logo1_squash_y_ = 1.0f + (prog1 * 0.3f);
logo1_stretch_x_ = 1.0f - (prog1 * 0.2f);
logo1_rotation_ = prog1 * 0.1f;
float prog2 = std::min(1.0f, fade_progress_logo2);
logo2_scale_ = 1.0f + (prog2 * prog2 * 0.2f);
logo2_squash_y_ = 1.0f + (prog2 * 0.3f);
logo2_stretch_x_ = 1.0f - (prog2 * 0.2f);
logo2_rotation_ = prog2 * 0.1f;
}
break;
case AppLogoAnimationType::ROTATE_SPIRAL:
{
float prog1 = std::min(1.0f, fade_progress_logo1);
float ease_t1 = easeInOutQuad(prog1);
logo1_scale_ = 1.0f - (ease_t1 * 0.7f);
logo1_rotation_ = prog1 * 6.28f;
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
float prog2 = std::min(1.0f, fade_progress_logo2);
float ease_t2 = easeInOutQuad(prog2);
logo2_scale_ = 1.0f - (ease_t2 * 0.7f);
logo2_rotation_ = prog2 * 6.28f;
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
}
break;
case AppLogoAnimationType::BOUNCE_SQUASH:
{
float prog1 = std::min(1.0f, fade_progress_logo1);
if (prog1 < 0.2f) {
float squash_t = prog1 / 0.2f;
logo1_squash_y_ = 1.0f - (squash_t * 0.3f);
logo1_stretch_x_ = 1.0f + (squash_t * 0.2f);
} else {
float jump_t = (prog1 - 0.2f) / 0.8f;
logo1_squash_y_ = 0.7f + (jump_t * 0.5f);
logo1_stretch_x_ = 1.2f - (jump_t * 0.2f);
}
logo1_scale_ = 1.0f + (prog1 * 0.3f);
logo1_rotation_ = 0.0f;
float prog2 = std::min(1.0f, fade_progress_logo2);
if (prog2 < 0.2f) {
float squash_t = prog2 / 0.2f;
logo2_squash_y_ = 1.0f - (squash_t * 0.3f);
logo2_stretch_x_ = 1.0f + (squash_t * 0.2f);
} else {
float jump_t = (prog2 - 0.2f) / 0.8f;
logo2_squash_y_ = 0.7f + (jump_t * 0.5f);
logo2_stretch_x_ = 1.2f - (jump_t * 0.2f);
}
logo2_scale_ = 1.0f + (prog2 * 0.3f);
logo2_rotation_ = 0.0f;
}
break;
}
}
}
break;
}
}
void AppLogo::render() {
// Renderizar si NO está en estado HIDDEN (incluye FADE_IN, VISIBLE, FADE_OUT)
if (state_ != AppLogoState::HIDDEN) {
// Renderizar LOGO1 primero (fondo), luego LOGO2 (encima)
renderWithGeometry(1);
renderWithGeometry(2);
}
}
void AppLogo::updateScreenSize(int screen_width, int screen_height) {
screen_width_ = screen_width;
screen_height_ = screen_height;
// ========================================================================
// Detectar si coincide con resolución nativa o base, cambiar texturas
// ========================================================================
bool is_native = (screen_width == native_screen_width_ && screen_height == native_screen_height_);
if (is_native) {
// Cambiar a texturas nativas (F4 fullscreen)
logo1_current_texture_ = logo1_native_texture_;
logo1_current_width_ = logo1_native_width_;
logo1_current_height_ = logo1_native_height_;
logo2_current_texture_ = logo2_native_texture_;
logo2_current_width_ = logo2_native_width_;
logo2_current_height_ = logo2_native_height_;
std::cout << "AppLogo: Cambiado a texturas NATIVAS" << std::endl;
} else {
// Cambiar a texturas base (ventana redimensionable)
logo1_current_texture_ = logo1_base_texture_;
logo1_current_width_ = logo1_base_width_;
logo1_current_height_ = logo1_base_height_;
logo2_current_texture_ = logo2_base_texture_;
logo2_current_width_ = logo2_base_width_;
logo2_current_height_ = logo2_base_height_;
std::cout << "AppLogo: Cambiado a texturas BASE" << std::endl;
}
// Nota: No es necesario recalcular escalas porque las texturas están pre-escaladas
// al tamaño exacto de pantalla. Solo renderizamos al 100% (o con deformaciones de animación).
}
// ============================================================================
// Funciones de easing para animaciones
// ============================================================================
float AppLogo::easeOutElastic(float t) {
// Elastic easing out: bounce elástico al final
const float c4 = (2.0f * 3.14159f) / 3.0f;
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
return powf(2.0f, -10.0f * t) * sinf((t * 10.0f - 0.75f) * c4) + 1.0f;
}
float AppLogo::easeOutBack(float t) {
// Back easing out: overshoot suave al final
const float c1 = 1.70158f;
const float c3 = c1 + 1.0f;
return 1.0f + c3 * powf(t - 1.0f, 3.0f) + c1 * powf(t - 1.0f, 2.0f);
}
float AppLogo::easeOutBounce(float t) {
// Bounce easing out: rebotes decrecientes (para BOUNCE_SQUASH)
const float n1 = 7.5625f;
const float d1 = 2.75f;
if (t < 1.0f / d1) {
return n1 * t * t;
} else if (t < 2.0f / d1) {
t -= 1.5f / d1;
return n1 * t * t + 0.75f;
} else if (t < 2.5f / d1) {
t -= 2.25f / d1;
return n1 * t * t + 0.9375f;
} else {
t -= 2.625f / d1;
return n1 * t * t + 0.984375f;
}
}
float AppLogo::easeInOutQuad(float t) {
// Quadratic easing in/out: aceleración suave (para ROTATE_SPIRAL)
if (t < 0.5f) {
return 2.0f * t * t;
} else {
return 1.0f - powf(-2.0f * t + 2.0f, 2.0f) / 2.0f;
}
}
// ============================================================================
// Función auxiliar para aleatorización
// ============================================================================
AppLogoAnimationType AppLogo::getRandomAnimation() {
// Generar número aleatorio entre 0 y 3 (4 tipos de animación)
int random_value = rand() % 4;
switch (random_value) {
case 0:
return AppLogoAnimationType::ZOOM_ONLY;
case 1:
return AppLogoAnimationType::ELASTIC_STICK;
case 2:
return AppLogoAnimationType::ROTATE_SPIRAL;
case 3:
default:
return AppLogoAnimationType::BOUNCE_SQUASH;
}
}
// ============================================================================
// Renderizado con geometría (para todos los logos, con deformaciones)
// ============================================================================
void AppLogo::renderWithGeometry(int logo_index) {
if (!renderer_) return;
// Seleccionar variables según el logo_index (1 = logo1, 2 = logo2)
SDL_Texture* texture;
int base_width, base_height;
float scale, squash_y, stretch_x, rotation;
if (logo_index == 1) {
if (!logo1_current_texture_) return;
texture = logo1_current_texture_;
base_width = logo1_current_width_;
base_height = logo1_current_height_;
scale = logo1_scale_;
squash_y = logo1_squash_y_;
stretch_x = logo1_stretch_x_;
rotation = logo1_rotation_;
} else if (logo_index == 2) {
if (!logo2_current_texture_) return;
texture = logo2_current_texture_;
base_width = logo2_current_width_;
base_height = logo2_current_height_;
scale = logo2_scale_;
squash_y = logo2_squash_y_;
stretch_x = logo2_stretch_x_;
rotation = logo2_rotation_;
} else {
return; // Índice inválido
}
// Aplicar alpha específico de cada logo (con retraso para logo2)
int alpha = (logo_index == 1) ? logo1_alpha_ : logo2_alpha_;
float alpha_normalized = static_cast<float>(alpha) / 255.0f; // Convertir 0-255 → 0.0-1.0
// NO usar SDL_SetTextureAlphaMod - aplicar alpha directamente a vértices
// Calcular padding desde bordes derecho e inferior
float padding_x = screen_width_ * APPLOGO_PADDING_PERCENT;
float padding_y = screen_height_ * APPLOGO_PADDING_PERCENT;
// Calcular esquina BASE (sin escala) para obtener centro FIJO
// Esto asegura que el centro no se mueva cuando cambia scale/squash/stretch
float corner_x_base = screen_width_ - base_width - padding_x;
float corner_y_base = screen_height_ - base_height - padding_y;
// Centro FIJO del logo (no cambia con scale/squash/stretch)
float center_x = corner_x_base + (base_width / 2.0f);
float center_y = corner_y_base + (base_height / 2.0f);
// Calcular tamaño ESCALADO (para vértices)
// (base_width y base_height ya están pre-escalados al tamaño correcto de pantalla)
float width = base_width * scale * stretch_x;
float height = base_height * scale * squash_y;
// Pre-calcular seno y coseno de rotación
float cos_rot = cosf(rotation);
float sin_rot = sinf(rotation);
// Crear 4 vértices del quad (centrado en center_x, center_y)
SDL_Vertex vertices[4];
// Offset desde el centro
float half_w = width / 2.0f;
float half_h = height / 2.0f;
// Vértice superior izquierdo (rotado)
{
float local_x = -half_w;
float local_y = -half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot;
float rotated_y = local_x * sin_rot + local_y * cos_rot;
vertices[0].position = {center_x + rotated_x, center_y + rotated_y};
vertices[0].tex_coord = {0.0f, 0.0f};
vertices[0].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice
}
// Vértice superior derecho (rotado)
{
float local_x = half_w;
float local_y = -half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot;
float rotated_y = local_x * sin_rot + local_y * cos_rot;
vertices[1].position = {center_x + rotated_x, center_y + rotated_y};
vertices[1].tex_coord = {1.0f, 0.0f};
vertices[1].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice
}
// Vértice inferior derecho (rotado)
{
float local_x = half_w;
float local_y = half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot;
float rotated_y = local_x * sin_rot + local_y * cos_rot;
vertices[2].position = {center_x + rotated_x, center_y + rotated_y};
vertices[2].tex_coord = {1.0f, 1.0f};
vertices[2].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice
}
// Vértice inferior izquierdo (rotado)
{
float local_x = -half_w;
float local_y = half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot;
float rotated_y = local_x * sin_rot + local_y * cos_rot;
vertices[3].position = {center_x + rotated_x, center_y + rotated_y};
vertices[3].tex_coord = {0.0f, 1.0f};
vertices[3].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice
}
// Índices para 2 triángulos
int indices[6] = {0, 1, 2, 2, 3, 0};
// Renderizar con la textura del logo correspondiente
SDL_RenderGeometry(renderer_, texture, vertices, 4, indices, 6);
}

117
source/ui/app_logo.hpp Normal file
View File

@@ -0,0 +1,117 @@
#pragma once
#include <SDL3/SDL_render.h> // for SDL_Renderer
#include <memory> // for unique_ptr, shared_ptr
#include "defines.hpp" // for AppMode
class Texture;
class Sprite;
// Estados de la máquina de estados del logo
enum class AppLogoState {
HIDDEN, // Logo oculto, esperando APPLOGO_DISPLAY_INTERVAL
FADE_IN, // Apareciendo (alpha 0 → 255)
VISIBLE, // Completamente visible, esperando APPLOGO_DISPLAY_DURATION
FADE_OUT // Desapareciendo (alpha 255 → 0)
};
// Tipo de animación de entrada/salida
enum class AppLogoAnimationType {
ZOOM_ONLY, // A: Solo zoom simple (120% → 100% → 120%)
ELASTIC_STICK, // B: Zoom + deformación elástica tipo "pegatina"
ROTATE_SPIRAL, // C: Rotación en espiral (entra girando, sale girando)
BOUNCE_SQUASH // D: Rebote con aplastamiento (cae rebotando, salta)
};
class AppLogo {
public:
AppLogo() = default;
~AppLogo(); // Necesario para liberar las 4 texturas SDL
// Inicializar textura y sprite del logo
bool initialize(SDL_Renderer* renderer, int screen_width, int screen_height);
// Actualizar temporizadores y estado de visibilidad
void update(float delta_time, AppMode current_mode);
// Renderizar logo si está visible
void render();
// Actualizar tamaño de pantalla (reposicionar logo)
void updateScreenSize(int screen_width, int screen_height);
private:
// ====================================================================
// Texturas pre-escaladas (4 texturas: 2 logos × 2 resoluciones)
// ====================================================================
SDL_Texture* logo1_base_texture_ = nullptr; // Logo1 para resolución base
SDL_Texture* logo1_native_texture_ = nullptr; // Logo1 para resolución nativa (F4)
SDL_Texture* logo2_base_texture_ = nullptr; // Logo2 para resolución base
SDL_Texture* logo2_native_texture_ = nullptr; // Logo2 para resolución nativa (F4)
// Dimensiones pre-calculadas para cada textura
int logo1_base_width_ = 0, logo1_base_height_ = 0;
int logo1_native_width_ = 0, logo1_native_height_ = 0;
int logo2_base_width_ = 0, logo2_base_height_ = 0;
int logo2_native_width_ = 0, logo2_native_height_ = 0;
// Texturas actualmente en uso (apuntan a base o native según resolución)
SDL_Texture* logo1_current_texture_ = nullptr;
SDL_Texture* logo2_current_texture_ = nullptr;
int logo1_current_width_ = 0, logo1_current_height_ = 0;
int logo2_current_width_ = 0, logo2_current_height_ = 0;
// Resoluciones conocidas
int base_screen_width_ = 0, base_screen_height_ = 0; // Resolución inicial
int native_screen_width_ = 0, native_screen_height_ = 0; // Resolución nativa (F4)
// ====================================================================
// Variables COMPARTIDAS (sincronización de ambos logos)
// ====================================================================
AppLogoState state_ = AppLogoState::HIDDEN; // Estado actual de la máquina de estados
float timer_ = 0.0f; // Contador de tiempo para estado actual
// Alpha INDEPENDIENTE para cada logo (Logo 2 con retraso)
int logo1_alpha_ = 0; // Alpha de Logo 1 (0-255)
int logo2_alpha_ = 0; // Alpha de Logo 2 (0-255, con retraso)
// Animación COMPARTIDA (misma para ambos logos, misma entrada y salida)
AppLogoAnimationType current_animation_ = AppLogoAnimationType::ZOOM_ONLY;
// Variables de deformación INDEPENDIENTES para logo1
float logo1_scale_ = 1.0f; // Escala actual de logo1 (1.0 = 100%)
float logo1_squash_y_ = 1.0f; // Factor de aplastamiento vertical logo1
float logo1_stretch_x_ = 1.0f; // Factor de estiramiento horizontal logo1
float logo1_rotation_ = 0.0f; // Rotación en radianes logo1
// Variables de deformación INDEPENDIENTES para logo2
float logo2_scale_ = 1.0f; // Escala actual de logo2 (1.0 = 100%)
float logo2_squash_y_ = 1.0f; // Factor de aplastamiento vertical logo2
float logo2_stretch_x_ = 1.0f; // Factor de estiramiento horizontal logo2
float logo2_rotation_ = 0.0f; // Rotación en radianes logo2
int screen_width_ = 0; // Ancho de pantalla (para centrar)
int screen_height_ = 0; // Alto de pantalla (para centrar)
// Tamaño base del logo (calculado una vez)
float base_width_ = 0.0f;
float base_height_ = 0.0f;
// SDL renderer (necesario para renderizado con geometría)
SDL_Renderer* renderer_ = nullptr;
// Métodos privados auxiliares
void updateLogoPosition(); // Centrar ambos logos en pantalla (superpuestos)
void renderWithGeometry(int logo_index); // Renderizar logo con vértices deformados (1 o 2)
// Funciones de easing
float easeOutElastic(float t); // Elastic bounce out
float easeOutBack(float t); // Overshoot out
float easeOutBounce(float t); // Bounce easing (para BOUNCE_SQUASH)
float easeInOutQuad(float t); // Quadratic easing (para ROTATE_SPIRAL)
// Función auxiliar para elegir animación aleatoria
AppLogoAnimationType getRandomAnimation();
};

157
source/ui/logo_scaler.cpp Normal file
View File

@@ -0,0 +1,157 @@
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "logo_scaler.hpp"
#include <SDL3/SDL_error.h> // Para SDL_GetError
#include <SDL3/SDL_log.h> // Para SDL_Log
#include <SDL3/SDL_pixels.h> // Para SDL_PixelFormat
#include <SDL3/SDL_render.h> // Para SDL_CreateTexture
#include <SDL3/SDL_surface.h> // Para SDL_CreateSurfaceFrom
#include <SDL3/SDL_video.h> // Para SDL_GetDisplays
#include <cstdlib> // Para free()
#include <iostream> // Para std::cout
#include "external/stb_image.h" // Para stbi_load, stbi_image_free
#include "external/stb_image_resize2.h" // Para stbir_resize_uint8_srgb
#include "resource_manager.hpp" // Para cargar desde pack
// ============================================================================
// Detectar resolución nativa del monitor principal
// ============================================================================
bool LogoScaler::detectNativeResolution(int& native_width, int& native_height) {
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays == nullptr || num_displays == 0) {
SDL_Log("Error al obtener displays: %s", SDL_GetError());
return false;
}
// Obtener resolución del display principal (displays[0])
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm == nullptr) {
SDL_Log("Error al obtener modo del display: %s", SDL_GetError());
SDL_free(displays);
return false;
}
native_width = dm->w;
native_height = dm->h;
SDL_free(displays);
std::cout << "Resolución nativa detectada: " << native_width << "x" << native_height << std::endl;
return true;
}
// ============================================================================
// Cargar PNG y escalar al tamaño especificado
// ============================================================================
unsigned char* LogoScaler::loadAndScale(const std::string& path,
int target_width, int target_height,
int& out_width, int& out_height) {
// 1. Intentar cargar imagen desde ResourceManager (pack o disco)
int orig_width, orig_height, orig_channels;
unsigned char* orig_data = nullptr;
// 1a. Cargar desde ResourceManager
unsigned char* resourceData = nullptr;
size_t resourceSize = 0;
if (ResourceManager::loadResource(path, resourceData, resourceSize)) {
// Descodificar imagen desde memoria usando stb_image
orig_data = stbi_load_from_memory(resourceData, static_cast<int>(resourceSize),
&orig_width, &orig_height, &orig_channels, STBI_rgb_alpha);
delete[] resourceData; // Liberar buffer temporal
}
// 1b. Si falla todo, error
if (orig_data == nullptr) {
SDL_Log("Error al cargar imagen %s: %s", path.c_str(), stbi_failure_reason());
return nullptr;
}
std::cout << "Imagen cargada: " << path << " (" << orig_width << "x" << orig_height << ")" << std::endl;
// 2. Calcular tamaño final manteniendo aspect ratio
// El alto está fijado por target_height (APPLOGO_HEIGHT_PERCENT)
// Calcular ancho proporcional al aspect ratio original
float aspect_ratio = static_cast<float>(orig_width) / static_cast<float>(orig_height);
out_width = static_cast<int>(target_height * aspect_ratio);
out_height = target_height;
std::cout << " Escalando a: " << out_width << "x" << out_height << std::endl;
// 3. Alocar buffer para imagen escalada (RGBA = 4 bytes por píxel)
unsigned char* scaled_data = static_cast<unsigned char*>(malloc(out_width * out_height * 4));
if (scaled_data == nullptr) {
SDL_Log("Error al alocar memoria para imagen escalada");
stbi_image_free(orig_data);
return nullptr;
}
// 4. Escalar con stb_image_resize2 (algoritmo Mitchell, espacio sRGB)
// La función devuelve el puntero de salida, o nullptr si falla
unsigned char* result = stbir_resize_uint8_srgb(
orig_data, orig_width, orig_height, 0, // Input
scaled_data, out_width, out_height, 0, // Output
STBIR_RGBA // Formato píxel
);
// Liberar imagen original (ya no la necesitamos)
stbi_image_free(orig_data);
if (result == nullptr) {
SDL_Log("Error al escalar imagen");
free(scaled_data);
return nullptr;
}
std::cout << " Escalado completado correctamente" << std::endl;
return scaled_data;
}
// ============================================================================
// Crear textura SDL desde buffer RGBA
// ============================================================================
SDL_Texture* LogoScaler::createTextureFromBuffer(SDL_Renderer* renderer,
unsigned char* data,
int width, int height) {
if (renderer == nullptr || data == nullptr || width <= 0 || height <= 0) {
SDL_Log("Parámetros inválidos para createTextureFromBuffer");
return nullptr;
}
// 1. Crear surface SDL desde buffer RGBA
int pitch = width * 4; // 4 bytes por píxel (RGBA)
SDL_PixelFormat pixel_format = SDL_PIXELFORMAT_RGBA32;
SDL_Surface* surface = SDL_CreateSurfaceFrom(
width, height,
pixel_format,
data,
pitch
);
if (surface == nullptr) {
SDL_Log("Error al crear surface: %s", SDL_GetError());
return nullptr;
}
// 2. Crear textura desde surface
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
if (texture == nullptr) {
SDL_Log("Error al crear textura: %s", SDL_GetError());
SDL_DestroySurface(surface);
return nullptr;
}
// 3. Liberar surface (la textura ya tiene los datos)
SDL_DestroySurface(surface);
return texture;
}

61
source/ui/logo_scaler.hpp Normal file
View File

@@ -0,0 +1,61 @@
#pragma once
#include <SDL3/SDL_render.h> // Para SDL_Renderer, SDL_Texture
#include <SDL3/SDL_video.h> // Para SDL_DisplayID, SDL_GetDisplays
#include <string> // Para std::string
/**
* @brief Helper class para pre-escalar logos usando stb_image_resize2
*
* Proporciona funciones para:
* - Detectar resolución nativa del monitor
* - Cargar PNG y escalar a tamaño específico con algoritmos de alta calidad
* - Crear texturas SDL desde buffers escalados
*
* Usado por AppLogo para pre-generar versiones de logos al tamaño exacto
* de pantalla, eliminando el escalado dinámico de SDL y mejorando calidad visual.
*/
class LogoScaler {
public:
/**
* @brief Detecta la resolución nativa del monitor principal
*
* @param native_width [out] Ancho nativo del display en píxeles
* @param native_height [out] Alto nativo del display en píxeles
* @return true si se pudo detectar, false si hubo error
*/
static bool detectNativeResolution(int& native_width, int& native_height);
/**
* @brief Carga un PNG y lo escala al tamaño especificado
*
* Usa stb_image para cargar y stb_image_resize2 para escalar con
* algoritmo Mitchell (balance calidad/velocidad) en espacio sRGB.
*
* @param path Ruta al archivo PNG (ej: "data/logo/logo.png")
* @param target_width Ancho destino en píxeles
* @param target_height Alto destino en píxeles
* @param out_width [out] Ancho real de la imagen escalada
* @param out_height [out] Alto real de la imagen escalada
* @return Buffer RGBA (4 bytes por píxel) o nullptr si falla
* IMPORTANTE: El caller debe liberar con free() cuando termine
*/
static unsigned char* loadAndScale(const std::string& path,
int target_width, int target_height,
int& out_width, int& out_height);
/**
* @brief Crea una textura SDL desde un buffer RGBA
*
* @param renderer Renderizador SDL activo
* @param data Buffer RGBA (4 bytes por píxel)
* @param width Ancho del buffer en píxeles
* @param height Alto del buffer en píxeles
* @return Textura SDL creada o nullptr si falla
* IMPORTANTE: El caller debe destruir con SDL_DestroyTexture()
*/
static SDL_Texture* createTextureFromBuffer(SDL_Renderer* renderer,
unsigned char* data,
int width, int height);
};