Compare commits

20 Commits

Author SHA1 Message Date
c91cb1ca56 feat: Sistema dual de logos con animaciones independientes + ajuste de tamaño/posición
Implementación de sistema de 2 logos superpuestos con animaciones completamente independientes:

**Nuevas características:**
- Dos logos superpuestos (logo1.png + logo2.png) con animaciones independientes
- 4 tipos de animación: ZOOM_ONLY, ELASTIC_STICK, ROTATE_SPIRAL, BOUNCE_SQUASH
- Aleatorización independiente para entrada y salida de cada logo
- 256 combinaciones posibles (4×4 entrada × 4×4 salida)

**Ajuste de tamaño y posición:**
- Nueva constante APPLOGO_HEIGHT_PERCENT (40%) - altura del logo respecto a pantalla
- Nueva constante APPLOGO_PADDING_PERCENT (10%) - padding desde esquina inferior-derecha
- Logo anclado a esquina en lugar de centrado en cuadrante
- Valores fácilmente ajustables mediante constantes en defines.h

**Cambios técnicos:**
- Variables duplicadas para logo1 y logo2 (scale, squash, stretch, rotation)
- Variables compartidas para sincronización (state, timer, alpha)
- renderWithGeometry() acepta parámetro logo_index (1 o 2)
- Logo1 renderizado primero (fondo), Logo2 encima (overlay)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 10:01:32 +02:00
8d608357b4 feat: Animación elástica tipo pegatina para AppLogo
Implementación de deformación elástica con vértices para el logo:

FADE IN (0.5s):
- Scale: 120% → 100% con easing elástico (bounce)
- Squash Y: 0.6 → 1.0 con easing back (aplastamiento)
- Stretch X: compensación automática
- Efecto: logo se "pega" aplastándose y rebotando

FADE OUT (0.5s):
- Scale: 100% → 120% (aceleración cuadrática)
- Squash Y: 1.0 → 1.3 (estiramiento vertical)
- Stretch X: 1.0 → 0.8 (compresión horizontal)
- Rotación: 0° → ~5.7° (torsión sutil)
- Efecto: logo se "despega" estirándose y girando

Características técnicas:
- Enum AppLogoAnimationType (ZOOM_ONLY / ELASTIC_STICK)
- Renderizado con SDL_RenderGeometry para deformaciones
- Funciones de easing: easeOutElastic() y easeOutBack()
- Transformación de vértices con rotación y escala 2D
- Actualmente fijo en ELASTIC_STICK para testing

Limpieza adicional:
- Eliminado dbgtxt.h (no utilizado)
- Removidos SDL_Log de debug en HelpOverlay
- Comentada variable no usada en ShapeManager

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 01:46:56 +02:00
f73a133756 feat: Sistema de logo periódico con fade in/out
- Nuevo sistema AppLogo que muestra el logo cada 20 segundos por 5 segundos
- Fade in/out suave de 0.5 segundos con alpha blending
- Máquina de estados: HIDDEN → FADE_IN → VISIBLE → FADE_OUT
- Logo posicionado en cuadrante inferior derecho (1/4 de pantalla)
- Añadido método setAlpha() a Texture para control de transparencia
- Habilitado SDL_BLENDMODE_BLEND en todas las texturas
- Filtrado LINEAR para suavizado del logo escalado
- Desactivado automáticamente en modo SANDBOX

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 01:31:29 +02:00
de23327861 fix: Mantener gravedad OFF al cambiar escenario en modo BOIDS
Problema:
- Al cambiar de escenario (teclas 1-8) en modo BOIDS, la gravedad
  se reseteaba a 720 en lugar de mantenerse en 0
- SceneManager::changeScenario() reinicializa bolas con gravedad default
- Esto rompía el invariante: "modo BOIDS = gravedad OFF siempre"

Solución:
- Añadido check en Engine::changeScenario() para forzar gravedad OFF
  después del cambio de escenario si estamos en modo BOIDS
- Mantiene consistencia con el comportamiento de SHAPE mode

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 00:14:08 +02:00
f6402084eb feat: Bordes como obstáculos + Variables BOIDS ajustables + Fix tecla G
**1. Bordes como obstáculos (no más wrapping):**
- Implementada fuerza de repulsión cuando boids se acercan a bordes
- Nueva regla: Boundary Avoidance (evitar bordes)
- Fuerza proporcional a cercanía (0% en margen, 100% en colisión)
- Constantes: BOID_BOUNDARY_MARGIN (50px), BOID_BOUNDARY_WEIGHT (7200 px/s²)

**2. Variables ajustables en runtime:**
- Añadidas 11 variables miembro en BoidManager (inicializadas con defines.h)
- Permite modificar comportamiento sin recompilar
- Variables: radios (separation/alignment/cohesion), weights, speeds, boundary
- Base para futuras herramientas de debug/tweaking visual

**3. Fix tecla G (BOIDS → PHYSICS):**
- Corregido: toggleBoidsMode() ahora acepta parámetro force_gravity_on
- handleGravityToggle() pasa explícitamente false para preservar inercia
- Transición BOIDS→PHYSICS ahora mantiene gravedad OFF correctamente

**Implementación:**
- defines.h: +2 constantes (BOUNDARY_MARGIN, BOUNDARY_WEIGHT)
- boid_manager.h: +11 variables miembro ajustables
- boid_manager.cpp:
  - Constructor inicializa variables
  - Todas las funciones usan variables en lugar de constantes
  - applyBoundaries() completamente reescrito (repulsión vs wrapping)
- engine.h/cpp: toggleBoidsMode() con parámetro opcional

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 20:09:33 +02:00
9909d4c12d feat: Convertir BOIDS a sistema time-based (independiente de framerate)
- Conversión completa de físicas BOIDS de frame-based a time-based
- Velocidades: ×60 (px/frame → px/s)
- Aceleraciones (Separation, Cohesion): ×3600 (px/frame² → px/s²)
- Steering proporcional (Alignment): ×60
- Límites de velocidad: ×60

Constantes actualizadas en defines.h:
- BOID_SEPARATION_WEIGHT: 1.5 → 5400.0 (aceleración)
- BOID_COHESION_WEIGHT: 0.001 → 3.6 (aceleración)
- BOID_ALIGNMENT_WEIGHT: 1.0 → 60.0 (steering)
- BOID_MAX_SPEED: 2.5 → 150.0 px/s
- BOID_MIN_SPEED: 0.3 → 18.0 px/s
- BOID_MAX_FORCE: 0.05 → 3.0 px/s

Física ahora consistente en 60Hz, 144Hz, 240Hz screens.
Transiciones BOIDS↔PHYSICS preservan velocidad correctamente.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 20:05:49 +02:00
a929346463 fix: SHAPE mode - regenerar figuras al cambiar tamaño con F4
Al entrar o salir del modo fullscreen real (F4), el área de juego cambia
de tamaño. Si estábamos en modo SHAPE, las bolas aparecían centradas pero
sin formar la figura.

PROBLEMA RAÍZ:
- changeScenario() recrea las bolas en nuevas posiciones
- NO se regeneraban los targets de la figura (puntos 3D)
- NO se reactivaba la atracción física hacia los targets
- Resultado: bolas centradas sin formar figura

SOLUCIÓN:
Después de changeScenario() en ambas transiciones de F4:
1. Llamar a generateShape() (Engine, no ShapeManager)
2. Reactivar enableShapeAttraction(true) en todas las bolas

Esto sigue el mismo patrón usado en Engine::changeScenario() (línea 515).

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 09:34:20 +02:00
c4075f68db fix: Debug HUD usa viewport físico en F3 (coordenadas reales)
Problema:
- En modo F3 (letterbox/integer scale), el debug HUD se pintaba
  fuera del área de juego (en las barras negras laterales)
- SDL_GetRenderViewport() devuelve coordenadas LÓGICAS cuando hay
  presentación lógica activa
- printAbsolute() trabaja en píxeles FÍSICOS
- Mismatch de coordenadas causaba alineación derecha incorrecta

Solución:
- Nuevo helper getPhysicalViewport() que:
  1. Guarda estado de presentación lógica
  2. Deshabilita presentación lógica temporalmente
  3. Obtiene viewport en coordenadas físicas
  4. Restaura presentación lógica
  5. Retorna viewport físico

- UIManager::renderDebugHUD() ahora usa physical_viewport.w
  para cálculo de alineación derecha (9 referencias actualizadas)

Resultado:
- Debug HUD alineado correctamente en F3 letterbox
- Debug HUD alineado correctamente en F4 integer scale
- Modo ventana sigue funcionando correctamente

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 08:08:04 +02:00
399650f8da fix: Notifier usa viewport físico en F3 (coordenadas reales)
Problema:
- En modo F3 (letterbox/integer scale), las notificaciones se pintaban
  fuera del área de juego (en las barras negras)
- SDL_GetRenderViewport() devuelve coordenadas LÓGICAS cuando hay
  presentación lógica activa
- printAbsolute() trabaja en píxeles FÍSICOS
- Mismatch de coordenadas causaba centrado incorrecto

Solución:
- Nuevo helper getPhysicalViewport() que:
  1. Guarda estado de presentación lógica
  2. Deshabilita presentación lógica temporalmente
  3. Obtiene viewport en coordenadas físicas
  4. Restaura presentación lógica
  5. Retorna viewport físico

- Notifier::render() ahora usa physical_viewport.w para centrado

Resultado:
- Notificaciones centradas correctamente en F3 letterbox
- Notificaciones centradas correctamente en F4 integer scale
- Modo ventana sigue funcionando correctamente

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 08:08:01 +02:00
9b8afa1219 fix: HUD de debug alineado correcto en viewport (F3 letterbox)
Problema:
- Columna derecha del HUD (FPS, info de pelota) se alineaba usando dimensión física
- En modo letterbox (F3 INTEGER/LETTERBOX) aparecía en barras negras o fuera de pantalla
- Mismo issue que tenían Notifier y Help Overlay

Causa:
- ui_manager.cpp:renderDebugHUD() usaba `physical_window_width_` para alinear a la derecha
- En F3 letterbox: viewport visible < ventana física
- Ejemplo: ventana 1920px, viewport 1280px con offset 320px
- Cálculo: fps_x = 1920 - width - margin
- printAbsolute() aplicaba offset: 1920 - width + 320 = fuera de pantalla
- Resultado: texto del HUD invisible o en barras negras

Solución:
- Obtener viewport con SDL_GetRenderViewport() al inicio de renderDebugHUD()
- Reemplazar TODAS las referencias a `physical_window_width_` con `viewport.w`
- Coordenadas relativas al viewport, printAbsolute() aplica offset automáticamente

Código modificado:
- ui_manager.cpp:208-211 - Obtención de viewport
- ui_manager.cpp:315, 326, 333, 340, 347, 353, 360, 366, 375 - Alineación con viewport.w

Líneas afectadas (9 totales):
- FPS counter
- Posición X/Y primera pelota
- Velocidad X/Y
- Fuerza de gravedad
- Estado superficie
- Coeficiente de rebote (loss)
- Dirección de gravedad
- Convergencia (LOGO mode)

Resultado:
 HUD de debug alineado correctamente al borde derecho del viewport
 Columna derecha visible dentro del área de juego
 No aparece en barras negras en F3
 Funciona correctamente en ventana, F3 y F4

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 07:47:50 +02:00
5b674c8ea6 fix: Notifier centrado correcto en viewport (F3 letterbox)
Problema:
- Notificaciones se centraban usando dimensión física de ventana
- En modo letterbox (F3 INTEGER/LETTERBOX) aparecían en barras negras
- Mismo issue que tenía Help Overlay

Causa:
- notifier.cpp:165 usaba `window_width_` para calcular centrado
- En F3 letterbox: viewport visible < ventana física
- Ejemplo: ventana 1920px, viewport 1280px con offset 320px
- Resultado: notificación descentrada fuera del área visible

Solución:
- Obtener viewport con SDL_GetRenderViewport() antes de calcular posición
- Usar `viewport.w` en lugar de `window_width_` para centrado
- Coordenadas relativas al viewport, printAbsolute() aplica offset automáticamente

Código modificado:
- notifier.cpp:162-170 - Centrado usando viewport dimensions

Resultado:
 Notificaciones centradas en área visible (viewport)
 No aparecen en barras negras en F3
 Funciona correctamente en ventana, F3 y F4

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 07:47:10 +02:00
7fac103c51 fix: Help Overlay - ambas columnas respetan padding inferior
Problema:
- Solo columna 0 verificaba si cabía más texto antes de escribir
- Columna 1 (derecha) escribía fuera del overlay si no cabía
- En ventanas de 600px altura, columna 1 se desbordaba

Solución:
- Eliminada restricción `&& current_column == 0` del check de padding
- Ahora AMBAS columnas verifican si caben antes de escribir
- Si columna 1 está llena: omitir texto restante (continue)
- Si columna 0 está llena: cambiar a columna 1

Comportamiento preferido por el usuario:
"prefiero que 'falte texto' a que el texto se escriba por fuera del overlay"

Resultado:
 Columna 0 cambia a columna 1 cuando se llena
 Columna 1 omite texto que no cabe
 Overlay nunca muestra texto fuera de sus límites
 Funciona correctamente en ventanas pequeñas (600px)

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 07:33:17 +02:00
bcceb94c9e fix: Help Overlay centrado correcto en modo F3 letterbox
Problemas resueltos:
- En modo F3 (letterbox), el overlay se centraba en pantalla física
  en lugar de en el viewport visible, quedando desplazado
- Al salir de F3 a ventana, el overlay seguía roto
- Padding inferior no se respetaba correctamente

Cambios implementados:
1. render() ahora usa SDL_GetRenderViewport() para obtener área visible
2. Centrado calculado dentro del viewport (con offset de barras negras)
3. toggleFullscreen() restaura tamaño de ventana al salir de F3
4. Padding check movido ANTES de escribir línea (>= en lugar de >)
5. Debug logging añadido para diagnóstico de dimensiones

Resultado:
 Overlay centrado correctamente en F3 letterbox
 Overlay se regenera correctamente al salir de F3
 Padding inferior respetado en columna 0

Pendiente:
- Columna 2 (índice 1) todavía no respeta padding inferior
- Verificar que F4 (real fullscreen) siga funcionando correctamente

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-17 07:30:31 +02:00
1b3d32ba84 fix: Help Overlay - fullscreen resize roto y padding inferior
Correcciones críticas para overlay en fullscreen y padding:

1. Fullscreen/resize roto CORREGIDO:
   - Problema: orden incorrecto de actualizaciones causaba mezcla de
     dimensiones antiguas (800x600) con font nuevo (24px)
   - Solución: nuevo método updateAll() que actualiza font Y dimensiones
     de forma atómica
   - Flujo correcto: dimensiones físicas → font → recalcular box
   - Antes: overlay gigante y descuadrado al cambiar fullscreen
   - Ahora: overlay se reposiciona y escala correctamente

2. Padding inferior inexistente CORREGIDO:
   - Problema: calculateTextDimensions() usaba num_lines/2 asumiendo
     división perfecta entre columnas
   - Problema 2: rebuildCachedTexture() no verificaba límite inferior
     en columna 1
   - Solución: contar líneas REALES en cada columna y usar el máximo
   - Fórmula correcta: line_height*2 + max_column_lines*line_height + padding*2
   - Ahora: padding inferior respetado siempre

3. Implementación técnica:
   - HelpOverlay::updateAll(font, width, height) nuevo método unificado
   - UIManager llama updateAll() en lugar de reinitializeFontSize() +
     updatePhysicalWindowSize() separadamente
   - Elimina race condition entre actualización de font y dimensiones

Resultado:
- F3/F4 (fullscreen) funciona correctamente
- Resize ventana (F1/F2) funciona correctamente
- Padding inferior respetado en ambas columnas
- Sin overlays gigantes o descuadrados

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 21:32:32 +02:00
7c0a60f140 fix: Help Overlay - corregir solapamiento de columnas y layout
Tres correcciones importantes para el Help Overlay:

1. Solapamiento de columnas corregido:
   - Añadidos column1_width_ y column2_width_ para anchos reales
   - calculateTextDimensions() ahora incluye encabezados en cálculo
   - rebuildCachedTexture() usa anchos reales de columnas
   - Columna 2 empieza en padding + column1_width_ + padding
   - Elimina cálculo erróneo column_width = (box_width_ - padding*3)/2

2. Layout en alta resolución corregido:
   - Eliminado ancho mínimo forzado del 90% de dimensión menor
   - box_width_ ahora usa directamente text_width (justo lo necesario)
   - Antes: 1920x1080 → min 972px aunque contenido necesite 600px
   - Ahora: box ajustado al contenido sin espacio vacío extra

3. Fullscreen/resize corregido:
   - reinitializeFontSize() ya NO llama a calculateBoxDimensions()
   - Evita recalcular con physical_width_ y physical_height_ antiguos
   - Confía en updatePhysicalWindowSize() que se llama después
   - Antes: textura cacheada creada con dimensiones incorrectas
   - Ahora: textura siempre creada con dimensiones correctas

Resultado:
- Columnas no se montan entre sí
- Box ajustado al contenido sin espacio vacío derecha
- Cambios fullscreen/ventana funcionan correctamente
- Overlay se recalcula apropiadamente en todos los casos

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 21:22:12 +02:00
250b1a640d feat: Dynamic text scaling based on physical window size
Sistema de escalado dinámico de texto con 3 tamaños según área de ventana:

1. TextRenderer improvements:
   - Añadido reinitialize(int new_font_size) para cambiar tamaño en runtime
   - Almacena font_path_ para permitir recarga de fuente
   - Cierra fuente anterior y abre nueva con diferente tamaño
   - Verifica si tamaño es igual antes de reinicializar (optimización)

2. UIManager - Font size calculation:
   - Añadido calculateFontSize() con stepped scaling por área:
     * SMALL (< 800x600): 14px
     * MEDIUM (800x600 a 1920x1080): 18px
     * LARGE (> 1920x1080): 24px
   - Tracking de current_font_size_ para detectar cambios
   - Inicialización con tamaño dinámico en initialize()
   - Reinitialización automática en updatePhysicalWindowSize()

3. UIManager - Propagation:
   - Reinitializa 3 TextRenderer instances cuando cambia tamaño
   - Propaga nuevo tamaño a HelpOverlay
   - Detecta cambios solo cuando área cruza umbrales (eficiencia)

4. HelpOverlay integration:
   - Acepta font_size como parámetro en initialize()
   - Añadido reinitializeFontSize() para cambios dinámicos
   - Recalcula dimensiones del box cuando cambia fuente
   - Marca textura para rebuild completo tras cambio

Resultado:
- Ventanas pequeñas: texto 14px (más espacio para contenido)
- Ventanas medianas: texto 18px (tamaño original, óptimo)
- Ventanas grandes: texto 24px (mejor legibilidad)
- Cambios automáticos al redimensionar ventana (F1/F2/F3/F4)
- Sin impacto en performance (solo recalcula al cruzar umbrales)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 17:41:19 +02:00
795fa33e50 feat: Help Overlay - dynamic width + render-to-texture caching
Mejoras de rendimiento y usabilidad del Help Overlay:

