13 Commits

Author SHA1 Message Date
5f89299444 Fix: Notifier aplica offset de viewport para consistencia en letterbox
Aplica el mismo patrón de viewport offset usado en TextRenderer
al renderizado del fondo de notificaciones para consistencia completa.

## Cambios

**notifier.cpp - renderBackground():**
- Obtener viewport ANTES de deshabilitar presentación lógica
- Aplicar offset del viewport a coordenadas del rectángulo:
  - `bg_rect.x = x + viewport.x`
  - `bg_rect.y = y + viewport.y`

## Resultado

 Fondo de notificaciones respeta offset de viewport
 Consistencia completa entre texto y fondo en modo letterbox
 Compatible con F3 (letterbox), F4 (stretch), y ventana normal

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 11:19:01 +02:00
33728857ac Fix: TextRenderer aplica offset de viewport para modo letterbox (F3)
Corrige el posicionamiento del texto HUD en modo F3 (letterbox) cuando
SDL3 crea barras negras y ajusta el viewport con offset para centrar.

## Problema

En modo letterbox (F3), SDL_LOGICAL_PRESENTATION_LETTERBOX crea:
- Barras negras para mantener aspect ratio
- Viewport con offset (x, y) para centrar la imagen renderizada

Cuando printAbsolute() deshabilitaba temporalmente la presentación
lógica, perdía el offset del viewport y pintaba en (0,0) absoluto,
cayendo en las barras negras.

## Solución

**textrenderer.cpp - printAbsolute():**
- Obtener viewport ANTES de deshabilitar presentación lógica
- Aplicar offset del viewport a coordenadas físicas:
  - `dest_rect.x = physical_x + viewport.x`
  - `dest_rect.y = physical_y + viewport.y`

## Resultado

 HUD se pinta dentro del área visible con offset de letterbox
 Compatible con todos los modos:
   - Ventana normal
   - F3 letterbox (con barras negras)
   - F4 stretch fullscreen

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 11:18:16 +02:00
d2f170d313 Fix: HUD debug usa coordenadas físicas absolutas (como notificaciones)
Migra el sistema de renderizado de HUD debug desde printPhysical()
(coordenadas lógicas escaladas) a printAbsolute() (píxeles físicos absolutos).

## Cambios

**engine.cpp (líneas 830-951):**
- Eliminadas líneas de cálculo de factores de escala (text_scale_x/y)
- Todas las coordenadas ahora en píxeles físicos absolutos
- FPS: `physical_window_width_ - text_width - margin` (esquina derecha física)
- 10 llamadas printPhysical() → printAbsolute() con SDL_Color
- 4 llamadas getTextWidth() → getTextWidthPhysical()

## Resultado

 HUD de tamaño fijo independiente de resolución lógica
 FPS siempre pegado a esquina derecha física
 Espaciado constante entre líneas
 Funciona en modo ventana y F4 (stretch fullscreen)
⚠️  PENDIENTE: Ajustar offset para modo F3 con letterbox

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 09:39:17 +02:00
aa57ac7012 Fix: Sistema de zoom y fullscreen con parámetros CLI
Corrige bugs críticos en el manejo de ventanas cuando se inician
con parámetros de línea de comandos (-w, -h, -z).

## Problemas Resueltos

**1. Zoom incorrecto con parámetros CLI**
- El zoom calculado no se guardaba en current_window_zoom_
- F1/F2 usaban valor default (3) en lugar del zoom actual
- Resultado: Posicionamiento erróneo de ventana al hacer zoom

**2. Ventana no centrada al iniciar**
- Faltaba SDL_SetWindowPosition() después de crear ventana
- Ventana aparecía en posición aleatoria

**3. F4 restauraba tamaño incorrecto**
- toggleRealFullscreen() usaba DEFAULT_WINDOW_ZOOM hardcoded
- Al salir de fullscreen real, ventana cambiaba de tamaño
- No re-centraba ventana después de restaurar

## Cambios Implementados

**engine.cpp:initialize() línea 86-87:**
- Guardar zoom calculado en current_window_zoom_ antes de crear ventana
- Asegura consistencia entre zoom real y zoom guardado

**engine.cpp:initialize() línea 114-117:**
- Centrar ventana con SDL_WINDOWPOS_CENTERED al iniciar
- Solo si no está en modo fullscreen

**engine.cpp:toggleRealFullscreen() línea 1174-1175:**
- Usar current_window_zoom_ en lugar de DEFAULT_WINDOW_ZOOM
- Re-centrar ventana con SDL_WINDOWPOS_CENTERED al salir de F4

## Casos de Prueba Verificados

 Sin parámetros: vibe3_physics.exe
 Con resolución: vibe3_physics.exe -w 640 -h 480
 Con zoom: vibe3_physics.exe -z 2
 Combinado: vibe3_physics.exe -w 1920 -h 1080 -z 1

## Teclas Afectadas

- F1 (Zoom Out):  Funciona correctamente
- F2 (Zoom In):  Funciona correctamente
- F3 (Fullscreen Toggle):  Funciona correctamente
- F4 (Real Fullscreen):  Ahora restaura tamaño correcto

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 09:20:24 +02:00
0d069da29d Refactor: Traduce todas las notificaciones a castellano
Unifica el idioma de todas las notificaciones del sistema a castellano
para mantener consistencia en la interfaz de usuario.

## Traducciones Realizadas

### Gravedad
- "Gravity Off/On" → "Gravedad Off/On"
- "Gravity Up" → "Gravedad Arriba"
- "Gravity Down" → "Gravedad Abajo"
- "Gravity Left" → "Gravedad Izquierda"
- "Gravity Right" → "Gravedad Derecha"

### Modos
- "Physics Mode" → "Modo Física"

### Figuras 3D (array shape_names[] + notificaciones)
- "None" → "Ninguna"
- "Sphere" → "Esfera"
- "Cube" → "Cubo"
- "Helix" → "Hélice"
- "Torus" → "Toroide"
- "Lissajous" → "Lissajous" (mantiene nombre técnico)
- "Cylinder" → "Cilindro"
- "Icosahedron" → "Icosaedro"
- "Atom" → "Átomo"
- "PNG Shape" → "Forma PNG"

### Profundidad
- "Depth Zoom On/Off" → "Profundidad On/Off"

## Mantienen Inglés

- **Sprite**: Término técnico común en desarrollo
- **Nombres de temas**: Usan getCurrentThemeNameES() (ya en español)
- **Modos de aplicación**: Ya estaban en español
- **Número de pelotas**: Ya estaban en español
- **Escala**: Ya estaba en español
- **Páginas**: Ya estaban en español

## Resultado

 Interfaz de usuario 100% en castellano
 Consistencia en todas las notificaciones
 Mantiene términos técnicos apropiados (Lissajous, Sprite)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 09:02:11 +02:00
eb3dd03579 Fix: LOGO sale incorrectamente a DEMO al pulsar F manualmente
Corrige bug donde pulsar F en modo LOGO (llegando desde DEMO) causaba
salida automática a DEMO debido a uso incorrecto de previous_app_mode_
como flag de "¿puede salir automáticamente?".

## Problema

**Flujo con bug:**
1. SANDBOX → D → DEMO
2. DEMO → K → LOGO (guarda previous_app_mode_ = DEMO)
3. LOGO (SHAPE) → F → LOGO (PHYSICS) ← Acción MANUAL
4. updateDemoMode() ejecuta lógica de LOGO
5. Línea 1628: `if (previous_app_mode_ != SANDBOX && rand() < 60%)`
6. Como previous_app_mode_ == DEMO → Sale a DEMO 

**Causa raíz:**
La variable previous_app_mode_ se usaba para dos propósitos:
- Guardar a dónde volver (correcto)
- Decidir si puede salir automáticamente (incorrecto)

Esto causaba que acciones manuales del usuario (como F) activaran
la probabilidad de salida automática.

## Solución Implementada

**Nueva variable explícita:**
```cpp
bool logo_entered_manually_;  // true si tecla K, false si desde DEMO
```

**Asignación en enterLogoMode():**
```cpp
logo_entered_manually_ = !from_demo;
```

**Condición corregida en updateDemoMode():**
```cpp
// ANTES (incorrecto):
if (previous_app_mode_ != AppMode::SANDBOX && rand() % 100 < 60)

// AHORA (correcto):
if (!logo_entered_manually_ && rand() % 100 < 60)
```

## Ventajas

 **Separación de responsabilidades:**
- previous_app_mode_: Solo para saber a dónde volver
- logo_entered_manually_: Solo para control de salida automática

 **Semántica clara:**
- Código más legible y expresivo

 **Más robusto:**
- No depende de comparaciones indirectas

## Flujos Verificados

**Flujo 1 (Manual desde SANDBOX):**
- SANDBOX → K → LOGO (logo_entered_manually_ = true)
- LOGO → F → PHYSICS
- No sale automáticamente 

**Flujo 2 (Manual desde DEMO):**
- SANDBOX → D → DEMO → K → LOGO (logo_entered_manually_ = true)
- LOGO → F → PHYSICS
- No sale automáticamente 

**Flujo 3 (Automático desde DEMO):**
- SANDBOX → D → DEMO → auto → LOGO (logo_entered_manually_ = false)
- LOGO ejecuta acciones automáticas
- Sale a DEMO con 60% probabilidad 

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 08:58:20 +02:00
a1e2c03efd Fix: Notificación tecla F muestra nombre correcto de figura
Corrige desajuste entre el orden del enum ShapeType y el array
de nombres shape_names[] en el handler de tecla F (toggle).

## Problema

Al pulsar F para toggle PHYSICS ↔ SHAPE, la notificación mostraba
nombre incorrecto de la figura debido a que el array shape_names[]
NO coincidía con el orden del enum ShapeType.

**Enum ShapeType (defines.h):**
0=NONE, 1=SPHERE, 2=CUBE, 3=HELIX, 4=TORUS, 5=LISSAJOUS,
6=CYLINDER, 7=ICOSAHEDRON, 8=ATOM, 9=PNG_SHAPE

**Array previo (incorrecto):**
{"Sphere", "Lissajous", "Helix", "Torus", "Cube", ...}

Orden erróneo causaba que al activar CUBE (enum=2) mostrara
"Helix" (array[2]), etc.

## Solución

Reordenar array para coincidir exactamente con enum ShapeType:

```cpp
const char* shape_names[] = {
    "None",        // 0 = NONE
    "Sphere",      // 1 = SPHERE
    "Cube",        // 2 = CUBE
    "Helix",       // 3 = HELIX
    "Torus",       // 4 = TORUS
    "Lissajous",   // 5 = LISSAJOUS
    "Cylinder",    // 6 = CYLINDER
    "Icosahedron", // 7 = ICOSAHEDRON
    "Atom",        // 8 = ATOM
    "PNG Shape"    // 9 = PNG_SHAPE
};
```

## Resultado

 Tecla F muestra nombre correcto al activar cada figura
 Comentario documentando correspondencia con enum
 "None" añadido en índice 0 (nunca usado, pero completa array)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 08:45:18 +02:00
684ac9823b Add: Reglas DEMO/LOGO - PNG_SHAPE exclusivo y temas aleatorios
Implementa restricciones para modos DEMO y LOGO garantizando que
PNG_SHAPE sea exclusivo del modo LOGO y nunca aparezca en DEMO/DEMO_LITE.

## Cambios en Modo LOGO (enterLogoMode)

**Textura:**
- Cambiado de "tiny" a "small" como textura obligatoria

**Tema aleatorio:**
- Antes: Siempre MONOCHROME (tema 5)
- Ahora: Selección aleatoria entre 4 temas:
  - MONOCHROME (5)
  - LAVENDER (6)
  - CRIMSON (7)
  - ESMERALDA (8)

**Comportamiento:**
- No cambia de tema automáticamente durante ejecución
- Mantiene tema seleccionado hasta salir del modo

## Cambios en Transición LOGO → DEMO

**exitLogoMode (automático):**
- Al volver automáticamente a DEMO desde LOGO
- Si figura activa es PNG_SHAPE → cambia a figura aleatoria válida
- Excluye PNG_SHAPE de selección (8 figuras disponibles)

**randomizeOnDemoStart (manual):**
- Al entrar manualmente a DEMO/DEMO_LITE con tecla D/L
- Check inicial: si current_shape_type_ == PNG_SHAPE
- Fuerza cambio a figura aleatoria antes de randomización
- Soluciona bug: D → DEMO → K → LOGO → D dejaba PNG_SHAPE activa

## Garantías Implementadas

 PNG_SHAPE nunca aparece en acciones aleatorias de DEMO/DEMO_LITE
 PNG_SHAPE se cambia automáticamente al salir de LOGO (manual o auto)
 Modo LOGO elige tema aleatorio al entrar (4 opciones monocromáticas)
 Modo LOGO usa textura SMALL en lugar de TINY

## Flujos Verificados

- Manual: DEMO → LOGO → DEMO (tecla D) 
- Manual: DEMO_LITE → LOGO → DEMO_LITE (tecla L) 
- Automático: DEMO → LOGO → DEMO (5% probabilidad) 
- Dentro DEMO: PNG_SHAPE nunca seleccionada 

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 08:41:13 +02:00
82e5b6798c Refactor: Notificaciones muestran solo la última (sin cola FIFO)
Cambio de comportamiento:
- ANTES: Notificaciones en cola FIFO esperaban turno secuencialmente
- AHORA: Solo se muestra la última notificación inmediatamente

Implementación:
- show() destruye notificación actual con current_notification_.reset()
- Vacía cola completa descartando notificaciones pendientes
- Activa nueva notificación inmediatamente sin esperar en cola

Resultado:
-  Usuario pulsa 5 teclas rápido → Solo ve la 5ª notificación
-  Feedback inmediato → Nueva reemplaza vieja instantáneamente
-  Sin esperas → Siempre información actualizada
-  UX mejorada → Respuesta inmediata a última acción del usuario

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 07:59:28 +02:00
d93ac04ee3 Fix: Notificaciones con colores dinámicos y transiciones LERP
Problema resuelto:
1. Color del tema saliente: Notificaciones mostraban color del tema ANTIGUO
2. Sin transiciones LERP: Notificaciones no participaban en transiciones suaves

