diff --git a/source/ball.h b/source/ball.h index d9c23c3..522b53e 100644 --- a/source/ball.h +++ b/source/ball.h @@ -73,9 +73,10 @@ class Ball { bool isOnSurface() const { return on_surface_; } bool isStopped() const { return stopped_; } - // Getters para batch rendering + // Getters/Setters para batch rendering SDL_FRect getPosition() const { return pos_; } Color getColor() const { return color_; } + void setColor(const Color& color) { color_ = color; } // Funciones para modo RotoBall void setRotoBallPosition3D(float x, float y, float z); diff --git a/source/engine.cpp b/source/engine.cpp index 246f2d7..3128d0b 100644 --- a/source/engine.cpp +++ b/source/engine.cpp @@ -11,6 +11,7 @@ #include // for std::min, std::max, std::sort #include // for sqrtf, acosf, cosf, sinf (funciones matemáticas) #include // for rand, srand +#include // for strlen #include // for time #include // for cout #include // for string @@ -165,6 +166,25 @@ void Engine::update() { if (show_text_) { show_text_ = !(SDL_GetTicks() - text_init_time_ > TEXT_DURATION); } + + // Actualizar transición de tema (LERP) + if (transitioning_) { + transition_progress_ += delta_time_ / transition_duration_; + + if (transition_progress_ >= 1.0f) { + // Transición completa + transition_progress_ = 1.0f; + current_theme_ = target_theme_; + transitioning_ = false; + + // Actualizar colores de las pelotas al tema final + const ThemeColors& theme = themes_[static_cast(current_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]); + } + } + } } void Engine::handleEvents() { @@ -285,40 +305,36 @@ void Engine::handleEvents() { // Ciclar temas de color (movido de T a B) case SDLK_B: - // Ciclar al siguiente tema - current_theme_ = static_cast((static_cast(current_theme_) + 1) % (sizeof(themes_) / sizeof(themes_[0]))); - initBalls(scenario_); // Regenerar bolas con nueva paleta + // Ciclar al siguiente tema con transición suave (LERP) + { + ColorTheme next_theme = static_cast((static_cast(current_theme_) + 1) % (sizeof(themes_) / sizeof(themes_[0]))); + startThemeTransition(next_theme); + } break; - // Temas de colores con teclado numérico + // Temas de colores con teclado numérico (con transición suave) case SDLK_KP_1: - current_theme_ = ColorTheme::SUNSET; - initBalls(scenario_); + startThemeTransition(ColorTheme::SUNSET); break; case SDLK_KP_2: - current_theme_ = ColorTheme::OCEAN; - initBalls(scenario_); + startThemeTransition(ColorTheme::OCEAN); break; case SDLK_KP_3: - current_theme_ = ColorTheme::NEON; - initBalls(scenario_); + startThemeTransition(ColorTheme::NEON); break; case SDLK_KP_4: - current_theme_ = ColorTheme::FOREST; - initBalls(scenario_); + startThemeTransition(ColorTheme::FOREST); break; case SDLK_KP_5: - current_theme_ = ColorTheme::RGB; - initBalls(scenario_); + startThemeTransition(ColorTheme::RGB); break; case SDLK_KP_6: - current_theme_ = ColorTheme::MONOCHROME; - initBalls(scenario_); + startThemeTransition(ColorTheme::MONOCHROME); break; // Control de escala de figura (solo en modo SHAPE) @@ -463,7 +479,7 @@ void Engine::render() { // Renderizar en orden de profundidad (fondo → frente) for (size_t idx : render_order) { SDL_FRect pos = balls_[idx]->getPosition(); - Color color = balls_[idx]->getColor(); + Color color = getInterpolatedColor(idx); // Usar color interpolado (LERP) float brightness = balls_[idx]->getDepthBrightness(); float depth_scale = balls_[idx]->getDepthScale(); @@ -480,10 +496,12 @@ void Engine::render() { } } else { // MODO PHYSICS: Renderizar en orden normal del vector (sin escala de profundidad) + size_t idx = 0; for (auto &ball : balls_) { SDL_FRect pos = ball->getPosition(); - Color color = ball->getColor(); + Color color = getInterpolatedColor(idx); // Usar color interpolado (LERP) addSpriteToBatch(pos.x, pos.y, pos.w, pos.h, color.r, color.g, color.b, 1.0f); + idx++; } } @@ -493,27 +511,19 @@ void Engine::render() { } if (show_text_) { - // Colores acordes a cada tema (para texto del número de pelotas y nombre del tema) - int theme_colors[][3] = { - {255, 140, 60}, // ATARDECER: Naranja cálido - {80, 200, 255}, // OCEANO: Azul océano - {255, 60, 255}, // NEON: Magenta brillante - {100, 255, 100}, // BOSQUE: Verde natural - {100, 100, 100} // RGB: Gris oscuro (para contraste con fondo blanco) - }; + // Obtener tema actual int theme_idx = static_cast(current_theme_); + const ThemeColors& current = themes_[theme_idx]; // Texto del número de pelotas con color del tema - dbg_print(text_pos_, 8, text_.c_str(), theme_colors[theme_idx][0], theme_colors[theme_idx][1], theme_colors[theme_idx][2]); + dbg_print(text_pos_, 8, text_.c_str(), current.text_color_r, current.text_color_g, current.text_color_b); // Mostrar nombre del tema en castellano debajo del número de pelotas - std::string theme_names_es[] = {"ATARDECER", "OCEANO", "NEON", "BOSQUE", "RGB"}; - std::string theme_name = theme_names_es[static_cast(current_theme_)]; - int theme_text_width = static_cast(theme_name.length() * 8); // 8 píxeles por carácter - int theme_x = (current_screen_width_ - theme_text_width) / 2; // Centrar horizontalmente + int theme_text_width = static_cast(strlen(current.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, theme_name.c_str(), theme_colors[theme_idx][0], theme_colors[theme_idx][1], theme_colors[theme_idx][2]); + dbg_print(theme_x, 24, current.name_es, current.text_color_r, current.text_color_g, current.text_color_b); } // Debug display (solo si está activado con tecla H) @@ -553,8 +563,7 @@ void Engine::render() { } // Debug: Mostrar tema actual - std::string theme_names[] = {"SUNSET", "OCEAN", "NEON", "FOREST", "RGB"}; - std::string theme_text = "THEME " + theme_names[static_cast(current_theme_)]; + std::string theme_text = std::string("THEME ") + themes_[static_cast(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 @@ -759,16 +768,32 @@ void Engine::renderGradientBackground() { // Crear quad de pantalla completa con degradado SDL_Vertex bg_vertices[4]; - // Obtener colores del tema actual - ThemeColors &theme = themes_[static_cast(current_theme_)]; + // Obtener colores (con LERP si estamos en transición) + float top_r, top_g, top_b, bottom_r, bottom_g, bottom_b; - float top_r = theme.bg_top_r; - float top_g = theme.bg_top_g; - float top_b = theme.bg_top_b; + if (transitioning_) { + // Interpolar entre tema actual y tema destino + ThemeColors ¤t = themes_[static_cast(current_theme_)]; + ThemeColors &target = themes_[static_cast(target_theme_)]; - float bottom_r = theme.bg_bottom_r; - float bottom_g = theme.bg_bottom_g; - float bottom_b = theme.bg_bottom_b; + top_r = lerp(current.bg_top_r, target.bg_top_r, transition_progress_); + top_g = lerp(current.bg_top_g, target.bg_top_g, transition_progress_); + top_b = lerp(current.bg_top_b, target.bg_top_b, transition_progress_); + + bottom_r = lerp(current.bg_bottom_r, target.bg_bottom_r, transition_progress_); + 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 + ThemeColors &theme = themes_[static_cast(current_theme_)]; + top_r = theme.bg_top_r; + top_g = theme.bg_top_g; + top_b = theme.bg_top_b; + + bottom_r = theme.bg_bottom_r; + bottom_g = theme.bg_bottom_g; + bottom_b = theme.bg_bottom_b; + } // Vértice superior izquierdo bg_vertices[0].position = {0, 0}; @@ -927,6 +952,8 @@ void Engine::zoomOut() { void Engine::initializeThemes() { // SUNSET: Naranjas, rojos, amarillos, rosas (8 colores) themes_[0] = { + "SUNSET", "ATARDECER", // Nombres (inglés, español) + 255, 140, 60, // Color texto: naranja cálido 180.0f / 255.0f, 140.0f / 255.0f, 100.0f / 255.0f, // Fondo superior (naranja suave) 40.0f / 255.0f, 20.0f / 255.0f, 60.0f / 255.0f, // Fondo inferior (púrpura oscuro) {{255, 140, 0}, {255, 69, 0}, {255, 215, 0}, {255, 20, 147}, {255, 99, 71}, {255, 165, 0}, {255, 192, 203}, {220, 20, 60}} @@ -934,6 +961,8 @@ void Engine::initializeThemes() { // OCEAN: Azules, turquesas, blancos (8 colores) themes_[1] = { + "OCEAN", "OCEANO", // Nombres (inglés, español) + 80, 200, 255, // Color texto: azul océano 100.0f / 255.0f, 150.0f / 255.0f, 200.0f / 255.0f, // Fondo superior (azul cielo) 20.0f / 255.0f, 40.0f / 255.0f, 80.0f / 255.0f, // Fondo inferior (azul marino) {{0, 191, 255}, {0, 255, 255}, {32, 178, 170}, {176, 224, 230}, {70, 130, 180}, {0, 206, 209}, {240, 248, 255}, {64, 224, 208}} @@ -941,6 +970,8 @@ void Engine::initializeThemes() { // NEON: Cian, magenta, verde lima, amarillo vibrante (8 colores) themes_[2] = { + "NEON", "NEON", // Nombres (inglés, español) + 255, 60, 255, // Color texto: magenta brillante 20.0f / 255.0f, 20.0f / 255.0f, 40.0f / 255.0f, // Fondo superior (negro azulado) 0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior (negro) {{0, 255, 255}, {255, 0, 255}, {50, 205, 50}, {255, 255, 0}, {255, 20, 147}, {0, 255, 127}, {138, 43, 226}, {255, 69, 0}} @@ -948,6 +979,8 @@ void Engine::initializeThemes() { // FOREST: Verdes, marrones, amarillos otoño (8 colores) themes_[3] = { + "FOREST", "BOSQUE", // Nombres (inglés, español) + 100, 255, 100, // Color texto: verde natural 144.0f / 255.0f, 238.0f / 255.0f, 144.0f / 255.0f, // Fondo superior (verde claro) 101.0f / 255.0f, 67.0f / 255.0f, 33.0f / 255.0f, // Fondo inferior (marrón tierra) {{34, 139, 34}, {107, 142, 35}, {154, 205, 50}, {255, 215, 0}, {210, 180, 140}, {160, 82, 45}, {218, 165, 32}, {50, 205, 50}} @@ -955,6 +988,8 @@ void Engine::initializeThemes() { // RGB: Círculo cromático con 24 puntos (cada 15°) - Ultra precisión matemática themes_[4] = { + "RGB", "RGB", // Nombres (inglés, español) + 100, 100, 100, // Color texto: gris oscuro (contraste con fondo blanco) 1.0f, 1.0f, 1.0f, // Fondo superior (blanco puro) 1.0f, 1.0f, 1.0f, // Fondo inferior (blanco puro) - sin degradado { @@ -987,21 +1022,53 @@ void Engine::initializeThemes() { // MONOCHROME: Fondo negro degradado, sprites blancos monocromáticos (8 tonos grises) themes_[5] = { + "MONOCHROME", "MONOCROMO", // Nombres (inglés, español) + 200, 200, 200, // Color texto: gris claro 20.0f / 255.0f, 20.0f / 255.0f, 20.0f / 255.0f, // Fondo superior (gris muy oscuro) - 0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior (negro puro) + 0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior (negro) { - {255, 255, 255}, // Blanco puro - {230, 230, 230}, // Gris muy claro - {200, 200, 200}, // Gris claro - {170, 170, 170}, // Gris medio-claro - {140, 140, 140}, // Gris medio - {110, 110, 110}, // Gris medio-oscuro - {80, 80, 80}, // Gris oscuro - {50, 50, 50} // Gris muy oscuro + {255, 255, 255}, // Blanco puro - todas las pelotas del mismo color + {255, 255, 255}, + {255, 255, 255}, + {255, 255, 255}, + {255, 255, 255}, + {255, 255, 255}, + {255, 255, 255}, + {255, 255, 255} } }; } +void Engine::startThemeTransition(ColorTheme new_theme) { + if (new_theme == current_theme_) return; // Ya estamos en ese tema + + target_theme_ = new_theme; + transitioning_ = true; + transition_progress_ = 0.0f; +} + +Color Engine::getInterpolatedColor(size_t ball_index) const { + if (!transitioning_) { + // 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_)) + }; +} + void Engine::checkAutoRestart() { // Verificar si TODAS las pelotas están paradas bool all_stopped = true; diff --git a/source/engine.h b/source/engine.h index e3f504a..5a0beb9 100644 --- a/source/engine.h +++ b/source/engine.h @@ -70,9 +70,16 @@ private: // Sistema de temas ColorTheme current_theme_ = ColorTheme::SUNSET; + ColorTheme target_theme_ = ColorTheme::SUNSET; // Tema destino para transición + bool transitioning_ = false; // ¿Estamos en transición? + float transition_progress_ = 0.0f; // Progreso de 0.0 a 1.0 + float transition_duration_ = 0.5f; // Duración en segundos // Estructura de tema de colores struct ThemeColors { + const char* name_en; // Nombre en inglés (para debug) + const char* name_es; // Nombre en español (para display) + int text_color_r, text_color_g, text_color_b; // Color del texto del tema float bg_top_r, bg_top_g, bg_top_b; float bg_bottom_r, bg_bottom_g, bg_bottom_b; std::vector ball_colors; @@ -116,6 +123,11 @@ private: void checkAutoRestart(); void performRandomRestart(); + // Sistema de transiciones LERP + float lerp(float a, float b, float t) const { return a + (b - a) * t; } + Color getInterpolatedColor(size_t ball_index) const; // Obtener color interpolado durante transición + void startThemeTransition(ColorTheme new_theme); + // Sistema de zoom dinámico int calculateMaxWindowZoom() const; void setWindowZoom(int new_zoom);