1. Anchura dinámica basada en contenido:
   - Ya no es siempre cuadrado (box_size_)
   - Calcula ancho real según texto más largo por columna
   - Mantiene mínimo del 90% dimensión menor como antes
   - Nueva función calculateTextDimensions()

2. Render-to-texture caching para optimización:
   - Renderiza overlay completo a textura una sola vez
   - Detecta cambios de color con umbral (threshold 5/255)
   - Soporta temas dinámicos con LERP sin rebuild constante
   - Regenera solo cuando colores cambian o ventana redimensiona

3. Impacto en performance:
   - Antes: 1200 FPS → 200 FPS con overlay activo
   - Después: 1200 FPS → 1000-1200 FPS (casi sin impacto)
   - Temas estáticos: 1 render total (~∞x más rápido)
   - Temas dinámicos: regenera cada ~20-30 frames (~25x más rápido)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 17:36:53 +02:00
e7dc8f6d13 feat: añadir cambio de dirección de gravedad en modo LOGO
El modo LOGO ahora incluye cambios automáticos de dirección de
gravedad como parte de sus variaciones, aumentando la diversidad
visual de la demostración.

Cambios:
- Nueva acción en modo LOGO (PHYSICS): cambiar dirección gravedad (16%)
- Rebalanceo de probabilidades existentes:
  • PHYSICS → SHAPE: 60% → 50%
  • Gravedad ON: 20% → 18%
  • Gravedad OFF: 20% → 16%
  • Dirección gravedad: nuevo 16%
- Al cambiar dirección, se fuerza gravedad ON para visibilidad

Antes el modo LOGO solo alternaba entre figura/física y gravedad
on/off, pero nunca cambiaba la dirección. Ahora tiene las mismas
capacidades de variación que los modos DEMO y DEMO_LITE.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 17:11:22 +02:00
9cabbd867f fix: SHAPE mode - regenerar figuras automáticamente al cambiar escenario
PROBLEMA RESUELTO:
En modo SHAPE (figuras 3D), al cambiar el número de pelotas (teclas 1-8),
las nuevas pelotas aparecían en pantalla pero NO formaban la figura hasta
pulsar de nuevo la tecla de figura (Q/W/E/R/T).

CAUSA RAÍZ:
1. changeScenario() creaba pelotas nuevas en centro de pantalla
2. generateShape() generaba puntos target de la figura
3. PERO las pelotas nuevas no tenían shape_attraction_active=true
4. Sin atracción activa, las pelotas no se movían hacia sus targets

CAMBIOS IMPLEMENTADOS:

1. Ball class (ball.h/ball.cpp):
   - Constructor ahora acepta parámetro Y explícito
   - Eliminado hardcodeo Y=0.0f en inicialización de pos_

2. SceneManager (scene_manager.cpp):
   - PHYSICS mode: Y = 0.0f (parte superior, comportamiento original)
   - SHAPE mode: Y = screen_height_/2.0f (centro vertical) 
   - BOIDS mode: Y = rand() (posición Y aleatoria)
   - Ball constructor llamado con parámetro Y según modo

3. Engine (engine.cpp:514-521):
   - Tras generateShape(), activar enableShapeAttraction(true) en todas
     las pelotas nuevas
   - Garantiza que las pelotas converjan inmediatamente hacia figura

RESULTADO:
 Cambiar escenario (1-8) en modo SHAPE regenera automáticamente la figura
 No requiere pulsar tecla de figura de nuevo
 Transición suave e inmediata hacia nueva configuración

ARCHIVOS MODIFICADOS:
- source/ball.h: Constructor acepta parámetro Y
- source/ball.cpp: Usar Y en lugar de hardcode 0.0f
- source/scene/scene_manager.cpp: Inicializar Y según SimulationMode
- source/engine.cpp: Activar shape attraction tras changeScenario()

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 16:48:52 +02:00
8c2a8857fc fix: Preservar SimulationMode y mejorar Debug HUD
CAMBIOS:
- Debug HUD reorganizado en layout de 2 columnas (LEFT/RIGHT, sin centro)
- Añadidos getters públicos en Engine para info de sistema
- changeScenario() ahora preserva el SimulationMode actual
- Inicialización de pelotas según modo (PHYSICS/SHAPE/BOIDS)
- Eliminada duplicación de logo_entered_manually_ (ahora en StateManager)

ARCHIVOS MODIFICADOS:
- engine.h: Añadidos 8 getters públicos para UIManager
- engine.cpp: changeScenario() pasa current_mode_ a SceneManager
- scene_manager.h: changeScenario() acepta parámetro SimulationMode
- scene_manager.cpp: Inicialización según modo (RULES.md líneas 23-26)
- ui_manager.h: render() acepta Engine* y renderDebugHUD() actualizado
- ui_manager.cpp: Debug HUD con columnas LEFT (sistema) y RIGHT (física)

REGLAS.md IMPLEMENTADO:
 Líneas 23-26: Inicialización diferenciada por modo
  - PHYSICS: Top, 75% distribución central en X, velocidades aleatorias
  - SHAPE: Centro de pantalla, sin velocidad inicial
  - BOIDS: Posiciones y velocidades aleatorias
 Líneas 88-96: Debug HUD con información de sistema completa

BUGS CORREGIDOS:
- Fix: Cambiar escenario (1-8) en FIGURE ya no resetea a PHYSICS 
- Fix: Las pelotas se inicializan correctamente según el modo activo
- Fix: AppMode movido de centro a izquierda en Debug HUD

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 09:52:33 +02:00
26 changed files with 1905 additions and 357 deletions

128
RULES.md Normal file
View File

@@ -0,0 +1,128 @@
Documento de especificaciones de ViBe3 Physics
# Codigo
* Se preferira el uso de #pragma once a #ifndef
* Se preferira el uso de C++ frente a C
* Se preferirá el uso de verisiones mas moderdas de C++ frente a las mas viejas, es decir, C++20 frente a C++17, por ejemplo
* Se preferirá el uso de smart pointers frente a new/delete y sobretodo antes que malloc/free
* Los archivos de cabecera que definan clases, colocaran primero la parte publica y luego la privada. Agruparan los metodos por categorias. Todas las variables, constantes, estructuras, enumeraciones, metodos, llevaran el comentario a la derecha
* Se respetarán las reglas definidas en los ficheros .clang-tidy y .clang-format que hay en la raíz o en las subcarpetas
# Funcionamiento
* El programa tiene modos de funcionamiento (AppMode). El funcionamiento de cada uno de ellos se describirá mas adelante. Son estados exclusivos que van automatizando cambios en el SimulationMode, Theme y Scene y serian:
* SANDBOX
* DEMO
* DEMO LITE
* LOGO
* LOGO LITE
* El progama tiene otros modos de funcionamiento (SimulationMode). El funcionamiento de cada uno de ellos se describirá mas adelante. Son estados exclusivos:
* PHYISICS
* FIGURE
* BOIDS
* El programa tiene un gestor de temas (Theme) que cambia los colores de lo que se ve en pantalla. Hay temas estáticos y dinamicos. El cambio de tema se realiza mediante LERP y no afecta en nada ni al AppMode ni al SimulationMode, es decir, no modifica sus estados.
* El programa tiene escenarios (Scene). Cada escena tiene un numero de pelotas. Cuando se cambia el escenario, se elimina el vector de pelotas y se crea uno nuevo. En funcion del SimulationMode actual se inicializan las pelotas de manera distinta:
* PHYSICS: Se crean todas las pelotas cerca de la parte superior de la pantalla distribuidas en el 75% central del eje X (es como está ahora)
* FIGURE: Se crean todas las pelotas en el punto central de la pantalla
* BOIDS: Se crean todas las pelotas en posiciones al azar de la pantalla con velocidades y direcciones aleatorias
* El cambio de SimulationMode ha de preservar la inercia (velocidad, aceleracion, direccion) de cada pelota. El cambio se produce tanto de forma manual (pulsacion de una tecla por el usuario) como de manera automatica (cualquier AppMode que no sea SANDBOX)
* PHYSICS a FIGURE:
* Pulsando la tecla de la figura correspondiente
* Pulsando la tecla F (ultima figura seleccionada)
* PHYSICS a BOIDS:
* Pulsando la tecla B
* FIGURE a PHYSICS:
* Pulsando los cursores: Gravedad ON en la direccion del cursor
* Pulsando la tecla G: Gravedad OFF
* Pulsando la tecla F: Ultima gravedad seleccionada (direccion o OFF)
* FIGURE a BOIDS:
* Pulsando la tecla B
* BOIDS a PHYSICS:
* Pulsando la tecla G: Gravedad OFF
* Pulsando los cursores: Gravedad ON en la direccion del cursor
* BOIDS a FIGURE:
* Pulsando la tecla de la figura
* Pulsando la tecla F (ultima figura)
# AppMode
* SANDBOX
* No hay ningun automatismo. El usuario va pulsando teclas para ejecutar acciones.
* Si pulsa una de estas teclas, cambia de modo:
* D: DEMO
* L: DEMO LITE
* K: LOGO
* DEMO
* En el modo DEMO el programa va cambiando el SimulationMode de manera automatica (como está ahora es correcto)
* Se inicializa con un Theme al azar, Scene al azar, SimulationMode al azar. Restringido FIGURE->PNG_SHAPE
* Va cambiando de Theme
* Va cambiando de Scene
* Cambia la escala de la Figure
* Cambia el Sprite de las pelotas
* NO PUEDE cambiar a la figura PNG_SHAPE
* Eventualmente puede cambiar de manera automatica a LOGO LITE, sin restricciones
* El usuario puede cambiar el SimulationMode, el Theme o el Scene. Esto no hace que se salga del modo DEMO
* El usuario puede cambiar de AppMode pulsando:
* D: SANDBOX
* L: DEMO LITE
* K: LOGO
* DEMO LITE
* En el modo DEMO el programa va cambiando el SimulationMode de manera automatica (como está ahora es correcto)
* Se inicializa con un Theme al azar, Scene al azar, SimulationMode al azar. Restringido FIGURE->PNG_SHAPE
* Este modo es exactamente igual a DEMO pero NO PUEDE:
* Cambiar de Scene
* Cambiar de Theme
* Cambiar el Sprite de las pelotas
* Eventualmente puede cambiar de manera automatica a LOGO LITE, sin restricciones
* NO PUEDE cambiar a la figura PNG_SHAPE
* El usuario puede cambiar el SimulationMode, el Theme o el Scene. Esto no hace que se salga del modo DEMO LITE
* El usuario puede cambiar de AppMode pulsando:
* D: DEMO
* L: SANDBOX
* K: LOGO
* LOGO
* Se inicializa con la Scene de 5.000 pelotas, con el tamaño de Sprite->Small, con SimulationMode en FIGURE->PNG_SHAPE, con un tema al azar de los permitidos
* No cambia de Scene
* No cambia el tamaño de Sprite
* No cambia la escala de FIGURE
* Los temas permitidos son MONOCROMO, LAVANDA, CARMESI, ESMERALDA o cualquiera de los temas dinamicos
* En este modo SOLO aparece la figura PNG_SHAPE
* Solo cambiara a los temas permitidos
* Cambia el SimulationMode de PHYSICS a FIGURE (como hace ahora) pero no a BOIDS. BOIDS prohibido
* El usuario puede cambiar el SimulationMode, el Theme o el Scene. Esto no hace que se salga del modo LOGO. Incluso puede poner un Theme no permitido o otro Scene.
* El automatismo no cambia nunca de Theme así que se mantiene el del usuario.
* El automatismo no cambia nunca de Scene asi que se mantiene el del usuario.
* El usuario puede cambiar de AppMode pulsando:
* D: DEMO
* L: DEMO LITE
* K: SANDBOX
* B: SANDBOX->BOIDS
* LOGO LITE
* Este modo es exactamente igual al modo LOGO pero con unas pequeñas diferencias:
* Solo se accede a el de manera automatica, el usuario no puede invocarlo. No hay tecla
* Como se accede de manera automatica solo se puede llegar a él desde DEMO o DEMO LITE. Hay que guardar el estado en el que se encontraba AppMode, EngindeMode, Scene, Theme, Sprite, Scale... etc
* Este modo tiene una muy alta probabilidad de terminar, volviendo al estado anterior desde donde se invocó.
* El usuario puede cambiar de AppMode pulsando:
* D: Si el modo anterior era DEMO -> SANDBOX, else -> DEMO)
* L: Si el modo anterior era DEMO LITE -> SANDBOX, else -> DEMO LITE)
* K: LOGO
* B: SANDBOX->BOIDS
# Debug Hud
* En el debug hud hay que añadir que se vea SIEMPRE el AppMode (actualmente aparece centrado, hay que ponerlo a la izquierda) y no solo cietos AppModes
* Tiene que aparecer tambien el SimulationMode
* El modo de Vsync
* El modo de escalado entero, stretched, ventana
* la resolucion fisica
* la resolucion logica
* el refresco del panel
* El resto de cosas que salen
# Ventana de ayuda
* La ventana de ayuda actualmente es cuadrada
* Esa es la anchura minima que ha de tener
* Hay que ver cual es la linea mas larga, multiplicarla por el numero de columnas, añadirle los paddings y que ese sea el nuevo ancho
* Actualmente se renderiza a cada frame. El rendimiento cae de los 1200 frames por segundo a 200 frames por segundo. Habria que renderizarla a una textura o algo. El problema es que el cambio de Theme con LERP afecta a los colores de la ventana. Hay que investigar qué se puede hacer.
# Bugs actuales
* En el modo LOGO, si se pulsa un cursor, se activa la gravedad y deja de funcionar los automatismos. Incluso he llegado a ver como sale solo del modo LOGO sin pulsar nada
* En el modo BOIDS, pulsar la G activa la gravedad. La G deberia pasar al modo PHYSICS con la gravedad en OFF y que las pelotas mantuvieran el momento/inercia

BIN
data/logo/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

BIN
data/logo/logo2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

635
source/app_logo.cpp Normal file
View File

