#include "scoreboard.h" #include // Para SDL_DestroyTexture, SDL_SetRenderDrawColor, SDL_SetRenderTarget, SDL_CreateTexture, SDL_GetRenderTarget, SDL_GetTicks, SDL_RenderClear, SDL_RenderLine, SDL_RenderTexture, SDL_SetTextureBlendMode, SDL_FRect, SDL_BLENDMODE_BLEND, SDL_PixelFormat, SDL_Texture, SDL_TextureAccess #include // Para max #include // Para roundf #include // Para operator<<, setfill, setw #include // Para basic_ostream, basic_ostringstream, basic_ostream::operator<<, ostringstream #include "color.h" #include "enter_name.h" // Para NAME_SIZE #include "lang.h" // Para getText #include "param.h" // Para Param, ParamScoreboard, param #include "resource.h" // Para Resource #include "screen.h" // Para Screen #include "sprite.h" // Para Sprite #include "text.h" // Para Text, Text::CENTER, Text::COLOR #include "texture.h" // Para Texture #include "utils.h" // Para easeOutCubic // .at(SINGLETON) Hay que definir las variables estáticas, desde el .h sólo la hemos declarado Scoreboard* Scoreboard::instance = nullptr; // .at(SINGLETON) Crearemos el objeto score_board con esta función estática void Scoreboard::init() { Scoreboard::instance = new Scoreboard(); } // .at(SINGLETON) Destruiremos el objeto score_board con esta función estática void Scoreboard::destroy() { delete Scoreboard::instance; } // .at(SINGLETON) Con este método obtenemos el objeto score_board y podemos trabajar con él auto Scoreboard::get() -> Scoreboard* { return Scoreboard::instance; } // Constructor Scoreboard::Scoreboard() : renderer_(Screen::get()->getRenderer()), game_power_meter_texture_(Resource::get()->getTexture("game_power_meter.png")), power_meter_sprite_(std::make_unique(game_power_meter_texture_)), text_(Resource::get()->getText("8bithud")) { // Inicializa variables for (size_t i = 0; i < static_cast(Id::SIZE); ++i) { name_.at(i).clear(); enter_name_.at(i).clear(); selector_pos_.at(i) = 0; score_.at(i) = 0; mult_.at(i) = 0; continue_counter_.at(i) = 0; carousel_prev_index_.at(i) = -1; // Inicializar a -1 para detectar primera inicialización enter_name_ref_.at(i) = nullptr; text_slide_offset_.at(i) = 0.0f; } panel_.at(static_cast(Id::LEFT)).mode = Mode::SCORE; panel_.at(static_cast(Id::RIGHT)).mode = Mode::SCORE; panel_.at(static_cast(Id::CENTER)).mode = Mode::STAGE_INFO; // Recalcula las anclas de los elementos recalculateAnchors(); power_meter_sprite_->setPosition(SDL_FRect{ static_cast(slot4_2_.x - 20), slot4_2_.y, 40, 7}); // Crea la textura de fondo background_ = nullptr; createBackgroundTexture(); // Crea las texturas de los paneles createPanelTextures(); // Rellena la textura de fondo fillBackgroundTexture(); // Inicializa el ciclo de colores para el nombre name_color_cycle_ = Colors::generateMirroredCycle(color_.INVERSE(), ColorCycleStyle::VIBRANT); animated_color_ = name_color_cycle_.at(0); } Scoreboard::~Scoreboard() { if (background_ != nullptr) { SDL_DestroyTexture(background_); } for (auto* texture : panel_texture_) { if (texture != nullptr) { SDL_DestroyTexture(texture); } } } // Configura la animación del carrusel void Scoreboard::setCarouselAnimation(Id id, int selected_index, EnterName* enter_name_ptr) { size_t idx = static_cast(id); // Guardar referencia a EnterName enter_name_ref_.at(idx) = enter_name_ptr; if (!enter_name_ptr || selected_index < 0) { return; } // Primera inicialización: posicionar directamente sin animar if (carousel_prev_index_.at(idx) == -1) { carousel_position_.at(idx) = static_cast(selected_index); carousel_target_.at(idx) = static_cast(selected_index); carousel_prev_index_.at(idx) = selected_index; } else { // Detectar cambio en el índice del carácter seleccionado int prev_index = carousel_prev_index_.at(idx); if (selected_index != prev_index) { // Calcular dirección del movimiento int direction = selected_index - prev_index; // Obtener tamaño de la lista para manejar wrap-around const int LIST_SIZE = enter_name_ptr->getCharacterList().size(); // Manejar wrap-around circular if (direction > LIST_SIZE / 2) { direction = -(LIST_SIZE - direction); // Wrap backward (ej: Z → A) } else if (direction < -LIST_SIZE / 2) { direction = LIST_SIZE + direction; // Wrap forward (ej: A → Z) } // Normalizar a -1 o +1 direction = (direction > 0) ? 1 : ((direction < 0) ? -1 : 0); if (direction != 0) { // Actualizar target con movimiento relativo carousel_target_.at(idx) = carousel_position_.at(idx) + static_cast(direction); // Guardar nuevo índice carousel_prev_index_.at(idx) = selected_index; } } } } // Transforma un valor numérico en una cadena de 7 cifras auto Scoreboard::updateScoreText(int num) -> std::string { std::ostringstream oss; oss << std::setw(7) << std::setfill('0') << num; return oss.str(); } // Actualiza el contador void Scoreboard::updateTimeCounter() { constexpr int TICKS_SPEED = 100; if (SDL_GetTicks() - ticks_ > TICKS_SPEED) { ticks_ = SDL_GetTicks(); ++time_counter_; } } // Actualiza el índice del color animado del nombre void Scoreboard::updateNameColorIndex() { constexpr Uint64 COLOR_UPDATE_INTERVAL = 100; // 100ms entre cambios de color if (SDL_GetTicks() - name_color_last_update_ >= COLOR_UPDATE_INTERVAL) { ++name_color_index_; name_color_last_update_ = SDL_GetTicks(); } // Precalcular el color actual del ciclo animated_color_ = name_color_cycle_.at(name_color_index_ % name_color_cycle_.size()); } // Actualiza la animación del carrusel void Scoreboard::updateCarouselAnimation(float deltaTime) { constexpr float CAROUSEL_SPEED = 8.0f; // Posiciones por segundo for (size_t i = 0; i < carousel_position_.size(); ++i) { // Solo animar si no hemos llegado al target if (std::abs(carousel_position_.at(i) - carousel_target_.at(i)) > 0.01f) { // Determinar dirección float direction = (carousel_target_.at(i) > carousel_position_.at(i)) ? 1.0f : -1.0f; // Calcular movimiento float movement = CAROUSEL_SPEED * deltaTime * direction; // Mover, pero no sobrepasar el target float new_position = carousel_position_.at(i) + movement; // Clamp para no sobrepasar if (direction > 0) { carousel_position_.at(i) = std::min(new_position, carousel_target_.at(i)); } else { carousel_position_.at(i) = std::max(new_position, carousel_target_.at(i)); } } else { // Forzar al target exacto cuando estamos muy cerca carousel_position_.at(i) = carousel_target_.at(i); } } } // Actualiza la animación de deslizamiento de texto (transición ENTER_NAME -> SHOW_NAME) void Scoreboard::updateTextSlideAnimation(float deltaTime) { for (size_t i = 0; i < static_cast(Id::SIZE); ++i) { Mode current_mode = panel_.at(i).mode; if (current_mode == Mode::ENTER_TO_SHOW_NAME) { // Incrementar progreso de animación (0.0 a 1.0) text_slide_offset_.at(i) += deltaTime / TEXT_SLIDE_DURATION; // Terminar animación y cambiar a SHOW_NAME cuando se complete if (text_slide_offset_.at(i) >= 1.0f) { text_slide_offset_.at(i) = 1.0f; panel_.at(i).mode = Mode::SHOW_NAME; } } else if (current_mode != Mode::ENTER_NAME) { // Resetear offset si no estamos en ENTER_NAME ni en transición text_slide_offset_.at(i) = 0.0f; } } } // Actualiza la lógica del marcador void Scoreboard::update(float deltaTime) { updateTimeCounter(); updateNameColorIndex(); updateCarouselAnimation(deltaTime); updateTextSlideAnimation(deltaTime); fillBackgroundTexture(); // Renderizar DESPUÉS de actualizar } // Pinta el marcador void Scoreboard::render() { SDL_RenderTexture(renderer_, background_, nullptr, &rect_); } // Establece el valor de la variable void Scoreboard::setColor(Color color) { // Actualiza las variables de colores color_ = color; text_color1_ = param.scoreboard.text_autocolor ? color_.LIGHTEN(100) : param.scoreboard.text_color1; text_color2_ = param.scoreboard.text_autocolor ? color_.LIGHTEN(150) : param.scoreboard.text_color2; // Aplica los colores power_meter_sprite_->getTexture()->setColor(text_color2_); fillBackgroundTexture(); name_color_cycle_ = Colors::generateMirroredCycle(color_.INVERSE(), ColorCycleStyle::VIBRANT); } // Establece el valor de la variable void Scoreboard::setPos(SDL_FRect rect) { rect_ = rect; recalculateAnchors(); // Recalcula las anclas de los elementos createBackgroundTexture(); // Crea la textura de fondo createPanelTextures(); // Crea las texturas de los paneles fillBackgroundTexture(); // Rellena la textura de fondo } // Rellena los diferentes paneles del marcador void Scoreboard::fillPanelTextures() { // Guarda a donde apunta actualmente el renderizador auto* temp = SDL_GetRenderTarget(renderer_); // Genera el contenido de cada panel_ for (size_t i = 0; i < static_cast(Id::SIZE); ++i) { // Cambia el destino del renderizador SDL_SetRenderTarget(renderer_, panel_texture_.at(i)); // Dibuja el fondo de la textura SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0); SDL_RenderClear(renderer_); renderPanelContent(i); } // Deja el renderizador apuntando donde estaba SDL_SetRenderTarget(renderer_, temp); } void Scoreboard::renderPanelContent(size_t panel_index) { switch (panel_.at(panel_index).mode) { case Mode::SCORE: renderScoreMode(panel_index); break; case Mode::DEMO: renderDemoMode(); break; case Mode::WAITING: renderWaitingMode(); break; case Mode::GAME_OVER: renderGameOverMode(); break; case Mode::STAGE_INFO: renderStageInfoMode(); break; case Mode::CONTINUE: renderContinueMode(panel_index); break; case Mode::ENTER_NAME: renderEnterNameMode(panel_index); break; case Mode::ENTER_TO_SHOW_NAME: renderEnterToShowNameMode(panel_index); break; case Mode::SHOW_NAME: renderShowNameMode(panel_index); break; case Mode::GAME_COMPLETED: renderGameCompletedMode(panel_index); break; default: break; } } void Scoreboard::renderScoreMode(size_t panel_index) { // SCORE text_->writeDX(Text::COLOR | Text::CENTER, slot4_1_.x, slot4_1_.y, name_.at(panel_index), 1, text_color1_); text_->writeDX(Text::COLOR | Text::CENTER, slot4_2_.x, slot4_2_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_); // MULT text_->writeDX(Text::COLOR | Text::CENTER, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 3"), 1, text_color1_); text_->writeDX(Text::COLOR | Text::CENTER, slot4_4_.x, slot4_4_.y, "x" + std::to_string(mult_.at(panel_index)).substr(0, 3), 1, text_color2_); } void Scoreboard::renderDemoMode() { // DEMO MODE text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + 4, Lang::getText("[SCOREBOARD] 6"), 1, text_color1_); // PRESS START TO PLAY if (time_counter_ % 10 < 8) { text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - 2, Lang::getText("[SCOREBOARD] 8"), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - 2, Lang::getText("[SCOREBOARD] 9"), 1, text_color1_); } } void Scoreboard::renderWaitingMode() { // GAME OVER text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + 4, Lang::getText("[SCOREBOARD] 7"), 1, text_color1_); // PRESS START TO PLAY if (time_counter_ % 10 < 8) { text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - 2, Lang::getText("[SCOREBOARD] 8"), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - 2, Lang::getText("[SCOREBOARD] 9"), 1, text_color1_); } } void Scoreboard::renderGameOverMode() { // GAME OVER text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + 4, Lang::getText("[SCOREBOARD] 7"), 1, text_color1_); // PLEASE WAIT if (time_counter_ % 10 < 8) { text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - 2, Lang::getText("[SCOREBOARD] 12"), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - 2, Lang::getText("[SCOREBOARD] 13"), 1, text_color1_); } } void Scoreboard::renderStageInfoMode() { // STAGE text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, Lang::getText("[SCOREBOARD] 5") + " " + std::to_string(stage_), 1, text_color1_); // POWERMETER power_meter_sprite_->setSpriteClip(0, 0, 40, 7); power_meter_sprite_->render(); power_meter_sprite_->setSpriteClip(40, 0, int(power_ * 40.0F), 7); power_meter_sprite_->render(); // HI-SCORE text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 4"), 1, text_color1_); const std::string NAME = hi_score_name_.empty() ? "" : hi_score_name_ + " - "; text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y, NAME + updateScoreText(hi_score_), 1, text_color2_); } void Scoreboard::renderContinueMode(size_t panel_index) { // SCORE text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, name_.at(panel_index), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_); // CONTINUE text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 10"), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y, std::to_string(continue_counter_.at(panel_index)), 1, text_color2_); } void Scoreboard::renderEnterNameMode(size_t panel_index) { /* // SCORE text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, name_.at(panel_index), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_); // ENTER NAME text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 11"), 1, text_color1_); renderNameInputField(panel_index); */ // SCORE text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_); // ENTER NAME text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y, Lang::getText("[SCOREBOARD] 11"), 1, text_color1_); // NAME text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, enter_name_.at(panel_index), 1, text_color2_); // CARRUSEL renderCarousel(panel_index, slot4_4_.x, slot4_4_.y); } void Scoreboard::renderEnterToShowNameMode(size_t panel_index) { // Calcular progreso suavizado de la animación (0.0 a 1.0) float progress = text_slide_offset_.at(panel_index); progress = std::min(progress, 1.0f); // Clampear a 1.0 const float t = static_cast(easeOutCubic(progress)); const float ROW_SIZE = rect_.h / 4.0f; const float y_offset = t * ROW_SIZE; // Desplazamiento en píxeles // ========== Texto que ENTRA desde arriba ========== // name_ (entra desde arriba hacia ROW1) text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + y_offset - ROW_SIZE, name_.at(panel_index), 1, text_color1_); // ========== Textos que SE MUEVEN (renderizar UNA sola vez) ========== // SCORE (se mueve de ROW1 a ROW2) text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + y_offset, updateScoreText(score_.at(panel_index)), 1, text_color2_); // "ENTER NAME" (se mueve de ROW2 a ROW3) text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y + y_offset, Lang::getText("[SCOREBOARD] 11"), 1, text_color1_); // enter_name_ (se mueve de ROW3 a ROW4) text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y + y_offset, enter_name_.at(panel_index), 1, text_color2_); // ========== Elemento que SALE hacia abajo ========== // CARRUSEL (sale desde ROW4 hacia abajo, fuera de pantalla) renderCarousel(panel_index, slot4_4_.x, static_cast(slot4_4_.y + y_offset)); } void Scoreboard::renderShowNameMode(size_t panel_index) { // NOMBRE DEL JUGADOR text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y, name_.at(panel_index), 1, text_color1_); // SCORE text_->writeDX(Text::CENTER | Text::COLOR, slot4_2_.x, slot4_2_.y, updateScoreText(score_.at(panel_index)), 1, text_color2_); // "ENTER NAME" text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y, Lang::getText("[SCOREBOARD] 11"), 1, text_color1_); // NOMBRE INTRODUCIDO (con color animado) text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y, enter_name_.at(panel_index), 1, animated_color_); } void Scoreboard::renderGameCompletedMode(size_t panel_index) { // GAME OVER text_->writeDX(Text::CENTER | Text::COLOR, slot4_1_.x, slot4_1_.y + 4, Lang::getText("[SCOREBOARD] 7"), 1, text_color1_); // SCORE if (time_counter_ % 10 < 8) { text_->writeDX(Text::CENTER | Text::COLOR, slot4_3_.x, slot4_3_.y - 2, Lang::getText("[SCOREBOARD] 14"), 1, text_color1_); text_->writeDX(Text::CENTER | Text::COLOR, slot4_4_.x, slot4_4_.y - 2, updateScoreText(score_.at(panel_index)), 1, text_color2_); } } // Rellena la textura de fondo void Scoreboard::fillBackgroundTexture() { // Rellena los diferentes paneles del marcador fillPanelTextures(); // Cambia el destino del renderizador SDL_Texture* temp = SDL_GetRenderTarget(renderer_); SDL_SetRenderTarget(renderer_, background_); // Dibuja el fondo del marcador SDL_SetRenderDrawColor(renderer_, color_.r, color_.g, color_.b, 255); SDL_RenderClear(renderer_); // Copia las texturas de los paneles for (int i = 0; i < static_cast(Id::SIZE); ++i) { SDL_RenderTexture(renderer_, panel_texture_.at(i), nullptr, &panel_.at(i).pos); } // Dibuja la linea que separa la zona de juego del marcador renderSeparator(); // Deja el renderizador apuntando donde estaba SDL_SetRenderTarget(renderer_, temp); } // Recalcula las anclas de los elementos void Scoreboard::recalculateAnchors() { // Recalcula la posición y el tamaño de los paneles const float PANEL_WIDTH = rect_.w / (float)static_cast(Id::SIZE); for (int i = 0; i < static_cast(Id::SIZE); ++i) { panel_.at(i).pos.x = roundf(PANEL_WIDTH * i); panel_.at(i).pos.y = 0; panel_.at(i).pos.w = roundf(PANEL_WIDTH * (i + 1)) - panel_.at(i).pos.x; panel_.at(i).pos.h = rect_.h; } // Constantes para definir las zonas del panel_: 4 filas y 1 columna const int ROW_SIZE = rect_.h / 4; const int TEXT_HEIGHT = 7; // Filas const float ROW1 = 1 + (ROW_SIZE * 0) + (TEXT_HEIGHT / 2); const float ROW2 = 1 + (ROW_SIZE * 1) + (TEXT_HEIGHT / 2) - 1; const float ROW3 = 1 + (ROW_SIZE * 2) + (TEXT_HEIGHT / 2) - 2; const float ROW4 = 1 + (ROW_SIZE * 3) + (TEXT_HEIGHT / 2) - 3; // Columna const float COL = PANEL_WIDTH / 2; // Slots de 4 slot4_1_ = {.x = COL, .y = ROW1}; slot4_2_ = {.x = COL, .y = ROW2}; slot4_3_ = {.x = COL, .y = ROW3}; slot4_4_ = {.x = COL, .y = ROW4}; // Primer cuadrado para poner el nombre de record const int ENTER_NAME_LENGTH = text_->length(std::string(EnterName::MAX_NAME_SIZE, 'A')); enter_name_pos_.x = COL - (ENTER_NAME_LENGTH / 2); enter_name_pos_.y = ROW4; // Recoloca los sprites if (power_meter_sprite_) { power_meter_sprite_->setX(slot4_2_.x - 20); power_meter_sprite_->setY(slot4_2_.y); } } // Crea la textura de fondo void Scoreboard::createBackgroundTexture() { // Elimina la textura en caso de existir if (background_ != nullptr) { SDL_DestroyTexture(background_); } // Recrea la textura de fondo background_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, rect_.w, rect_.h); SDL_SetTextureBlendMode(background_, SDL_BLENDMODE_BLEND); } // Crea las texturas de los paneles void Scoreboard::createPanelTextures() { // Elimina las texturas en caso de existir for (auto* texture : panel_texture_) { if (texture != nullptr) { SDL_DestroyTexture(texture); } } panel_texture_.clear(); // Crea las texturas para cada panel_ for (auto& i : panel_) { SDL_Texture* tex = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, i.pos.w, i.pos.h); SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND); panel_texture_.push_back(tex); } } // Dibuja la linea que separa la zona de juego del marcador void Scoreboard::renderSeparator() { // Dibuja la linea que separa el marcador de la zona de juego auto color = param.scoreboard.separator_autocolor ? color_.DARKEN() : param.scoreboard.separator_color; SDL_SetRenderDrawColor(renderer_, color.r, color.g, color.b, 255); SDL_RenderLine(renderer_, 0, 0, rect_.w, 0); } // Pinta el carrusel de caracteres con efecto de color LERP y animación suave void Scoreboard::renderCarousel(size_t panel_index, int center_x, int y) { // Obtener referencia a EnterName EnterName* enter_name = enter_name_ref_.at(panel_index); if (!enter_name) { return; } // Obtener la lista completa de caracteres const std::string& char_list = enter_name->getCharacterList(); if (char_list.empty()) { return; } // Espacio extra entre letras constexpr int EXTRA_SPACING = 2; // Carrusel extendido: usar constante de clase constexpr int HALF_VISIBLE = CAROUSEL_VISIBLE_LETTERS / 2; // 4 // Posición flotante actual del carrusel (índice en character_list_) const float carousel_pos = carousel_position_.at(panel_index); // Calcular ancho promedio de una letra (asumimos ancho uniforme) std::string sample_char(1, char_list[0]); const int AVG_CHAR_WIDTH = text_->length(sample_char, 1); const int CHAR_STEP = AVG_CHAR_WIDTH + EXTRA_SPACING; // Calcular offset de píxeles basado en la parte fraccionaria de carousel_pos const float fractional_offset = carousel_pos - std::floor(carousel_pos); const int pixel_offset = static_cast(fractional_offset * CHAR_STEP); // Índice base en character_list_ (centro del carrusel) const int base_index = static_cast(std::floor(carousel_pos)); const int char_list_size = static_cast(char_list.size()); // Calcular posición X inicial (centrar el conjunto de 9 letras) int start_x = center_x - (HALF_VISIBLE * CHAR_STEP) - (AVG_CHAR_WIDTH / 2) - pixel_offset; // Renderizar las 9 letras visibles for (int i = -HALF_VISIBLE; i <= HALF_VISIBLE; ++i) { // Índice real en character_list_ (con wrap-around circular) int char_index = base_index + i; // Wrap-around circular char_index = char_index % char_list_size; if (char_index < 0) { char_index += char_list_size; } // Obtener el carácter directamente de character_list_ std::string single_char(1, char_list[char_index]); // Calcular distancia flotante al centro visual basada en posición real del carácter float distance_from_center = std::abs(static_cast(char_index) - carousel_pos); // Manejar wrap-around circular: elegir el camino más corto if (distance_from_center > static_cast(char_list_size) / 2.0f) { distance_from_center = static_cast(char_list_size) - distance_from_center; } // Calcular color con LERP dinámico continuo Color letter_color; if (distance_from_center < 0.5f) { // Letra cerca del centro: LERP hacia animated_color_ // distance_from_center va de 0.0 (centro exacto) a 0.5 (borde) float lerp_to_animated = distance_from_center / 0.5f; // 0.0 a 1.0 letter_color = animated_color_.LERP(text_color1_, lerp_to_animated); } else { // Letras alejadas: LERP hacia color_ (fade out) float base_lerp = (distance_from_center - 0.5f) / (HALF_VISIBLE - 0.5f); base_lerp = std::min(base_lerp, 1.0f); const float LERP_FACTOR = base_lerp * 0.85f; letter_color = text_color1_.LERP(color_, LERP_FACTOR); } // Calcular posición X de esta letra const int letter_x = start_x + (i + HALF_VISIBLE) * CHAR_STEP; // Pintar la letra text_->writeDX(Text::COLOR, letter_x, y, single_char, 1, letter_color); } }