Cambios implementados:
- Arquitectura cambiada de estática a dinámica
- Notifier ahora consulta ThemeManager cada frame en render()
- Eliminados colores estáticos de struct Notification
- Notifier::init() recibe puntero a ThemeManager
- Notifier::show() ya no recibe parámetros de color
- Simplificado showNotificationForAction() (-23 líneas)

Fix crítico de inicialización:
- ThemeManager ahora se inicializa ANTES de updatePhysicalWindowSize()
- Previene nullptr en notifier_.init() que causaba que no se mostraran

Resultado:
-  Notificaciones usan color del tema DESTINO (no origen)
-  Transiciones LERP suaves automáticas durante cambios de tema
-  Código más limpio y centralizado en ThemeManager
-  -50 líneas de código duplicado eliminadas

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 07:55:55 +02:00
10a4234d49 Refactor: Sistema de modos y notificaciones mejorado
Cambios principales:
- Renombrado AppMode::MANUAL → AppMode::SANDBOX (nomenclatura más clara)
- Notificaciones ahora funcionan en TODAS las transiciones de modo
- Lógica de teclas D/L/K simplificada: toggle exclusivo modo ↔ SANDBOX
- Mensajes simplificados: "MODO DEMO", "MODO SANDBOX", etc. (sin ON/OFF)
- Eliminado check restrictivo en showNotificationForAction()

Comportamiento nuevo:
- Tecla D: Toggle DEMO ↔ SANDBOX
- Tecla L: Toggle DEMO_LITE ↔ SANDBOX
- Tecla K: Toggle LOGO ↔ SANDBOX
- Cada tecla activa su modo o vuelve a SANDBOX si ya está activo
- Notificaciones visibles tanto al activar como desactivar modos

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-10 07:44:57 +02:00
0d1608712b Add: Sistema de notificaciones con colores de fondo temáticos
CARACTERÍSTICAS:
- Notificaciones con fondo personalizado por tema (15 temas)
- Soporte completo para temas estáticos y dinámicos
- Interpolación LERP de colores durante transiciones
- Actualización por frame durante animaciones de temas

IMPLEMENTACIÓN:

Theme System:
- Añadido getNotificationBackgroundColor() a interfaz Theme
- StaticTheme: Color fijo por tema
- DynamicTheme: Interpolación entre keyframes
- ThemeManager: LERP durante transiciones (PHASE 3)
- ThemeSnapshot: Captura color para transiciones suaves

Colores por Tema:
Estáticos (9):
  - SUNSET: Púrpura oscuro (120, 40, 80)
  - OCEAN: Azul marino (20, 50, 90)
  - NEON: Púrpura oscuro (60, 0, 80)
  - FOREST: Marrón tierra (70, 50, 30)
  - RGB: Gris claro (220, 220, 220)
  - MONOCHROME: Gris oscuro (50, 50, 50)
  - LAVENDER: Violeta oscuro (80, 50, 100)
  - CRIMSON: Rojo oscuro (80, 10, 10)
  - EMERALD: Verde oscuro (10, 80, 10)

Dinámicos (6, 20 keyframes totales):
  - SUNRISE: 3 keyframes (noche→alba→día)
  - OCEAN_WAVES: 2 keyframes (profundo→claro)
  - NEON_PULSE: 2 keyframes (apagado→encendido)
  - FIRE: 4 keyframes (brasas→llamas→inferno→llamas)
  - AURORA: 4 keyframes (verde→violeta→cian→violeta)
  - VOLCANIC: 4 keyframes (ceniza→erupción→lava→enfriamiento)

Notifier:
- Añadido SDL_Color bg_color a estructura Notification
- Método show() acepta parámetro bg_color
- renderBackground() usa color dinámico (no negro fijo)
- Soporte para cambios de color cada frame

Engine:
- Obtiene color de fondo desde ThemeManager
- Pasa bg_color al notifier en cada notificación
- Sincronizado con tema activo y transiciones

FIXES:
- TEXT_ABSOLUTE_SIZE cambiado de 16px a 12px (múltiplo nativo)
- Centrado de notificaciones corregido en F3 fullscreen
- updatePhysicalWindowSize() usa SDL_GetCurrentDisplayMode en F3
- Notificaciones centradas correctamente en ventana/F3/F4

🎨 Generated with Claude Code
2025-10-10 07:17:06 +02:00
68381dc92d Treballant en text independent de la resolucio 2025-10-09 20:43:34 +02:00
17 changed files with 1119 additions and 213 deletions

View File