@@ -0,0 +1,635 @@
#include "app_logo.h"
#include <SDL3/SDL_render.h> // for SDL_SCALEMODE_LINEAR, SDL_RenderGeometry
#include <cmath> // for powf, sinf, cosf
#include "external/sprite.h" // for Sprite
#include "external/texture.h" // for Texture
bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_height) {
renderer_ = renderer;
screen_width_ = screen_width;
screen_height_ = screen_height;
std::string resources_dir = getResourcesDirectory();
// ========================================================================
// Cargar LOGO1 desde data/logo/logo.png
// ========================================================================
std::string logo1_path = resources_dir + "/data/logo/logo.png";
logo1_texture_ = std::make_shared<Texture>(renderer, logo1_path);
if (logo1_texture_->getWidth() == 0 || logo1_texture_->getHeight() == 0) {
// Error al cargar textura logo1
return false;
}
// Configurar filtrado LINEAR para suavizado
logo1_texture_->setScaleMode(SDL_SCALEMODE_LINEAR);
// Crear sprite con la textura
logo1_sprite_ = std::make_unique<Sprite>(logo1_texture_);
// Configurar el clip para que use toda la textura
float logo1_width = static_cast<float>(logo1_texture_->getWidth());
float logo1_height = static_cast<float>(logo1_texture_->getHeight());
logo1_sprite_->setClip({0.0f, 0.0f, logo1_width, logo1_height});
// ========================================================================
// Cargar LOGO2 desde data/logo/logo2.png
// ========================================================================
std::string logo2_path = resources_dir + "/data/logo/logo2.png";
logo2_texture_ = std::make_shared<Texture>(renderer, logo2_path);
if (logo2_texture_->getWidth() == 0 || logo2_texture_->getHeight() == 0) {
// Error al cargar textura logo2
return false;
}
// Configurar filtrado LINEAR para suavizado
logo2_texture_->setScaleMode(SDL_SCALEMODE_LINEAR);
// Crear sprite con la textura
logo2_sprite_ = std::make_unique<Sprite>(logo2_texture_);
// Configurar el clip para que use toda la textura
float logo2_width = static_cast<float>(logo2_texture_->getWidth());
float logo2_height = static_cast<float>(logo2_texture_->getHeight());
logo2_sprite_->setClip({0.0f, 0.0f, logo2_width, logo2_height});
// ========================================================================
// Calcular tamaño base (asumimos mismo tamaño para ambos logos)
// El logo debe tener una altura de APPLOGO_HEIGHT_PERCENT (40%) de la pantalla
// ========================================================================
float target_height = screen_height_ * APPLOGO_HEIGHT_PERCENT;
float scale = target_height / logo1_height;
base_width_ = logo1_width * scale;
base_height_ = target_height; // = logo1_height * scale
// Aplicar escala inicial a ambos sprites
logo1_sprite_->setSize(base_width_, base_height_);
logo2_sprite_->setSize(base_width_, base_height_);
// Posicionar ambos logos en el centro del cuadrante inferior derecho (superpuestos)
updateLogoPosition();
return true;
}
void AppLogo::update(float delta_time, AppMode current_mode) {
// Si estamos en SANDBOX, resetear y no hacer nada (logo desactivado)
if (current_mode == AppMode::SANDBOX) {
state_ = AppLogoState::HIDDEN;
timer_ = 0.0f;
current_alpha_ = 0;
return;
}
// Máquina de estados con fade in/out
timer_ += delta_time;
switch (state_) {
case AppLogoState::HIDDEN:
// Esperando el intervalo de espera
if (timer_ >= APPLOGO_DISPLAY_INTERVAL) {
state_ = AppLogoState::FADE_IN;
timer_ = 0.0f;
current_alpha_ = 0;
// Elegir animaciones de entrada aleatorias (independientes para cada logo)
logo1_entry_animation_ = getRandomAnimation();
logo2_entry_animation_ = getRandomAnimation();
}
break;
case AppLogoState::FADE_IN:
// Fade in: alpha de 0 a 255, animaciones independientes para logo1 y logo2
{
float fade_progress = timer_ / APPLOGO_FADE_DURATION;
if (fade_progress >= 1.0f) {
// Fade in completado
state_ = AppLogoState::VISIBLE;
timer_ = 0.0f;
current_alpha_ = 255;
// Resetear variables de ambos logos
logo1_scale_ = 1.0f;
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
logo1_rotation_ = 0.0f;
logo2_scale_ = 1.0f;
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
logo2_rotation_ = 0.0f;
} else {
// Interpolar alpha linealmente (0 → 255) - compartido
current_alpha_ = static_cast<int>(fade_progress * 255.0f);
// ================================================================
// Aplicar animación de LOGO1 según logo1_entry_animation_
// ================================================================
switch (logo1_entry_animation_) {
case AppLogoAnimationType::ZOOM_ONLY:
logo1_scale_ = 1.2f - (fade_progress * 0.2f);
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
logo1_rotation_ = 0.0f;
break;
case AppLogoAnimationType::ELASTIC_STICK:
{
float elastic_t = easeOutElastic(fade_progress);
logo1_scale_ = 1.2f - (elastic_t * 0.2f);
float squash_t = easeOutBack(fade_progress);
logo1_squash_y_ = 0.6f + (squash_t * 0.4f);
logo1_stretch_x_ = 1.0f + (1.0f - logo1_squash_y_) * 0.5f;
logo1_rotation_ = 0.0f;
}
break;
case AppLogoAnimationType::ROTATE_SPIRAL:
{
float ease_t = easeInOutQuad(fade_progress);
logo1_scale_ = 0.3f + (ease_t * 0.7f);
logo1_rotation_ = (1.0f - fade_progress) * 6.28f;
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
}
break;
case AppLogoAnimationType::BOUNCE_SQUASH:
{
float bounce_t = easeOutBounce(fade_progress);
logo1_scale_ = 1.0f;
float squash_amount = (1.0f - bounce_t) * 0.3f;
logo1_squash_y_ = 1.0f - squash_amount;
logo1_stretch_x_ = 1.0f + squash_amount * 0.5f;
logo1_rotation_ = 0.0f;
}
break;
}
// ================================================================
// Aplicar animación de LOGO2 según logo2_entry_animation_
// ================================================================
switch (logo2_entry_animation_) {
case AppLogoAnimationType::ZOOM_ONLY:
logo2_scale_ = 1.2f - (fade_progress * 0.2f);
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
logo2_rotation_ = 0.0f;
break;
case AppLogoAnimationType::ELASTIC_STICK:
{
float elastic_t = easeOutElastic(fade_progress);
logo2_scale_ = 1.2f - (elastic_t * 0.2f);
float squash_t = easeOutBack(fade_progress);
logo2_squash_y_ = 0.6f + (squash_t * 0.4f);
logo2_stretch_x_ = 1.0f + (1.0f - logo2_squash_y_) * 0.5f;
logo2_rotation_ = 0.0f;
}
break;
case AppLogoAnimationType::ROTATE_SPIRAL:
{
float ease_t = easeInOutQuad(fade_progress);
logo2_scale_ = 0.3f + (ease_t * 0.7f);
logo2_rotation_ = (1.0f - fade_progress) * 6.28f;
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
}
break;
case AppLogoAnimationType::BOUNCE_SQUASH:
{
float bounce_t = easeOutBounce(fade_progress);
logo2_scale_ = 1.0f;
float squash_amount = (1.0f - bounce_t) * 0.3f;
logo2_squash_y_ = 1.0f - squash_amount;
logo2_stretch_x_ = 1.0f + squash_amount * 0.5f;
logo2_rotation_ = 0.0f;
}
break;
}
}
}
break;
case AppLogoState::VISIBLE:
// Logo completamente visible, esperando duración
if (timer_ >= APPLOGO_DISPLAY_DURATION) {
state_ = AppLogoState::FADE_OUT;
timer_ = 0.0f;
current_alpha_ = 255;
// Elegir animaciones de salida aleatorias (independientes para cada logo)
logo1_exit_animation_ = getRandomAnimation();
logo2_exit_animation_ = getRandomAnimation();
}
break;
case AppLogoState::FADE_OUT:
// Fade out: alpha de 255 a 0, animaciones independientes para logo1 y logo2
{
float fade_progress = timer_ / APPLOGO_FADE_DURATION;
if (fade_progress >= 1.0f) {
// Fade out completado, volver a HIDDEN
state_ = AppLogoState::HIDDEN;
timer_ = 0.0f;
current_alpha_ = 0;
// Resetear variables de ambos logos
logo1_scale_ = 1.0f;
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
logo1_rotation_ = 0.0f;
logo2_scale_ = 1.0f;
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
logo2_rotation_ = 0.0f;
} else {
// Interpolar alpha linealmente (255 → 0) - compartido
current_alpha_ = static_cast<int>((1.0f - fade_progress) * 255.0f);
// ================================================================
// Aplicar animación de LOGO1 según logo1_exit_animation_
// ================================================================
switch (logo1_exit_animation_) {
case AppLogoAnimationType::ZOOM_ONLY:
logo1_scale_ = 1.0f + (fade_progress * 0.2f);
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
logo1_rotation_ = 0.0f;
break;
case AppLogoAnimationType::ELASTIC_STICK:
logo1_scale_ = 1.0f + (fade_progress * fade_progress * 0.2f);
logo1_squash_y_ = 1.0f + (fade_progress * 0.3f);
logo1_stretch_x_ = 1.0f - (fade_progress * 0.2f);
logo1_rotation_ = fade_progress * 0.1f;
break;
case AppLogoAnimationType::ROTATE_SPIRAL:
{
float ease_t = easeInOutQuad(fade_progress);
logo1_scale_ = 1.0f - (ease_t * 0.7f);
logo1_rotation_ = fade_progress * 6.28f;
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
}
break;
case AppLogoAnimationType::BOUNCE_SQUASH:
{
if (fade_progress < 0.2f) {
float squash_t = fade_progress / 0.2f;
logo1_squash_y_ = 1.0f - (squash_t * 0.3f);
logo1_stretch_x_ = 1.0f + (squash_t * 0.2f);
} else {
float jump_t = (fade_progress - 0.2f) / 0.8f;
logo1_squash_y_ = 0.7f + (jump_t * 0.5f);
logo1_stretch_x_ = 1.2f - (jump_t * 0.2f);
}
logo1_scale_ = 1.0f + (fade_progress * 0.3f);
logo1_rotation_ = 0.0f;
}
break;
}
// ================================================================
// Aplicar animación de LOGO2 según logo2_exit_animation_
// ================================================================
switch (logo2_exit_animation_) {
case AppLogoAnimationType::ZOOM_ONLY:
logo2_scale_ = 1.0f + (fade_progress * 0.2f);
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
logo2_rotation_ = 0.0f;
break;
case AppLogoAnimationType::ELASTIC_STICK:
logo2_scale_ = 1.0f + (fade_progress * fade_progress * 0.2f);
logo2_squash_y_ = 1.0f + (fade_progress * 0.3f);
logo2_stretch_x_ = 1.0f - (fade_progress * 0.2f);
logo2_rotation_ = fade_progress * 0.1f;
break;
case AppLogoAnimationType::ROTATE_SPIRAL:
{
float ease_t = easeInOutQuad(fade_progress);
logo2_scale_ = 1.0f - (ease_t * 0.7f);
logo2_rotation_ = fade_progress * 6.28f;
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
}
break;
case AppLogoAnimationType::BOUNCE_SQUASH:
{
if (fade_progress < 0.2f) {
float squash_t = fade_progress / 0.2f;
logo2_squash_y_ = 1.0f - (squash_t * 0.3f);
logo2_stretch_x_ = 1.0f + (squash_t * 0.2f);
} else {
float jump_t = (fade_progress - 0.2f) / 0.8f;
logo2_squash_y_ = 0.7f + (jump_t * 0.5f);
logo2_stretch_x_ = 1.2f - (jump_t * 0.2f);
}
logo2_scale_ = 1.0f + (fade_progress * 0.3f);
logo2_rotation_ = 0.0f;
}
break;
}
}
}
break;
}
// Aplicar alpha a ambos logos (compartido - sincronizado)
if (logo1_texture_) {
logo1_texture_->setAlpha(current_alpha_);
}
if (logo2_texture_) {
logo2_texture_->setAlpha(current_alpha_);
}
// Aplicar escala animada INDEPENDIENTE a cada logo
if (logo1_sprite_) {
float scaled_width = base_width_ * logo1_scale_;
float scaled_height = base_height_ * logo1_scale_;
logo1_sprite_->setSize(scaled_width, scaled_height);
}
if (logo2_sprite_) {
float scaled_width = base_width_ * logo2_scale_;
float scaled_height = base_height_ * logo2_scale_;
logo2_sprite_->setSize(scaled_width, scaled_height);
}
// Recentrar ambos logos (están superpuestos, misma posición)
updateLogoPosition();
}
void AppLogo::render() {
// Renderizar si NO está en estado HIDDEN (incluye FADE_IN, VISIBLE, FADE_OUT)
if (state_ != AppLogoState::HIDDEN) {
// Determinar animaciones actuales para cada logo
AppLogoAnimationType logo1_anim = (state_ == AppLogoState::FADE_IN) ? logo1_entry_animation_ : logo1_exit_animation_;
AppLogoAnimationType logo2_anim = (state_ == AppLogoState::FADE_IN) ? logo2_entry_animation_ : logo2_exit_animation_;
// ====================================================================
// Renderizar LOGO1 primero (fondo)
// ====================================================================
if (logo1_anim != AppLogoAnimationType::ZOOM_ONLY) {
// Usar renderizado con geometría para deformaciones/rotación
renderWithGeometry(1);
} else if (logo1_sprite_) {
// Usar renderizado simple con Sprite (solo ZOOM_ONLY)
logo1_sprite_->render();
}
// ====================================================================
// Renderizar LOGO2 después (encima de logo1)
// ====================================================================
if (logo2_anim != AppLogoAnimationType::ZOOM_ONLY) {
// Usar renderizado con geometría para deformaciones/rotación
renderWithGeometry(2);
} else if (logo2_sprite_) {
// Usar renderizado simple con Sprite (solo ZOOM_ONLY)
logo2_sprite_->render();
}
}
}
void AppLogo::updateScreenSize(int screen_width, int screen_height) {
screen_width_ = screen_width;
screen_height_ = screen_height;
// Recalcular tamaño base para la nueva resolución (asumimos mismo tamaño para ambos logos)
if (logo1_sprite_ && logo1_texture_) {
float logo_width = static_cast<float>(logo1_texture_->getWidth());
float logo_height = static_cast<float>(logo1_texture_->getHeight());
// El logo debe tener una altura de APPLOGO_HEIGHT_PERCENT (40%) de la pantalla
float target_height = screen_height_ * APPLOGO_HEIGHT_PERCENT;
float scale = target_height / logo_height;
// Recalcular tamaño base
base_width_ = logo_width * scale;
base_height_ = target_height; // = logo_height * scale
// Aplicar escala actual a AMBOS logos (respeta la animación en curso)
if (logo1_sprite_) {
float scaled_width = base_width_ * logo1_scale_;
float scaled_height = base_height_ * logo1_scale_;
logo1_sprite_->setSize(scaled_width, scaled_height);
}
if (logo2_sprite_) {
float scaled_width = base_width_ * logo2_scale_;
float scaled_height = base_height_ * logo2_scale_;
logo2_sprite_->setSize(scaled_width, scaled_height);
}
// Reposicionar ambos logos
updateLogoPosition();
}
}
void AppLogo::updateLogoPosition() {
// Calcular padding desde bordes derecho e inferior
float padding_x = screen_width_ * APPLOGO_PADDING_PERCENT;
float padding_y = screen_height_ * APPLOGO_PADDING_PERCENT;
// Posicionar LOGO1 (anclado a esquina inferior derecha con padding)
if (logo1_sprite_) {
float logo1_width = base_width_ * logo1_scale_;
float logo1_height = base_height_ * logo1_scale_;
float pos_x = screen_width_ - logo1_width - padding_x;
float pos_y = screen_height_ - logo1_height - padding_y;
logo1_sprite_->setPos({pos_x, pos_y});
}
// Posicionar LOGO2 (anclado a esquina inferior derecha con padding, superpuesto a logo1)
if (logo2_sprite_) {
float logo2_width = base_width_ * logo2_scale_;
float logo2_height = base_height_ * logo2_scale_;
float pos_x = screen_width_ - logo2_width - padding_x;
float pos_y = screen_height_ - logo2_height - padding_y;
logo2_sprite_->setPos({pos_x, pos_y});
}
}
// ============================================================================
// Funciones de easing para animaciones
// ============================================================================
float AppLogo::easeOutElastic(float t) {
// Elastic easing out: bounce elástico al final
const float c4 = (2.0f * 3.14159f) / 3.0f;
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
return powf(2.0f, -10.0f * t) * sinf((t * 10.0f - 0.75f) * c4) + 1.0f;
}
float AppLogo::easeOutBack(float t) {
// Back easing out: overshoot suave al final
const float c1 = 1.70158f;
const float c3 = c1 + 1.0f;
return 1.0f + c3 * powf(t - 1.0f, 3.0f) + c1 * powf(t - 1.0f, 2.0f);
}
float AppLogo::easeOutBounce(float t) {
// Bounce easing out: rebotes decrecientes (para BOUNCE_SQUASH)
const float n1 = 7.5625f;
const float d1 = 2.75f;
if (t < 1.0f / d1) {
return n1 * t * t;
} else if (t < 2.0f / d1) {
t -= 1.5f / d1;
return n1 * t * t + 0.75f;
} else if (t < 2.5f / d1) {
t -= 2.25f / d1;
return n1 * t * t + 0.9375f;
} else {
t -= 2.625f / d1;
return n1 * t * t + 0.984375f;
}
}
float AppLogo::easeInOutQuad(float t) {
// Quadratic easing in/out: aceleración suave (para ROTATE_SPIRAL)
if (t < 0.5f) {
return 2.0f * t * t;
} else {
return 1.0f - powf(-2.0f * t + 2.0f, 2.0f) / 2.0f;
}
}
// ============================================================================
// Función auxiliar para aleatorización
// ============================================================================
AppLogoAnimationType AppLogo::getRandomAnimation() {
// Generar número aleatorio entre 0 y 3 (4 tipos de animación)
int random_value = rand() % 4;
switch (random_value) {
case 0:
return AppLogoAnimationType::ZOOM_ONLY;
case 1:
return AppLogoAnimationType::ELASTIC_STICK;
case 2:
return AppLogoAnimationType::ROTATE_SPIRAL;
case 3:
default:
return AppLogoAnimationType::BOUNCE_SQUASH;
}
}
// ============================================================================
// Renderizado con geometría deformada (para animación ELASTIC_STICK)
// ============================================================================
void AppLogo::renderWithGeometry(int logo_index) {
if (!renderer_) return;
// Seleccionar variables según el logo_index (1 = logo1, 2 = logo2)
std::shared_ptr<Texture> texture;
float scale, squash_y, stretch_x, rotation;
if (logo_index == 1) {
if (!logo1_texture_) return;
texture = logo1_texture_;
scale = logo1_scale_;
squash_y = logo1_squash_y_;
stretch_x = logo1_stretch_x_;
rotation = logo1_rotation_;
} else if (logo_index == 2) {
if (!logo2_texture_) return;
texture = logo2_texture_;
scale = logo2_scale_;
squash_y = logo2_squash_y_;
stretch_x = logo2_stretch_x_;
rotation = logo2_rotation_;
} else {
return; // Índice inválido
}
// Calcular tamaño con escala y deformaciones aplicadas
float width = base_width_ * scale * stretch_x;
float height = base_height_ * scale * squash_y;
// Calcular padding desde bordes derecho e inferior
float padding_x = screen_width_ * APPLOGO_PADDING_PERCENT;
float padding_y = screen_height_ * APPLOGO_PADDING_PERCENT;
// Calcular esquina del logo (anclado a esquina inferior derecha con padding)
float corner_x = screen_width_ - width - padding_x;
float corner_y = screen_height_ - height - padding_y;
// Centro del logo (para rotación) = esquina + mitad del tamaño
float center_x = corner_x + (width / 2.0f);
float center_y = corner_y + (height / 2.0f);
// Pre-calcular seno y coseno de rotación
float cos_rot = cosf(rotation);
float sin_rot = sinf(rotation);
// Crear 4 vértices del quad (centrado en center_x, center_y)
SDL_Vertex vertices[4];
// Offset desde el centro
float half_w = width / 2.0f;
float half_h = height / 2.0f;
// Vértice superior izquierdo (rotado)
{
float local_x = -half_w;
float local_y = -half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot;
float rotated_y = local_x * sin_rot + local_y * cos_rot;
vertices[0].position = {center_x + rotated_x, center_y + rotated_y};
vertices[0].tex_coord = {0.0f, 0.0f};
vertices[0].color = {1.0f, 1.0f, 1.0f, 1.0f}; // Color blanco (textura se modula con alpha)
}
// Vértice superior derecho (rotado)
{
float local_x = half_w;
float local_y = -half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot;
float rotated_y = local_x * sin_rot + local_y * cos_rot;
vertices[1].position = {center_x + rotated_x, center_y + rotated_y};
vertices[1].tex_coord = {1.0f, 0.0f};
vertices[1].color = {1.0f, 1.0f, 1.0f, 1.0f};
}
// Vértice inferior derecho (rotado)
{
float local_x = half_w;
float local_y = half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot;
float rotated_y = local_x * sin_rot + local_y * cos_rot;
vertices[2].position = {center_x + rotated_x, center_y + rotated_y};
vertices[2].tex_coord = {1.0f, 1.0f};
vertices[2].color = {1.0f, 1.0f, 1.0f, 1.0f};
}
// Vértice inferior izquierdo (rotado)
{
float local_x = -half_w;
float local_y = half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot;
float rotated_y = local_x * sin_rot + local_y * cos_rot;
vertices[3].position = {center_x + rotated_x, center_y + rotated_y};
vertices[3].tex_coord = {0.0f, 1.0f};
vertices[3].color = {1.0f, 1.0f, 1.0f, 1.0f};
}
// Índices para 2 triángulos
int indices[6] = {0, 1, 2, 2, 3, 0};
// Renderizar con la textura del logo correspondiente
SDL_RenderGeometry(renderer_, texture->getSDLTexture(), vertices, 4, indices, 6);
}

97
source/app_logo.h Normal file
View File

