diff --git a/source/defines.h b/source/defines.h index 66c450d..32a77c7 100644 --- a/source/defines.h +++ b/source/defines.h @@ -1,6 +1,7 @@ #pragma once #include // for Uint64 +#include // 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 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 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) @@ -56,13 +80,16 @@ enum class GravityDirection { // Enum para temas de colores (seleccionables con teclado numérico) 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 + 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 + DYNAMIC_1 = 7, // Tema dinámico 1: SUNRISE (amanecer) + DYNAMIC_2 = 8, // Tema dinámico 2: OCEAN WAVES (olas oceánicas) + DYNAMIC_3 = 9 // Tema dinámico 3: NEON PULSE (pulso neón) }; // Enum para tipo de figura 3D diff --git a/source/engine.cpp b/source/engine.cpp index 10ab169..456eba7 100644 --- a/source/engine.cpp +++ b/source/engine.cpp @@ -206,6 +206,7 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) { srand(static_cast(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(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(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(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(text_.length() * 8)) / 2; + show_text_ = true; + text_init_time_ = SDL_GetTicks(); + } } break; @@ -1060,8 +1083,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(current_theme_)]; ThemeColors& target = themes_[static_cast(target_theme_)]; @@ -1073,7 +1109,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(current_theme_)]; top_r = theme.bg_top_r; top_g = theme.bg_top_g; @@ -1386,9 +1422,133 @@ 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 + // Desactivar tema dinámico si estaba activo + if (dynamic_theme_active_) { + dynamic_theme_active_ = false; + current_dynamic_theme_index_ = -1; + } + target_theme_ = new_theme; transitioning_ = true; transition_progress_ = 0.0f; @@ -1403,25 +1563,129 @@ 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 actual y destino + const DynamicThemeKeyframe& current_kf = theme.keyframes[current_keyframe_index_]; + 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 + } + } + + // Actualizar colores de pelotas al keyframe actual + for (size_t i = 0; i < balls_.size(); i++) { + size_t color_index = i % current_kf.ball_colors.size(); + balls_[i]->setColor(current_kf.ball_colors[color_index]); + } + } +} + +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; + + // Actualizar current_theme_ al enum correspondiente + current_theme_ = static_cast(static_cast(ColorTheme::DYNAMIC_1) + index); + + // 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(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(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_]; + + Color current_color = balls_[ball_index]->getColor(); + size_t color_index = ball_index % target_kf.ball_colors.size(); + Color target_color = target_kf.ball_colors[color_index]; + + // Interpolar RGB entre keyframes + return { + static_cast(lerp(static_cast(current_color.r), static_cast(target_color.r), dynamic_transition_progress_)), + static_cast(lerp(static_cast(current_color.g), static_cast(target_color.g), dynamic_transition_progress_)), + static_cast(lerp(static_cast(current_color.b), static_cast(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(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(lerp(static_cast(current_color.r), static_cast(target_color.r), transition_progress_)), + static_cast(lerp(static_cast(current_color.g), static_cast(target_color.g), transition_progress_)), + static_cast(lerp(static_cast(current_color.b), static_cast(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(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(lerp(static_cast(current_color.r), static_cast(target_color.r), transition_progress_)), - static_cast(lerp(static_cast(current_color.g), static_cast(target_color.g), transition_progress_)), - static_cast(lerp(static_cast(current_color.b), static_cast(target_color.b), transition_progress_))}; } // ============================================================================ diff --git a/source/engine.h b/source/engine.h index dcf33df..ba117c0 100644 --- a/source/engine.h +++ b/source/engine.h @@ -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)