4 Commits

Author SHA1 Message Date
77a585092d Fix: Transición instantánea dinámico→estático (evita fondo negro)
PROBLEMA:
Al cambiar de tema dinámico (SUNRISE/OCEAN WAVES/NEON PULSE) a tema
estático (SUNSET/LAVENDER/etc), el fondo se volvía negro o mostraba
colores corruptos durante la transición LERP.

CAUSA:
1. activateDynamicTheme() NO actualiza current_theme_ (queda en valor previo)
2. startThemeTransition() desactiva dynamic_theme_active_
3. renderGradientBackground() intenta LERP desde themes_[current_theme_]
4. Si current_theme_ era LAVENDER (índice 6), accede themes_[6] OK
5. Pero si era cualquier valor >= 7, accede fuera de bounds → basura

SOLUCIÓN IMPLEMENTADA:
 Detectar transición desde tema dinámico en startThemeTransition()
 Si dynamic_theme_active_ == true:
   - Hacer transición INSTANTÁNEA (sin LERP)
   - Cambiar current_theme_ inmediatamente al tema destino
   - Actualizar colores de pelotas sin interpolación
   - Evitar acceso a themes_[] con índices inválidos

 Eliminar asignación de current_theme_ en activateDynamicTheme()
   - Cuando dynamic_theme_active_=true, se usa current_dynamic_theme_index_
   - current_theme_ solo se usa cuando dynamic_theme_active_=false

RESULTADO:
- Dinámico → Estático: Cambio instantáneo limpio 
- Estático → Estático: Transición LERP suave (sin cambios) 
- Estático → Dinámico: Cambio instantáneo (sin cambios) 
- Dinámico → Dinámico: Cambio instantáneo (sin cambios) 

TRADE-OFF:
- Perdemos transición suave dinámico→estático
- Ganamos estabilidad y eliminamos fondo negro/corrupto
- Para implementar LERP correcto se requiere refactor mayor
  (unificar todos los temas bajo sistema dinámico)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 20:42:54 +02:00
ebeec288ee Fix: Crash al acceder a themes[] con índices de temas dinámicos
PROBLEMA:
Crash (segfault) al activar temas dinámicos OCEAN WAVES (tecla 9) y
NEON PULSE (tecla 0). Funcionamiento correcto con SUNRISE (tecla 8).

CAUSA:
Múltiples lugares del código accedían a themes_[current_theme_] sin
verificar si current_theme_ era un tema dinámico (índices 7/8/9).

El array themes_[] solo tiene 7 elementos (índices 0-6):
- SUNSET, OCEAN, NEON, FOREST, RGB, MONOCHROME, LAVENDER

Los temas dinámicos están en dynamic_themes_[] (índices 0-2):
- DYNAMIC_1=7 (SUNRISE), DYNAMIC_2=8 (OCEAN WAVES), DYNAMIC_3=9 (NEON PULSE)

Acceder a themes_[7/8/9] causaba out-of-bounds → puntero inválido
→ crash en strlen(name_es).

PUNTOS DE FALLO IDENTIFICADOS:
1. render() línea ~738: Mostrar nombre del tema en pantalla
2. render() línea ~808: Debug display "THEME XXX"
3. initBalls() línea ~864: Seleccionar colores para pelotas nuevas

SOLUCIÓN:
 Añadir verificación dynamic_theme_active_ antes de acceder a arrays
 Si tema dinámico: usar dynamic_themes_[current_dynamic_theme_index_]
 Si tema estático: usar themes_[static_cast<int>(current_theme_)]

CORRECCIONES APLICADAS:
- render() (show_text_): Obtener color y nombre desde DynamicTheme
- render() (show_debug_): Obtener name_en desde DynamicTheme
- initBalls(): Seleccionar colores desde keyframe actual de DynamicTheme

RESULTADO:
-  SUNRISE (Numpad 8) funciona correctamente
-  OCEAN WAVES (Numpad 9) funciona correctamente (antes crasheaba)
-  NEON PULSE (Numpad 0) funciona correctamente (antes crasheaba)
-  Temas estáticos (1-7) siguen funcionando normalmente

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 20:31:31 +02:00
871bdf49ce Fix: Corregir interpolación LERP de temas dinámicos
PROBLEMA:
Las pelotas cambiaban bruscamente de color durante transiciones
de temas dinámicos en lugar de tener transiciones suaves.

CAUSAS IDENTIFICADAS:
1. getInterpolatedColor() interpolaba desde Ball::color_ (obsoleto)
   en lugar de usar el color del keyframe actual