@@ -25,7 +25,7 @@ if (NOT SDL3_ttf_FOUND)
endif()
# Archivos fuente (excluir main_old.cpp)
file(GLOB SOURCE_FILES source/*.cpp source/external/*.cpp source/shapes/*.cpp source/themes/*.cpp source/text/*.cpp)
file(GLOB SOURCE_FILES source/*.cpp source/external/*.cpp source/shapes/*.cpp source/themes/*.cpp source/text/*.cpp source/ui/*.cpp)
list(REMOVE_ITEM SOURCE_FILES "${CMAKE_SOURCE_DIR}/source/main_old.cpp")
# Comprobar si se encontraron archivos fuente

View File

@@ -22,9 +22,18 @@ constexpr int WINDOW_DECORATION_HEIGHT = 30; // Altura estimada de decoraciones
constexpr float GRAVITY_FORCE = 0.2f; // Fuerza de gravedad (píxeles/frame²)
// Configuración de interfaz
constexpr Uint64 TEXT_DURATION = 2000; // Duración del texto informativo (ms)
constexpr Uint64 TEXT_DURATION = 2000; // Duración del texto informativo (ms) - OBSOLETO, usar NOTIFICATION_DURATION
constexpr float THEME_TRANSITION_DURATION = 0.5f; // Duración de transiciones LERP entre temas (segundos)
// Configuración de notificaciones (sistema Notifier)
constexpr int TEXT_ABSOLUTE_SIZE = 12; // Tamaño fuente base en píxeles físicos (múltiplo de 12px, tamaño nativo de la fuente)
constexpr Uint64 NOTIFICATION_DURATION = 2000; // Duración default de notificaciones (ms)
constexpr Uint64 NOTIFICATION_SLIDE_TIME = 300; // Duración animación entrada (ms)
constexpr Uint64 NOTIFICATION_FADE_TIME = 200; // Duración animación salida (ms)
constexpr float NOTIFICATION_BG_ALPHA = 0.7f; // Opacidad fondo semitransparente (0.0-1.0)
constexpr int NOTIFICATION_PADDING = 10; // Padding interno del fondo (píxeles físicos)
constexpr int NOTIFICATION_TOP_MARGIN = 20; // Margen superior desde borde pantalla (píxeles físicos)
// Configuración de pérdida aleatoria en rebotes
constexpr float BASE_BOUNCE_COEFFICIENT = 0.75f; // Coeficiente base IGUAL para todas las pelotas
constexpr float BOUNCE_RANDOM_LOSS_PERCENT = 0.1f; // 0-10% pérdida adicional aleatoria en cada rebote
@@ -65,6 +74,9 @@ struct DynamicThemeKeyframe {
float bg_top_r, bg_top_g, bg_top_b;
float bg_bottom_r, bg_bottom_g, bg_bottom_b;
// Color de fondo de notificaciones
int notif_bg_r, notif_bg_g, notif_bg_b;
// Colores de pelotas en este keyframe
std::vector<Color> ball_colors;

View File

@@ -83,6 +83,9 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
window_zoom = 1;
}
// Guardar zoom calculado ANTES de crear la ventana (para F1/F2/F3/F4)
current_window_zoom_ = window_zoom;
// Calcular tamaño de ventana
int window_width = logical_width * window_zoom;
int window_height = logical_height * window_zoom;
@@ -96,6 +99,8 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
// SDL ya inicializado arriba para validación
{
// Crear ventana principal (fullscreen si se especifica)
// NOTA: SDL_WINDOW_HIGH_PIXEL_DENSITY removido por incompatibilidad con STRETCH mode (F4)
// El DPI se detectará manualmente con SDL_GetWindowSizeInPixels()
Uint32 window_flags = SDL_WINDOW_OPENGL;
if (fullscreen) {
window_flags |= SDL_WINDOW_FULLSCREEN;
@@ -106,6 +111,11 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
std::cout << "¡No se pudo crear la ventana! Error de SDL: " << SDL_GetError() << std::endl;
success = false;
} else {
// Centrar ventana en pantalla si no está en fullscreen
if (!fullscreen) {
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
// Crear renderizador
renderer_ = SDL_CreateRenderer(window_, nullptr);
if (renderer_ == nullptr) {
@@ -203,26 +213,14 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
srand(static_cast<unsigned>(time(nullptr)));
// Calcular tamaño de fuente escalado según resolución
// Usa las constantes configurables de la clase
int font_size = (base_screen_height_ * TEXT_BASE_SIZE) / 240;
// Inicializar TextRenderer para display (texto centrado)
if (!text_renderer_.init(renderer_, TEXT_FONT_PATH, font_size, TEXT_ANTIALIASING)) {
SDL_Log("Error al inicializar TextRenderer (display)");
return false;
}
// Inicializar TextRenderer para debug (HUD)
if (!text_renderer_debug_.init(renderer_, TEXT_FONT_PATH, font_size, TEXT_ANTIALIASING)) {
SDL_Log("Error al inicializar TextRenderer (debug)");
return false;
}
// Inicializar ThemeManager
// Inicializar ThemeManager PRIMERO (requerido por Notifier)
theme_manager_ = std::make_unique<ThemeManager>();
theme_manager_->initialize();
// Calcular tamaño físico de ventana y tamaño de fuente absoluto
// NOTA: Debe llamarse DESPUÉS de inicializar ThemeManager porque notifier_.init() lo necesita
updatePhysicalWindowSize();
initBalls(scenario_);
}
@@ -298,11 +296,14 @@ void Engine::update() {
updateShape();
}
// Actualizar texto (sin cambios en la lógica)
// Actualizar texto (OBSOLETO: sistema antiguo, se mantiene por compatibilidad temporal)
if (show_text_) {
show_text_ = !(SDL_GetTicks() - text_init_time_ > TEXT_DURATION);
}
// Actualizar sistema de notificaciones
notifier_.update(current_time);
// Actualizar Modo DEMO (auto-play)
updateDemoMode();
@@ -337,8 +338,12 @@ void Engine::handleEvents() {
// Si estamos en modo figura, salir a modo física SIN GRAVEDAD
if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeMode(false); // Desactivar figura sin forzar gravedad ON
showNotificationForAction("Gravedad Off");
} else {
switchBallsGravity(); // Toggle normal en modo física
// Determinar estado actual de gravedad (gravity_force_ != 0.0f significa ON)
bool gravity_on = balls_.empty() ? true : (balls_[0]->getGravityForce() != 0.0f);
showNotificationForAction(gravity_on ? "Gravedad On" : "Gravedad Off");
}
break;
@@ -351,6 +356,7 @@ void Engine::handleEvents() {
enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
changeGravityDirection(GravityDirection::UP);
showNotificationForAction("Gravedad Arriba");
break;
case SDLK_DOWN:
@@ -361,6 +367,7 @@ void Engine::handleEvents() {
enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
changeGravityDirection(GravityDirection::DOWN);
showNotificationForAction("Gravedad Abajo");
break;
case SDLK_LEFT:
@@ -371,6 +378,7 @@ void Engine::handleEvents() {
enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
changeGravityDirection(GravityDirection::LEFT);
showNotificationForAction("Gravedad Izquierda");
break;
case SDLK_RIGHT:
@@ -381,6 +389,7 @@ void Engine::handleEvents() {
enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
changeGravityDirection(GravityDirection::RIGHT);
showNotificationForAction("Gravedad Derecha");
break;
case SDLK_V:
@@ -394,43 +403,61 @@ void Engine::handleEvents() {
// Toggle Física ↔ Última Figura (antes era C)
case SDLK_F:
toggleShapeMode();
// Mostrar notificación según el modo actual después del toggle
if (current_mode_ == SimulationMode::PHYSICS) {
showNotificationForAction("Modo Física");
} else {
// Mostrar nombre de la figura actual (orden debe coincidir con enum ShapeType)
// Índices: 0=NONE, 1=SPHERE, 2=CUBE, 3=HELIX, 4=TORUS, 5=LISSAJOUS, 6=CYLINDER, 7=ICOSAHEDRON, 8=ATOM, 9=PNG_SHAPE
const char* shape_names[] = {"Ninguna", "Esfera", "Cubo", "Hélice", "Toroide", "Lissajous", "Cilindro", "Icosaedro", "Átomo", "Forma PNG"};
showNotificationForAction(shape_names[static_cast<int>(current_shape_type_)]);
}
break;
// Selección directa de figuras 3D
case SDLK_Q:
activateShape(ShapeType::SPHERE);
showNotificationForAction("Esfera");
break;
case SDLK_W:
activateShape(ShapeType::LISSAJOUS);
showNotificationForAction("Lissajous");
break;
case SDLK_E:
activateShape(ShapeType::HELIX);
showNotificationForAction("Hélice");
break;
case SDLK_R:
activateShape(ShapeType::TORUS);
showNotificationForAction("Toroide");
break;
case SDLK_T:
activateShape(ShapeType::CUBE);
showNotificationForAction("Cubo");
break;
case SDLK_Y:
activateShape(ShapeType::CYLINDER);
showNotificationForAction("Cilindro");
break;
case SDLK_U:
activateShape(ShapeType::ICOSAHEDRON);
showNotificationForAction("Icosaedro");
break;
case SDLK_I:
activateShape(ShapeType::ATOM);
showNotificationForAction("Átomo");
break;
case SDLK_O:
activateShape(ShapeType::PNG_SHAPE);
showNotificationForAction("Forma PNG");
break;
// Ciclar temas de color (movido de T a B)
@@ -446,13 +473,8 @@ void Engine::handleEvents() {
theme_manager_->cycleTheme();
}
// Mostrar nombre del tema (solo si NO estamos en modo demo)
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
// Mostrar notificación con el nombre del tema
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
@@ -462,12 +484,7 @@ void Engine::handleEvents() {
{
int theme_index = (theme_page_ == 0) ? 0 : 10;
theme_manager_->switchToTheme(theme_index);
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
@@ -476,12 +493,7 @@ void Engine::handleEvents() {
{
int theme_index = (theme_page_ == 0) ? 1 : 11;
theme_manager_->switchToTheme(theme_index);
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
@@ -490,12 +502,7 @@ void Engine::handleEvents() {
{
int theme_index = (theme_page_ == 0) ? 2 : 12;
theme_manager_->switchToTheme(theme_index);
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
@@ -504,12 +511,7 @@ void Engine::handleEvents() {
{
int theme_index = (theme_page_ == 0) ? 3 : 13;
theme_manager_->switchToTheme(theme_index);
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
@@ -518,12 +520,7 @@ void Engine::handleEvents() {
{
int theme_index = (theme_page_ == 0) ? 4 : 14;
theme_manager_->switchToTheme(theme_index);
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
@@ -531,12 +528,7 @@ void Engine::handleEvents() {
// Solo página 0: MONOCHROME (5)
if (theme_page_ == 0) {
theme_manager_->switchToTheme(5); // MONOCHROME
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
@@ -544,12 +536,7 @@ void Engine::handleEvents() {
// Solo página 0: LAVENDER (6)
if (theme_page_ == 0) {
theme_manager_->switchToTheme(6); // LAVENDER
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
@@ -557,12 +544,7 @@ void Engine::handleEvents() {
// Solo página 0: CRIMSON (7)
if (theme_page_ == 0) {
theme_manager_->switchToTheme(7); // CRIMSON
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
@@ -570,12 +552,7 @@ void Engine::handleEvents() {
// Solo página 0: EMERALD (8)
if (theme_page_ == 0) {
theme_manager_->switchToTheme(8); // EMERALD
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
@@ -583,12 +560,7 @@ void Engine::handleEvents() {
// Solo página 0: SUNRISE (9)
if (theme_page_ == 0) {
theme_manager_->switchToTheme(9); // SUNRISE
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme_manager_->getCurrentThemeNameES();
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
showNotificationForAction(theme_manager_->getCurrentThemeNameES());
}
break;
@@ -596,14 +568,7 @@ void Engine::handleEvents() {
case SDLK_KP_ENTER:
// Alternar entre página 0 y página 1
theme_page_ = (theme_page_ == 0) ? 1 : 0;
// Mostrar feedback visual (solo si NO estamos en modo demo)
if (current_app_mode_ == AppMode::MANUAL) {
text_ = (theme_page_ == 0) ? "Página 1" : "Página 2";
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
showNotificationForAction((theme_page_ == 0) ? "Página 1" : "Página 2");
break;
// Cambio de sprite/textura dinámico
@@ -616,11 +581,7 @@ void Engine::handleEvents() {
if (current_mode_ == SimulationMode::SHAPE) {
shape_scale_factor_ += SHAPE_SCALE_STEP;
clampShapeScale();
text_ = "Escala " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%";
int text_width = static_cast<int>(text_.length() * 8);
text_pos_ = (current_screen_width_ - text_width) / 2;
text_init_time_ = SDL_GetTicks();
show_text_ = true;
showNotificationForAction("Escala " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%");
}
break;
@@ -628,74 +589,70 @@ void Engine::handleEvents() {
if (current_mode_ == SimulationMode::SHAPE) {
shape_scale_factor_ -= SHAPE_SCALE_STEP;
clampShapeScale();
text_ = "Escala " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%";
int text_width = static_cast<int>(text_.length() * 8);
text_pos_ = (current_screen_width_ - text_width) / 2;
text_init_time_ = SDL_GetTicks();
show_text_ = true;
showNotificationForAction("Escala " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%");
}
break;
case SDLK_KP_MULTIPLY:
if (current_mode_ == SimulationMode::SHAPE) {
shape_scale_factor_ = SHAPE_SCALE_DEFAULT;
text_ = "Escala reiniciada (100%)";
int text_width = static_cast<int>(text_.length() * 8);
text_pos_ = (current_screen_width_ - text_width) / 2;
text_init_time_ = SDL_GetTicks();
show_text_ = true;
showNotificationForAction("Escala 100%");
}
break;
case SDLK_KP_DIVIDE:
if (current_mode_ == SimulationMode::SHAPE) {
depth_zoom_enabled_ = !depth_zoom_enabled_;
text_ = depth_zoom_enabled_ ? "Zoom profundidad: On" : "Zoom profundidad: Off";
int text_width = static_cast<int>(text_.length() * 8);
text_pos_ = (current_screen_width_ - text_width) / 2;
text_init_time_ = SDL_GetTicks();
show_text_ = true;
showNotificationForAction(depth_zoom_enabled_ ? "Profundidad On" : "Profundidad Off");
}
break;
case SDLK_1:
scenario_ = 0;
initBalls(scenario_);
showNotificationForAction("10 Pelotas");
break;
case SDLK_2:
scenario_ = 1;
initBalls(scenario_);
showNotificationForAction("50 Pelotas");
break;
case SDLK_3:
scenario_ = 2;
initBalls(scenario_);
showNotificationForAction("100 Pelotas");
break;
case SDLK_4:
scenario_ = 3;
initBalls(scenario_);
showNotificationForAction("500 Pelotas");
break;
case SDLK_5:
scenario_ = 4;
initBalls(scenario_);
showNotificationForAction("1,000 Pelotas");
break;
case SDLK_6:
scenario_ = 5;
initBalls(scenario_);
showNotificationForAction("5,000 Pelotas");
break;
case SDLK_7:
scenario_ = 6;
initBalls(scenario_);
showNotificationForAction("10,000 Pelotas");
break;
case SDLK_8:
scenario_ = 7;
initBalls(scenario_);
showNotificationForAction("50,000 Pelotas");
break;
// Controles de zoom dinámico (solo si no estamos en fullscreen)
@@ -732,22 +689,16 @@ void Engine::handleEvents() {
if (event.key.mod & SDL_KMOD_SHIFT) {
theme_manager_->pauseDynamic();
} else {
// D sin Shift = Toggle modo DEMO
// D sin Shift = Toggle DEMO ↔ SANDBOX
if (current_app_mode_ == AppMode::DEMO) {
// Desactivar DEMO → MANUAL
setState(AppMode::MANUAL);
text_ = "Modo Demo: Off";
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
// Ya estamos en DEMO → volver a SANDBOX
setState(AppMode::SANDBOX);
showNotificationForAction("MODO SANDBOX");
} else {
// Activar DEMO (desde cualquier otro modo)
// Estamos en otro modo → ir a DEMO
setState(AppMode::DEMO);
randomizeOnDemoStart(false);
text_ = "Modo Demo: On";
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
showNotificationForAction("MODO DEMO");
}
}
break;
@@ -755,35 +706,28 @@ void Engine::handleEvents() {
// Toggle Modo DEMO LITE (solo física/figuras)
case SDLK_L:
if (current_app_mode_ == AppMode::DEMO_LITE) {
// Desactivar DEMO_LITE → MANUAL
setState(AppMode::MANUAL);
text_ = "Modo Demo Lite: Off";
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
// Ya estamos en DEMO_LITE → volver a SANDBOX
setState(AppMode::SANDBOX);
showNotificationForAction("MODO SANDBOX");
} else {
// Activar DEMO_LITE (desde cualquier otro modo)
// Estamos en otro modo → ir a DEMO_LITE
setState(AppMode::DEMO_LITE);
randomizeOnDemoStart(true);
text_ = "Modo Demo Lite: On";
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
showNotificationForAction("MODO DEMO LITE");
}
break;
// Toggle Modo LOGO (easter egg - marca de agua)
case SDLK_K:
toggleLogoMode();
// Mostrar texto informativo
if (current_app_mode_ == AppMode::LOGO) {
text_ = "Modo Logo: On";
// Ya estamos en LOGO → volver a SANDBOX
exitLogoMode(false);
showNotificationForAction("MODO SANDBOX");
} else {
text_ = "Modo Logo: Off";
// Estamos en otro modo → ir a LOGO
enterLogoMode(false);
showNotificationForAction("MODO LOGO");
}
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
break;
}
}
@@ -883,6 +827,10 @@ void Engine::render() {
SDL_RenderGeometry(renderer_, texture_->getSDLTexture(), batch_vertices_.data(), static_cast<int>(batch_vertices_.size()), batch_indices_.data(), static_cast<int>(batch_indices_.size()));
}
// SISTEMA DE TEXTO ANTIGUO DESHABILITADO
// Reemplazado completamente por el sistema de notificaciones (Notifier)
// El doble renderizado causaba que aparecieran textos duplicados detrás de las notificaciones
/*
if (show_text_) {
// Obtener datos del tema actual (delegado a ThemeManager)
int text_color_r, text_color_g, text_color_b;
@@ -894,7 +842,7 @@ void Engine::render() {
int margin = 8;
// Texto del número de pelotas con color del tema
text_renderer_.print(text_pos_, margin, text_.c_str(), text_color_r, text_color_g, text_color_b);
text_renderer_.printPhysical(text_pos_, margin, text_.c_str(), text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y);
// Mostrar nombre del tema en castellano debajo del número de pelotas
// (solo si text_ NO es ya el nombre del tema, para evitar duplicación)
@@ -904,24 +852,25 @@ void Engine::render() {
int theme_y = margin + line_height; // Espaciado dinámico
// Texto del nombre del tema con el mismo color
text_renderer_.print(theme_x, theme_y, theme_name_es, text_color_r, text_color_g, text_color_b);
text_renderer_.printPhysical(theme_x, theme_y, theme_name_es, text_color_r, text_color_g, text_color_b, text_scale_x, text_scale_y);
}
}
*/
// Debug display (solo si está activado con tecla H)
if (show_debug_) {
// Obtener altura de línea para espaciado dinámico (usando fuente debug)
int line_height = text_renderer_debug_.getTextHeight();
int margin = 8; // Margen constante
int current_y = margin; // Y inicial
int margin = 8; // Margen constante en píxeles físicos
int current_y = margin; // Y inicial en píxeles físicos
// Mostrar contador de FPS en esquina superior derecha
int fps_text_width = text_renderer_debug_.getTextWidth(fps_text_.c_str());
int fps_x = current_screen_width_ - fps_text_width - margin;
text_renderer_debug_.print(fps_x, current_y, fps_text_.c_str(), 255, 255, 0); // Amarillo
int fps_text_width = text_renderer_debug_.getTextWidthPhysical(fps_text_.c_str());
int fps_x = physical_window_width_ - fps_text_width - margin;
text_renderer_debug_.printAbsolute(fps_x, current_y, fps_text_.c_str(), {255, 255, 0, 255}); // Amarillo
// Mostrar estado V-Sync en esquina superior izquierda
text_renderer_debug_.print(margin, current_y, vsync_text_.c_str(), 0, 255, 255); // Cian
text_renderer_debug_.printAbsolute(margin, current_y, vsync_text_.c_str(), {0, 255, 255, 255}); // Cian
current_y += line_height;
// Debug: Mostrar valores de la primera pelota (si existe)
@@ -929,35 +878,35 @@ void Engine::render() {
// Línea 1: Gravedad
int grav_int = static_cast<int>(balls_[0]->getGravityForce());
std::string grav_text = "Gravedad: " + std::to_string(grav_int);
text_renderer_debug_.print(margin, current_y, grav_text.c_str(), 255, 0, 255); // Magenta
text_renderer_debug_.printAbsolute(margin, current_y, grav_text.c_str(), {255, 0, 255, 255}); // Magenta
current_y += line_height;
// Línea 2: Velocidad Y
int vy_int = static_cast<int>(balls_[0]->getVelocityY());
std::string vy_text = "Velocidad Y: " + std::to_string(vy_int);
text_renderer_debug_.print(margin, current_y, vy_text.c_str(), 255, 0, 255); // Magenta
text_renderer_debug_.printAbsolute(margin, current_y, vy_text.c_str(), {255, 0, 255, 255}); // Magenta
current_y += line_height;
// Línea 3: Estado superficie
std::string surface_text = balls_[0]->isOnSurface() ? "Superficie: Sí" : "Superficie: No";
text_renderer_debug_.print(margin, current_y, surface_text.c_str(), 255, 0, 255); // Magenta
text_renderer_debug_.printAbsolute(margin, current_y, surface_text.c_str(), {255, 0, 255, 255}); // Magenta
current_y += line_height;
// Línea 4: Coeficiente de rebote (loss)
float loss_val = balls_[0]->getLossCoefficient();
std::string loss_text = "Rebote: " + std::to_string(loss_val).substr(0, 4);
text_renderer_debug_.print(margin, current_y, loss_text.c_str(), 255, 0, 255); // Magenta
text_renderer_debug_.printAbsolute(margin, current_y, loss_text.c_str(), {255, 0, 255, 255}); // Magenta
current_y += line_height;
// Línea 5: Dirección de gravedad
std::string gravity_dir_text = "Dirección: " + gravityDirectionToString(current_gravity_);
text_renderer_debug_.print(margin, current_y, gravity_dir_text.c_str(), 255, 255, 0); // Amarillo
text_renderer_debug_.printAbsolute(margin, current_y, gravity_dir_text.c_str(), {255, 255, 0, 255}); // Amarillo
current_y += line_height;
}
// Debug: Mostrar tema actual (delegado a ThemeManager)
std::string theme_text = std::string("Tema: ") + theme_manager_->getCurrentThemeNameEN();
text_renderer_debug_.print(margin, current_y, theme_text.c_str(), 255, 255, 128); // Amarillo claro
text_renderer_debug_.printAbsolute(margin, current_y, theme_text.c_str(), {255, 255, 128, 255}); // Amarillo claro
current_y += line_height;
// Debug: Mostrar modo de simulación actual
@@ -969,14 +918,14 @@ void Engine::render() {
} else {
mode_text = "Modo: Forma";
}
text_renderer_debug_.print(margin, current_y, mode_text.c_str(), 0, 255, 128); // Verde claro
text_renderer_debug_.printAbsolute(margin, current_y, mode_text.c_str(), {0, 255, 128, 255}); // Verde claro
current_y += line_height;
// Debug: Mostrar convergencia en modo LOGO (solo cuando está activo)
if (current_app_mode_ == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) {
int convergence_percent = static_cast<int>(shape_convergence_ * 100.0f);
std::string convergence_text = "Convergencia: " + std::to_string(convergence_percent) + "%";
text_renderer_debug_.print(margin, current_y, convergence_text.c_str(), 255, 128, 0); // Naranja
text_renderer_debug_.printAbsolute(margin, current_y, convergence_text.c_str(), {255, 128, 0, 255}); // Naranja
current_y += line_height;
}
@@ -985,22 +934,25 @@ void Engine::render() {
int fixed_y = margin + (line_height * 2); // Tercera fila fija
if (current_app_mode_ == AppMode::LOGO) {
const char* logo_text = "Modo Logo";
int logo_text_width = text_renderer_debug_.getTextWidth(logo_text);
int logo_x = (current_screen_width_ - logo_text_width) / 2;
text_renderer_debug_.print(logo_x, fixed_y, logo_text, 255, 128, 0); // Naranja
int logo_text_width = text_renderer_debug_.getTextWidthPhysical(logo_text);
int logo_x = (physical_window_width_ - logo_text_width) / 2;
text_renderer_debug_.printAbsolute(logo_x, fixed_y, logo_text, {255, 128, 0, 255}); // Naranja
} else if (current_app_mode_ == AppMode::DEMO) {
const char* demo_text = "Modo Demo";
int demo_text_width = text_renderer_debug_.getTextWidth(demo_text);
int demo_x = (current_screen_width_ - demo_text_width) / 2;
text_renderer_debug_.print(demo_x, fixed_y, demo_text, 255, 165, 0); // Naranja
int demo_text_width = text_renderer_debug_.getTextWidthPhysical(demo_text);
int demo_x = (physical_window_width_ - demo_text_width) / 2;
text_renderer_debug_.printAbsolute(demo_x, fixed_y, demo_text, {255, 165, 0, 255}); // Naranja
} else if (current_app_mode_ == AppMode::DEMO_LITE) {
const char* lite_text = "Modo Demo Lite";
int lite_text_width = text_renderer_debug_.getTextWidth(lite_text);
int lite_x = (current_screen_width_ - lite_text_width) / 2;
text_renderer_debug_.print(lite_x, fixed_y, lite_text, 255, 200, 0); // Amarillo-naranja
int lite_text_width = text_renderer_debug_.getTextWidthPhysical(lite_text);
int lite_x = (physical_window_width_ - lite_text_width) / 2;
text_renderer_debug_.printAbsolute(lite_x, fixed_y, lite_text, {255, 200, 0, 255}); // Amarillo-naranja
}
}
// Renderizar notificaciones (siempre al final, sobre todo lo demás)
notifier_.render();
SDL_RenderPresent(renderer_);
}
@@ -1035,24 +987,47 @@ void Engine::initBalls(int value) {
float mass_factor = GRAVITY_MASS_MIN + (rand() % 1000) / 1000.0f * (GRAVITY_MASS_MAX - GRAVITY_MASS_MIN);
balls_.emplace_back(std::make_unique<Ball>(X, VX, VY, COLOR, texture_, current_screen_width_, current_screen_height_, current_ball_size_, current_gravity_, mass_factor));
}
setText(); // Actualiza el texto
// NOTA: setText() removido - las notificaciones ahora se llaman manualmente desde cada tecla
}
void Engine::setText() {
// Suprimir textos durante modos demo
if (current_app_mode_ != AppMode::MANUAL) return;
if (current_app_mode_ != AppMode::SANDBOX) return;
// Generar texto de número de pelotas
int num_balls = BALL_COUNT_SCENARIOS[scenario_];
std::string notification_text;
if (num_balls == 1) {
text_ = "1 pelota";
notification_text = "1 Pelota";
} else if (num_balls < 1000) {
notification_text = std::to_string(num_balls) + " Pelotas";
} else {
text_ = std::to_string(num_balls) + " pelotas";
// Formato con separador de miles para números grandes
notification_text = std::to_string(num_balls / 1000) + "," +
(num_balls % 1000 < 100 ? "0" : "") +
(num_balls % 1000 < 10 ? "0" : "") +
std::to_string(num_balls % 1000) + " Pelotas";
}
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2; // Centrar texto
// Mostrar notificación (colores se obtienen dinámicamente desde ThemeManager)
notifier_.show(notification_text, NOTIFICATION_DURATION);
// Sistema antiguo (mantener temporalmente para compatibilidad)
text_ = notification_text;
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
void Engine::showNotificationForAction(const std::string& text) {
// IMPORTANTE: Esta función solo se llama desde handlers de teclado (acciones manuales)
// NUNCA se llama desde código automático (DEMO/LOGO), por lo tanto siempre mostramos notificación
// Los colores se obtienen dinámicamente cada frame desde ThemeManager en render()
// Esto permite transiciones LERP suaves y siempre usar el color del tema actual
notifier_.show(text, NOTIFICATION_DURATION);
}
void Engine::pushBallsAwayFromGravity() {
for (auto& ball : balls_) {
const int SIGNO = ((rand() % 2) * 2) - 1;
@@ -1145,6 +1120,9 @@ void Engine::toggleFullscreen() {
fullscreen_enabled_ = !fullscreen_enabled_;
SDL_SetWindowFullscreen(window_, fullscreen_enabled_);
// Actualizar dimensiones físicas después del cambio
updatePhysicalWindowSize();
}
void Engine::toggleRealFullscreen() {
@@ -1174,6 +1152,9 @@ void Engine::toggleRealFullscreen() {
// Actualizar presentación lógica del renderizador
SDL_SetRenderLogicalPresentation(renderer_, current_screen_width_, current_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
// Reinicar la escena con nueva resolución
initBalls(scenario_);
}
@@ -1184,13 +1165,17 @@ void Engine::toggleRealFullscreen() {
current_screen_width_ = base_screen_width_;
current_screen_height_ = base_screen_height_;
// Restaurar ventana normal
// Restaurar ventana normal con el zoom actual (no hardcoded)
SDL_SetWindowFullscreen(window_, false);
SDL_SetWindowSize(window_, base_screen_width_ * DEFAULT_WINDOW_ZOOM, base_screen_height_ * DEFAULT_WINDOW_ZOOM);
SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
// Restaurar presentación lógica base
SDL_SetRenderLogicalPresentation(renderer_, base_screen_width_, base_screen_height_, SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
// Reinicar la escena con resolución original
initBalls(scenario_);
}
@@ -1376,6 +1361,9 @@ void Engine::setWindowZoom(int new_zoom) {
SDL_SetWindowSize(window_, new_width, new_height);
SDL_SetWindowPosition(window_, new_x, new_y);
current_window_zoom_ = new_zoom;
// Actualizar tamaño físico de ventana y fuentes
updatePhysicalWindowSize();
}
void Engine::zoomIn() {
@@ -1386,6 +1374,72 @@ void Engine::zoomOut() {
setWindowZoom(current_window_zoom_ - 1);
}
void Engine::updatePhysicalWindowSize() {
if (real_fullscreen_enabled_) {
// En fullscreen real (F4), usar resolución del display
physical_window_width_ = current_screen_width_;
physical_window_height_ = current_screen_height_;
} else if (fullscreen_enabled_) {
// En fullscreen F3, obtener tamaño REAL del display (no del framebuffer lógico)
// SDL_GetRenderOutputSize() falla en F3 (devuelve tamaño lógico 960x720)
// Necesitamos el tamaño FÍSICO real de la pantalla
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays != nullptr && num_displays > 0) {
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm != nullptr) {
physical_window_width_ = dm->w;
physical_window_height_ = dm->h;
}
SDL_free(displays);
}
} else {
// En modo ventana, obtener tamaño FÍSICO real del framebuffer
int window_w = 0, window_h = 0;
SDL_GetWindowSizeInPixels(window_, &window_w, &window_h);
physical_window_width_ = window_w;
physical_window_height_ = window_h;
}
// Recalcular tamaño de fuente basado en altura física
// Referencia: 8px a 1440p (monitor del usuario)
int font_size = (physical_window_height_ * TEXT_BASE_SIZE) / 1440;
if (font_size < 6) font_size = 6; // Tamaño mínimo legible
// Reinicializar TextRenderers con nuevo tamaño de fuente
text_renderer_.cleanup();
text_renderer_debug_.cleanup();
text_renderer_notifier_.cleanup();
text_renderer_.init(renderer_, TEXT_FONT_PATH, font_size, TEXT_ANTIALIASING);
text_renderer_debug_.init(renderer_, TEXT_FONT_PATH, font_size, TEXT_ANTIALIASING);
// TextRenderer para notificaciones: Detectar DPI y ajustar tamaño
// En pantallas Retina/HiDPI, el texto necesita ser más grande para ser legible
int logical_w = 0, logical_h = 0;
SDL_GetWindowSize(window_, &logical_w, &logical_h);
// Usar physical_window_width_ que ya contiene el tamaño real del framebuffer
// (calculado arriba con SDL_GetRenderOutputSize o current_screen_width_)
int pixels_w = physical_window_width_;
// Calcular escala DPI (1.0 normal, 2.0 Retina, 3.0 en algunos displays)
float dpi_scale = (logical_w > 0) ? static_cast<float>(pixels_w) / static_cast<float>(logical_w) : 1.0f;
// Ajustar tamaño de fuente base (16px) por escala DPI
// Retina macOS: 16px * 2.0 = 32px (legible)
// Normal: 16px * 1.0 = 16px
int notification_font_size = static_cast<int>(TEXT_ABSOLUTE_SIZE * dpi_scale);
if (notification_font_size < 12) notification_font_size = 12; // Mínimo legible
text_renderer_notifier_.init(renderer_, TEXT_FONT_PATH, notification_font_size, TEXT_ANTIALIASING);
// Inicializar/actualizar Notifier con nuevas dimensiones y ThemeManager
// NOTA: init() es seguro de llamar múltiples veces, solo actualiza punteros y dimensiones
// Esto asegura que el notifier tenga las referencias correctas tras resize/fullscreen
notifier_.init(renderer_, &text_renderer_notifier_, theme_manager_.get(), physical_window_width_, physical_window_height_);
}
// ============================================================================
// Sistema de gestión de estados (MANUAL/DEMO/DEMO_LITE/LOGO)
// ============================================================================
@@ -1436,7 +1490,7 @@ void Engine::setState(AppMode new_mode) {
void Engine::updateDemoMode() {
// Verificar si algún modo demo está activo (DEMO, DEMO_LITE o LOGO)
if (current_app_mode_ == AppMode::MANUAL) return;
if (current_app_mode_ == AppMode::SANDBOX) return;
// Actualizar timer
demo_timer_ += delta_time_;
@@ -1574,9 +1628,10 @@ void Engine::updateDemoMode() {
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
}
// Solo salir automáticamente si NO llegamos desde MANUAL
// Solo salir automáticamente si la entrada a LOGO fue automática (desde DEMO)
// No salir si el usuario entró manualmente con tecla K
// Probabilidad de salir: 60% en cada acción → sale rápido (relación DEMO:LOGO = 6:1)
if (previous_app_mode_ != AppMode::MANUAL && rand() % 100 < 60) {
if (!logo_entered_manually_ && rand() % 100 < 60) {
exitLogoMode(true); // Volver a DEMO/DEMO_LITE
}
}
@@ -1775,7 +1830,7 @@ void Engine::performDemoAction(bool is_lite) {
// Cambiar sprite (2%)
accumulated_weight += DEMO_WEIGHT_SPRITE;
if (random_value < accumulated_weight) {
switchTexture();
switchTexture(false); // Suprimir notificación en modo automático
return;
}
}
@@ -1783,6 +1838,15 @@ void Engine::performDemoAction(bool is_lite) {
// Randomizar todo al iniciar modo DEMO
void Engine::randomizeOnDemoStart(bool is_lite) {
// Si venimos de LOGO con PNG_SHAPE, cambiar figura obligatoriamente
// PNG_SHAPE es exclusivo del modo LOGO y no debe aparecer en DEMO/DEMO_LITE
if (current_shape_type_ == ShapeType::PNG_SHAPE) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX,
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
ShapeType::ICOSAHEDRON, ShapeType::ATOM};
activateShape(shapes[rand() % 8]);
}
if (is_lite) {
// DEMO LITE: Solo randomizar física/figura + gravedad
// Elegir aleatoriamente entre modo física o figura
@@ -1818,7 +1882,7 @@ void Engine::randomizeOnDemoStart(bool is_lite) {
// 3. Sprite
if (rand() % 2 == 0) {
switchTexture();
switchTexture(false); // Suprimir notificación al activar modo DEMO
}
// 4. Física o Figura
@@ -1884,18 +1948,18 @@ void Engine::enterLogoMode(bool from_demo) {
logo_previous_texture_index_ = current_texture_index_;
logo_previous_shape_scale_ = shape_scale_factor_;
// Buscar índice de textura "tiny"
size_t tiny_index = current_texture_index_; // Por defecto mantener actual
// Buscar índice de textura "small"
size_t small_index = current_texture_index_; // Por defecto mantener actual
for (size_t i = 0; i < texture_names_.size(); i++) {
if (texture_names_[i] == "tiny") {
tiny_index = i;
if (texture_names_[i] == "small") {
small_index = i;
break;
}
}
// Aplicar configuración fija del Modo Logo
if (tiny_index != current_texture_index_) {
current_texture_index_ = tiny_index;
if (small_index != current_texture_index_) {
current_texture_index_ = small_index;
int old_size = current_ball_size_;
current_ball_size_ = textures_[current_texture_index_]->getWidth();
updateBallSizes(old_size, current_ball_size_);
@@ -1907,8 +1971,10 @@ void Engine::enterLogoMode(bool from_demo) {
}
}
// Cambiar a tema MONOCHROME
theme_manager_->switchToTheme(5); // MONOCHROME
// Cambiar a tema aleatorio entre: MONOCHROME, LAVENDER, CRIMSON, ESMERALDA
int logo_themes[] = {5, 6, 7, 8}; // MONOCHROME, LAVENDER, CRIMSON, ESMERALDA
int random_theme = logo_themes[rand() % 4];
theme_manager_->switchToTheme(random_theme);
// Establecer escala a 120%
shape_scale_factor_ = LOGO_MODE_SHAPE_SCALE;
@@ -1932,6 +1998,9 @@ void Engine::enterLogoMode(bool from_demo) {
logo_target_flip_percentage_ = 0.0f;
logo_current_flip_count_ = 0;
// Guardar si entrada fue manual (tecla K) o automática (desde DEMO)
logo_entered_manually_ = !from_demo;
// Cambiar a modo LOGO (guarda previous_app_mode_ automáticamente)
setState(AppMode::LOGO);
}
@@ -1969,12 +2038,23 @@ void Engine::exitLogoMode(bool return_to_demo) {
}
}
// Resetear flag de entrada manual
logo_entered_manually_ = false;
if (!return_to_demo) {
// Salida manual (tecla K): volver a MANUAL
setState(AppMode::MANUAL);
setState(AppMode::SANDBOX);
} else {
// Volver al modo previo (DEMO o DEMO_LITE)
setState(previous_app_mode_);
// Si la figura activa es PNG_SHAPE, cambiar a otra figura aleatoria
if (current_shape_type_ == ShapeType::PNG_SHAPE) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX,
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
ShapeType::ICOSAHEDRON, ShapeType::ATOM};
activateShape(shapes[rand() % 8]);
}
}
}
@@ -2031,7 +2111,7 @@ void Engine::updateBallSizes(int old_size, int new_size) {
}
}
void Engine::switchTexture() {
void Engine::switchTexture(bool show_notification) {
if (textures_.empty()) return;
// Guardar tamaño antiguo
@@ -2053,16 +2133,11 @@ void Engine::switchTexture() {
// Ajustar posiciones según el cambio de tamaño
updateBallSizes(old_size, new_size);
// Mostrar texto informativo (solo si NO estamos en modo demo)
if (current_app_mode_ == AppMode::MANUAL) {
// Obtener nombre de textura (uppercase)
// Mostrar notificación con el nombre de la textura (solo si se solicita)
if (show_notification) {
std::string texture_name = texture_names_[current_texture_index_];
std::transform(texture_name.begin(), texture_name.end(), texture_name.begin(), ::toupper);
text_ = "Sprite: " + texture_name;
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
showNotificationForAction("Sprite: " + texture_name);
}
}
@@ -2104,7 +2179,7 @@ void Engine::toggleShapeMode(bool force_gravity_on_exit) {
}
// Mostrar texto informativo (solo si NO estamos en modo demo o logo)
if (current_app_mode_ == AppMode::MANUAL) {
if (current_app_mode_ == AppMode::SANDBOX) {
text_ = "Modo Física";
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
text_init_time_ = SDL_GetTicks();
@@ -2168,7 +2243,7 @@ void Engine::activateShape(ShapeType type) {
}
// Mostrar texto informativo con nombre de figura (solo si NO estamos en modo demo o logo)
if (active_shape_ && current_app_mode_ == AppMode::MANUAL) {
if (active_shape_ && current_app_mode_ == AppMode::SANDBOX) {
text_ = std::string("Modo ") + active_shape_->getName();
text_pos_ = (current_screen_width_ - text_renderer_.getTextWidth(text_.c_str())) / 2;
text_init_time_ = SDL_GetTicks();

View File

@@ -16,10 +16,11 @@
#include "shapes/shape.h" // for Shape (interfaz polimórfica)
#include "text/textrenderer.h" // for TextRenderer
#include "theme_manager.h" // for ThemeManager
#include "ui/notifier.h" // for Notifier
// Modos de aplicación mutuamente excluyentes
enum class AppMode {
MANUAL, // Control manual del usuario
SANDBOX, // Control manual del usuario (modo sandbox)
DEMO, // Modo demo completo (auto-play)
DEMO_LITE, // Modo demo lite (solo física/figuras)
LOGO // Modo logo (easter egg)
@@ -54,9 +55,11 @@ class Engine {
// UI y debug
bool show_debug_ = false;
bool show_text_ = true;
TextRenderer text_renderer_; // Sistema de renderizado de texto para display (centrado)
TextRenderer text_renderer_debug_; // Sistema de renderizado de texto para debug (HUD)
bool show_text_ = true; // OBSOLETO: usar notifier_ en su lugar
TextRenderer text_renderer_; // Sistema de renderizado de texto para display (centrado)
TextRenderer text_renderer_debug_; // Sistema de renderizado de texto para debug (HUD)
TextRenderer text_renderer_notifier_; // Sistema de renderizado de texto para notificaciones (tamaño fijo)
Notifier notifier_; // Sistema de notificaciones estilo iOS/Android
// Sistema de zoom dinámico
int current_window_zoom_ = DEFAULT_WINDOW_ZOOM;
@@ -83,6 +86,10 @@ class Engine {
int current_screen_width_ = DEFAULT_SCREEN_WIDTH;
int current_screen_height_ = DEFAULT_SCREEN_HEIGHT;
// Resolución física real de ventana/pantalla (para texto absoluto)
int physical_window_width_ = DEFAULT_SCREEN_WIDTH;
int physical_window_height_ = DEFAULT_SCREEN_HEIGHT;
// Sistema de temas (delegado a ThemeManager)
std::unique_ptr<ThemeManager> theme_manager_;
int theme_page_ = 0; // Página actual de temas (0 o 1) para acceso por Numpad
@@ -96,8 +103,8 @@ class Engine {
bool depth_zoom_enabled_ = true; // Zoom por profundidad Z activado
// Sistema de Modo DEMO (auto-play)
AppMode current_app_mode_ = AppMode::MANUAL; // Modo actual (mutuamente excluyente)
AppMode previous_app_mode_ = AppMode::MANUAL; // Modo previo antes de entrar a LOGO
AppMode current_app_mode_ = AppMode::SANDBOX; // Modo actual (mutuamente excluyente)
AppMode previous_app_mode_ = AppMode::SANDBOX; // Modo previo antes de entrar a LOGO
float demo_timer_ = 0.0f; // Contador de tiempo para próxima acción
float demo_next_action_time_ = 0.0f; // Tiempo aleatorio hasta próxima acción (segundos)
@@ -113,6 +120,9 @@ class Engine {
float logo_target_flip_percentage_ = 0.0f; // % de flip a esperar (0.2-0.8)
int logo_current_flip_count_ = 0; // Flips observados hasta ahora
// Control de entrada manual vs automática a LOGO MODE
bool logo_entered_manually_ = false; // true si se activó con tecla K, false si automático desde DEMO
// Estado previo antes de entrar a Logo Mode (para restaurar al salir)
int logo_previous_theme_ = 0; // Índice de tema (0-9)
size_t logo_previous_texture_index_ = 0;
@@ -124,7 +134,7 @@ class Engine {
// Configuración del sistema de texto (constantes configurables)
static constexpr const char* TEXT_FONT_PATH = "data/fonts/determination.ttf";
static constexpr int TEXT_BASE_SIZE = 8; // Tamaño base para 240p
static constexpr int TEXT_BASE_SIZE = 24; // Tamaño base para 240p
static constexpr bool TEXT_ANTIALIASING = true; // true = suavizado, false = píxeles nítidos
// Métodos principales del loop
@@ -135,7 +145,8 @@ class Engine {
// Métodos auxiliares
void initBalls(int value);
void setText();
void setText(); // DEPRECATED - usar showNotificationForAction() en su lugar
void showNotificationForAction(const std::string& text); // Mostrar notificación solo en modo MANUAL
void pushBallsAwayFromGravity();
void switchBallsGravity();
void enableBallsGravityIfDisabled();
@@ -163,14 +174,15 @@ class Engine {
void exitLogoMode(bool return_to_demo = false); // Salir del modo logo
// Sistema de cambio de sprites dinámico
void switchTexture(); // Cambia a siguiente textura disponible
void updateBallSizes(int old_size, int new_size); // Ajusta posiciones al cambiar tamaño
void switchTexture(bool show_notification = true); // Cambia a siguiente textura disponible
void updateBallSizes(int old_size, int new_size); // Ajusta posiciones al cambiar tamaño
// Sistema de zoom dinámico
int calculateMaxWindowZoom() const;
void setWindowZoom(int new_zoom);
void zoomIn();
void zoomOut();
void updatePhysicalWindowSize(); // Actualizar tamaño físico real de ventana
// Rendering
void addSpriteToBatch(float x, float y, float w, float h, int r, int g, int b, float scale = 1.0f);

View File

@@ -90,6 +90,136 @@ void TextRenderer::print(int x, int y, const std::string& text, uint8_t r, uint8
print(x, y, text.c_str(), r, g, b);
}
void TextRenderer::printPhysical(int logical_x, int logical_y, const char* text, uint8_t r, uint8_t g, uint8_t b, float scale_x, float scale_y) {
if (!isInitialized() || text == nullptr || text[0] == '\0') {
return;
}
// Convertir coordenadas lógicas a físicas
int physical_x = static_cast<int>(logical_x * scale_x);
int physical_y = static_cast<int>(logical_y * scale_y);
// Crear superficie con el texto renderizado
SDL_Color color = {r, g, b, 255};
SDL_Surface* text_surface = nullptr;
if (use_antialiasing_) {
text_surface = TTF_RenderText_Blended(font_, text, strlen(text), color);
} else {
text_surface = TTF_RenderText_Solid(font_, text, strlen(text), color);
}
if (text_surface == nullptr) {
SDL_Log("Error al renderizar texto: %s", SDL_GetError());
return;
}
// Crear textura desde la superficie
SDL_Texture* text_texture = SDL_CreateTextureFromSurface(renderer_, text_surface);
if (text_texture == nullptr) {
SDL_Log("Error al crear textura: %s", SDL_GetError());
SDL_DestroySurface(text_surface);
return;
}
// Renderizar en coordenadas físicas (bypass presentación lógica)
// Usar SDL_RenderTexture con coordenadas absolutas de ventana
SDL_FRect dest_rect;
dest_rect.x = static_cast<float>(physical_x);
dest_rect.y = static_cast<float>(physical_y);
dest_rect.w = static_cast<float>(text_surface->w);
dest_rect.h = static_cast<float>(text_surface->h);
// Deshabilitar temporalmente presentación lógica para renderizar en píxeles físicos
int logical_w = 0, logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode);
// Renderizar sin presentación lógica (coordenadas absolutas)
SDL_SetRenderLogicalPresentation(renderer_, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED);
SDL_RenderTexture(renderer_, text_texture, nullptr, &dest_rect);
// Restaurar presentación lógica
SDL_SetRenderLogicalPresentation(renderer_, logical_w, logical_h, presentation_mode);
// Limpiar recursos
SDL_DestroyTexture(text_texture);
SDL_DestroySurface(text_surface);
}
void TextRenderer::printPhysical(int logical_x, int logical_y, const std::string& text, uint8_t r, uint8_t g, uint8_t b, float scale_x, float scale_y) {
printPhysical(logical_x, logical_y, text.c_str(), r, g, b, scale_x, scale_y);
}
void TextRenderer::printAbsolute(int physical_x, int physical_y, const char* text, SDL_Color color) {
if (!isInitialized() || text == nullptr || text[0] == '\0') {
return;
}
// Crear superficie con el texto renderizado
SDL_Surface* text_surface = nullptr;
if (use_antialiasing_) {
text_surface = TTF_RenderText_Blended(font_, text, strlen(text), color);
} else {
text_surface = TTF_RenderText_Solid(font_, text, strlen(text), color);
}
if (text_surface == nullptr) {
SDL_Log("Error al renderizar texto: %s", SDL_GetError());
return;
}
// Crear textura desde la superficie
SDL_Texture* text_texture = SDL_CreateTextureFromSurface(renderer_, text_surface);
if (text_texture == nullptr) {
SDL_Log("Error al crear textura: %s", SDL_GetError());
SDL_DestroySurface(text_surface);
return;
}
// Configurar alpha blending si el color tiene transparencia
if (color.a < 255) {
SDL_SetTextureBlendMode(text_texture, SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaModFloat(text_texture, color.a / 255.0f);
}
// Obtener viewport ANTES de deshabilitar presentación lógica
// En modo letterbox (F3), SDL crea un viewport con offset para centrar la imagen
SDL_Rect viewport;
SDL_GetRenderViewport(renderer_, &viewport);
// Preparar rectángulo de destino en coordenadas físicas absolutas
// Aplicar offset del viewport para que el texto se pinte dentro del área visible
SDL_FRect dest_rect;
dest_rect.x = static_cast<float>(physical_x + viewport.x);
dest_rect.y = static_cast<float>(physical_y + viewport.y);
dest_rect.w = static_cast<float>(text_surface->w);
dest_rect.h = static_cast<float>(text_surface->h);
// Deshabilitar temporalmente presentación lógica para renderizar en píxeles físicos
int logical_w = 0, logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode);
// Renderizar sin presentación lógica (coordenadas absolutas con offset de viewport)
SDL_SetRenderLogicalPresentation(renderer_, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED);
SDL_RenderTexture(renderer_, text_texture, nullptr, &dest_rect);
// Restaurar presentación lógica
SDL_SetRenderLogicalPresentation(renderer_, logical_w, logical_h, presentation_mode);
// Limpiar recursos
SDL_DestroyTexture(text_texture);
SDL_DestroySurface(text_surface);
}
void TextRenderer::printAbsolute(int physical_x, int physical_y, const std::string& text, SDL_Color color) {
printAbsolute(physical_x, physical_y, text.c_str(), color);
}
int TextRenderer::getTextWidth(const char* text) {
if (!isInitialized() || text == nullptr) {
return 0;
@@ -103,6 +233,23 @@ int TextRenderer::getTextWidth(const char* text) {
return width;
}
int TextRenderer::getTextWidthPhysical(const char* text) {
// Retorna el ancho REAL en píxeles físicos (sin escalado lógico)
// Idéntico a getTextWidth() pero semánticamente diferente:
// - Este método se usa cuando se necesita el ancho REAL de la fuente
// - Útil para calcular dimensiones de UI en coordenadas físicas absolutas
if (!isInitialized() || text == nullptr) {
return 0;
}
int width = 0;
int height = 0;
if (!TTF_GetStringSize(font_, text, strlen(text), &width, &height)) {
return 0;
}
return width; // Ancho real de la textura generada por TTF
}
int TextRenderer::getTextHeight() {
if (!isInitialized()) {
return 0;

View File

@@ -19,9 +19,22 @@ public:
void print(int x, int y, const char* text, uint8_t r, uint8_t g, uint8_t b);
void print(int x, int y, const std::string& text, uint8_t r, uint8_t g, uint8_t b);
// Obtiene el ancho de un texto renderizado
// Renderiza texto en coordenadas lógicas, pero convierte a físicas para tamaño absoluto
void printPhysical(int logical_x, int logical_y, const char* text, uint8_t r, uint8_t g, uint8_t b, float scale_x, float scale_y);
void printPhysical(int logical_x, int logical_y, const std::string& text, uint8_t r, uint8_t g, uint8_t b, float scale_x, float scale_y);
// Renderiza texto en coordenadas físicas absolutas (tamaño fijo independiente de resolución)
// NOTA: Este método usa el tamaño de fuente tal cual fue cargado, sin escalado
void printAbsolute(int physical_x, int physical_y, const char* text, SDL_Color color);
void printAbsolute(int physical_x, int physical_y, const std::string& text, SDL_Color color);
// Obtiene el ancho de un texto renderizado (en píxeles lógicos para compatibilidad)
int getTextWidth(const char* text);
// Obtiene el ancho de un texto en píxeles FÍSICOS reales (sin escalado)
// Útil para notificaciones y elementos UI de tamaño fijo
int getTextWidthPhysical(const char* text);
// Obtiene la altura de la fuente
int getTextHeight();

View File

@@ -20,6 +20,7 @@ void ThemeManager::initialize() {
"Sunset",
"Atardecer",
255, 140, 60, // Color texto: naranja cálido
120, 40, 80, // Color fondo notificación: púrpura oscuro (contrasta con naranja)
180.0f / 255.0f, 140.0f / 255.0f, 100.0f / 255.0f, // Fondo superior: naranja suave
40.0f / 255.0f, 20.0f / 255.0f, 60.0f / 255.0f, // Fondo inferior: púrpura oscuro
std::vector<Color>{
@@ -33,6 +34,7 @@ void ThemeManager::initialize() {
"Ocean",
"Océano",
80, 200, 255, // Color texto: azul océano
20, 50, 90, // Color fondo notificación: azul marino oscuro (contrasta con cian)
100.0f / 255.0f, 150.0f / 255.0f, 200.0f / 255.0f, // Fondo superior: azul cielo
20.0f / 255.0f, 40.0f / 255.0f, 80.0f / 255.0f, // Fondo inferior: azul marino
std::vector<Color>{
@@ -46,6 +48,7 @@ void ThemeManager::initialize() {
"Neon",
"Neón",
255, 60, 255, // Color texto: magenta brillante
60, 0, 80, // Color fondo notificación: púrpura muy oscuro (contrasta con neón)
20.0f / 255.0f, 20.0f / 255.0f, 40.0f / 255.0f, // Fondo superior: negro azulado
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior: negro
std::vector<Color>{
@@ -59,6 +62,7 @@ void ThemeManager::initialize() {
"Forest",
"Bosque",
100, 255, 100, // Color texto: verde natural
70, 50, 30, // Color fondo notificación: marrón oscuro tierra (contrasta con verde)
144.0f / 255.0f, 238.0f / 255.0f, 144.0f / 255.0f, // Fondo superior: verde claro
101.0f / 255.0f, 67.0f / 255.0f, 33.0f / 255.0f, // Fondo inferior: marrón tierra
std::vector<Color>{
@@ -72,6 +76,7 @@ void ThemeManager::initialize() {
"RGB",
"RGB",
100, 100, 100, // Color texto: gris oscuro
220, 220, 220, // Color fondo notificación: gris muy claro (contrasta sobre blanco)
1.0f, 1.0f, 1.0f, // Fondo superior: blanco puro
1.0f, 1.0f, 1.0f, // Fondo inferior: blanco puro (sin degradado)
std::vector<Color>{
@@ -107,6 +112,7 @@ void ThemeManager::initialize() {
"Monochrome",
"Monocromo",
200, 200, 200, // Color texto: gris claro
50, 50, 50, // Color fondo notificación: gris medio oscuro (contrasta con texto claro)
20.0f / 255.0f, 20.0f / 255.0f, 20.0f / 255.0f, // Fondo superior: gris muy oscuro
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior: negro
std::vector<Color>{
@@ -120,6 +126,7 @@ void ThemeManager::initialize() {
"Lavender",
"Lavanda",
255, 200, 100, // Color texto: amarillo cálido
80, 50, 100, // Color fondo notificación: violeta muy oscuro (contrasta con amarillo)
120.0f / 255.0f, 80.0f / 255.0f, 140.0f / 255.0f, // Fondo superior: violeta oscuro
25.0f / 255.0f, 30.0f / 255.0f, 60.0f / 255.0f, // Fondo inferior: azul medianoche
std::vector<Color>{
@@ -133,6 +140,7 @@ void ThemeManager::initialize() {
"Crimson",
"Carmesí",
255, 100, 100, // Color texto: rojo claro
80, 10, 10, // Color fondo notificación: rojo muy oscuro (contrasta con texto claro)
40.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo superior: rojo muy oscuro
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior: negro puro
std::vector<Color>{
@@ -146,6 +154,7 @@ void ThemeManager::initialize() {
"Emerald",
"Esmeralda",
100, 255, 100, // Color texto: verde claro
10, 80, 10, // Color fondo notificación: verde muy oscuro (contrasta con texto claro)
0.0f / 255.0f, 40.0f / 255.0f, 0.0f / 255.0f, // Fondo superior: verde muy oscuro
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior: negro puro
std::vector<Color>{
@@ -168,6 +177,7 @@ void ThemeManager::initialize() {
{
20.0f / 255.0f, 25.0f / 255.0f, 60.0f / 255.0f, // Fondo superior: azul medianoche
10.0f / 255.0f, 10.0f / 255.0f, 30.0f / 255.0f, // Fondo inferior: azul muy oscuro
20, 30, 80, // Color fondo notificación: azul oscuro (noche)
std::vector<Color>{
{100, 100, 150}, {120, 120, 170}, {90, 90, 140}, {110, 110, 160},
{95, 95, 145}, {105, 105, 155}, {100, 100, 150}, {115, 115, 165}
@@ -178,6 +188,7 @@ void ThemeManager::initialize() {
{
180.0f / 255.0f, 100.0f / 255.0f, 120.0f / 255.0f, // Fondo superior: naranja-rosa
255.0f / 255.0f, 140.0f / 255.0f, 100.0f / 255.0f, // Fondo inferior: naranja cálido
140, 60, 80, // Color fondo notificación: naranja-rojo oscuro (alba)
std::vector<Color>{
{255, 180, 100}, {255, 160, 80}, {255, 200, 120}, {255, 150, 90},
{255, 190, 110}, {255, 170, 95}, {255, 185, 105}, {255, 165, 88}
@@ -188,6 +199,7 @@ void ThemeManager::initialize() {
{
255.0f / 255.0f, 240.0f / 255.0f, 180.0f / 255.0f, // Fondo superior: amarillo claro
255.0f / 255.0f, 255.0f / 255.0f, 220.0f / 255.0f, // Fondo inferior: amarillo muy claro
200, 180, 140, // Color fondo notificación: amarillo oscuro (día)
std::vector<Color>{
{255, 255, 200}, {255, 255, 180}, {255, 255, 220}, {255, 255, 190},
{255, 255, 210}, {255, 255, 185}, {255, 255, 205}, {255, 255, 195}
@@ -209,6 +221,7 @@ void ThemeManager::initialize() {
{
20.0f / 255.0f, 50.0f / 255.0f, 100.0f / 255.0f, // Fondo superior: azul marino
10.0f / 255.0f, 30.0f / 255.0f, 60.0f / 255.0f, // Fondo inferior: azul muy oscuro
10, 30, 70, // Color fondo notificación: azul muy oscuro (profundidad)
std::vector<Color>{
{60, 100, 180}, {50, 90, 170}, {70, 110, 190}, {55, 95, 175},
{65, 105, 185}, {58, 98, 172}, {62, 102, 182}, {52, 92, 168}
@@ -219,6 +232,7 @@ void ThemeManager::initialize() {
{
100.0f / 255.0f, 200.0f / 255.0f, 230.0f / 255.0f, // Fondo superior: turquesa claro
50.0f / 255.0f, 150.0f / 255.0f, 200.0f / 255.0f, // Fondo inferior: turquesa medio
30, 100, 140, // Color fondo notificación: turquesa oscuro (aguas poco profundas)
std::vector<Color>{
{100, 220, 255}, {90, 210, 245}, {110, 230, 255}, {95, 215, 250},
{105, 225, 255}, {98, 218, 248}, {102, 222, 252}, {92, 212, 242}
@@ -240,6 +254,7 @@ void ThemeManager::initialize() {
{
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo superior: negro
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior: negro
30, 30, 30, // Color fondo notificación: gris muy oscuro (apagado)
std::vector<Color>{
{40, 40, 40}, {50, 50, 50}, {45, 45, 45}, {48, 48, 48},
{42, 42, 42}, {47, 47, 47}, {44, 44, 44}, {46, 46, 46}
@@ -250,6 +265,7 @@ void ThemeManager::initialize() {
{
20.0f / 255.0f, 20.0f / 255.0f, 40.0f / 255.0f, // Fondo superior: azul oscuro
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior: negro
60, 0, 80, // Color fondo notificación: púrpura oscuro (neón encendido)
std::vector<Color>{
{0, 255, 255}, {255, 0, 255}, {0, 255, 200}, {255, 50, 255},
{50, 255, 255}, {255, 0, 200}, {0, 255, 230}, {255, 80, 255}
@@ -271,6 +287,7 @@ void ThemeManager::initialize() {
{
60.0f / 255.0f, 20.0f / 255.0f, 10.0f / 255.0f, // Fondo superior: rojo muy oscuro
20.0f / 255.0f, 10.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior: casi negro
70, 20, 10, // Color fondo notificación: rojo muy oscuro (brasas)
std::vector<Color>{
{120, 40, 20}, {140, 35, 15}, {130, 38, 18}, {125, 42, 22},
{135, 37, 16}, {128, 40, 20}, {132, 39, 19}, {138, 36, 17}
@@ -281,6 +298,7 @@ void ThemeManager::initialize() {
{
180.0f / 255.0f, 80.0f / 255.0f, 20.0f / 255.0f, // Fondo superior: naranja fuerte
100.0f / 255.0f, 30.0f / 255.0f, 10.0f / 255.0f, // Fondo inferior: rojo oscuro
110, 40, 10, // Color fondo notificación: naranja-rojo oscuro (llamas)
std::vector<Color>{
{255, 140, 0}, {255, 120, 10}, {255, 160, 20}, {255, 130, 5},
{255, 150, 15}, {255, 125, 8}, {255, 145, 12}, {255, 135, 18}
@@ -291,6 +309,7 @@ void ThemeManager::initialize() {
{
255.0f / 255.0f, 180.0f / 255.0f, 80.0f / 255.0f, // Fondo superior: amarillo-naranja brillante
220.0f / 255.0f, 100.0f / 255.0f, 30.0f / 255.0f, // Fondo inferior: naranja intenso
160, 80, 30, // Color fondo notificación: naranja oscuro (inferno)
std::vector<Color>{
{255, 220, 100}, {255, 200, 80}, {255, 240, 120}, {255, 210, 90},
{255, 230, 110}, {255, 205, 85}, {255, 225, 105}, {255, 215, 95}
@@ -301,6 +320,7 @@ void ThemeManager::initialize() {
{
180.0f / 255.0f, 80.0f / 255.0f, 20.0f / 255.0f, // Fondo superior: naranja fuerte
100.0f / 255.0f, 30.0f / 255.0f, 10.0f / 255.0f, // Fondo inferior: rojo oscuro
110, 40, 10, // Color fondo notificación: naranja-rojo oscuro (llamas)
std::vector<Color>{
{255, 140, 0}, {255, 120, 10}, {255, 160, 20}, {255, 130, 5},
{255, 150, 15}, {255, 125, 8}, {255, 145, 12}, {255, 135, 18}
@@ -322,6 +342,7 @@ void ThemeManager::initialize() {
{
30.0f / 255.0f, 80.0f / 255.0f, 60.0f / 255.0f, // Fondo superior: verde oscuro
10.0f / 255.0f, 20.0f / 255.0f, 30.0f / 255.0f, // Fondo inferior: azul muy oscuro
15, 50, 40, // Color fondo notificación: verde muy oscuro (aurora verde)
std::vector<Color>{
{100, 255, 180}, {80, 240, 160}, {120, 255, 200}, {90, 245, 170},
{110, 255, 190}, {85, 242, 165}, {105, 252, 185}, {95, 248, 175}
@@ -332,6 +353,7 @@ void ThemeManager::initialize() {
{
120.0f / 255.0f, 60.0f / 255.0f, 180.0f / 255.0f, // Fondo superior: violeta
40.0f / 255.0f, 20.0f / 255.0f, 80.0f / 255.0f, // Fondo inferior: violeta oscuro
70, 30, 100, // Color fondo notificación: violeta oscuro (aurora violeta)
std::vector<Color>{
{200, 100, 255}, {180, 80, 240}, {220, 120, 255}, {190, 90, 245},
{210, 110, 255}, {185, 85, 242}, {205, 105, 252}, {195, 95, 248}
@@ -342,6 +364,7 @@ void ThemeManager::initialize() {
{
60.0f / 255.0f, 180.0f / 255.0f, 220.0f / 255.0f, // Fondo superior: cian brillante
20.0f / 255.0f, 80.0f / 255.0f, 120.0f / 255.0f, // Fondo inferior: azul oscuro
20, 90, 120, // Color fondo notificación: cian oscuro (aurora cian)
std::vector<Color>{
{100, 220, 255}, {80, 200, 240}, {120, 240, 255}, {90, 210, 245},
{110, 230, 255}, {85, 205, 242}, {105, 225, 252}, {95, 215, 248}
@@ -352,6 +375,7 @@ void ThemeManager::initialize() {
{
120.0f / 255.0f, 60.0f / 255.0f, 180.0f / 255.0f, // Fondo superior: violeta
40.0f / 255.0f, 20.0f / 255.0f, 80.0f / 255.0f, // Fondo inferior: violeta oscuro
70, 30, 100, // Color fondo notificación: violeta oscuro (aurora violeta)
std::vector<Color>{
{200, 100, 255}, {180, 80, 240}, {220, 120, 255}, {190, 90, 245},
{210, 110, 255}, {185, 85, 242}, {205, 105, 252}, {195, 95, 248}
@@ -373,6 +397,7 @@ void ThemeManager::initialize() {
{
40.0f / 255.0f, 40.0f / 255.0f, 45.0f / 255.0f, // Fondo superior: gris oscuro
20.0f / 255.0f, 15.0f / 255.0f, 15.0f / 255.0f, // Fondo inferior: casi negro
50, 50, 55, // Color fondo notificación: gris oscuro (ceniza)
std::vector<Color>{
{80, 80, 90}, {75, 75, 85}, {85, 85, 95}, {78, 78, 88},
{82, 82, 92}, {76, 76, 86}, {84, 84, 94}, {79, 79, 89}
@@ -383,6 +408,7 @@ void ThemeManager::initialize() {
{
180.0f / 255.0f, 60.0f / 255.0f, 30.0f / 255.0f, // Fondo superior: naranja-rojo
80.0f / 255.0f, 20.0f / 255.0f, 10.0f / 255.0f, // Fondo inferior: rojo oscuro
120, 30, 15, // Color fondo notificación: naranja-rojo oscuro (erupción)
std::vector<Color>{
{255, 80, 40}, {255, 100, 50}, {255, 70, 35}, {255, 90, 45},
{255, 75, 38}, {255, 95, 48}, {255, 85, 42}, {255, 78, 40}
@@ -393,6 +419,7 @@ void ThemeManager::initialize() {
{
220.0f / 255.0f, 120.0f / 255.0f, 40.0f / 255.0f, // Fondo superior: naranja brillante
180.0f / 255.0f, 60.0f / 255.0f, 20.0f / 255.0f, // Fondo inferior: naranja-rojo
150, 70, 25, // Color fondo notificación: naranja oscuro (lava)
std::vector<Color>{
{255, 180, 80}, {255, 200, 100}, {255, 170, 70}, {255, 190, 90},
{255, 175, 75}, {255, 195, 95}, {255, 185, 85}, {255, 178, 78}
@@ -403,6 +430,7 @@ void ThemeManager::initialize() {
{
100.0f / 255.0f, 80.0f / 255.0f, 70.0f / 255.0f, // Fondo superior: gris-naranja
50.0f / 255.0f, 40.0f / 255.0f, 35.0f / 255.0f, // Fondo inferior: gris oscuro
80, 60, 50, // Color fondo notificación: gris-naranja oscuro (enfriamiento)
std::vector<Color>{
{150, 120, 100}, {140, 110, 90}, {160, 130, 110}, {145, 115, 95},
{155, 125, 105}, {142, 112, 92}, {158, 128, 108}, {148, 118, 98}
@@ -567,6 +595,22 @@ void ThemeManager::getCurrentThemeTextColor(int& r, int& g, int& b) const {
b = static_cast<int>(lerp(static_cast<float>(source_snapshot_->text_color_b), static_cast<float>(target_b), transition_progress_));
}
void ThemeManager::getCurrentNotificationBackgroundColor(int& r, int& g, int& b) const {
if (!transitioning_ || !source_snapshot_) {
// Sin transición: color directo del tema activo
themes_[current_theme_index_]->getNotificationBackgroundColor(r, g, b);
return;
}
// PHASE 3: Con transición: LERP entre snapshot origen y tema destino
int target_r, target_g, target_b;
themes_[current_theme_index_]->getNotificationBackgroundColor(target_r, target_g, target_b);
r = static_cast<int>(lerp(static_cast<float>(source_snapshot_->notif_bg_r), static_cast<float>(target_r), transition_progress_));
g = static_cast<int>(lerp(static_cast<float>(source_snapshot_->notif_bg_g), static_cast<float>(target_g), transition_progress_));
b = static_cast<int>(lerp(static_cast<float>(source_snapshot_->notif_bg_b), static_cast<float>(target_b), transition_progress_));
}
Color ThemeManager::getInitialBallColor(int random_index) const {
// Obtener color inicial del tema activo (progress = 0.0f)
return themes_[current_theme_index_]->getBallColor(random_index, 0.0f);
@@ -588,6 +632,10 @@ std::unique_ptr<ThemeSnapshot> ThemeManager::captureCurrentSnapshot() const {
themes_[current_theme_index_]->getTextColor(
snapshot->text_color_r, snapshot->text_color_g, snapshot->text_color_b);
// Capturar color de fondo de notificaciones
themes_[current_theme_index_]->getNotificationBackgroundColor(
snapshot->notif_bg_r, snapshot->notif_bg_g, snapshot->notif_bg_b);
// Capturar nombres
snapshot->name_en = themes_[current_theme_index_]->getNameEN();
snapshot->name_es = themes_[current_theme_index_]->getNameES();

View File

@@ -65,6 +65,7 @@ class ThemeManager {
const char* getCurrentThemeNameEN() const;
const char* getCurrentThemeNameES() const;
void getCurrentThemeTextColor(int& r, int& g, int& b) const;
void getCurrentNotificationBackgroundColor(int& r, int& g, int& b) const;
// Obtener color inicial para nuevas pelotas (usado en initBalls)
Color getInitialBallColor(int random_index) const;

View File

@@ -120,3 +120,16 @@ void DynamicTheme::getBackgroundColors(float progress,
bg = lerp(current_kf.bg_bottom_g, target_kf.bg_bottom_g, t);
bb = lerp(current_kf.bg_bottom_b, target_kf.bg_bottom_b, t);
}
void DynamicTheme::getNotificationBackgroundColor(int& r, int& g, int& b) const {
// Obtener keyframes actual y objetivo
const auto& current_kf = keyframes_[current_keyframe_index_];
const auto& target_kf = keyframes_[target_keyframe_index_];
// Interpolar color de fondo de notificación usando progreso interno
float t = transition_progress_;
r = static_cast<int>(lerp(static_cast<float>(current_kf.notif_bg_r), static_cast<float>(target_kf.notif_bg_r), t));
g = static_cast<int>(lerp(static_cast<float>(current_kf.notif_bg_g), static_cast<float>(target_kf.notif_bg_g), t));
b = static_cast<int>(lerp(static_cast<float>(current_kf.notif_bg_b), static_cast<float>(target_kf.notif_bg_b), t));
}

View File

@@ -48,6 +48,7 @@ class DynamicTheme : public Theme {
g = text_g_;
b = text_b_;
}
void getNotificationBackgroundColor(int& r, int& g, int& b) const override;
// ========================================
// CORE: OBTENER COLORES (interpolados)

View File

@@ -2,12 +2,14 @@
StaticTheme::StaticTheme(const char* name_en, const char* name_es,
int text_r, int text_g, int text_b,
int notif_bg_r, int notif_bg_g, int notif_bg_b,
float bg_top_r, float bg_top_g, float bg_top_b,
float bg_bottom_r, float bg_bottom_g, float bg_bottom_b,
std::vector<Color> ball_colors)
: name_en_(name_en),
name_es_(name_es),
text_r_(text_r), text_g_(text_g), text_b_(text_b),
notif_bg_r_(notif_bg_r), notif_bg_g_(notif_bg_g), notif_bg_b_(notif_bg_b),
bg_top_r_(bg_top_r), bg_top_g_(bg_top_g), bg_top_b_(bg_top_b),
bg_bottom_r_(bg_bottom_r), bg_bottom_g_(bg_bottom_g), bg_bottom_b_(bg_bottom_b),
ball_colors_(std::move(ball_colors)) {

View File

@@ -23,12 +23,14 @@ class StaticTheme : public Theme {
* @param name_en: Nombre en inglés
* @param name_es: Nombre en español
* @param text_r, text_g, text_b: Color de texto UI
* @param notif_bg_r, notif_bg_g, notif_bg_b: Color de fondo de notificaciones
* @param bg_top_r, bg_top_g, bg_top_b: Color superior de fondo
* @param bg_bottom_r, bg_bottom_g, bg_bottom_b: Color inferior de fondo
* @param ball_colors: Paleta de colores para pelotas
*/
StaticTheme(const char* name_en, const char* name_es,
int text_r, int text_g, int text_b,
int notif_bg_r, int notif_bg_g, int notif_bg_b,
float bg_top_r, float bg_top_g, float bg_top_b,
float bg_bottom_r, float bg_bottom_g, float bg_bottom_b,
std::vector<Color> ball_colors);
@@ -46,6 +48,11 @@ class StaticTheme : public Theme {
g = text_g_;
b = text_b_;
}
void getNotificationBackgroundColor(int& r, int& g, int& b) const override {
r = notif_bg_r_;
g = notif_bg_g_;
b = notif_bg_b_;
}
// ========================================
// CORE: OBTENER COLORES
@@ -66,6 +73,7 @@ class StaticTheme : public Theme {
std::string name_en_;
std::string name_es_;
int text_r_, text_g_, text_b_;
int notif_bg_r_, notif_bg_g_, notif_bg_b_;
float bg_top_r_, bg_top_g_, bg_top_b_;
float bg_bottom_r_, bg_bottom_g_, bg_bottom_b_;
std::vector<Color> ball_colors_;

View File

@@ -26,6 +26,7 @@ class Theme {
virtual const char* getNameEN() const = 0;
virtual const char* getNameES() const = 0;
virtual void getTextColor(int& r, int& g, int& b) const = 0;
virtual void getNotificationBackgroundColor(int& r, int& g, int& b) const = 0;
// ========================================
// CORE: OBTENER COLORES (polimórfico)

View File

@@ -31,6 +31,9 @@ struct ThemeSnapshot {
// Color de texto UI
int text_color_r, text_color_g, text_color_b;
// Color de fondo de notificaciones
int notif_bg_r, notif_bg_g, notif_bg_b;
// Nombres del tema (para mostrar "SOURCE → TARGET" durante transición)
std::string name_en;
std::string name_es;

246
source/ui/notifier.cpp Normal file
View File

@@ -0,0 +1,246 @@
#include "notifier.h"
#include "../text/textrenderer.h"
#include "../theme_manager.h"
#include "../defines.h"
#include "../utils/easing_functions.h"
#include <SDL3/SDL.h>
Notifier::Notifier()
: renderer_(nullptr)
, text_renderer_(nullptr)
, theme_manager_(nullptr)
, window_width_(0)
, window_height_(0)
, current_notification_(nullptr) {
}
Notifier::~Notifier() {
clear();
}
bool Notifier::init(SDL_Renderer* renderer, TextRenderer* text_renderer, ThemeManager* theme_manager, int window_width, int window_height) {
renderer_ = renderer;
text_renderer_ = text_renderer;
theme_manager_ = theme_manager;
window_width_ = window_width;
window_height_ = window_height;
return (renderer_ != nullptr && text_renderer_ != nullptr && theme_manager_ != nullptr);
}
void Notifier::updateWindowSize(int window_width, int window_height) {
window_width_ = window_width;
window_height_ = window_height;
}
void Notifier::show(const std::string& text, Uint64 duration) {
if (text.empty()) {
return;
}
// Usar duración default si no se especifica
if (duration == 0) {
duration = NOTIFICATION_DURATION;
}
// NUEVO: Limpiar notificación actual y cola (solo mostrar la última)
// Si hay una notificación activa, destruirla inmediatamente
current_notification_.reset();
// Vaciar cola completa (descartar notificaciones pendientes)
while (!notification_queue_.empty()) {
notification_queue_.pop();
}
// Crear nueva notificación
Notification notif;
notif.text = text;
notif.created_time = SDL_GetTicks();
notif.duration = duration;
notif.state = NotificationState::SLIDING_IN;
notif.alpha = 1.0f;
notif.y_offset = -50.0f; // Comienza 50px arriba (fuera de pantalla)
// NOTA: Los colores se obtienen dinámicamente desde ThemeManager en render()
// Activar inmediatamente como notificación actual (sin esperar en cola)
current_notification_ = std::make_unique<Notification>(notif);
}
void Notifier::update(Uint64 current_time) {
// Activar siguiente notificación si no hay ninguna activa
if (!current_notification_ && !notification_queue_.empty()) {
processQueue();
}
// Actualizar notificación actual
if (current_notification_) {
Uint64 elapsed = current_time - current_notification_->created_time;
switch (current_notification_->state) {
case NotificationState::SLIDING_IN: {
// Animación de entrada (NOTIFICATION_SLIDE_TIME ms)
if (elapsed < NOTIFICATION_SLIDE_TIME) {
float progress = static_cast<float>(elapsed) / static_cast<float>(NOTIFICATION_SLIDE_TIME);
float eased = Easing::easeOutBack(progress); // Efecto con ligero overshoot
current_notification_->y_offset = -50.0f + (50.0f * eased); // De -50 a 0
} else {
// Transición a VISIBLE
current_notification_->y_offset = 0.0f;
current_notification_->state = NotificationState::VISIBLE;
}
break;
}
case NotificationState::VISIBLE: {
// Esperar hasta que se cumpla la duración
Uint64 visible_time = current_notification_->duration - NOTIFICATION_FADE_TIME;
if (elapsed >= visible_time) {
current_notification_->state = NotificationState::FADING_OUT;
}
break;
}
case NotificationState::FADING_OUT: {
// Animación de salida (NOTIFICATION_FADE_TIME ms)
Uint64 fade_start = current_notification_->duration - NOTIFICATION_FADE_TIME;
Uint64 fade_elapsed = elapsed - fade_start;
if (fade_elapsed < NOTIFICATION_FADE_TIME) {
float progress = static_cast<float>(fade_elapsed) / static_cast<float>(NOTIFICATION_FADE_TIME);
float eased = Easing::easeInQuad(progress); // Fade suave
current_notification_->alpha = 1.0f - eased;
} else {
// Transición a DONE
current_notification_->alpha = 0.0f;
current_notification_->state = NotificationState::DONE;
}
break;
}
case NotificationState::DONE: {
// Eliminar notificación actual
current_notification_.reset();
break;
}
}
}
}
void Notifier::render() {
if (!current_notification_ || !text_renderer_ || !renderer_ || !theme_manager_) {
return;
}
// Obtener colores DINÁMICOS desde ThemeManager (incluye LERP automático)
int text_r, text_g, text_b;
theme_manager_->getCurrentThemeTextColor(text_r, text_g, text_b);
SDL_Color text_color = {
static_cast<Uint8>(text_r),
static_cast<Uint8>(text_g),
static_cast<Uint8>(text_b),
static_cast<Uint8>(current_notification_->alpha * 255.0f)
};
int bg_r, bg_g, bg_b;
theme_manager_->getCurrentNotificationBackgroundColor(bg_r, bg_g, bg_b);
SDL_Color bg_color = {
static_cast<Uint8>(bg_r),
static_cast<Uint8>(bg_g),
static_cast<Uint8>(bg_b),
255
};
// Calcular dimensiones del texto en píxeles FÍSICOS
// IMPORTANTE: Usar getTextWidthPhysical() en lugar de getTextWidth()
// para obtener el ancho REAL de la fuente (sin escalado lógico)
int text_width = text_renderer_->getTextWidthPhysical(current_notification_->text.c_str());
int text_height = text_renderer_->getTextHeight();
// Calcular dimensiones del fondo con padding (en píxeles físicos)
int bg_width = text_width + (NOTIFICATION_PADDING * 2);
int bg_height = text_height + (NOTIFICATION_PADDING * 2);
// Centrar en la ventana FÍSICA (no usar viewport lógico)
// CRÍTICO: Como renderizamos en píxeles físicos absolutos (bypass de presentación lógica),
// debemos centrar usando dimensiones físicas, no el viewport lógico de SDL
int x = (window_width_ / 2) - (bg_width / 2);
int y = NOTIFICATION_TOP_MARGIN + static_cast<int>(current_notification_->y_offset);
// Renderizar fondo semitransparente (con bypass de presentación lógica)
float bg_alpha = current_notification_->alpha * NOTIFICATION_BG_ALPHA;
renderBackground(x, y, bg_width, bg_height, bg_alpha, bg_color);
// Renderizar texto con alpha usando printAbsolute (tamaño físico fijo)
int text_x = x + NOTIFICATION_PADDING;
int text_y = y + NOTIFICATION_PADDING;
// printAbsolute() ya maneja el bypass de presentación lógica internamente
text_renderer_->printAbsolute(text_x, text_y, current_notification_->text.c_str(), text_color);
}
void Notifier::renderBackground(int x, int y, int width, int height, float alpha, SDL_Color bg_color) {
if (!renderer_) {
return;
}
// Obtener viewport ANTES de deshabilitar presentación lógica
// En modo letterbox (F3), SDL crea un viewport con offset para centrar la imagen
SDL_Rect viewport;
SDL_GetRenderViewport(renderer_, &viewport);
// Crear rectángulo para el fondo (en coordenadas físicas)
// Aplicar offset del viewport para que el fondo se pinte dentro del área visible
SDL_FRect bg_rect;
bg_rect.x = static_cast<float>(x + viewport.x);
bg_rect.y = static_cast<float>(y + viewport.y);
bg_rect.w = static_cast<float>(width);
bg_rect.h = static_cast<float>(height);
// Color del tema con alpha
Uint8 bg_alpha = static_cast<Uint8>(alpha * 255.0f);
SDL_SetRenderDrawColor(renderer_, bg_color.r, bg_color.g, bg_color.b, bg_alpha);
// Habilitar blending para transparencia
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
// CRÍTICO: Deshabilitar presentación lógica para renderizar en píxeles físicos absolutos
// (igual que printAbsolute() en TextRenderer)
int logical_w = 0, logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode);
// Renderizar sin presentación lógica (coordenadas físicas absolutas con offset de viewport)
SDL_SetRenderLogicalPresentation(renderer_, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED);
SDL_RenderFillRect(renderer_, &bg_rect);
// Restaurar presentación lógica
SDL_SetRenderLogicalPresentation(renderer_, logical_w, logical_h, presentation_mode);
// Restaurar blend mode
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_NONE);
}
bool Notifier::isActive() const {
return (current_notification_ != nullptr);
}
void Notifier::clear() {
// Vaciar cola
while (!notification_queue_.empty()) {
notification_queue_.pop();
}
// Eliminar notificación actual
current_notification_.reset();
}
void Notifier::processQueue() {
if (notification_queue_.empty()) {
return;
}
// Sacar siguiente notificación de la cola
Notification next_notif = notification_queue_.front();
notification_queue_.pop();
// Activarla como notificación actual
current_notification_ = std::make_unique<Notification>(next_notif);
}

111
source/ui/notifier.h Normal file
View File

@@ -0,0 +1,111 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include <queue>
#include <memory>
// Forward declarations
class TextRenderer;
class ThemeManager;
/**
* @brief Sistema de notificaciones estilo iOS/Android
*
* Maneja notificaciones temporales con animaciones suaves:
* - Slide-in desde arriba
* - Fade-out al desaparecer
* - Cola FIFO de mensajes
* - Fondo semitransparente
* - Texto de tamaño fijo independiente de resolución
*/
class Notifier {
public:
enum class NotificationState {
SLIDING_IN, // Animación de entrada desde arriba
VISIBLE, // Visible estático
FADING_OUT, // Animación de salida (fade)
DONE // Completado, listo para eliminar
};
struct Notification {
std::string text;
Uint64 created_time;
Uint64 duration;
NotificationState state;
float alpha; // Opacidad 0.0-1.0
float y_offset; // Offset Y para animación slide (píxeles)
// NOTA: Los colores se obtienen dinámicamente desde ThemeManager en render()
};
Notifier();
~Notifier();
/**
* @brief Inicializa el notifier con un TextRenderer y ThemeManager
* @param renderer SDL renderer para dibujar
* @param text_renderer TextRenderer configurado con tamaño absoluto
* @param theme_manager ThemeManager para obtener colores dinámicos con LERP
* @param window_width Ancho de ventana física
* @param window_height Alto de ventana física
* @return true si inicialización exitosa
*/
bool init(SDL_Renderer* renderer, TextRenderer* text_renderer, ThemeManager* theme_manager, int window_width, int window_height);
/**
* @brief Actualiza las dimensiones de la ventana (llamar en resize)
* @param window_width Nuevo ancho de ventana física
* @param window_height Nuevo alto de ventana física
*/
void updateWindowSize(int window_width, int window_height);
/**
* @brief Muestra una nueva notificación
* @param text Texto a mostrar
* @param duration Duración en milisegundos (0 = usar default)
* @note Los colores se obtienen dinámicamente desde ThemeManager cada frame
*/
void show(const std::string& text, Uint64 duration = 0);
/**
* @brief Actualiza las animaciones de notificaciones
* @param current_time Tiempo actual en ms (SDL_GetTicks())
*/
void update(Uint64 current_time);
/**
* @brief Renderiza la notificación activa
*/
void render();
/**
* @brief Verifica si hay una notificación activa (visible)
* @return true si hay notificación mostrándose
*/
bool isActive() const;
/**
* @brief Limpia todas las notificaciones pendientes
*/
void clear();
private:
SDL_Renderer* renderer_;
TextRenderer* text_renderer_;
ThemeManager* theme_manager_; // Gestor de temas para obtener colores dinámicos con LERP
int window_width_;
int window_height_;
std::queue<Notification> notification_queue_;
std::unique_ptr<Notification> current_notification_;
/**
* @brief Procesa la cola y activa la siguiente notificación si es posible
*/
void processQueue();
/**
* @brief Dibuja el fondo semitransparente de la notificación
*/
void renderBackground(int x, int y, int width, int height, float alpha, SDL_Color bg_color);
};

View File

@@ -0,0 +1,213 @@
#pragma once
/**
* @file easing_functions.h
* @brief Funciones de suavizado (easing) para animaciones
*
* Colección de funciones matemáticas para interpolar valores de forma suave.
* Todas las funciones toman un parámetro t (0.0 - 1.0) y devuelven un valor interpolado.
*
* Uso típico:
* float progress = elapsed_time / total_duration; // 0.0 a 1.0
* float eased = easeOutCubic(progress);
* float current_value = start + (end - start) * eased;
*
* Referencias:
* - https://easings.net/
* - Robert Penner's Easing Functions
*/
#include <cmath>
namespace Easing {
/**
* @brief Interpolación lineal (sin suavizado)
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado linealmente
*
* Uso: Movimiento constante, sin aceleración
*/
inline float linear(float t) {
return t;
}
/**
* @brief Aceleración cuadrática (slow start)
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado con aceleración cuadrática
*
* Uso: Inicio lento que acelera
*/
inline float easeInQuad(float t) {
return t * t;
}
/**
* @brief Desaceleración cuadrática (slow end)
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado con desaceleración cuadrática
*
* Uso: Llegada suave, objetos que frenan
*/
inline float easeOutQuad(float t) {
return t * (2.0f - t);
}
/**
* @brief Aceleración y desaceleración cuadrática (slow start & end)
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado con aceleración y desaceleración
*
* Uso: Movimiento suave en ambos extremos
*/
inline float easeInOutQuad(float t) {
return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
}
/**
* @brief Aceleración cúbica (slower start)
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado con aceleración cúbica
*
* Uso: Inicio muy lento, aceleración pronunciada
*/
inline float easeInCubic(float t) {
return t * t * t;
}
/**
* @brief Desaceleración cúbica (slower end)
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado con desaceleración cúbica
*
* Uso: Frenado suave y natural
*/
inline float easeOutCubic(float t) {
float f = t - 1.0f;
return f * f * f + 1.0f;
}
/**
* @brief Aceleración y desaceleración cúbica
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado con curva cúbica
*
* Uso: Animaciones muy suaves en ambos extremos
*/
inline float easeInOutCubic(float t) {
if (t < 0.5f) {
return 4.0f * t * t * t;
} else {
float f = (2.0f * t - 2.0f);
return 0.5f * f * f * f + 1.0f;
}
}
/**
* @brief Efecto elástico con rebote al final
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado con rebote elástico
*
* Uso: Elementos UI que "rebotan" al llegar, notificaciones
* ⚠️ Puede sobrepasar el valor 1.0 temporalmente (overshoot)
*/
inline float easeOutElastic(float t) {
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
constexpr float p = 0.3f;
constexpr float s = p / 4.0f;
return powf(2.0f, -10.0f * t) * sinf((t - s) * (2.0f * 3.14159265358979323846f) / p) + 1.0f;
}
/**
* @brief Sobrepaso suave al final (back easing)
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado con sobrepaso
*
* Uso: Elementos que "se pasan" levemente y vuelven, efecto cartoon
* ⚠️ Puede sobrepasar el valor 1.0 temporalmente (overshoot ~10%)
*/
inline float easeOutBack(float t) {
constexpr float c1 = 1.70158f;
constexpr float c3 = c1 + 1.0f;
return 1.0f + c3 * powf(t - 1.0f, 3.0f) + c1 * powf(t - 1.0f, 2.0f);
}
/**
* @brief Sobrepaso suave al inicio (back easing)
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado con retroceso inicial
*
* Uso: Elementos que "retroceden" antes de avanzar
* ⚠️ Puede generar valores negativos al inicio (undershoot ~10%)
*/
inline float easeInBack(float t) {
constexpr float c1 = 1.70158f;
constexpr float c3 = c1 + 1.0f;
return c3 * t * t * t - c1 * t * t;
}
/**
* @brief Rebote físico al final (bounce effect)
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado con rebotes
*
* Uso: Objetos que caen y rebotan, físicas cartoon
*/
inline float easeOutBounce(float t) {
constexpr float n1 = 7.5625f;
constexpr 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;
}
}
/**
* @brief Aceleración exponencial (muy dramática)
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado exponencialmente
*
* Uso: Efectos dramáticos, zoom in rápido
*/
inline float easeInExpo(float t) {
return t == 0.0f ? 0.0f : powf(2.0f, 10.0f * (t - 1.0f));
}
/**
* @brief Desaceleración exponencial (muy dramática)
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado exponencialmente
*
* Uso: Fade outs dramáticos, zoom out rápido
*/
inline float easeOutExpo(float t) {
return t == 1.0f ? 1.0f : 1.0f - powf(2.0f, -10.0f * t);
}
/**
* @brief Curva circular (cuarto de círculo)
* @param t Progreso normalizado (0.0 - 1.0)
* @return Valor interpolado con curva circular
*
* Uso: Movimientos muy suaves y naturales
*/
inline float easeOutCirc(float t) {
return sqrtf(1.0f - powf(t - 1.0f, 2.0f));
}
} // namespace Easing