@@ -0,0 +1,97 @@
#pragma once
#include <SDL3/SDL_render.h> // for SDL_Renderer
#include <memory> // for unique_ptr, shared_ptr
#include "defines.h" // for AppMode
class Texture;
class Sprite;
// Estados de la máquina de estados del logo
enum class AppLogoState {
HIDDEN, // Logo oculto, esperando APPLOGO_DISPLAY_INTERVAL
FADE_IN, // Apareciendo (alpha 0 → 255)
VISIBLE, // Completamente visible, esperando APPLOGO_DISPLAY_DURATION
FADE_OUT // Desapareciendo (alpha 255 → 0)
};
// Tipo de animación de entrada/salida
enum class AppLogoAnimationType {
ZOOM_ONLY, // A: Solo zoom simple (120% → 100% → 120%)
ELASTIC_STICK, // B: Zoom + deformación elástica tipo "pegatina"
ROTATE_SPIRAL, // C: Rotación en espiral (entra girando, sale girando)
BOUNCE_SQUASH // D: Rebote con aplastamiento (cae rebotando, salta)
};
class AppLogo {
public:
AppLogo() = default;
~AppLogo() = default;
// Inicializar textura y sprite del logo
bool initialize(SDL_Renderer* renderer, int screen_width, int screen_height);
// Actualizar temporizadores y estado de visibilidad
void update(float delta_time, AppMode current_mode);
// Renderizar logo si está visible
void render();
// Actualizar tamaño de pantalla (reposicionar logo)
void updateScreenSize(int screen_width, int screen_height);
private:
// Texturas y sprites (x2 - logo1 y logo2 superpuestos)
std::shared_ptr<Texture> logo1_texture_; // Textura del logo1 (data/logo/logo.png)
std::unique_ptr<Sprite> logo1_sprite_; // Sprite para renderizar logo1
std::shared_ptr<Texture> logo2_texture_; // Textura del logo2 (data/logo/logo2.png)
std::unique_ptr<Sprite> logo2_sprite_; // Sprite para renderizar logo2
// Variables COMPARTIDAS (sincronización de ambos logos)
AppLogoState state_ = AppLogoState::HIDDEN; // Estado actual de la máquina de estados
float timer_ = 0.0f; // Contador de tiempo para estado actual
int current_alpha_ = 0; // Alpha actual (0-255)
// Animaciones INDEPENDIENTES para cada logo
AppLogoAnimationType logo1_entry_animation_ = AppLogoAnimationType::ZOOM_ONLY;
AppLogoAnimationType logo1_exit_animation_ = AppLogoAnimationType::ZOOM_ONLY;
AppLogoAnimationType logo2_entry_animation_ = AppLogoAnimationType::ZOOM_ONLY;
AppLogoAnimationType logo2_exit_animation_ = AppLogoAnimationType::ZOOM_ONLY;
// Variables de deformación INDEPENDIENTES para logo1
float logo1_scale_ = 1.0f; // Escala actual de logo1 (1.0 = 100%)
float logo1_squash_y_ = 1.0f; // Factor de aplastamiento vertical logo1
float logo1_stretch_x_ = 1.0f; // Factor de estiramiento horizontal logo1
float logo1_rotation_ = 0.0f; // Rotación en radianes logo1
// Variables de deformación INDEPENDIENTES para logo2
float logo2_scale_ = 1.0f; // Escala actual de logo2 (1.0 = 100%)
float logo2_squash_y_ = 1.0f; // Factor de aplastamiento vertical logo2
float logo2_stretch_x_ = 1.0f; // Factor de estiramiento horizontal logo2
float logo2_rotation_ = 0.0f; // Rotación en radianes logo2
int screen_width_ = 0; // Ancho de pantalla (para centrar)
int screen_height_ = 0; // Alto de pantalla (para centrar)
// Tamaño base del logo (calculado una vez)
float base_width_ = 0.0f;
float base_height_ = 0.0f;
// SDL renderer (necesario para renderizado con geometría)
SDL_Renderer* renderer_ = nullptr;
// Métodos privados auxiliares
void updateLogoPosition(); // Centrar ambos logos en pantalla (superpuestos)
void renderWithGeometry(int logo_index); // Renderizar logo con vértices deformados (1 o 2)
// Funciones de easing
float easeOutElastic(float t); // Elastic bounce out
float easeOutBack(float t); // Overshoot out
float easeOutBounce(float t); // Bounce easing (para BOUNCE_SQUASH)
float easeInOutQuad(float t); // Quadratic easing (para ROTATE_SPIRAL)
// Función auxiliar para elegir animación aleatoria
AppLogoAnimationType getRandomAnimation();
};

View File