2. updateDynamicTheme() actualizaba Ball::color_ incorrectamente
   al final de cada transición

SOLUCIÓN:
 getInterpolatedColor():
   - Ahora interpola desde current_kf.ball_colors[index]
   - Hasta target_kf.ball_colors[index]
   - Elimina dependencia de Ball::color_ almacenado

 updateDynamicTheme():
   - Elimina setColor() redundante al completar transición
   - getInterpolatedColor() ya calcula color correcto cada frame
   - Cuando progress=1.0, devuelve exactamente color destino

RESULTADO:
- Transiciones LERP suaves de 0% a 100% sin saltos bruscos
- Interpolación correcta entre keyframes actual y destino
- Coherencia entre renderizado y lógica de animación

ARCHIVOS MODIFICADOS:
- source/engine.cpp (2 funciones corregidas)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 20:15:46 +02:00
9a6cfdaaeb Implementar Temas Dinámicos (Keyframe Sequence System)
 NUEVAS CARACTERÍSTICAS:

**Sistema de Temas Dinámicos Animados:**
- 3 temas dinámicos predefinidos con transiciones automáticas
- Keyframe sequence: múltiples estados intermedios con duraciones configurables
- Interpolación LERP entre keyframes (fondo + colores de pelotas)
- Loop infinito automático

**Temas Implementados:**
1. **SUNRISE (Numpad 8)** - Amanecer: Noche → Alba → Día
   - 4 keyframes: Azul nocturno → Naranja-rosa alba → Amarillo brillante día → Loop
   - Duraciones: 0s → 4s → 3s → 5s (total: 12s por ciclo)

2. **OCEAN WAVES (Numpad 9)** - Olas Oceánicas: Profundidad ↔ Superficie
   - 3 keyframes: Azul marino oscuro ↔ Turquesa brillante
   - Duraciones: 0s → 4s → 4s (total: 8s por ciclo)

3. **NEON PULSE (Numpad 0)** - Pulso Neón: Apagado ↔ Encendido
   - 3 keyframes: Negro apagado ↔ Cian-magenta vibrante
   - Duraciones: 0s → 1.5s → 1.5s (total: 3s ping-pong rápido)

**Controles:**
- Numpad 8/9/0: Activar tema dinámico SUNRISE/OCEAN WAVES/NEON PULSE
- Shift+D: Pausar/reanudar animación de tema dinámico activo
- Temas estáticos (1-7) desactivan modo dinámico automáticamente

**Arquitectura:**
- defines.h: Estructuras DynamicThemeKeyframe + DynamicTheme
- engine.h: Estado dinámico (keyframes, progress, pausa)
- engine.cpp:
  - initializeDynamicThemes(): 3 temas predefinidos con keyframes
  - updateDynamicTheme(): Motor de animación (avance keyframes + loop)
  - activateDynamicTheme(): Iniciar tema dinámico
  - pauseDynamicTheme(): Toggle pausa con Shift+D
  - renderGradientBackground(): Soporte interpolación keyframes
  - getInterpolatedColor(): Soporte colores dinámicos

**Detalles Técnicos:**
- Transiciones suaves independientes del framerate (delta_time)
- Compatibilidad total con sistema LERP estático existente
- Desactivación mutua: tema estático desactiva dinámico (y viceversa)
- Velocidades variables por transición (1.5s - 5s configurables)
- Display automático de nombre de tema al activar

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 20:09:49 +02:00
3 changed files with 414 additions and 55 deletions

View File