@@ -22,9 +22,9 @@ float generateLateralLoss() {
}
// Constructor
Ball::Ball(float x, float vx, float vy, Color color, std::shared_ptr<Texture> texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir, float mass_factor)
Ball::Ball(float x, float y, float vx, float vy, Color color, std::shared_ptr<Texture> texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir, float mass_factor)
: sprite_(std::make_unique<Sprite>(texture)),
pos_({x, 0.0f, static_cast<float>(ball_size), static_cast<float>(ball_size)}) {
pos_({x, y, static_cast<float>(ball_size), static_cast<float>(ball_size)}) {
// Convertir velocidades de píxeles/frame a píxeles/segundo (multiplicar por 60)
vx_ = vx * 60.0f;
vy_ = vy * 60.0f;

View File

@@ -31,7 +31,7 @@ class Ball {
public:
// Constructor
Ball(float x, float vx, float vy, Color color, std::shared_ptr<Texture> texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir = GravityDirection::DOWN, float mass_factor = 1.0f);
Ball(float x, float y, float vx, float vy, Color color, std::shared_ptr<Texture> texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir = GravityDirection::DOWN, float mass_factor = 1.0f);
// Destructor
~Ball() = default;

View File

@@ -17,7 +17,18 @@ BoidManager::BoidManager()
, screen_width_(0)
, screen_height_(0)
, boids_active_(false)
, spatial_grid_(800, 600, BOID_GRID_CELL_SIZE) { // Tamaño por defecto, se actualiza en initialize()
, spatial_grid_(800, 600, BOID_GRID_CELL_SIZE) // Tamaño por defecto, se actualiza en initialize()
, separation_radius_(BOID_SEPARATION_RADIUS)
, alignment_radius_(BOID_ALIGNMENT_RADIUS)
, cohesion_radius_(BOID_COHESION_RADIUS)
, separation_weight_(BOID_SEPARATION_WEIGHT)
, alignment_weight_(BOID_ALIGNMENT_WEIGHT)
, cohesion_weight_(BOID_COHESION_WEIGHT)
, max_speed_(BOID_MAX_SPEED)
, min_speed_(BOID_MIN_SPEED)
, max_force_(BOID_MAX_FORCE)
, boundary_margin_(BOID_BOUNDARY_MARGIN)
, boundary_weight_(BOID_BOUNDARY_WEIGHT) {
}
BoidManager::~BoidManager() {
@@ -57,9 +68,9 @@ void BoidManager::activateBoids() {
float vx, vy;
ball->getVelocity(vx, vy);
if (vx == 0.0f && vy == 0.0f) {
// Velocidad aleatoria entre -1 y 1
vx = (rand() % 200 - 100) / 100.0f;
vy = (rand() % 200 - 100) / 100.0f;
// Velocidad aleatoria entre -60 y +60 px/s (time-based)
vx = ((rand() % 200 - 100) / 100.0f) * 60.0f;
vy = ((rand() % 200 - 100) / 100.0f) * 60.0f;
ball->setVelocity(vx, vy);
}
}
@@ -118,14 +129,14 @@ void BoidManager::update(float delta_time) {
limitSpeed(ball.get());
}
// Actualizar posiciones con velocidades resultantes
// Actualizar posiciones con velocidades resultantes (time-based)
for (auto& ball : balls) {
float vx, vy;
ball->getVelocity(vx, vy);
SDL_FRect pos = ball->getPosition();
pos.x += vx;
pos.y += vy;
pos.x += vx * delta_time; // time-based
pos.y += vy * delta_time;
ball->setPosition(pos.x, pos.y);
}
@@ -146,7 +157,7 @@ void BoidManager::applySeparation(Ball* boid, float delta_time) {
float center_y = pos.y + pos.h / 2.0f;
// FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n))
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, BOID_SEPARATION_RADIUS);
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, separation_radius_);
for (Ball* other : neighbors) {
if (other == boid) continue; // Ignorar a sí mismo
@@ -159,10 +170,10 @@ void BoidManager::applySeparation(Ball* boid, float delta_time) {
float dy = center_y - other_y;
float distance = std::sqrt(dx * dx + dy * dy);
if (distance > 0.0f && distance < BOID_SEPARATION_RADIUS) {
if (distance > 0.0f && distance < separation_radius_) {
// FASE 1.3: Separación más fuerte cuando más cerca (inversamente proporcional a distancia)
// Fuerza proporcional a cercanía: 0% en radio máximo, 100% en colisión
float separation_strength = (BOID_SEPARATION_RADIUS - distance) / BOID_SEPARATION_RADIUS;
float separation_strength = (separation_radius_ - distance) / separation_radius_;
steer_x += (dx / distance) * separation_strength;
steer_y += (dy / distance) * separation_strength;
count++;
@@ -177,8 +188,8 @@ void BoidManager::applySeparation(Ball* boid, float delta_time) {
// Aplicar fuerza de separación
float vx, vy;
boid->getVelocity(vx, vy);
vx += steer_x * BOID_SEPARATION_WEIGHT * delta_time;
vy += steer_y * BOID_SEPARATION_WEIGHT * delta_time;
vx += steer_x * separation_weight_ * delta_time;
vy += steer_y * separation_weight_ * delta_time;
boid->setVelocity(vx, vy);
}
}
@@ -194,7 +205,7 @@ void BoidManager::applyAlignment(Ball* boid, float delta_time) {
float center_y = pos.y + pos.h / 2.0f;
// FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n))
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, BOID_ALIGNMENT_RADIUS);
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, alignment_radius_);
for (Ball* other : neighbors) {
if (other == boid) continue;
@@ -207,7 +218,7 @@ void BoidManager::applyAlignment(Ball* boid, float delta_time) {
float dy = center_y - other_y;
float distance = std::sqrt(dx * dx + dy * dy);
if (distance < BOID_ALIGNMENT_RADIUS) {
if (distance < alignment_radius_) {
float other_vx, other_vy;
other->getVelocity(other_vx, other_vy);
avg_vx += other_vx;
@@ -224,14 +235,14 @@ void BoidManager::applyAlignment(Ball* boid, float delta_time) {
// Steering hacia la velocidad promedio
float vx, vy;
boid->getVelocity(vx, vy);
float steer_x = (avg_vx - vx) * BOID_ALIGNMENT_WEIGHT * delta_time;
float steer_y = (avg_vy - vy) * BOID_ALIGNMENT_WEIGHT * delta_time;
float steer_x = (avg_vx - vx) * alignment_weight_ * delta_time;
float steer_y = (avg_vy - vy) * alignment_weight_ * delta_time;
// Limitar fuerza máxima de steering
float steer_mag = std::sqrt(steer_x * steer_x + steer_y * steer_y);
if (steer_mag > BOID_MAX_FORCE) {
steer_x = (steer_x / steer_mag) * BOID_MAX_FORCE;
steer_y = (steer_y / steer_mag) * BOID_MAX_FORCE;
if (steer_mag > max_force_) {
steer_x = (steer_x / steer_mag) * max_force_;
steer_y = (steer_y / steer_mag) * max_force_;
}
vx += steer_x;
@@ -251,7 +262,7 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
float center_y = pos.y + pos.h / 2.0f;
// FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n))
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, BOID_COHESION_RADIUS);
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, cohesion_radius_);
for (Ball* other : neighbors) {
if (other == boid) continue;
@@ -264,7 +275,7 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
float dy = center_y - other_y;
float distance = std::sqrt(dx * dx + dy * dy);
if (distance < BOID_COHESION_RADIUS) {
if (distance < cohesion_radius_) {
center_of_mass_x += other_x;
center_of_mass_y += other_y;
count++;
@@ -284,14 +295,14 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
// Solo aplicar si hay distancia al centro (evitar división por cero)
if (distance_to_center > 0.1f) {
// Normalizar vector dirección (fuerza independiente de distancia)
float steer_x = (dx_to_center / distance_to_center) * BOID_COHESION_WEIGHT * delta_time;
float steer_y = (dy_to_center / distance_to_center) * BOID_COHESION_WEIGHT * delta_time;
float steer_x = (dx_to_center / distance_to_center) * cohesion_weight_ * delta_time;
float steer_y = (dy_to_center / distance_to_center) * cohesion_weight_ * delta_time;
// Limitar fuerza máxima de steering
float steer_mag = std::sqrt(steer_x * steer_x + steer_y * steer_y);
if (steer_mag > BOID_MAX_FORCE) {
steer_x = (steer_x / steer_mag) * BOID_MAX_FORCE;
steer_y = (steer_y / steer_mag) * BOID_MAX_FORCE;
if (steer_mag > max_force_) {
steer_x = (steer_x / steer_mag) * max_force_;
steer_y = (steer_y / steer_mag) * max_force_;
}
float vx, vy;
@@ -304,32 +315,69 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
}
void BoidManager::applyBoundaries(Ball* boid) {
// Mantener boids dentro de los límites de la pantalla
// Comportamiento "wrapping" (teletransporte al otro lado)
// NUEVA IMPLEMENTACIÓN: Bordes como obstáculos (repulsión en lugar de wrapping)
// Cuando un boid se acerca a un borde, se aplica una fuerza alejándolo
SDL_FRect pos = boid->getPosition();
float center_x = pos.x + pos.w / 2.0f;
float center_y = pos.y + pos.h / 2.0f;
bool wrapped = false;
float steer_x = 0.0f;
float steer_y = 0.0f;
if (center_x < 0) {
pos.x = screen_width_ - pos.w / 2.0f;
wrapped = true;
} else if (center_x > screen_width_) {
pos.x = -pos.w / 2.0f;
wrapped = true;
// Borde izquierdo (x < boundary_margin_)
if (center_x < boundary_margin_) {
float distance = center_x; // Distancia al borde (0 = colisión)
if (distance < boundary_margin_) {
// Fuerza proporcional a cercanía: 0% en margen, 100% en colisión
float repulsion_strength = (boundary_margin_ - distance) / boundary_margin_;
steer_x += repulsion_strength; // Empujar hacia la derecha
}
}
if (center_y < 0) {
pos.y = screen_height_ - pos.h / 2.0f;
wrapped = true;
} else if (center_y > screen_height_) {
pos.y = -pos.h / 2.0f;
wrapped = true;
// Borde derecho (x > screen_width_ - boundary_margin_)
if (center_x > screen_width_ - boundary_margin_) {
float distance = screen_width_ - center_x;
if (distance < boundary_margin_) {
float repulsion_strength = (boundary_margin_ - distance) / boundary_margin_;
steer_x -= repulsion_strength; // Empujar hacia la izquierda
}
}
if (wrapped) {
boid->setPosition(pos.x, pos.y);
// Borde superior (y < boundary_margin_)
if (center_y < boundary_margin_) {
float distance = center_y;
if (distance < boundary_margin_) {
float repulsion_strength = (boundary_margin_ - distance) / boundary_margin_;
steer_y += repulsion_strength; // Empujar hacia abajo
}
}
// Borde inferior (y > screen_height_ - boundary_margin_)
if (center_y > screen_height_ - boundary_margin_) {
float distance = screen_height_ - center_y;
if (distance < boundary_margin_) {
float repulsion_strength = (boundary_margin_ - distance) / boundary_margin_;
steer_y -= repulsion_strength; // Empujar hacia arriba
}
}
// Aplicar fuerza de repulsión si hay alguna
if (steer_x != 0.0f || steer_y != 0.0f) {
float vx, vy;
boid->getVelocity(vx, vy);
// Normalizar fuerza de repulsión (para que todas las direcciones tengan la misma intensidad)
float steer_mag = std::sqrt(steer_x * steer_x + steer_y * steer_y);
if (steer_mag > 0.0f) {
steer_x /= steer_mag;
steer_y /= steer_mag;
}
// Aplicar aceleración de repulsión (time-based)
// boundary_weight_ es más fuerte que separation para garantizar que no escapen
vx += steer_x * boundary_weight_ * (1.0f / 60.0f); // Simular delta_time fijo para independencia
vy += steer_y * boundary_weight_ * (1.0f / 60.0f);
boid->setVelocity(vx, vy);
}
}
@@ -341,16 +389,16 @@ void BoidManager::limitSpeed(Ball* boid) {
float speed = std::sqrt(vx * vx + vy * vy);
// Limitar velocidad máxima
if (speed > BOID_MAX_SPEED) {
vx = (vx / speed) * BOID_MAX_SPEED;
vy = (vy / speed) * BOID_MAX_SPEED;
if (speed > max_speed_) {
vx = (vx / speed) * max_speed_;
vy = (vy / speed) * max_speed_;
boid->setVelocity(vx, vy);
}
// FASE 1.2: Aplicar velocidad mínima (evitar boids estáticos)
if (speed > 0.0f && speed < BOID_MIN_SPEED) {
vx = (vx / speed) * BOID_MIN_SPEED;
vy = (vy / speed) * BOID_MIN_SPEED;
if (speed > 0.0f && speed < min_speed_) {
vx = (vx / speed) * min_speed_;
vy = (vy / speed) * min_speed_;
boid->setVelocity(vx, vy);
}
}

View File

@@ -103,10 +103,24 @@ class BoidManager {
// FASE 2: Grid reutilizable para búsquedas de vecinos
SpatialGrid spatial_grid_;
// === Parámetros ajustables en runtime (inicializados con valores de defines.h) ===
// Permite modificar comportamiento sin recompilar (para tweaking/debug visual)
float separation_radius_; // Radio de separación (evitar colisiones)
float alignment_radius_; // Radio de alineación (matching de velocidad)
float cohesion_radius_; // Radio de cohesión (centro de masa)
float separation_weight_; // Peso fuerza de separación (aceleración px/s²)
float alignment_weight_; // Peso fuerza de alineación (steering proporcional)
float cohesion_weight_; // Peso fuerza de cohesión (aceleración px/s²)
float max_speed_; // Velocidad máxima (px/s)
float min_speed_; // Velocidad mínima (px/s)
float max_force_; // Fuerza máxima de steering (px/s)
float boundary_margin_; // Margen para repulsión de bordes (px)
float boundary_weight_; // Peso fuerza de repulsión de bordes (aceleración px/s²)
// Métodos privados para las reglas de Reynolds
void applySeparation(Ball* boid, float delta_time);
void applyAlignment(Ball* boid, float delta_time);
void applyCohesion(Ball* boid, float delta_time);
void applyBoundaries(Ball* boid); // Mantener boids dentro de pantalla
void applyBoundaries(Ball* boid); // Repulsión de bordes (ya no wrapping)
void limitSpeed(Ball* boid); // Limitar velocidad máxima
};

View File

@@ -288,17 +288,31 @@ constexpr float LOGO_FLIP_TRIGGER_MIN = 0.20f; // 20% mínimo de progres
constexpr float LOGO_FLIP_TRIGGER_MAX = 0.80f; // 80% máximo de progreso de flip para trigger
constexpr int LOGO_FLIP_WAIT_PROBABILITY = 50; // 50% probabilidad de elegir el camino "esperar flip"
// Configuración de AppLogo (logo periódico en pantalla)
constexpr float APPLOGO_DISPLAY_INTERVAL = 20.0f; // Intervalo entre apariciones del logo (segundos)
constexpr float APPLOGO_DISPLAY_DURATION = 5.0f; // Duración de visibilidad del logo (segundos)
constexpr float APPLOGO_FADE_DURATION = 0.5f; // Duración del fade in/out (segundos)
constexpr float APPLOGO_HEIGHT_PERCENT = 0.4f; // Altura del logo = 40% de la altura de pantalla
constexpr float APPLOGO_PADDING_PERCENT = 0.1f; // Padding desde esquina inferior-derecha = 10%
// Configuración de Modo BOIDS (comportamiento de enjambre)
// FASE 1.1 REVISADA: Parámetros ajustados tras detectar cohesión mal normalizada
// TIME-BASED CONVERSION (frame-based → time-based):
// - Radios: sin cambios (píxeles)
// - Velocidades (MAX_SPEED, MIN_SPEED): ×60 (px/frame → px/s)
// - Aceleraciones puras (SEPARATION, COHESION): ×60² = ×3600 (px/frame² → px/s²)
// - Steering proporcional (ALIGNMENT): ×60 (proporcional a velocidad)
// - Límite velocidad (MAX_FORCE): ×60 (px/frame → px/s)
constexpr float BOID_SEPARATION_RADIUS = 30.0f; // Radio para evitar colisiones (píxeles)
constexpr float BOID_ALIGNMENT_RADIUS = 50.0f; // Radio para alinear velocidad con vecinos
constexpr float BOID_COHESION_RADIUS = 80.0f; // Radio para moverse hacia centro del grupo
constexpr float BOID_SEPARATION_WEIGHT = 1.5f; // Peso de separación
constexpr float BOID_ALIGNMENT_WEIGHT = 1.0f; // Peso de alineación
constexpr float BOID_COHESION_WEIGHT = 0.001f; // Peso de cohesión (MICRO - 1000x menor por falta de normalización)
constexpr float BOID_MAX_SPEED = 2.5f; // Velocidad máxima (píxeles/frame - REDUCIDA)
constexpr float BOID_MAX_FORCE = 0.05f; // Fuerza máxima de steering (REDUCIDA para evitar aceleración excesiva)
constexpr float BOID_MIN_SPEED = 0.3f; // Velocidad mínima (evita boids estáticos)
constexpr float BOID_SEPARATION_WEIGHT = 5400.0f; // Aceleración de separación (px/s²) [era 1.5 × 3600]
constexpr float BOID_ALIGNMENT_WEIGHT = 60.0f; // Steering de alineación (proporcional) [era 1.0 × 60]
constexpr float BOID_COHESION_WEIGHT = 3.6f; // Aceleración de cohesión (px/s²) [era 0.001 × 3600]
constexpr float BOID_MAX_SPEED = 150.0f; // Velocidad máxima (px/s) [era 2.5 × 60]
constexpr float BOID_MAX_FORCE = 3.0f; // Fuerza máxima de steering (px/s) [era 0.05 × 60]
constexpr float BOID_MIN_SPEED = 18.0f; // Velocidad mínima (px/s) [era 0.3 × 60]
constexpr float BOID_BOUNDARY_MARGIN = 50.0f; // Distancia a borde para activar repulsión (píxeles)
constexpr float BOID_BOUNDARY_WEIGHT = 7200.0f; // Aceleración de repulsión de bordes (px/s²) [más fuerte que separation]
// FASE 2: Spatial Hash Grid para optimización O(n²) → O(n)
constexpr float BOID_GRID_CELL_SIZE = 100.0f; // Tamaño de celda del grid (píxeles)

View File

@@ -254,6 +254,14 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
boid_manager_ = std::make_unique<BoidManager>();
boid_manager_->initialize(this, scene_manager_.get(), ui_manager_.get(), state_manager_.get(),
current_screen_width_, current_screen_height_);
// Inicializar AppLogo (logo periódico en pantalla)
app_logo_ = std::make_unique<AppLogo>();
if (!app_logo_->initialize(renderer_, current_screen_width_, current_screen_height_)) {
std::cerr << "Advertencia: No se pudo inicializar AppLogo (logo periódico)" << std::endl;
// No es crítico, continuar sin logo
app_logo_.reset();
}
}
return success;
@@ -334,16 +342,25 @@ void Engine::update() {
// Actualizar transiciones de temas (delegado a ThemeManager)
theme_manager_->update(delta_time_);
// Actualizar AppLogo (logo periódico)
if (app_logo_) {
app_logo_->update(delta_time_, state_manager_->getCurrentMode());
}
}
// === IMPLEMENTACIÓN DE MÉTODOS PÚBLICOS PARA INPUT HANDLER ===
// Gravedad y física
void Engine::handleGravityToggle() {
// Si estamos en modo boids, salir a modo física primero
// Si estamos en modo boids, salir a modo física CON GRAVEDAD OFF
// Según RULES.md: "BOIDS a PHYSICS: Pulsando la tecla G: Gravedad OFF"
if (current_mode_ == SimulationMode::BOIDS) {
toggleBoidsMode(); // Esto cambia a PHYSICS y activa gravedad
return; // La notificación ya se muestra en toggleBoidsMode
toggleBoidsMode(false); // Cambiar a PHYSICS sin activar gravedad (preserva inercia)
// NO llamar a forceBallsGravityOff() porque aplica impulsos que destruyen la inercia de BOIDS
// La gravedad ya está desactivada por BoidManager::activateBoids() y se mantiene al salir
showNotificationForAction("Modo Física - Gravedad Off");
return;
}
// Si estamos en modo figura, salir a modo física SIN GRAVEDAD
@@ -360,18 +377,19 @@ void Engine::handleGravityToggle() {
}
void Engine::handleGravityDirectionChange(GravityDirection direction, const char* notification_text) {
// Si estamos en modo boids, salir a modo física primero
// Si estamos en modo boids, salir a modo física primero PRESERVANDO VELOCIDAD
if (current_mode_ == SimulationMode::BOIDS) {
toggleBoidsMode(); // Esto cambia a PHYSICS y activa gravedad
// Continuar para aplicar la dirección de gravedad
current_mode_ = SimulationMode::PHYSICS;
boid_manager_->deactivateBoids(false); // NO activar gravedad aún (preservar momentum)
scene_manager_->forceBallsGravityOn(); // Activar gravedad SIN impulsos (preserva velocidad)
}
// Si estamos en modo figura, salir a modo física CON gravedad
if (current_mode_ == SimulationMode::SHAPE) {
else if (current_mode_ == SimulationMode::SHAPE) {
toggleShapeModeInternal(); // Desactivar figura (activa gravedad automáticamente)
} else {
scene_manager_->enableBallsGravityIfDisabled(); // Reactivar gravedad si estaba OFF
}
scene_manager_->changeGravityDirection(direction);
showNotificationForAction(notification_text);
}
@@ -431,11 +449,11 @@ void Engine::toggleDepthZoom() {
}
// Boids (comportamiento de enjambre)
void Engine::toggleBoidsMode() {
void Engine::toggleBoidsMode(bool force_gravity_on) {
if (current_mode_ == SimulationMode::BOIDS) {
// Salir del modo boids
// Salir del modo boids (velocidades ya son time-based, no requiere conversión)
current_mode_ = SimulationMode::PHYSICS;
boid_manager_->deactivateBoids();
boid_manager_->deactivateBoids(force_gravity_on); // Pasar parámetro para control preciso
} else {
// Entrar al modo boids (desde PHYSICS o SHAPE)
if (current_mode_ == SimulationMode::SHAPE) {
@@ -504,13 +522,25 @@ void Engine::switchTexture() {
// Escenarios (número de pelotas)
void Engine::changeScenario(int scenario_id, const char* notification_text) {
// Resetear modo SHAPE si está activo
// Pasar el modo actual al SceneManager para inicialización correcta
scene_manager_->changeScenario(scenario_id, current_mode_);
// Si estamos en modo SHAPE, regenerar la figura con nuevo número de pelotas
if (current_mode_ == SimulationMode::SHAPE) {
current_mode_ = SimulationMode::PHYSICS;
active_shape_.reset();
generateShape();
// Activar atracción física en las bolas nuevas (crítico tras changeScenario)
auto& balls = scene_manager_->getBallsMutable();
for (auto& ball : balls) {
ball->enableShapeAttraction(true);
}
}
// Si estamos en modo BOIDS, desactivar gravedad (modo BOIDS = gravedad OFF siempre)
if (current_mode_ == SimulationMode::BOIDS) {
scene_manager_->forceBallsGravityOff();
}
scene_manager_->changeScenario(scenario_id);
showNotificationForAction(notification_text);
}
@@ -695,10 +725,15 @@ void Engine::render() {
*/
// Renderizar UI (debug HUD, texto obsoleto, notificaciones) - delegado a UIManager
ui_manager_->render(renderer_, scene_manager_.get(), current_mode_, state_manager_->getCurrentMode(),
ui_manager_->render(renderer_, this, scene_manager_.get(), current_mode_, state_manager_->getCurrentMode(),
active_shape_.get(), shape_convergence_,
physical_window_width_, physical_window_height_, current_screen_width_);
// Renderizar AppLogo (logo periódico) - después de UI, antes de present
if (app_logo_) {
app_logo_->render();
}
SDL_RenderPresent(renderer_);
}
@@ -733,6 +768,12 @@ void Engine::toggleFullscreen() {
fullscreen_enabled_ = !fullscreen_enabled_;
SDL_SetWindowFullscreen(window_, fullscreen_enabled_);
// Si acabamos de salir de fullscreen, restaurar tamaño de ventana
if (!fullscreen_enabled_) {
SDL_SetWindowSize(window_, base_screen_width_ * current_window_zoom_, base_screen_height_ * current_window_zoom_);
SDL_SetWindowPosition(window_, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
}
// Actualizar dimensiones físicas después del cambio
updatePhysicalWindowSize();
}
@@ -769,10 +810,26 @@ void Engine::toggleRealFullscreen() {
// Reinicar la escena con nueva resolución
scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
scene_manager_->changeScenario(scene_manager_->getCurrentScenario());
scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_);
// Actualizar tamaño de pantalla para boids (wrapping boundaries)
boid_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
// Actualizar AppLogo con nueva resolución
if (app_logo_) {
app_logo_->updateScreenSize(current_screen_width_, current_screen_height_);
}
// Si estamos en modo SHAPE, regenerar la figura con nuevas dimensiones
if (current_mode_ == SimulationMode::SHAPE) {
generateShape(); // Regenerar figura con nuevas dimensiones de pantalla
// Activar atracción física en las bolas nuevas (crítico tras changeScenario)
auto& balls = scene_manager_->getBallsMutable();
for (auto& ball : balls) {
ball->enableShapeAttraction(true);
}
}
}
SDL_free(displays);
}
@@ -794,7 +851,23 @@ void Engine::toggleRealFullscreen() {
// Reinicar la escena con resolución original
scene_manager_->updateScreenSize(current_screen_width_, current_screen_height_);
scene_manager_->changeScenario(scene_manager_->getCurrentScenario());
scene_manager_->changeScenario(scene_manager_->getCurrentScenario(), current_mode_);
// Actualizar AppLogo con resolución restaurada
if (app_logo_) {
app_logo_->updateScreenSize(current_screen_width_, current_screen_height_);
}
// Si estamos en modo SHAPE, regenerar la figura con nuevas dimensiones
if (current_mode_ == SimulationMode::SHAPE) {
generateShape(); // Regenerar figura con nuevas dimensiones de pantalla
// Activar atracción física en las bolas nuevas (crítico tras changeScenario)
auto& balls = scene_manager_->getBallsMutable();
for (auto& ball : balls) {
ball->enableShapeAttraction(true);
}
}
}
}
@@ -1129,20 +1202,26 @@ void Engine::performLogoAction(bool logo_waiting_for_flip) {
demo_next_action_time_ = logo_min_time_ + (rand() % 1000) / 1000.0f * interval_range;
}
} else {
// Logo animado (PHYSICS) → 3 opciones posibles
if (action < 60) {
// 60%: PHYSICS → SHAPE (reconstruir logo y ver rotaciones)
// Logo animado (PHYSICS) → 4 opciones posibles
if (action < 50) {
// 50%: PHYSICS → SHAPE (reconstruir logo y ver rotaciones)
toggleShapeModeInternal(false);
// Resetear variables de espera de flips al volver a SHAPE
logo_waiting_for_flip_ = false;
logo_current_flip_count_ = 0;
} else if (action < 80) {
// 20%: Forzar gravedad ON (empezar a caer mientras da vueltas)
} else if (action < 68) {
// 18%: Forzar gravedad ON (empezar a caer mientras da vueltas)
scene_manager_->forceBallsGravityOn();
} else {
// 20%: Forzar gravedad OFF (flotar mientras da vueltas)
} else if (action < 84) {
// 16%: Forzar gravedad OFF (flotar mientras da vueltas)
scene_manager_->forceBallsGravityOff();
} else {
// 16%: Cambiar dirección de gravedad (nueva variación)
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
scene_manager_->changeGravityDirection(new_direction);
// Si la gravedad está OFF, activarla para que el cambio sea visible
scene_manager_->forceBallsGravityOn();
}
// Resetear timer con intervalos escalados
@@ -1154,7 +1233,7 @@ void Engine::performLogoAction(bool logo_waiting_for_flip) {
// 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 (!logo_entered_manually_ && rand() % 100 < 60) {
if (!state_manager_->getLogoEnteredManually() && rand() % 100 < 60) {
state_manager_->exitLogoMode(true); // Volver a DEMO/DEMO_LITE
}
}
@@ -1313,7 +1392,19 @@ void Engine::executeDemoAction(bool is_lite) {
// Escenarios válidos: índices 1, 2, 3, 4, 5 (10, 100, 500, 1000, 10000 pelotas)
int valid_scenarios[] = {1, 2, 3, 4, 5};
int new_scenario = valid_scenarios[rand() % 5];
scene_manager_->changeScenario(new_scenario);
scene_manager_->changeScenario(new_scenario, current_mode_);
// Si estamos en modo SHAPE, regenerar la figura con nuevo número de pelotas
if (current_mode_ == SimulationMode::SHAPE) {
generateShape();
// Activar atracción física en las bolas nuevas (crítico tras changeScenario)
auto& balls = scene_manager_->getBallsMutable();
for (auto& ball : balls) {
ball->enableShapeAttraction(true);
}
}
return;
}
@@ -1398,7 +1489,7 @@ void Engine::executeRandomizeOnDemoStart(bool is_lite) {
// 1. Escenario (excluir índices 0, 6, 7)
int valid_scenarios[] = {1, 2, 3, 4, 5};
int new_scenario = valid_scenarios[rand() % 5];
scene_manager_->changeScenario(new_scenario);
scene_manager_->changeScenario(new_scenario, current_mode_);
// 2. Tema (elegir entre TODOS los 15 temas)
int random_theme_index = rand() % 15;
@@ -1463,7 +1554,7 @@ void Engine::executeEnterLogoMode(size_t ball_count) {
// Verificar mínimo de pelotas
if (static_cast<int>(ball_count) < LOGO_MODE_MIN_BALLS) {
// Ajustar a 5000 pelotas automáticamente
scene_manager_->changeScenario(5); // Escenario 5000 pelotas (índice 5 en BALL_COUNT_SCENARIOS)
scene_manager_->changeScenario(5, current_mode_); // Escenario 5000 pelotas (índice 5 en BALL_COUNT_SCENARIOS)
}
// Guardar estado previo (para restaurar al salir)
@@ -1528,6 +1619,15 @@ void Engine::executeExitLogoMode() {
clampShapeScale();
generateShape();
// Activar atracción física si estamos en modo SHAPE
// (crítico para que las bolas se muevan hacia la figura restaurada)
if (current_mode_ == SimulationMode::SHAPE) {
auto& balls = scene_manager_->getBallsMutable();
for (auto& ball : balls) {
ball->enableShapeAttraction(true);
}
}
// Desactivar modo LOGO en PNG_SHAPE (volver a flip intervals normales)
if (active_shape_) {
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());

View File

@@ -10,6 +10,7 @@
#include <string> // for string
#include <vector> // for vector
#include "app_logo.h" // for AppLogo
#include "ball.h" // for Ball
#include "boids_mgr/boid_manager.h" // for BoidManager
#include "defines.h" // for GravityDirection, ColorTheme, ShapeType
@@ -49,7 +50,7 @@ class Engine {
void toggleDepthZoom();
// Boids (comportamiento de enjambre)
void toggleBoidsMode();
void toggleBoidsMode(bool force_gravity_on = true);
// Temas de colores
void cycleTheme(bool forward);
@@ -87,6 +88,16 @@ class Engine {
void executeEnterLogoMode(size_t ball_count);
void executeExitLogoMode();
// === Getters públicos para UIManager (Debug HUD) ===
bool getVSyncEnabled() const { return vsync_enabled_; }
bool getFullscreenEnabled() const { return fullscreen_enabled_; }
bool getRealFullscreenEnabled() const { return real_fullscreen_enabled_; }
ScalingMode getCurrentScalingMode() const { return current_scaling_mode_; }
int getCurrentScreenWidth() const { return current_screen_width_; }
int getCurrentScreenHeight() const { return current_screen_height_; }
int getBaseScreenWidth() const { return base_screen_width_; }
int getBaseScreenHeight() const { return base_screen_height_; }
private:
// === Componentes del sistema (Composición) ===
std::unique_ptr<InputHandler> input_handler_; // Manejo de entradas SDL
@@ -95,6 +106,7 @@ class Engine {
std::unique_ptr<BoidManager> boid_manager_; // Gestión de comportamiento boids
std::unique_ptr<StateManager> state_manager_; // Gestión de estados (DEMO/LOGO)
std::unique_ptr<UIManager> ui_manager_; // Gestión de UI (HUD, FPS, notificaciones)
std::unique_ptr<AppLogo> app_logo_; // Gestión de logo periódico en pantalla
// Recursos SDL
SDL_Window* window_ = nullptr;
@@ -150,7 +162,6 @@ class Engine {
// Sistema de Modo DEMO (auto-play) y LOGO
// NOTA: Engine mantiene estado de implementación para callbacks performLogoAction()
// StateManager coordina los triggers y timers, Engine ejecuta las acciones
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)
@@ -168,9 +179,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
// Determina si LOGO debe salir automáticamente o esperar input del usuario
bool logo_entered_manually_ = false; // true si se activó con tecla K, false si automático desde DEMO
// NOTA: logo_entered_manually_ fue eliminado de Engine (duplicado)
// Ahora se obtiene de StateManager con state_manager_->getLogoEnteredManually()
// Esto evita desincronización entre Engine y StateManager
// Estado previo antes de entrar a Logo Mode (para restaurar al salir)
// Guardado por executeEnterLogoMode(), restaurado por executeExitLogoMode()

View File

@@ -1,78 +0,0 @@
#pragma once
namespace {
SDL_Texture* dbg_tex = nullptr;
SDL_Renderer* dbg_ren = nullptr;
} // namespace
inline void dbg_init(SDL_Renderer* renderer) {
dbg_ren = renderer;
Uint8 font[448] = {0x42, 0x4D, 0xC0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, 0x01, 0x00, 0x00, 0x12, 0x0B, 0x00, 0x00, 0x12, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x18, 0xF3, 0x83, 0x83, 0xCF, 0x83, 0x87, 0x00, 0x00, 0xF3, 0x39, 0x39, 0xCF, 0x79, 0xF3, 0x00, 0x00, 0x01, 0xF9, 0x39, 0xCF, 0x61, 0xF9, 0x00, 0x00, 0x33, 0xF9, 0x03, 0xE7, 0x87, 0x81, 0x00, 0x00, 0x93, 0x03, 0x3F, 0xF3, 0x1B, 0x39, 0x00, 0x00, 0xC3, 0x3F, 0x9F, 0x39, 0x3B, 0x39, 0x00, 0x41, 0xE3, 0x03, 0xC3, 0x01, 0x87, 0x83, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xE7, 0x01, 0xC7, 0x81, 0x01, 0x83, 0x00, 0x00, 0xE7, 0x1F, 0x9B, 0xE7, 0x1F, 0x39, 0x00, 0x00, 0xE7, 0x8F, 0x39, 0xE7, 0x87, 0xF9, 0x00, 0x00, 0xC3, 0xC7, 0x39, 0xE7, 0xC3, 0xC3, 0x00, 0x00, 0x99, 0xE3, 0x39, 0xE7, 0xF1, 0xE7, 0x00, 0x00, 0x99, 0xF1, 0xB3, 0xC7, 0x39, 0xF3, 0x00, 0x00, 0x99, 0x01, 0xC7, 0xE7, 0x83, 0x81, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x83, 0xE7, 0x83, 0xEF, 0x39, 0x39, 0x00, 0x00, 0x39, 0xE7, 0x39, 0xC7, 0x11, 0x11, 0x00, 0x00, 0xF9, 0xE7, 0x39, 0x83, 0x01, 0x83, 0x00, 0x00, 0x83, 0xE7, 0x39, 0x11, 0x01, 0xC7, 0x00, 0x00, 0x3F, 0xE7, 0x39, 0x39, 0x29, 0x83, 0x00, 0x00, 0x33, 0xE7, 0x39, 0x39, 0x39, 0x11, 0x00, 0x00, 0x87, 0x81, 0x39, 0x39, 0x39, 0x39, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x39, 0x39, 0x83, 0x3F, 0x85, 0x31, 0x00, 0x00, 0x39, 0x31, 0x39, 0x3F, 0x33, 0x23, 0x00, 0x00, 0x29, 0x21, 0x39, 0x03, 0x21, 0x07, 0x00, 0x00, 0x01, 0x01, 0x39, 0x39, 0x39, 0x31, 0x00, 0x00, 0x01, 0x09, 0x39, 0x39, 0x39, 0x39, 0x00, 0x00, 0x11, 0x19, 0x39, 0x39, 0x39, 0x39, 0x00, 0x00, 0x39, 0x39, 0x83, 0x03, 0x83, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xC1, 0x39, 0x81, 0x83, 0x31, 0x01, 0x00, 0x00, 0x99, 0x39, 0xE7, 0x39, 0x23, 0x3F, 0x00, 0x00, 0x39, 0x39, 0xE7, 0xF9, 0x07, 0x3F, 0x00, 0x00, 0x31, 0x01, 0xE7, 0xF9, 0x0F, 0x3F, 0x00, 0x00, 0x3F, 0x39, 0xE7, 0xF9, 0x27, 0x3F, 0x00, 0x00, 0x9F, 0x39, 0xE7, 0xF9, 0x33, 0x3F, 0x00, 0x00, 0xC1, 0x39, 0x81, 0xF9, 0x39, 0x3F, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x39, 0x03, 0xC3, 0x07, 0x01, 0x3F, 0x00, 0x00, 0x39, 0x39, 0x99, 0x33, 0x3F, 0x3F, 0x00, 0x00, 0x01, 0x39, 0x3F, 0x39, 0x3F, 0x3F, 0x00, 0x00, 0x39, 0x03, 0x3F, 0x39, 0x03, 0x03, 0x00, 0x00, 0x39, 0x39, 0x3F, 0x39, 0x3F, 0x3F, 0x00, 0x00, 0x93, 0x39, 0x99, 0x33, 0x3F, 0x3F, 0x00, 0x00, 0xC7, 0x03, 0xC3, 0x07, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00};
// Cargar surface del bitmap font
SDL_Surface* font_surface = SDL_LoadBMP_IO(SDL_IOFromMem(font, 448), 1);
if (font_surface != nullptr) {
// Crear una nueva surface de 32 bits con canal alpha
SDL_Surface* rgba_surface = SDL_CreateSurface(font_surface->w, font_surface->h, SDL_PIXELFORMAT_RGBA8888);
if (rgba_surface != nullptr) {
// Obtener píxeles de ambas surfaces
Uint8* src_pixels = (Uint8*)font_surface->pixels;
Uint32* dst_pixels = (Uint32*)rgba_surface->pixels;
int width = font_surface->w;
int height = font_surface->h;
// Procesar cada píxel
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int byte_index = y * font_surface->pitch + (x / 8);
int bit_index = 7 - (x % 8);
// Extraer bit del bitmap monocromo
bool is_white = (src_pixels[byte_index] >> bit_index) & 1;
if (is_white) // Fondo blanco original -> transparente
{
dst_pixels[y * width + x] = 0x00000000; // Transparente
} else // Texto negro original -> blanco opaco
{
dst_pixels[y * width + x] = 0xFFFFFFFF; // Blanco opaco
}
}
}
dbg_tex = SDL_CreateTextureFromSurface(dbg_ren, rgba_surface);
SDL_DestroySurface(rgba_surface);
}
SDL_DestroySurface(font_surface);
}
// Configurar filtro nearest neighbor para píxel perfect del texto
if (dbg_tex != nullptr) {
SDL_SetTextureScaleMode(dbg_tex, SDL_SCALEMODE_NEAREST);
// Configurar blend mode para transparencia normal
SDL_SetTextureBlendMode(dbg_tex, SDL_BLENDMODE_BLEND);
}
}
inline void dbg_print(int x, int y, const char* text, Uint8 r, Uint8 g, Uint8 b) {
int cc = 0;
SDL_SetTextureColorMod(dbg_tex, r, g, b);
SDL_FRect src = {0, 0, 8, 8};
SDL_FRect dst = {static_cast<float>(x), static_cast<float>(y), 8, 8};
while (text[cc] != 0) {
if (text[cc] != 32) {
if (text[cc] >= 65) {
src.x = ((text[cc] - 65) % 6) * 8;
src.y = ((text[cc] - 65) / 6) * 8;
} else {
src.x = ((text[cc] - 22) % 6) * 8;
src.y = ((text[cc] - 22) / 6) * 8;
}
SDL_RenderTexture(dbg_ren, dbg_tex, &src, &dst);
}
cc++;
dst.x += 8;
}
}

View File

@@ -128,6 +128,9 @@ bool Texture::loadFromFile(const std::string &file_path) {
// Configurar filtro nearest neighbor para píxel perfect
SDL_SetTextureScaleMode(new_texture, SDL_SCALEMODE_NEAREST);
// Habilitar alpha blending para transparencias
SDL_SetTextureBlendMode(new_texture, SDL_BLENDMODE_BLEND);
}
// Destruye la superficie cargada
@@ -169,3 +172,17 @@ int Texture::getHeight() {
void Texture::setColor(int r, int g, int b) {
SDL_SetTextureColorMod(texture_, r, g, b);
}
// Modula el alpha de la textura
void Texture::setAlpha(int alpha) {
if (texture_ != nullptr) {
SDL_SetTextureAlphaMod(texture_, static_cast<Uint8>(alpha));
}
}
// Configurar modo de escalado
void Texture::setScaleMode(SDL_ScaleMode mode) {
if (texture_ != nullptr) {
SDL_SetTextureScaleMode(texture_, mode);
}
}

View File

@@ -44,6 +44,12 @@ class Texture {
// Modula el color de la textura
void setColor(int r, int g, int b);
// Modula el alpha (transparencia) de la textura
void setAlpha(int alpha);
// Configurar modo de escalado (NEAREST para pixel art, LINEAR para suavizado)
void setScaleMode(SDL_ScaleMode mode);
// Getter para batch rendering
SDL_Texture *getSDLTexture() const { return texture_; }
};

View File

@@ -22,8 +22,8 @@ void SceneManager::initialize(int scenario, std::shared_ptr<Texture> texture, Th
theme_manager_ = theme_manager;
current_ball_size_ = texture_->getWidth();
// Crear bolas iniciales
changeScenario(scenario_);
// Crear bolas iniciales (siempre en modo PHYSICS al inicio)
changeScenario(scenario_, SimulationMode::PHYSICS);
}
void SceneManager::update(float delta_time) {
@@ -33,7 +33,7 @@ void SceneManager::update(float delta_time) {
}
}
void SceneManager::changeScenario(int scenario_id) {
void SceneManager::changeScenario(int scenario_id, SimulationMode mode) {
// Guardar escenario
scenario_ = scenario_id;
@@ -45,14 +45,53 @@ void SceneManager::changeScenario(int scenario_id) {
// Crear las bolas según el escenario
for (int i = 0; i < BALL_COUNT_SCENARIOS[scenario_id]; ++i) {
const int SIGN = ((rand() % 2) * 2) - 1; // Genera un signo aleatorio (+ o -)
float X, Y, VX, VY;
// Calcular spawn zone: margen a cada lado, zona central para spawn
const int margin = static_cast<int>(screen_width_ * BALL_SPAWN_MARGIN);
const int spawn_zone_width = screen_width_ - (2 * margin);
const float X = (rand() % spawn_zone_width) + margin; // Posición inicial en X
const float VX = (((rand() % 20) + 10) * 0.1f) * SIGN; // Velocidad en X
const float VY = ((rand() % 60) - 30) * 0.1f; // Velocidad en Y
// Inicialización según SimulationMode (RULES.md líneas 23-26)
switch (mode) {
case SimulationMode::PHYSICS: {
// PHYSICS: Parte superior, 75% distribución central en X
const int SIGN = ((rand() % 2) * 2) - 1;
const int margin = static_cast<int>(screen_width_ * BALL_SPAWN_MARGIN);
const int spawn_zone_width = screen_width_ - (2 * margin);
X = (rand() % spawn_zone_width) + margin;
Y = 0.0f; // Parte superior
VX = (((rand() % 20) + 10) * 0.1f) * SIGN;
VY = ((rand() % 60) - 30) * 0.1f;
break;
}
case SimulationMode::SHAPE: {
// SHAPE: Centro de pantalla, sin velocidad inicial
X = screen_width_ / 2.0f;
Y = screen_height_ / 2.0f; // Centro vertical
VX = 0.0f;
VY = 0.0f;
break;
}
case SimulationMode::BOIDS: {
// BOIDS: Posiciones aleatorias, velocidades aleatorias
const int SIGN_X = ((rand() % 2) * 2) - 1;
const int SIGN_Y = ((rand() % 2) * 2) - 1;
X = static_cast<float>(rand() % screen_width_);
Y = static_cast<float>(rand() % screen_height_); // Posición Y aleatoria
VX = (((rand() % 40) + 10) * 0.1f) * SIGN_X; // 1.0 - 5.0 px/frame
VY = (((rand() % 40) + 10) * 0.1f) * SIGN_Y;
break;
}
default:
// Fallback a PHYSICS por seguridad
const int SIGN = ((rand() % 2) * 2) - 1;
const int margin = static_cast<int>(screen_width_ * BALL_SPAWN_MARGIN);
const int spawn_zone_width = screen_width_ - (2 * margin);
X = (rand() % spawn_zone_width) + margin;
Y = 0.0f; // Parte superior
VX = (((rand() % 20) + 10) * 0.1f) * SIGN;
VY = ((rand() % 60) - 30) * 0.1f;
break;
}
// Seleccionar color de la paleta del tema actual (delegado a ThemeManager)
int random_index = rand();
@@ -62,7 +101,7 @@ void SceneManager::changeScenario(int scenario_id) {
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_,
X, Y, VX, VY, COLOR, texture_,
screen_width_, screen_height_, current_ball_size_,
current_gravity_, mass_factor
));

View File

@@ -51,8 +51,9 @@ class SceneManager {
/**
* @brief Cambia el número de bolas según escenario
* @param scenario_id Índice del escenario (0-7 para 10 a 50,000 bolas)
* @param mode Modo de simulación actual (afecta inicialización)
*/
void changeScenario(int scenario_id);
void changeScenario(int scenario_id, SimulationMode mode);
/**
* @brief Actualiza textura y tamaño de todas las bolas

View File

@@ -76,8 +76,10 @@ void ShapeManager::toggleShapeMode(bool force_gravity_on_exit) {
// Si estamos en LOGO MODE, generar threshold aleatorio de convergencia (75-100%)
if (state_mgr_ && state_mgr_->getCurrentMode() == AppMode::LOGO) {
/*
float logo_convergence_threshold = LOGO_CONVERGENCE_MIN +
(rand() % 1000) / 1000.0f * (LOGO_CONVERGENCE_MAX - LOGO_CONVERGENCE_MIN);
*/
shape_convergence_ = 0.0f; // Reset convergencia al entrar
}
} else {

View File

@@ -119,6 +119,11 @@ class StateManager {
*/
float getLogoPreviousShapeScale() const { return logo_previous_shape_scale_; }
/**
* @brief Obtiene si LOGO fue activado manualmente (tecla K) o automáticamente (desde DEMO)
*/
bool getLogoEnteredManually() const { return logo_entered_manually_; }
/**
* @brief Establece valores previos de LOGO (llamado por Engine antes de entrar)
*/

View File

@@ -13,6 +13,7 @@ bool TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_
renderer_ = renderer;
font_size_ = font_size;
use_antialiasing_ = use_antialiasing;
font_path_ = font_path; // Guardar ruta para reinitialize()
// Inicializar SDL_ttf si no está inicializado
if (!TTF_WasInit()) {
@@ -32,6 +33,38 @@ bool TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_
return true;
}
bool TextRenderer::reinitialize(int new_font_size) {
// Verificar que tenemos todo lo necesario
if (renderer_ == nullptr || font_path_.empty()) {
SDL_Log("Error: TextRenderer no inicializado correctamente para reinitialize()");
return false;
}
// Si el tamaño es el mismo, no hacer nada
if (new_font_size == font_size_) {
return true;
}
// Cerrar fuente actual
if (font_ != nullptr) {
TTF_CloseFont(font_);
font_ = nullptr;
}
// Cargar fuente con nuevo tamaño
font_ = TTF_OpenFont(font_path_.c_str(), new_font_size);
if (font_ == nullptr) {
SDL_Log("Error al recargar fuente '%s' con tamaño %d: %s",
font_path_.c_str(), new_font_size, SDL_GetError());
return false;
}
// Actualizar tamaño almacenado
font_size_ = new_font_size;
return true;
}
void TextRenderer::cleanup() {
if (font_ != nullptr) {
TTF_CloseFont(font_);

View File

@@ -12,6 +12,9 @@ public:
// Inicializa el renderizador de texto con una fuente
bool init(SDL_Renderer* renderer, const char* font_path, int font_size, bool use_antialiasing = true);
// Reinicializa el renderizador con un nuevo tamaño de fuente
bool reinitialize(int new_font_size);
// Libera recursos
void cleanup();
@@ -46,4 +49,5 @@ private:
TTF_Font* font_;
int font_size_;
bool use_antialiasing_;
std::string font_path_; // Almacenar ruta para reinitialize()
};

View File

@@ -12,9 +12,17 @@ HelpOverlay::HelpOverlay()
physical_width_(0),
physical_height_(0),
visible_(false),
box_size_(0),
box_width_(0),
box_height_(0),
box_x_(0),
box_y_(0) {
box_y_(0),
column1_width_(0),
column2_width_(0),
cached_texture_(nullptr),
last_category_color_({0, 0, 0, 255}),
last_content_color_({0, 0, 0, 255}),
last_bg_color_({0, 0, 0, 255}),
texture_needs_rebuild_(true) {
// Llenar lista de controles (organizados por categoría, equilibrado en 2 columnas)
key_bindings_ = {
// COLUMNA 1: SIMULACIÓN
@@ -70,18 +78,27 @@ HelpOverlay::HelpOverlay()
}
HelpOverlay::~HelpOverlay() {
// Destruir textura cacheada si existe
if (cached_texture_) {
SDL_DestroyTexture(cached_texture_);
cached_texture_ = nullptr;
}
delete text_renderer_;
}
void HelpOverlay::initialize(SDL_Renderer* renderer, ThemeManager* theme_mgr, int physical_width, int physical_height) {
void HelpOverlay::toggle() {
visible_ = !visible_;
}
void HelpOverlay::initialize(SDL_Renderer* renderer, ThemeManager* theme_mgr, int physical_width, int physical_height, int font_size) {
renderer_ = renderer;
theme_mgr_ = theme_mgr;
physical_width_ = physical_width;
physical_height_ = physical_height;
// Crear renderer de texto con tamaño reducido (18px en lugar de 24px)
// Crear renderer de texto con tamaño dinámico
text_renderer_ = new TextRenderer();
text_renderer_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", 18, true);
text_renderer_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", font_size, true);
calculateBoxDimensions();
}
@@ -90,69 +107,218 @@ void HelpOverlay::updatePhysicalWindowSize(int physical_width, int physical_heig
physical_width_ = physical_width;
physical_height_ = physical_height;
calculateBoxDimensions();
// Marcar textura para regeneración (dimensiones han cambiado)
texture_needs_rebuild_ = true;
}
void HelpOverlay::reinitializeFontSize(int new_font_size) {
if (!text_renderer_) return;
// Reinicializar text renderer con nuevo tamaño
text_renderer_->reinitialize(new_font_size);
// NOTA: NO recalcular dimensiones aquí porque physical_width_ y physical_height_
// pueden tener valores antiguos. updatePhysicalWindowSize() se llamará después
// con las dimensiones correctas y recalculará todo apropiadamente.
// Marcar textura para regeneración completa
texture_needs_rebuild_ = true;
}
void HelpOverlay::updateAll(int font_size, int physical_width, int physical_height) {
// Actualizar dimensiones físicas PRIMERO
physical_width_ = physical_width;
physical_height_ = physical_height;
// Reinicializar text renderer con nuevo tamaño (si cambió)
if (text_renderer_) {
text_renderer_->reinitialize(font_size);
}
// Recalcular dimensiones del box con nuevo font y nuevas dimensiones
calculateBoxDimensions();
// Marcar textura para regeneración completa
texture_needs_rebuild_ = true;
}
void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) {
if (!text_renderer_) {
max_width = 0;
total_height = 0;
return;
}
int line_height = text_renderer_->getTextHeight();
int padding = 25;
// Calcular ancho máximo por columna
int max_col1_width = 0;
int max_col2_width = 0;
int current_column = 0;
for (const auto& binding : key_bindings_) {
// Cambio de columna
if (strcmp(binding.key, "[new_col]") == 0) {
current_column = 1;
continue;
}
// Separador vacío (no tiene key ni description)
if (binding.key[0] == '\0') {
continue;
}
int line_width = 0;
if (binding.description[0] == '\0') {
// Es un encabezado (solo tiene key, sin description)
line_width = text_renderer_->getTextWidthPhysical(binding.key);
} else {
// Es una línea normal con key + description
int key_width = text_renderer_->getTextWidthPhysical(binding.key);
int desc_width = text_renderer_->getTextWidthPhysical(binding.description);
line_width = key_width + 10 + desc_width; // 10px de separación
}
// Actualizar máximo de columna correspondiente
if (current_column == 0) {
max_col1_width = std::max(max_col1_width, line_width);
} else {
max_col2_width = std::max(max_col2_width, line_width);
}
}
// Almacenar anchos de columnas en miembros para uso posterior
column1_width_ = max_col1_width;
column2_width_ = max_col2_width;
// Ancho total: 2 columnas + 3 paddings (izq, medio, der)
max_width = max_col1_width + max_col2_width + padding * 3;
// Altura: contar líneas REALES en cada columna
int col1_lines = 0;
int col2_lines = 0;
current_column = 0;
for (const auto& binding : key_bindings_) {
// Cambio de columna
if (strcmp(binding.key, "[new_col]") == 0) {
current_column = 1;
continue;
}
// Separador vacío no cuenta como línea
if (binding.key[0] == '\0') {
continue;
}
// Contar línea (ya sea encabezado o contenido)
if (current_column == 0) {
col1_lines++;
} else {
col2_lines++;
}
}
// Usar la columna más larga para calcular altura
int max_column_lines = std::max(col1_lines, col2_lines);
// Altura: título (2 líneas) + contenido + padding superior e inferior
total_height = line_height * 2 + max_column_lines * line_height + padding * 2;
}
void HelpOverlay::calculateBoxDimensions() {
// 90% de la dimensión más corta (cuadrado)
int min_dimension = std::min(physical_width_, physical_height_);
box_size_ = static_cast<int>(min_dimension * 0.9f);
// Calcular dimensiones necesarias según el texto
int text_width, text_height;
calculateTextDimensions(text_width, text_height);
// Usar directamente el ancho y altura calculados según el contenido
box_width_ = text_width;
// Altura: 90% de altura física o altura calculada, el que sea menor
int max_height = static_cast<int>(physical_height_ * 0.9f);
box_height_ = std::min(text_height, max_height);
// Centrar en pantalla
box_x_ = (physical_width_ - box_size_) / 2;
box_y_ = (physical_height_ - box_size_) / 2;
box_x_ = (physical_width_ - box_width_) / 2;
box_y_ = (physical_height_ - box_height_) / 2;
}
void HelpOverlay::render(SDL_Renderer* renderer) {
if (!visible_) return;
void HelpOverlay::rebuildCachedTexture() {
if (!renderer_ || !theme_mgr_ || !text_renderer_) return;
// CRÍTICO: Habilitar alpha blending para que la transparencia funcione
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
// Destruir textura anterior si existe
if (cached_texture_) {
SDL_DestroyTexture(cached_texture_);
cached_texture_ = nullptr;
}
// Obtener color de notificación del tema actual (para el fondo)
// Crear nueva textura del tamaño del overlay
cached_texture_ = SDL_CreateTexture(renderer_,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET,
box_width_,
box_height_);
if (!cached_texture_) {
SDL_Log("Error al crear textura cacheada: %s", SDL_GetError());
return;
}
// Habilitar alpha blending en la textura
SDL_SetTextureBlendMode(cached_texture_, SDL_BLENDMODE_BLEND);
// Guardar render target actual
SDL_Texture* prev_target = SDL_GetRenderTarget(renderer_);
// Cambiar render target a la textura cacheada
SDL_SetRenderTarget(renderer_, cached_texture_);
// Limpiar textura (completamente transparente)
SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0);
SDL_RenderClear(renderer_);
// Habilitar alpha blending
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
// Obtener colores actuales del tema
int notif_bg_r, notif_bg_g, notif_bg_b;
theme_mgr_->getCurrentNotificationBackgroundColor(notif_bg_r, notif_bg_g, notif_bg_b);
// Renderizar fondo semitransparente usando SDL_RenderGeometry (soporta alpha real)
// Renderizar fondo del overlay a la textura
float alpha = 0.85f;
SDL_Vertex bg_vertices[4];
// Convertir RGB a float [0.0, 1.0]
float r = notif_bg_r / 255.0f;
float g = notif_bg_g / 255.0f;
float b = notif_bg_b / 255.0f;
// Vértice superior izquierdo
bg_vertices[0].position = {static_cast<float>(box_x_), static_cast<float>(box_y_)};
// Vértices del fondo (posición relativa 0,0 porque estamos renderizando a textura)
bg_vertices[0].position = {0, 0};
bg_vertices[0].tex_coord = {0.0f, 0.0f};
bg_vertices[0].color = {r, g, b, alpha};
// Vértice superior derecho
bg_vertices[1].position = {static_cast<float>(box_x_ + box_size_), static_cast<float>(box_y_)};
bg_vertices[1].position = {static_cast<float>(box_width_), 0};
bg_vertices[1].tex_coord = {1.0f, 0.0f};
bg_vertices[1].color = {r, g, b, alpha};
// Vértice inferior derecho
bg_vertices[2].position = {static_cast<float>(box_x_ + box_size_), static_cast<float>(box_y_ + box_size_)};
bg_vertices[2].position = {static_cast<float>(box_width_), static_cast<float>(box_height_)};
bg_vertices[2].tex_coord = {1.0f, 1.0f};
bg_vertices[2].color = {r, g, b, alpha};
// Vértice inferior izquierdo
bg_vertices[3].position = {static_cast<float>(box_x_), static_cast<float>(box_y_ + box_size_)};
bg_vertices[3].position = {0, static_cast<float>(box_height_)};
bg_vertices[3].tex_coord = {0.0f, 1.0f};
bg_vertices[3].color = {r, g, b, alpha};
// Índices para 2 triángulos
int bg_indices[6] = {0, 1, 2, 2, 3, 0};
SDL_RenderGeometry(renderer_, nullptr, bg_vertices, 4, bg_indices, 6);
// Renderizar sin textura (nullptr) con alpha blending
SDL_RenderGeometry(renderer, nullptr, bg_vertices, 4, bg_indices, 6);
// Renderizar texto del overlay (ajustando coordenadas para que sean relativas a 0,0)
// Necesito renderizar el texto igual que en renderHelpText() pero con coordenadas ajustadas
// Renderizar texto de ayuda
renderHelpText();
}
void HelpOverlay::renderHelpText() {
// Obtener 2 colores del tema para diferenciación visual
// Obtener colores para el texto
int text_r, text_g, text_b;
theme_mgr_->getCurrentThemeTextColor(text_r, text_g, text_b);
SDL_Color category_color = {static_cast<Uint8>(text_r), static_cast<Uint8>(text_g), static_cast<Uint8>(text_b), 255};
@@ -160,75 +326,130 @@ void HelpOverlay::renderHelpText() {
Color ball_color = theme_mgr_->getInterpolatedColor(0);
SDL_Color content_color = {static_cast<Uint8>(ball_color.r), static_cast<Uint8>(ball_color.g), static_cast<Uint8>(ball_color.b), 255};
// Guardar colores actuales para comparación futura
last_category_color_ = category_color;
last_content_color_ = content_color;
last_bg_color_ = {static_cast<Uint8>(notif_bg_r), static_cast<Uint8>(notif_bg_g), static_cast<Uint8>(notif_bg_b), 255};
// Configuración de espaciado
int line_height = text_renderer_->getTextHeight();
int padding = 25; // Equilibrio entre espacio y márgenes
int column_width = (box_size_ - padding * 3) / 2; // Ancho de cada columna (2 columnas)
int padding = 25;
int current_x = box_x_ + padding;
int current_y = box_y_ + padding;
int current_column = 0; // 0 = izquierda, 1 = derecha
int current_x = padding; // Coordenadas relativas a la textura (0,0)
int current_y = padding;
int current_column = 0;
// Título principal
const char* title = "CONTROLES - ViBe3 Physics";
int title_width = text_renderer_->getTextWidthPhysical(title);
text_renderer_->printAbsolute(
box_x_ + box_size_ / 2 - title_width / 2,
current_y,
title,
category_color);
current_y += line_height * 2; // Espacio después del título
text_renderer_->printAbsolute(box_width_ / 2 - title_width / 2, current_y, title, category_color);
current_y += line_height * 2;
// Guardar Y inicial de contenido (después del título)
int content_start_y = current_y;
// Renderizar cada línea
for (const auto& binding : key_bindings_) {
// Si es un separador (descripción vacía), cambiar de columna
if (strcmp(binding.key, "[new_col]") == 0 && binding.description[0] == '\0') {
if (current_column == 0) {
// Cambiar a columna derecha
current_column = 1;
current_x = box_x_ + padding + column_width + padding;
current_y = content_start_y; // Reset Y a posición inicial de contenido
current_x = padding + column1_width_ + padding; // Usar ancho real de columna 1
current_y = content_start_y;
}
continue;
}
// Si es un encabezado de categoría (descripción vacía pero key no vacía)
// CHECK PADDING INFERIOR ANTES de escribir la línea (AMBAS COLUMNAS)
// Verificar si la PRÓXIMA línea cabrá dentro del box con padding inferior
if (current_y + line_height >= box_height_ - padding) {
if (current_column == 0) {
// Columna 0 llena: cambiar a columna 1
current_column = 1;
current_x = padding + column1_width_ + padding;
current_y = content_start_y;
} else {
// Columna 1 llena: omitir resto de texto (no cabe)
// Preferible omitir que sobresalir del overlay
continue;
}
}
if (binding.description[0] == '\0') {
// Renderizar encabezado con color de categoría
text_renderer_->printAbsolute(
current_x,
current_y,
binding.key,
category_color);
current_y += line_height + 2; // Espacio extra después de encabezado
text_renderer_->printAbsolute(current_x, current_y, binding.key, category_color);
current_y += line_height + 2;
continue;
}
// Renderizar tecla con color de contenido
text_renderer_->printAbsolute(
current_x,
current_y,
binding.key,
content_color);
// Renderizar descripción con color de contenido
text_renderer_->printAbsolute(current_x, current_y, binding.key, content_color);
int key_width = text_renderer_->getTextWidthPhysical(binding.key);
text_renderer_->printAbsolute(
current_x + key_width + 10, // Espacio entre tecla y descripción
current_y,
binding.description,
content_color);
text_renderer_->printAbsolute(current_x + key_width + 10, current_y, binding.description, content_color);
current_y += line_height;
// Si nos pasamos del borde inferior del recuadro, cambiar de columna
if (current_y > box_y_ + box_size_ - padding && current_column == 0) {
current_column = 1;
current_x = box_x_ + padding + column_width + padding;
current_y = content_start_y; // Reset Y a inicio de contenido
}
}
// Restaurar render target original
SDL_SetRenderTarget(renderer_, prev_target);
// Marcar que ya no necesita rebuild
texture_needs_rebuild_ = false;
}
void HelpOverlay::render(SDL_Renderer* renderer) {
if (!visible_) return;
// Obtener colores actuales del tema
int notif_bg_r, notif_bg_g, notif_bg_b;
theme_mgr_->getCurrentNotificationBackgroundColor(notif_bg_r, notif_bg_g, notif_bg_b);
int text_r, text_g, text_b;
theme_mgr_->getCurrentThemeTextColor(text_r, text_g, text_b);
Color ball_color = theme_mgr_->getInterpolatedColor(0);
// Crear colores actuales para comparación
SDL_Color current_bg = {static_cast<Uint8>(notif_bg_r), static_cast<Uint8>(notif_bg_g), static_cast<Uint8>(notif_bg_b), 255};
SDL_Color current_category = {static_cast<Uint8>(text_r), static_cast<Uint8>(text_g), static_cast<Uint8>(text_b), 255};
SDL_Color current_content = {static_cast<Uint8>(ball_color.r), static_cast<Uint8>(ball_color.g), static_cast<Uint8>(ball_color.b), 255};
// Detectar si los colores han cambiado significativamente (umbral: 5/255)
constexpr int COLOR_CHANGE_THRESHOLD = 5;
bool colors_changed =
(abs(current_bg.r - last_bg_color_.r) > COLOR_CHANGE_THRESHOLD ||
abs(current_bg.g - last_bg_color_.g) > COLOR_CHANGE_THRESHOLD ||
abs(current_bg.b - last_bg_color_.b) > COLOR_CHANGE_THRESHOLD ||
abs(current_category.r - last_category_color_.r) > COLOR_CHANGE_THRESHOLD ||
abs(current_category.g - last_category_color_.g) > COLOR_CHANGE_THRESHOLD ||
abs(current_category.b - last_category_color_.b) > COLOR_CHANGE_THRESHOLD ||
abs(current_content.r - last_content_color_.r) > COLOR_CHANGE_THRESHOLD ||
abs(current_content.g - last_content_color_.g) > COLOR_CHANGE_THRESHOLD ||
abs(current_content.b - last_content_color_.b) > COLOR_CHANGE_THRESHOLD);
// Regenerar textura si es necesario (colores cambiaron O flag de rebuild activo)
if (texture_needs_rebuild_ || colors_changed || !cached_texture_) {
rebuildCachedTexture();
}
// Si no hay textura cacheada (error), salir
if (!cached_texture_) return;
// CRÍTICO: Habilitar alpha blending para que la transparencia funcione
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
// Obtener viewport actual (en modo letterbox F3 tiene offset para centrar imagen)
SDL_Rect viewport;
SDL_GetRenderViewport(renderer, &viewport);
// Calcular posición centrada dentro del VIEWPORT, no de la pantalla física
// viewport.w y viewport.h son las dimensiones del área visible
// viewport.x y viewport.y son el offset de las barras negras
int centered_x = viewport.x + (viewport.w - box_width_) / 2;
int centered_y = viewport.y + (viewport.h - box_height_) / 2;
// Renderizar la textura cacheada centrada en el viewport
SDL_FRect dest_rect;
dest_rect.x = static_cast<float>(centered_x);
dest_rect.y = static_cast<float>(centered_y);
dest_rect.w = static_cast<float>(box_width_);
dest_rect.h = static_cast<float>(box_height_);
SDL_RenderTexture(renderer, cached_texture_, nullptr, &dest_rect);
}

View File

@@ -24,7 +24,7 @@ class HelpOverlay {
/**
* @brief Inicializa el overlay con renderer y theme manager
*/
void initialize(SDL_Renderer* renderer, ThemeManager* theme_mgr, int physical_width, int physical_height);
void initialize(SDL_Renderer* renderer, ThemeManager* theme_mgr, int physical_width, int physical_height, int font_size);
/**
* @brief Renderiza el overlay si está visible
@@ -36,10 +36,23 @@ class HelpOverlay {
*/
void updatePhysicalWindowSize(int physical_width, int physical_height);
/**
* @brief Reinitializa el tamaño de fuente (cuando cambia el tamaño de ventana)
*/
void reinitializeFontSize(int new_font_size);
/**
* @brief Actualiza font size Y dimensiones físicas de forma atómica
* @param font_size Tamaño de fuente actual
* @param physical_width Nueva anchura física
* @param physical_height Nueva altura física
*/
void updateAll(int font_size, int physical_width, int physical_height);
/**
* @brief Toggle visibilidad del overlay
*/
void toggle() { visible_ = !visible_; }
void toggle();
/**
* @brief Consulta si el overlay está visible
@@ -54,16 +67,31 @@ class HelpOverlay {
int physical_height_;
bool visible_;
// Dimensiones calculadas del recuadro (90% de dimensión menor, cuadrado, centrado)
int box_size_;
// Dimensiones calculadas del recuadro (anchura dinámica según texto, centrado)
int box_width_;
int box_height_;
int box_x_;
int box_y_;
// Calcular dimensiones del recuadro según tamaño de ventana
// Anchos individuales de cada columna (para evitar solapamiento)
int column1_width_;
int column2_width_;
// Sistema de caché para optimización de rendimiento
SDL_Texture* cached_texture_; // Textura cacheada del overlay completo
SDL_Color last_category_color_; // Último color de categorías renderizado
SDL_Color last_content_color_; // Último color de contenido renderizado
SDL_Color last_bg_color_; // Último color de fondo renderizado
bool texture_needs_rebuild_; // Flag para forzar regeneración de textura
// Calcular dimensiones del texto más largo
void calculateTextDimensions(int& max_width, int& total_height);
// Calcular dimensiones del recuadro según tamaño de ventana y texto
void calculateBoxDimensions();
// Renderizar texto de ayuda dentro del recuadro
void renderHelpText();
// Regenerar textura cacheada del overlay
void rebuildCachedTexture();
// Estructura para par tecla-descripción
struct KeyBinding {

View File

@@ -5,6 +5,31 @@
#include "../utils/easing_functions.h"
#include <SDL3/SDL.h>
// ============================================================================
// HELPER: Obtener viewport en coordenadas físicas (no lógicas)
// ============================================================================
// SDL_GetRenderViewport() devuelve coordenadas LÓGICAS cuando hay presentación
// lógica activa. Para obtener coordenadas FÍSICAS, necesitamos deshabilitar
// temporalmente la presentación lógica.
static SDL_Rect getPhysicalViewport(SDL_Renderer* renderer) {
// Guardar estado actual de presentación lógica
int logical_w = 0, logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer, &logical_w, &logical_h, &presentation_mode);
// Deshabilitar presentación lógica temporalmente
SDL_SetRenderLogicalPresentation(renderer, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED);
// Obtener viewport en coordenadas físicas (píxeles reales)
SDL_Rect physical_viewport;
SDL_GetRenderViewport(renderer, &physical_viewport);
// Restaurar presentación lógica
SDL_SetRenderLogicalPresentation(renderer, logical_w, logical_h, presentation_mode);
return physical_viewport;
}
Notifier::Notifier()
: renderer_(nullptr)
, text_renderer_(nullptr)
@@ -159,10 +184,14 @@ void Notifier::render() {
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);
// Obtener viewport FÍSICO (píxeles reales, no lógicos)
// CRÍTICO: En F3, SDL_GetRenderViewport() devuelve coordenadas LÓGICAS,
// pero printAbsolute() trabaja en píxeles FÍSICOS. Usar helper para obtener
// viewport en coordenadas físicas.
SDL_Rect physical_viewport = getPhysicalViewport(renderer_);
// Centrar en el viewport físico (coordenadas relativas al viewport)
int x = (physical_viewport.w / 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)

View File

@@ -5,6 +5,7 @@
#include "../ball.h" // for Ball
#include "../defines.h" // for TEXT_DURATION, NOTIFICATION_DURATION, AppMode, SimulationMode
#include "../engine.h" // for Engine (info de sistema)
#include "../scene/scene_manager.h" // for SceneManager
#include "../shapes/shape.h" // for Shape
#include "../text/textrenderer.h" // for TextRenderer
@@ -12,6 +13,31 @@
#include "notifier.h" // for Notifier
#include "help_overlay.h" // for HelpOverlay
// ============================================================================
// HELPER: Obtener viewport en coordenadas físicas (no lógicas)
// ============================================================================
// SDL_GetRenderViewport() devuelve coordenadas LÓGICAS cuando hay presentación
// lógica activa. Para obtener coordenadas FÍSICAS, necesitamos deshabilitar
// temporalmente la presentación lógica.
static SDL_Rect getPhysicalViewport(SDL_Renderer* renderer) {
// Guardar estado actual de presentación lógica
int logical_w = 0, logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer, &logical_w, &logical_h, &presentation_mode);
// Deshabilitar presentación lógica temporalmente
SDL_SetRenderLogicalPresentation(renderer, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED);
// Obtener viewport en coordenadas físicas (píxeles reales)
SDL_Rect physical_viewport;
SDL_GetRenderViewport(renderer, &physical_viewport);
// Restaurar presentación lógica
SDL_SetRenderLogicalPresentation(renderer, logical_w, logical_h, presentation_mode);
return physical_viewport;
}
UIManager::UIManager()
: text_renderer_(nullptr)
, text_renderer_debug_(nullptr)
@@ -31,7 +57,8 @@ UIManager::UIManager()
, renderer_(nullptr)
, theme_manager_(nullptr)
, physical_window_width_(0)
, physical_window_height_(0) {
, physical_window_height_(0)
, current_font_size_(18) { // Tamaño por defecto (medium)
}
UIManager::~UIManager() {
@@ -50,16 +77,18 @@ void UIManager::initialize(SDL_Renderer* renderer, ThemeManager* theme_manager,
physical_window_width_ = physical_width;
physical_window_height_ = physical_height;
// Calcular tamaño de fuente apropiado según dimensiones físicas
current_font_size_ = calculateFontSize(physical_width, physical_height);
// Crear renderers de texto
text_renderer_ = new TextRenderer();
text_renderer_debug_ = new TextRenderer();
text_renderer_notifier_ = new TextRenderer();
// Inicializar renderers
// (el tamaño se configura dinámicamente en Engine según resolución)
text_renderer_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", 18, true);
text_renderer_debug_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", 18, true);
text_renderer_notifier_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", 18, true);
// Inicializar renderers con tamaño dinámico
text_renderer_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", current_font_size_, true);
text_renderer_debug_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", current_font_size_, true);
text_renderer_notifier_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", current_font_size_, true);
// Crear y configurar sistema de notificaciones
notifier_ = new Notifier();
@@ -68,7 +97,7 @@ void UIManager::initialize(SDL_Renderer* renderer, ThemeManager* theme_manager,
// Crear y configurar sistema de ayuda (overlay)
help_overlay_ = new HelpOverlay();
help_overlay_->initialize(renderer, theme_manager_, physical_width, physical_height);
help_overlay_->initialize(renderer, theme_manager_, physical_width, physical_height, current_font_size_);
// Inicializar FPS counter
fps_last_time_ = SDL_GetTicks();
@@ -96,6 +125,7 @@ void UIManager::update(Uint64 current_time, float delta_time) {
}
void UIManager::render(SDL_Renderer* renderer,
const Engine* engine,
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
@@ -115,7 +145,7 @@ void UIManager::render(SDL_Renderer* renderer,
// Renderizar debug HUD si está activo
if (show_debug_) {
renderDebugHUD(scene_manager, current_mode, current_app_mode,
renderDebugHUD(engine, scene_manager, current_mode, current_app_mode,
active_shape, shape_convergence);
}
@@ -152,10 +182,33 @@ void UIManager::updateVSyncText(bool enabled) {
void UIManager::updatePhysicalWindowSize(int width, int height) {
physical_window_width_ = width;
physical_window_height_ = height;
notifier_->updateWindowSize(width, height);
if (help_overlay_) {
help_overlay_->updatePhysicalWindowSize(width, height);
// Calcular nuevo tamaño de fuente apropiado
int new_font_size = calculateFontSize(width, height);
// Si el tamaño cambió, reinicializar todos los text renderers
if (new_font_size != current_font_size_) {
current_font_size_ = new_font_size;
// Reinicializar text renderers con nuevo tamaño
if (text_renderer_) {
text_renderer_->reinitialize(current_font_size_);
}
if (text_renderer_debug_) {
text_renderer_debug_->reinitialize(current_font_size_);
}
if (text_renderer_notifier_) {
text_renderer_notifier_->reinitialize(current_font_size_);
}
}
// Actualizar help overlay con font size actual Y nuevas dimensiones (atómicamente)
if (help_overlay_) {
help_overlay_->updateAll(current_font_size_, width, height);
}
// Actualizar otros componentes de UI con nuevas dimensiones
notifier_->updateWindowSize(width, height);
}
void UIManager::setTextObsolete(const std::string& text, int pos, int current_screen_width) {
@@ -167,7 +220,8 @@ void UIManager::setTextObsolete(const std::string& text, int pos, int current_sc
// === Métodos privados ===
void UIManager::renderDebugHUD(const SceneManager* scene_manager,
void UIManager::renderDebugHUD(const Engine* engine,
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
const Shape* active_shape,
@@ -175,92 +229,198 @@ void UIManager::renderDebugHUD(const SceneManager* scene_manager,
// Obtener altura de línea para espaciado dinámico
int line_height = text_renderer_debug_->getTextHeight();
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
// Obtener viewport FÍSICO (píxeles reales, no lógicos)
// CRÍTICO: En F3, SDL_GetRenderViewport() devuelve coordenadas LÓGICAS,
// pero printAbsolute() trabaja en píxeles FÍSICOS. Usar helper para obtener
// viewport en coordenadas físicas.
SDL_Rect physical_viewport = getPhysicalViewport(renderer_);
// ===========================
// COLUMNA LEFT (Sistema)
// ===========================
int left_y = margin;
// AppMode (antes estaba centrado, ahora va a la izquierda)
std::string appmode_text;
SDL_Color appmode_color = {255, 255, 255, 255}; // Blanco por defecto
if (current_app_mode == AppMode::LOGO) {
appmode_text = "AppMode: LOGO";
appmode_color = {255, 128, 0, 255}; // Naranja
} else if (current_app_mode == AppMode::DEMO) {
appmode_text = "AppMode: DEMO";
appmode_color = {255, 165, 0, 255}; // Naranja
} else if (current_app_mode == AppMode::DEMO_LITE) {
appmode_text = "AppMode: DEMO LITE";
appmode_color = {255, 200, 0, 255}; // Amarillo-naranja
} else {
appmode_text = "AppMode: SANDBOX";
appmode_color = {0, 255, 128, 255}; // Verde claro
}
text_renderer_debug_->printAbsolute(margin, left_y, appmode_text.c_str(), appmode_color);
left_y += line_height;
// SimulationMode
std::string simmode_text;
if (current_mode == SimulationMode::PHYSICS) {
simmode_text = "SimMode: PHYSICS";
} else if (current_mode == SimulationMode::SHAPE) {
if (active_shape) {
simmode_text = std::string("SimMode: SHAPE (") + active_shape->getName() + ")";
} else {
simmode_text = "SimMode: SHAPE";
}
} else if (current_mode == SimulationMode::BOIDS) {
simmode_text = "SimMode: BOIDS";
}
text_renderer_debug_->printAbsolute(margin, left_y, simmode_text.c_str(), {0, 255, 255, 255}); // Cian
left_y += line_height;
// Número de pelotas (escenario actual)
size_t ball_count = scene_manager->getBallCount();
std::string balls_text;
if (ball_count >= 1000) {
// Formatear con separador de miles (ejemplo: 5,000 o 50,000)
std::string count_str = std::to_string(ball_count);
std::string formatted;
int digits = count_str.length();
for (int i = 0; i < digits; i++) {
if (i > 0 && (digits - i) % 3 == 0) {
formatted += ',';
}
formatted += count_str[i];
}
balls_text = "Balls: " + formatted;
} else {
balls_text = "Balls: " + std::to_string(ball_count);
}
text_renderer_debug_->printAbsolute(margin, left_y, balls_text.c_str(), {128, 255, 128, 255}); // Verde claro
left_y += line_height;
// V-Sync
text_renderer_debug_->printAbsolute(margin, left_y, vsync_text_.c_str(), {0, 255, 255, 255}); // Cian
left_y += line_height;
// Modo de escalado (INTEGER/LETTERBOX/STRETCH o WINDOWED si no está en fullscreen)
std::string scaling_text;
if (engine->getFullscreenEnabled() || engine->getRealFullscreenEnabled()) {
ScalingMode scaling = engine->getCurrentScalingMode();
if (scaling == ScalingMode::INTEGER) {
scaling_text = "Scaling: INTEGER";
} else if (scaling == ScalingMode::LETTERBOX) {
scaling_text = "Scaling: LETTERBOX";
} else if (scaling == ScalingMode::STRETCH) {
scaling_text = "Scaling: STRETCH";
}
} else {
scaling_text = "Scaling: WINDOWED";
}
text_renderer_debug_->printAbsolute(margin, left_y, scaling_text.c_str(), {255, 255, 0, 255}); // Amarillo
left_y += line_height;
// Resolución física (píxeles reales de la ventana)
std::string phys_res_text = "Physical: " + std::to_string(physical_window_width_) + "x" + std::to_string(physical_window_height_);
text_renderer_debug_->printAbsolute(margin, left_y, phys_res_text.c_str(), {255, 128, 255, 255}); // Magenta claro
left_y += line_height;
// Resolución lógica (resolución interna del renderizador)
std::string logic_res_text = "Logical: " + std::to_string(engine->getCurrentScreenWidth()) + "x" + std::to_string(engine->getCurrentScreenHeight());
text_renderer_debug_->printAbsolute(margin, left_y, logic_res_text.c_str(), {255, 128, 255, 255}); // Magenta claro
left_y += line_height;
// Display refresh rate (obtener de SDL)
std::string refresh_text;
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays && num_displays > 0) {
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm) {
refresh_text = "Refresh: " + std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz";
} else {
refresh_text = "Refresh: N/A";
}
SDL_free(displays);
} else {
refresh_text = "Refresh: N/A";
}
text_renderer_debug_->printAbsolute(margin, left_y, refresh_text.c_str(), {255, 255, 128, 255}); // Amarillo claro
left_y += line_height;
// Tema actual (delegado a ThemeManager)
std::string theme_text = std::string("Theme: ") + theme_manager_->getCurrentThemeNameEN();
text_renderer_debug_->printAbsolute(margin, left_y, theme_text.c_str(), {128, 255, 255, 255}); // Cian claro
left_y += line_height;
// ===========================
// COLUMNA RIGHT (Primera pelota)
// ===========================
int right_y = margin;
// FPS counter (esquina superior derecha)
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
int fps_x = physical_viewport.w - fps_text_width - margin;
text_renderer_debug_->printAbsolute(fps_x, right_y, fps_text_.c_str(), {255, 255, 0, 255}); // Amarillo
right_y += line_height;
// Mostrar estado V-Sync en esquina superior izquierda
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)
// Info de la primera pelota (si existe)
const Ball* first_ball = scene_manager->getFirstBall();
if (first_ball != nullptr) {
// Línea 1: Gravedad
int grav_int = static_cast<int>(first_ball->getGravityForce());
std::string grav_text = "Gravedad: " + std::to_string(grav_int);
text_renderer_debug_->printAbsolute(margin, current_y, grav_text.c_str(), {255, 0, 255, 255}); // Magenta
current_y += line_height;
// Posición X, Y
SDL_FRect pos = first_ball->getPosition();
std::string pos_text = "Pos: (" + std::to_string(static_cast<int>(pos.x)) + ", " + std::to_string(static_cast<int>(pos.y)) + ")";
int pos_width = text_renderer_debug_->getTextWidthPhysical(pos_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - pos_width - margin, right_y, pos_text.c_str(), {255, 128, 128, 255}); // Rojo claro
right_y += line_height;
// Línea 2: Velocidad Y
// Velocidad X
int vx_int = static_cast<int>(first_ball->getVelocityX());
std::string vx_text = "VelX: " + std::to_string(vx_int);
int vx_width = text_renderer_debug_->getTextWidthPhysical(vx_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - vx_width - margin, right_y, vx_text.c_str(), {128, 255, 128, 255}); // Verde claro
right_y += line_height;
// Velocidad Y
int vy_int = static_cast<int>(first_ball->getVelocityY());
std::string vy_text = "Velocidad Y: " + std::to_string(vy_int);
text_renderer_debug_->printAbsolute(margin, current_y, vy_text.c_str(), {255, 0, 255, 255}); // Magenta
current_y += line_height;
std::string vy_text = "VelY: " + std::to_string(vy_int);
int vy_width = text_renderer_debug_->getTextWidthPhysical(vy_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - vy_width - margin, right_y, vy_text.c_str(), {128, 255, 128, 255}); // Verde claro
right_y += line_height;
// Línea 3: Estado superficie
std::string surface_text = first_ball->isOnSurface() ? "Superficie: Sí" : "Superficie: No";
text_renderer_debug_->printAbsolute(margin, current_y, surface_text.c_str(), {255, 0, 255, 255}); // Magenta
current_y += line_height;
// Fuerza de gravedad
int grav_int = static_cast<int>(first_ball->getGravityForce());
std::string grav_text = "Gravity: " + std::to_string(grav_int);
int grav_width = text_renderer_debug_->getTextWidthPhysical(grav_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - grav_width - margin, right_y, grav_text.c_str(), {255, 255, 128, 255}); // Amarillo claro
right_y += line_height;
// Línea 4: Coeficiente de rebote (loss)
// Estado superficie
std::string surface_text = first_ball->isOnSurface() ? "Surface: YES" : "Surface: NO";
int surface_width = text_renderer_debug_->getTextWidthPhysical(surface_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - surface_width - margin, right_y, surface_text.c_str(), {255, 200, 128, 255}); // Naranja claro
right_y += line_height;
// Coeficiente de rebote (loss)
float loss_val = first_ball->getLossCoefficient();
std::string loss_text = "Rebote: " + std::to_string(loss_val).substr(0, 4);
text_renderer_debug_->printAbsolute(margin, current_y, loss_text.c_str(), {255, 0, 255, 255}); // Magenta
current_y += line_height;
std::string loss_text = "Loss: " + std::to_string(loss_val).substr(0, 4);
int loss_width = text_renderer_debug_->getTextWidthPhysical(loss_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - loss_width - margin, right_y, loss_text.c_str(), {255, 128, 255, 255}); // Magenta
right_y += line_height;
// Línea 5: Dirección de gravedad
std::string gravity_dir_text = "Dirección: " + gravityDirectionToString(static_cast<int>(scene_manager->getCurrentGravity()));
text_renderer_debug_->printAbsolute(margin, current_y, gravity_dir_text.c_str(), {255, 255, 0, 255}); // Amarillo
current_y += line_height;
// Dirección de gravedad
std::string gravity_dir_text = "Dir: " + gravityDirectionToString(static_cast<int>(scene_manager->getCurrentGravity()));
int dir_width = text_renderer_debug_->getTextWidthPhysical(gravity_dir_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - dir_width - margin, right_y, gravity_dir_text.c_str(), {128, 255, 255, 255}); // Cian claro
right_y += line_height;
}
// Debug: Mostrar tema actual (delegado a ThemeManager)
std::string theme_text = std::string("Tema: ") + theme_manager_->getCurrentThemeNameEN();
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
std::string mode_text;
if (current_mode == SimulationMode::PHYSICS) {
mode_text = "Modo: Física";
} else if (active_shape) {
mode_text = std::string("Modo: ") + active_shape->getName();
} else {
mode_text = "Modo: Forma";
}
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)
// Convergencia en modo LOGO (solo cuando está activo) - Parte inferior derecha
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_->printAbsolute(margin, current_y, convergence_text.c_str(), {255, 128, 0, 255}); // Naranja
current_y += line_height;
}
// Debug: Mostrar modo DEMO/LOGO activo (siempre visible cuando debug está ON)
// FIJO en tercera fila (no se mueve con otros elementos del HUD)
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_->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_->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_->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
std::string convergence_text = "Convergence: " + std::to_string(convergence_percent) + "%";
int conv_width = text_renderer_debug_->getTextWidthPhysical(convergence_text.c_str());
text_renderer_debug_->printAbsolute(physical_viewport.w - conv_width - margin, right_y, convergence_text.c_str(), {255, 128, 0, 255}); // Naranja
right_y += line_height;
}
}
@@ -294,3 +454,21 @@ std::string UIManager::gravityDirectionToString(int direction) const {
default: return "Desconocida";
}
}
int UIManager::calculateFontSize(int physical_width, int physical_height) const {
// Calcular área física de la ventana
int area = physical_width * physical_height;
// Stepped scaling con 3 tamaños:
// - SMALL: < 800x600 (480,000 pixels) → 14px
// - MEDIUM: 800x600 a 1920x1080 (2,073,600 pixels) → 18px
// - LARGE: > 1920x1080 → 24px
if (area < 480000) {
return 14; // Ventanas pequeñas
} else if (area < 2073600) {
return 18; // Ventanas medianas (default)
} else {
return 24; // Ventanas grandes
}
}

View File

@@ -11,6 +11,7 @@ class ThemeManager;
class TextRenderer;
class Notifier;
class HelpOverlay;
class Engine;
enum class SimulationMode;
enum class AppMode;
@@ -59,6 +60,7 @@ class UIManager {
/**
* @brief Renderiza todos los elementos UI
* @param renderer Renderizador SDL3
* @param engine Puntero a Engine (para info de sistema)
* @param scene_manager SceneManager (para info de debug)
* @param current_mode Modo de simulación actual (PHYSICS/SHAPE)
* @param current_app_mode Modo de aplicación (SANDBOX/DEMO/LOGO)
@@ -69,6 +71,7 @@ class UIManager {
* @param current_screen_width Ancho lógico de pantalla (para texto centrado)
*/
void render(SDL_Renderer* renderer,
const Engine* engine,
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
@@ -136,13 +139,15 @@ class UIManager {
private:
/**
* @brief Renderiza HUD de debug (solo si show_debug_ == true)
* @param engine Puntero a Engine (para info de sistema)
* @param scene_manager SceneManager (para info de pelotas)
* @param current_mode Modo de simulación (PHYSICS/SHAPE)
* @param current_app_mode Modo de aplicación (SANDBOX/DEMO/LOGO)
* @param active_shape Figura 3D activa (puede ser nullptr)
* @param shape_convergence % de convergencia en LOGO mode
*/
void renderDebugHUD(const SceneManager* scene_manager,
void renderDebugHUD(const Engine* engine,
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
const Shape* active_shape,
@@ -161,6 +166,14 @@ class UIManager {
*/
std::string gravityDirectionToString(int direction) const;
/**
* @brief Calcula tamaño de fuente apropiado según dimensiones físicas
* @param physical_width Ancho físico de ventana
* @param physical_height Alto físico de ventana
* @return Tamaño de fuente (14px/18px/24px)
*/
int calculateFontSize(int physical_width, int physical_height) const;
// === Recursos de renderizado ===
TextRenderer* text_renderer_; // Texto obsoleto (DEPRECATED)
TextRenderer* text_renderer_debug_; // HUD de debug
@@ -189,4 +202,7 @@ class UIManager {
ThemeManager* theme_manager_; // Gestor de temas (para colores)
int physical_window_width_; // Ancho físico de ventana (píxeles reales)
int physical_window_height_; // Alto físico de ventana (píxeles reales)
// === Sistema de escalado dinámico de texto ===
int current_font_size_; // Tamaño de fuente actual (14/18/24)
};