@@ -1,6 +1,7 @@
#pragma once
#include <SDL3/SDL_stdinc.h> // for Uint64
#include <vector> // for std::vector in DynamicThemeKeyframe/DynamicTheme
// Configuración de ventana y pantalla
constexpr char WINDOW_CAPTION[] = "ViBe3 Physics (JailDesigner 2025)";
@@ -46,6 +47,29 @@ struct Color {
int r, g, b; // Componentes rojo, verde, azul (0-255)
};
// Estructura para keyframe de tema dinámico
struct DynamicThemeKeyframe {
// Fondo degradado
float bg_top_r, bg_top_g, bg_top_b;
float bg_bottom_r, bg_bottom_g, bg_bottom_b;
// Colores de pelotas en este keyframe
std::vector<Color> ball_colors;
// Duración de transición HACIA este keyframe (segundos)
// 0.0 = estado inicial (sin transición)
float duration;
};
// Estructura para tema dinámico (animado)
struct DynamicTheme {
const char* name_en; // Nombre en inglés
const char* name_es; // Nombre en español
int text_color_r, text_color_g, text_color_b; // Color del texto del tema
std::vector<DynamicThemeKeyframe> keyframes; // Mínimo 2 keyframes
bool loop; // ¿Volver al inicio al terminar?
};
// Enum para dirección de gravedad
enum class GravityDirection {
DOWN, // ↓ Gravedad hacia abajo (por defecto)
@@ -55,14 +79,18 @@ enum class GravityDirection {
};
// Enum para temas de colores (seleccionables con teclado numérico)
// Todos los temas usan ahora sistema dinámico de keyframes
enum class ColorTheme {
SUNSET = 0, // Naranjas, rojos, amarillos, rosas
OCEAN = 1, // Azules, turquesas, blancos
NEON = 2, // Cian, magenta, verde lima, amarillo vibrante
FOREST = 3, // Verdes, marrones, amarillos otoño
RGB = 4, // RGB puros y subdivisiones matemáticas (fondo blanco)
MONOCHROME = 5, // Fondo negro degradado, sprites blancos monocromáticos
LAVENDER = 6 // Degradado violeta-azul, pelotas amarillo dorado
SUNSET = 0, // Naranjas, rojos, amarillos, rosas (estático: 1 keyframe)
OCEAN = 1, // Azules, turquesas, blancos (estático: 1 keyframe)
NEON = 2, // Cian, magenta, verde lima, amarillo vibrante (estático: 1 keyframe)
FOREST = 3, // Verdes, marrones, amarillos otoño (estático: 1 keyframe)
RGB = 4, // RGB puros y subdivisiones matemáticas - fondo blanco (estático: 1 keyframe)
MONOCHROME = 5, // Fondo negro degradado, sprites blancos monocromáticos (estático: 1 keyframe)
LAVENDER = 6, // Degradado violeta-azul, pelotas amarillo dorado (estático: 1 keyframe)
SUNRISE = 7, // Amanecer: Noche → Alba → Día (animado: 4 keyframes, 12s ciclo)
OCEAN_WAVES = 8, // Olas oceánicas: Azul oscuro ↔ Turquesa (animado: 3 keyframes, 8s ciclo)
NEON_PULSE = 9 // Pulso neón: Negro ↔ Neón vibrante (animado: 3 keyframes, 3s ping-pong)
};
// Enum para tipo de figura 3D

View File

@@ -206,6 +206,7 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
srand(static_cast<unsigned>(time(nullptr)));
dbg_init(renderer_);
initializeThemes();
initializeDynamicThemes();
initBalls(scenario_);
}
@@ -307,6 +308,9 @@ void Engine::update() {
}
}
}
// Actualizar tema dinámico (animación de keyframes)
updateDynamicTheme();
}
void Engine::handleEvents() {
@@ -470,6 +474,19 @@ void Engine::handleEvents() {
startThemeTransition(ColorTheme::LAVENDER);
break;
// Temas dinámicos (animados) - Solo Numpad 8/9/0 (teclas normales usadas para escenarios)
case SDLK_KP_8:
activateDynamicTheme(0); // SUNRISE
break;
case SDLK_KP_9:
activateDynamicTheme(1); // OCEAN WAVES
break;
case SDLK_KP_0:
activateDynamicTheme(2); // NEON PULSE
break;
// Cambio de sprite/textura dinámico
case SDLK_N:
switchTexture();
@@ -590,23 +607,29 @@ void Engine::handleEvents() {
toggleIntegerScaling();
break;
// Toggle Modo DEMO COMPLETO (auto-play)
// Toggle Modo DEMO COMPLETO (auto-play) o Pausar tema dinámico (Shift+D)
case SDLK_D:
if (current_app_mode_ == AppMode::DEMO) {
// Desactivar DEMO → MANUAL
setState(AppMode::MANUAL);
text_ = "DEMO MODE OFF";
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
// Shift+D = Pausar tema dinámico
if (event.key.mod & SDL_KMOD_SHIFT) {
pauseDynamicTheme();
} else {
// Activar DEMO (desde cualquier otro modo)
setState(AppMode::DEMO);
randomizeOnDemoStart(false);
text_ = "DEMO MODE ON";
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
// D sin Shift = Toggle modo DEMO
if (current_app_mode_ == AppMode::DEMO) {
// Desactivar DEMO → MANUAL
setState(AppMode::MANUAL);
text_ = "DEMO MODE OFF";
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
} else {
// Activar DEMO (desde cualquier otro modo)
setState(AppMode::DEMO);
randomizeOnDemoStart(false);
text_ = "DEMO MODE ON";
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
}
break;
@@ -710,22 +733,38 @@ void Engine::render() {
}
if (show_text_) {
// Obtener tema actual
int theme_idx = static_cast<int>(current_theme_);
const ThemeColors& current = themes_[theme_idx];
// Obtener datos del tema actual (estático o dinámico)
int text_color_r, text_color_g, text_color_b;
const char* theme_name_es = nullptr;
if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
// Tema dinámico activo
const DynamicTheme& dyn_theme = dynamic_themes_[current_dynamic_theme_index_];
text_color_r = dyn_theme.text_color_r;
text_color_g = dyn_theme.text_color_g;
text_color_b = dyn_theme.text_color_b;
theme_name_es = dyn_theme.name_es;
} else {
// Tema estático
int theme_idx = static_cast<int>(current_theme_);
const ThemeColors& current = themes_[theme_idx];
text_color_r = current.text_color_r;
text_color_g = current.text_color_g;
text_color_b = current.text_color_b;
theme_name_es = current.name_es;
}
// Texto del número de pelotas con color del tema
dbg_print(text_pos_, 8, text_.c_str(), current.text_color_r, current.text_color_g, current.text_color_b);
dbg_print(text_pos_, 8, text_.c_str(), text_color_r, text_color_g, text_color_b);
// Mostrar nombre del tema en castellano debajo del número de pelotas
// (solo si text_ NO es ya el nombre del tema actual o destino, para evitar duplicación durante LERP)
const ThemeColors& target = themes_[static_cast<int>(target_theme_)];
if (text_ != current.name_es && text_ != target.name_es) {
int theme_text_width = static_cast<int>(strlen(current.name_es) * 8); // 8 píxeles por carácter
int theme_x = (current_screen_width_ - theme_text_width) / 2; // Centrar horizontalmente
// (solo si text_ NO es ya el nombre del tema, para evitar duplicación)
if (theme_name_es != nullptr && text_ != theme_name_es) {
int theme_text_width = static_cast<int>(strlen(theme_name_es) * 8); // 8 píxeles por carácter
int theme_x = (current_screen_width_ - theme_text_width) / 2; // Centrar horizontalmente
// Texto del nombre del tema con el mismo color
dbg_print(theme_x, 24, current.name_es, current.text_color_r, current.text_color_g, current.text_color_b);
dbg_print(theme_x, 24, theme_name_es, text_color_r, text_color_g, text_color_b);
}
}
@@ -766,7 +805,12 @@ void Engine::render() {
}
// Debug: Mostrar tema actual
std::string theme_text = std::string("THEME ") + themes_[static_cast<int>(current_theme_)].name_en;
std::string theme_text;
if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
theme_text = std::string("THEME ") + dynamic_themes_[current_dynamic_theme_index_].name_en;
} else {
theme_text = std::string("THEME ") + themes_[static_cast<int>(current_theme_)].name_en;
}
dbg_print(8, 64, theme_text.c_str(), 255, 255, 128); // Amarillo claro para tema
// Debug: Mostrar modo de simulación actual
@@ -821,10 +865,21 @@ void Engine::initBalls(int value) {
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
// Seleccionar color de la paleta del tema actual
ThemeColors& theme = themes_[static_cast<int>(current_theme_)];
int color_index = rand() % theme.ball_colors.size(); // Cantidad variable de colores por tema
const Color COLOR = theme.ball_colors[color_index];
// Seleccionar color de la paleta del tema actual (estático o dinámico)
Color COLOR;
if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
// Tema dinámico: usar colores del keyframe actual
const DynamicTheme& dyn_theme = dynamic_themes_[current_dynamic_theme_index_];
const DynamicThemeKeyframe& current_kf = dyn_theme.keyframes[current_keyframe_index_];
int color_index = rand() % current_kf.ball_colors.size();
COLOR = current_kf.ball_colors[color_index];
} else {
// Tema estático
ThemeColors& theme = themes_[static_cast<int>(current_theme_)];
int color_index = rand() % theme.ball_colors.size();
COLOR = theme.ball_colors[color_index];
}
// Generar factor de masa aleatorio (0.7 = ligera, 1.3 = pesada)
float mass_factor = GRAVITY_MASS_MIN + (rand() % 1000) / 1000.0f * (GRAVITY_MASS_MAX - GRAVITY_MASS_MIN);
balls_.emplace_back(std::make_unique<Ball>(X, VX, VY, COLOR, texture_, current_screen_width_, current_screen_height_, current_ball_size_, current_gravity_, mass_factor));
@@ -1060,8 +1115,21 @@ void Engine::renderGradientBackground() {
// Obtener colores (con LERP si estamos en transición)
float top_r, top_g, top_b, bottom_r, bottom_g, bottom_b;
if (transitioning_) {
// Interpolar entre tema actual y tema destino
if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
// Tema dinámico activo: interpolar entre keyframes
DynamicTheme& theme = dynamic_themes_[current_dynamic_theme_index_];
const DynamicThemeKeyframe& current_kf = theme.keyframes[current_keyframe_index_];
const DynamicThemeKeyframe& target_kf = theme.keyframes[target_keyframe_index_];
top_r = lerp(current_kf.bg_top_r, target_kf.bg_top_r, dynamic_transition_progress_);
top_g = lerp(current_kf.bg_top_g, target_kf.bg_top_g, dynamic_transition_progress_);
top_b = lerp(current_kf.bg_top_b, target_kf.bg_top_b, dynamic_transition_progress_);
bottom_r = lerp(current_kf.bg_bottom_r, target_kf.bg_bottom_r, dynamic_transition_progress_);
bottom_g = lerp(current_kf.bg_bottom_g, target_kf.bg_bottom_g, dynamic_transition_progress_);
bottom_b = lerp(current_kf.bg_bottom_b, target_kf.bg_bottom_b, dynamic_transition_progress_);
} else if (transitioning_) {
// Transición estática: interpolar entre tema actual y tema destino
ThemeColors& current = themes_[static_cast<int>(current_theme_)];
ThemeColors& target = themes_[static_cast<int>(target_theme_)];
@@ -1073,7 +1141,7 @@ void Engine::renderGradientBackground() {
bottom_g = lerp(current.bg_bottom_g, target.bg_bottom_g, transition_progress_);
bottom_b = lerp(current.bg_bottom_b, target.bg_bottom_b, transition_progress_);
} else {
// Sin transición: usar tema actual directamente
// Sin transición: usar tema estático actual directamente
ThemeColors& theme = themes_[static_cast<int>(current_theme_)];
top_r = theme.bg_top_r;
top_g = theme.bg_top_g;
@@ -1386,9 +1454,155 @@ void Engine::initializeThemes() {
{255, 215, 0}}};
}
void Engine::initializeDynamicThemes() {
// ========================================================================
// DYNAMIC_1: "SUNRISE" (Amanecer) - Noche → Alba → Día → Loop
// ========================================================================
dynamic_themes_[0] = {
"SUNRISE",
"AMANECER",
255, 200, 100, // Color texto: amarillo cálido
{
// Keyframe 0: Noche oscura (estado inicial)
{
20.0f / 255.0f, 25.0f / 255.0f, 60.0f / 255.0f, // Fondo superior: azul medianoche
10.0f / 255.0f, 10.0f / 255.0f, 30.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{100, 100, 150}, {120, 120, 170}, {90, 90, 140}, {110, 110, 160},
{95, 95, 145}, {105, 105, 155}, {100, 100, 150}, {115, 115, 165}}, // Pelotas azules tenues
0.0f // Sin transición (estado inicial)
},
// Keyframe 1: Alba naranja-rosa
{
180.0f / 255.0f, 100.0f / 255.0f, 120.0f / 255.0f, // Fondo superior: naranja-rosa
255.0f / 255.0f, 140.0f / 255.0f, 100.0f / 255.0f, // Fondo inferior: naranja cálido
{{255, 180, 100}, {255, 160, 80}, {255, 200, 120}, {255, 150, 90},
{255, 190, 110}, {255, 170, 95}, {255, 185, 105}, {255, 165, 88}}, // Pelotas naranjas
4.0f // 4 segundos para llegar aquí
},
// Keyframe 2: Día brillante amarillo
{
255.0f / 255.0f, 240.0f / 255.0f, 180.0f / 255.0f, // Fondo superior: amarillo claro
255.0f / 255.0f, 255.0f / 255.0f, 220.0f / 255.0f, // Fondo inferior: amarillo muy claro
{{255, 255, 200}, {255, 255, 180}, {255, 255, 220}, {255, 255, 190},
{255, 255, 210}, {255, 255, 185}, {255, 255, 205}, {255, 255, 195}}, // Pelotas amarillas brillantes
3.0f // 3 segundos para llegar aquí
},
// Keyframe 3: Vuelta a noche (para loop)
{
20.0f / 255.0f, 25.0f / 255.0f, 60.0f / 255.0f, // Fondo superior: azul medianoche
10.0f / 255.0f, 10.0f / 255.0f, 30.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{100, 100, 150}, {120, 120, 170}, {90, 90, 140}, {110, 110, 160},
{95, 95, 145}, {105, 105, 155}, {100, 100, 150}, {115, 115, 165}}, // Pelotas azules tenues
5.0f // 5 segundos para volver a noche
}
},
true // Loop = true
};
// ========================================================================
// DYNAMIC_2: "OCEAN WAVES" (Olas Oceánicas) - Azul oscuro ↔ Turquesa
// ========================================================================
dynamic_themes_[1] = {
"OCEAN WAVES",
"OLAS OCEANICAS",
100, 220, 255, // Color texto: cian claro
{
// Keyframe 0: Profundidad oceánica (azul oscuro)
{
20.0f / 255.0f, 50.0f / 255.0f, 100.0f / 255.0f, // Fondo superior: azul marino
10.0f / 255.0f, 30.0f / 255.0f, 60.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{60, 100, 180}, {50, 90, 170}, {70, 110, 190}, {55, 95, 175},
{65, 105, 185}, {58, 98, 172}, {62, 102, 182}, {52, 92, 168}}, // Pelotas azul oscuro
0.0f // Estado inicial
},
// Keyframe 1: Aguas poco profundas (turquesa brillante)
{
100.0f / 255.0f, 200.0f / 255.0f, 230.0f / 255.0f, // Fondo superior: turquesa claro
50.0f / 255.0f, 150.0f / 255.0f, 200.0f / 255.0f, // Fondo inferior: turquesa medio
{{100, 220, 255}, {90, 210, 245}, {110, 230, 255}, {95, 215, 250},
{105, 225, 255}, {98, 218, 248}, {102, 222, 252}, {92, 212, 242}}, // Pelotas turquesa brillante
4.0f // 4 segundos para llegar
},
// Keyframe 2: Vuelta a profundidad (para loop)
{
20.0f / 255.0f, 50.0f / 255.0f, 100.0f / 255.0f, // Fondo superior: azul marino
10.0f / 255.0f, 30.0f / 255.0f, 60.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{60, 100, 180}, {50, 90, 170}, {70, 110, 190}, {55, 95, 175},
{65, 105, 185}, {58, 98, 172}, {62, 102, 182}, {52, 92, 168}}, // Pelotas azul oscuro
4.0f // 4 segundos para volver
}
},
true // Loop = true
};
// ========================================================================
// DYNAMIC_3: "NEON PULSE" (Pulso Neón) - Negro → Neón brillante (rápido)
// ========================================================================
dynamic_themes_[2] = {
"NEON PULSE",
"PULSO NEON",
255, 60, 255, // Color texto: magenta brillante
{
// Keyframe 0: Apagado (negro)
{
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo superior: negro
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior: negro
{{40, 40, 40}, {50, 50, 50}, {45, 45, 45}, {48, 48, 48},
{42, 42, 42}, {47, 47, 47}, {44, 44, 44}, {46, 46, 46}}, // Pelotas grises muy oscuras
0.0f // Estado inicial
},
// Keyframe 1: Encendido (neón cian-magenta)
{
20.0f / 255.0f, 20.0f / 255.0f, 40.0f / 255.0f, // Fondo superior: azul oscuro
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior: negro
{{0, 255, 255}, {255, 0, 255}, {0, 255, 200}, {255, 50, 255},
{50, 255, 255}, {255, 0, 200}, {0, 255, 230}, {255, 80, 255}}, // Pelotas neón vibrante
1.5f // 1.5 segundos para encender (rápido)
},
// Keyframe 2: Vuelta a apagado (para loop ping-pong)
{
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo superior: negro
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior: negro
{{40, 40, 40}, {50, 50, 50}, {45, 45, 45}, {48, 48, 48},
{42, 42, 42}, {47, 47, 47}, {44, 44, 44}, {46, 46, 46}}, // Pelotas grises muy oscuras
1.5f // 1.5 segundos para apagar
}
},
true // Loop = true
};
}
void Engine::startThemeTransition(ColorTheme new_theme) {
if (new_theme == current_theme_) return; // Ya estamos en ese tema
// Si venimos de tema dinámico, hacer transición instantánea (sin LERP)
// porque current_theme_ apunta a DYNAMIC_X (índice >= 7) y no existe en themes_[]
if (dynamic_theme_active_) {
dynamic_theme_active_ = false;
current_dynamic_theme_index_ = -1;
// Cambio instantáneo: sin transición LERP
current_theme_ = new_theme;
transitioning_ = false;
// Actualizar colores de pelotas al tema final
const ThemeColors& theme = themes_[static_cast<int>(new_theme)];
for (size_t i = 0; i < balls_.size(); i++) {
size_t color_index = i % theme.ball_colors.size();
balls_[i]->setColor(theme.ball_colors[color_index]);
}
// Mostrar nombre del tema
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme.name_es;
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
return;
}
// Transición normal entre temas estáticos
target_theme_ = new_theme;
transitioning_ = true;
transition_progress_ = 0.0f;
@@ -1403,25 +1617,127 @@ void Engine::startThemeTransition(ColorTheme new_theme) {
}
}
void Engine::updateDynamicTheme() {
if (!dynamic_theme_active_ || current_dynamic_theme_index_ < 0) return;
if (dynamic_theme_paused_) return; // Pausado con Shift+D
DynamicTheme& theme = dynamic_themes_[current_dynamic_theme_index_];
// Obtener keyframe destino para calcular duración
const DynamicThemeKeyframe& target_kf = theme.keyframes[target_keyframe_index_];
// Avanzar progreso de transición
dynamic_transition_progress_ += delta_time_ / target_kf.duration;
if (dynamic_transition_progress_ >= 1.0f) {
// Transición completa: avanzar al siguiente keyframe
dynamic_transition_progress_ = 0.0f;
current_keyframe_index_ = target_keyframe_index_;
// Calcular siguiente keyframe destino
target_keyframe_index_++;
if (target_keyframe_index_ >= theme.keyframes.size()) {
if (theme.loop) {
target_keyframe_index_ = 0; // Volver al inicio
} else {
target_keyframe_index_ = theme.keyframes.size() - 1; // Quedarse en el último
dynamic_theme_active_ = false; // Detener animación
}
}
// NOTA: No se actualiza Ball::color_ aquí porque getInterpolatedColor()
// calcula el color directamente desde los keyframes cada frame.
// Cuando progress=1.0, getInterpolatedColor() devuelve exactamente el color destino.
}
}
void Engine::activateDynamicTheme(int index) {
if (index < 0 || index >= 3) return; // Validar índice
// Desactivar transición estática si estaba activa
if (transitioning_) {
transitioning_ = false;
current_theme_ = target_theme_;
}
// Activar tema dinámico
dynamic_theme_active_ = true;
current_dynamic_theme_index_ = index;
current_keyframe_index_ = 0;
target_keyframe_index_ = 1;
dynamic_transition_progress_ = 0.0f;
dynamic_theme_paused_ = false;
// NOTA: No actualizamos current_theme_ porque cuando dynamic_theme_active_=true,
// todo el código usa current_dynamic_theme_index_ en su lugar
// Establecer colores iniciales del keyframe 0
DynamicTheme& theme = dynamic_themes_[index];
const DynamicThemeKeyframe& initial_kf = theme.keyframes[0];
for (size_t i = 0; i < balls_.size(); i++) {
size_t color_index = i % initial_kf.ball_colors.size();
balls_[i]->setColor(initial_kf.ball_colors[color_index]);
}
// Mostrar nombre del tema (solo si NO estamos en modo demo)
if (current_app_mode_ == AppMode::MANUAL) {
text_ = theme.name_es;
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
}
void Engine::pauseDynamicTheme() {
if (!dynamic_theme_active_) return; // Solo funciona si hay tema dinámico activo
dynamic_theme_paused_ = !dynamic_theme_paused_;
// Mostrar estado de pausa (solo si NO estamos en modo demo)
if (current_app_mode_ == AppMode::MANUAL) {
text_ = dynamic_theme_paused_ ? "PAUSADO" : "REANUDADO";
text_pos_ = (current_screen_width_ - static_cast<int>(text_.length() * 8)) / 2;
show_text_ = true;
text_init_time_ = SDL_GetTicks();
}
}
Color Engine::getInterpolatedColor(size_t ball_index) const {
if (!transitioning_) {
if (dynamic_theme_active_ && current_dynamic_theme_index_ >= 0) {
// Tema dinámico activo: interpolar entre keyframes
const DynamicTheme& theme = dynamic_themes_[current_dynamic_theme_index_];
const DynamicThemeKeyframe& current_kf = theme.keyframes[current_keyframe_index_];
const DynamicThemeKeyframe& target_kf = theme.keyframes[target_keyframe_index_];
// Obtener colores desde keyframes (NO desde Ball::color_)
size_t color_index = ball_index % current_kf.ball_colors.size();
Color current_color = current_kf.ball_colors[color_index]; // Color del keyframe actual
Color target_color = target_kf.ball_colors[color_index]; // Color del keyframe destino
// Interpolar RGB entre keyframes
return {
static_cast<int>(lerp(static_cast<float>(current_color.r), static_cast<float>(target_color.r), dynamic_transition_progress_)),
static_cast<int>(lerp(static_cast<float>(current_color.g), static_cast<float>(target_color.g), dynamic_transition_progress_)),
static_cast<int>(lerp(static_cast<float>(current_color.b), static_cast<float>(target_color.b), dynamic_transition_progress_))};
} else if (transitioning_) {
// Transición estática: interpolar entre tema actual y tema destino
Color current_color = balls_[ball_index]->getColor();
// Obtener el color destino (mismo índice de color en el tema destino)
const ThemeColors& target_theme = themes_[static_cast<int>(target_theme_)];
size_t color_index = ball_index % target_theme.ball_colors.size();
Color target_color = target_theme.ball_colors[color_index];
// Interpolar RGB
return {
static_cast<int>(lerp(static_cast<float>(current_color.r), static_cast<float>(target_color.r), transition_progress_)),
static_cast<int>(lerp(static_cast<float>(current_color.g), static_cast<float>(target_color.g), transition_progress_)),
static_cast<int>(lerp(static_cast<float>(current_color.b), static_cast<float>(target_color.b), transition_progress_))};
} else {
// Sin transición: devolver color actual
return balls_[ball_index]->getColor();
}
// En transición: interpolar entre color actual y color destino
Color current_color = balls_[ball_index]->getColor();
// Obtener el color destino (mismo índice de color en el tema destino)
const ThemeColors& target_theme = themes_[static_cast<int>(target_theme_)];
size_t color_index = ball_index % target_theme.ball_colors.size();
Color target_color = target_theme.ball_colors[color_index];
// Interpolar RGB
return {
static_cast<Uint8>(lerp(static_cast<float>(current_color.r), static_cast<float>(target_color.r), transition_progress_)),
static_cast<Uint8>(lerp(static_cast<float>(current_color.g), static_cast<float>(target_color.g), transition_progress_)),
static_cast<Uint8>(lerp(static_cast<float>(current_color.b), static_cast<float>(target_color.b), transition_progress_))};
}
// ============================================================================

View File

@@ -99,6 +99,15 @@ class Engine {
// Temas de colores definidos
ThemeColors themes_[7]; // 7 temas: SUNSET, OCEAN, NEON, FOREST, RGB, MONOCHROME, LAVENDER
// Sistema de Temas Dinámicos (animados)
DynamicTheme dynamic_themes_[3]; // 3 temas dinámicos predefinidos
bool dynamic_theme_active_ = false; // ¿Tema dinámico activo?
int current_dynamic_theme_index_ = -1; // Índice del tema dinámico actual (-1 = ninguno)
size_t current_keyframe_index_ = 0; // Keyframe actual
size_t target_keyframe_index_ = 1; // Próximo keyframe
float dynamic_transition_progress_ = 0.0f; // Progreso 0.0-1.0 hacia próximo keyframe
bool dynamic_theme_paused_ = false; // Pausa manual con Shift+D
// Sistema de Figuras 3D (polimórfico)
SimulationMode current_mode_ = SimulationMode::PHYSICS;
ShapeType current_shape_type_ = ShapeType::SPHERE; // Tipo de figura actual
@@ -150,6 +159,12 @@ class Engine {
std::string gravityDirectionToString(GravityDirection direction) const;
void initializeThemes();
// Sistema de Temas Dinámicos
void initializeDynamicThemes(); // Inicializar 3 temas dinámicos predefinidos
void updateDynamicTheme(); // Actualizar animación de tema dinámico (llamado cada frame)
void activateDynamicTheme(int index); // Activar tema dinámico (0-2)
void pauseDynamicTheme(); // Toggle pausa de animación (Shift+D)
// Sistema de gestión de estados (MANUAL/DEMO/DEMO_LITE/LOGO)
void setState(AppMode new_mode); // Cambiar modo de aplicación (mutuamente excluyente)