#include "png_shape.h" #include "../defines.h" #include "../external/stb_image.h" #include #include #include #include PNGShape::PNGShape(const char* png_path) { // Cargar PNG desde path if (!loadPNG(png_path)) { // Fallback: generar un cuadrado simple si falla la carga image_width_ = 10; image_height_ = 10; pixel_data_.resize(100, true); // Cuadrado 10x10 blanco } // Inicializar next_idle_time_ con valores apropiados (no hardcoded 5.0) next_idle_time_ = PNG_IDLE_TIME_MIN + (rand() % 1000) / 1000.0f * (PNG_IDLE_TIME_MAX - PNG_IDLE_TIME_MIN); } bool PNGShape::loadPNG(const char* path) { int width, height, channels; unsigned char* data = stbi_load(path, &width, &height, &channels, 1); // Forzar 1 canal (grayscale) if (!data) { return false; } image_width_ = width; image_height_ = height; pixel_data_.resize(width * height); // Convertir a mapa booleano (true = píxel blanco/visible, false = negro/transparente) for (int i = 0; i < width * height; i++) { pixel_data_[i] = (data[i] > 128); // Umbral: >128 = blanco } stbi_image_free(data); return true; } void PNGShape::detectEdges() { edge_points_.clear(); // Detectar píxeles del contorno (píxeles blancos con al menos un vecino negro) for (int y = 0; y < image_height_; y++) { for (int x = 0; x < image_width_; x++) { int idx = y * image_width_ + x; if (!pixel_data_[idx]) continue; // Solo píxeles blancos // Verificar vecinos (arriba, abajo, izq, der) bool is_edge = false; if (x == 0 || x == image_width_ - 1 || y == 0 || y == image_height_ - 1) { is_edge = true; // Bordes de la imagen } else { // Verificar 4 vecinos if (!pixel_data_[idx - 1] || // Izquierda !pixel_data_[idx + 1] || // Derecha !pixel_data_[idx - image_width_] || // Arriba !pixel_data_[idx + image_width_]) { // Abajo is_edge = true; } } if (is_edge) { edge_points_.push_back({static_cast(x), static_cast(y)}); } } } } void PNGShape::floodFill() { // TODO: Implementar flood-fill para Enfoque B (voxelización) // Por ahora, rellenar con todos los píxeles blancos filled_points_.clear(); for (int y = 0; y < image_height_; y++) { for (int x = 0; x < image_width_; x++) { int idx = y * image_width_ + x; if (pixel_data_[idx]) { filled_points_.push_back({static_cast(x), static_cast(y)}); } } } } void PNGShape::generateExtrudedPoints() { if (PNG_USE_EDGES_ONLY) { // Usar solo bordes (contorno) de las letras detectEdges(); } else { // Usar relleno completo (todos los píxeles blancos) floodFill(); } } void PNGShape::generatePoints(int num_points, float screen_width, float screen_height) { num_points_ = num_points; extrusion_depth_ = screen_height * PNG_EXTRUSION_DEPTH_FACTOR; num_layers_ = PNG_NUM_EXTRUSION_LAYERS; // Generar AMBOS conjuntos de puntos (relleno Y bordes) floodFill(); // Generar filled_points_ detectEdges(); // Generar edge_points_ // Guardar copias originales (las funciones de filtrado modifican los vectores) std::vector filled_points_original = filled_points_; std::vector edge_points_original = edge_points_; // Conjunto de puntos ACTIVO (será modificado por filtros) std::vector active_points_data; std::string mode_name = ""; // === SISTEMA DE DISTRIBUCIÓN ADAPTATIVA === // Estrategia: Optimizar según número de pelotas disponibles // Objetivo: SIEMPRE intentar usar relleno primero, solo bordes si es necesario size_t num_2d_points = 0; size_t total_3d_points = 0; // NIVEL 1: Decidir punto de partida (relleno o bordes por configuración) if (PNG_USE_EDGES_ONLY) { active_points_data = edge_points_original; mode_name = "BORDES (config)"; } else { active_points_data = filled_points_original; mode_name = "RELLENO"; } num_2d_points = active_points_data.size(); total_3d_points = num_2d_points * num_layers_; // NIVEL 2: Reducir capas AGRESIVAMENTE hasta 1 (priorizar calidad 2D sobre profundidad 3D) // Objetivo: Llenar bien el texto en 2D antes de reducir píxeles while (num_layers_ > 1 && num_points < static_cast(total_3d_points)) { num_layers_ = std::max(1, num_layers_ / 2); total_3d_points = num_2d_points * num_layers_; } // NIVEL 3: Filas alternas en RELLENO (solo si 1 capa no alcanza) // Esto permite usar relleno incluso con pocas pelotas int row_skip = 1; if (!PNG_USE_EDGES_ONLY) { // Solo si empezamos con relleno while (row_skip < 5 && num_points < static_cast(total_3d_points)) { row_skip++; // ✅ CLAVE: Recalcular desde el ORIGINAL cada vez (no desde el filtrado previo) active_points_data = extractAlternateRows(filled_points_original, row_skip); num_2d_points = active_points_data.size(); total_3d_points = num_2d_points * num_layers_; mode_name = "RELLENO + FILAS/" + std::to_string(row_skip); } } // NIVEL 4: Cambiar a BORDES (solo si relleno con filas alternas no alcanza) if (!PNG_USE_EDGES_ONLY && num_points < static_cast(total_3d_points)) { active_points_data = edge_points_original; mode_name = "BORDES (auto)"; num_2d_points = active_points_data.size(); total_3d_points = num_2d_points * num_layers_; row_skip = 1; // Reset row_skip para bordes } // NIVEL 5: Filas alternas en BORDES (si aún no alcanza) while (row_skip < 8 && num_points < static_cast(total_3d_points)) { row_skip++; // ✅ CLAVE: Recalcular desde edge_points_original cada vez active_points_data = extractAlternateRows(edge_points_original, row_skip); num_2d_points = active_points_data.size(); total_3d_points = num_2d_points * num_layers_; if (mode_name.find("FILAS") == std::string::npos) { mode_name += " + FILAS/" + std::to_string(row_skip); } } // NIVEL 6: Vértices/esquinas (último recurso, muy pocas pelotas) if (num_points < static_cast(total_3d_points) && num_points < 150) { // Determinar desde qué conjunto extraer vértices (el que esté activo actualmente) const std::vector& source_for_vertices = (mode_name.find("BORDES") != std::string::npos) ? edge_points_original : filled_points_original; std::vector vertices = extractCornerVertices(source_for_vertices); if (!vertices.empty() && vertices.size() < active_points_data.size()) { active_points_data = vertices; num_2d_points = active_points_data.size(); total_3d_points = num_2d_points * num_layers_; mode_name = "VÉRTICES"; } } // ✅ CLAVE: Guardar el conjunto de puntos optimizado final en optimized_points_ (usado por getPoint3D) optimized_points_ = active_points_data; // Calcular escala para centrar la imagen en pantalla float max_dimension = std::max(static_cast(image_width_), static_cast(image_height_)); scale_factor_ = (screen_height * PNG_SIZE_FACTOR) / max_dimension; // Calcular offset para centrar center_offset_x_ = image_width_ * 0.5f; center_offset_y_ = image_height_ * 0.5f; } // Extraer filas alternas de puntos (FUNCIÓN PURA: no modifica parámetros) // Recibe vector original y devuelve nuevo vector filtrado std::vector PNGShape::extractAlternateRows(const std::vector& source, int row_skip) { std::vector result; if (row_skip <= 1 || source.empty()) { return source; // Sin filtrado, devolver copia del original } // Organizar puntos por fila (Y) std::map> rows; for (const auto& p : source) { int row = static_cast(p.y); rows[row].push_back(p); } // Tomar solo cada N filas int row_counter = 0; for (const auto& [row_y, row_points] : rows) { if (row_counter % row_skip == 0) { result.insert(result.end(), row_points.begin(), row_points.end()); } row_counter++; } return result; } // Extraer vértices y esquinas (FUNCIÓN PURA: devuelve nuevo vector) std::vector PNGShape::extractCornerVertices(const std::vector& source) { std::vector result; if (source.empty()) { return result; } // Estrategia simple: tomar bordes extremos de cada fila // Esto da el "esqueleto" mínimo de las letras std::map> row_extremes; // Y -> (min_x, max_x) for (const auto& p : source) { int row = static_cast(p.y); if (row_extremes.find(row) == row_extremes.end()) { row_extremes[row] = {p.x, p.x}; } else { row_extremes[row].first = std::min(row_extremes[row].first, p.x); row_extremes[row].second = std::max(row_extremes[row].second, p.x); } } // Generar puntos en extremos de cada fila for (const auto& [row_y, extremes] : row_extremes) { result.push_back({extremes.first, static_cast(row_y)}); // Extremo izquierdo if (extremes.second != extremes.first) { // Solo añadir derecho si es diferente result.push_back({extremes.second, static_cast(row_y)}); // Extremo derecho } } return result; } void PNGShape::update(float delta_time, float screen_width, float screen_height) { if (!is_flipping_) { // Estado IDLE: texto de frente con pivoteo sutil // Solo contar tiempo para flips si: // - NO está en modo LOGO, O // - Está en modo LOGO Y ha alcanzado umbral de convergencia (80%) bool can_start_flip = !is_logo_mode_ || convergence_threshold_reached_; if (can_start_flip) { idle_timer_ += delta_time; } // Pivoteo sutil constante (movimiento orgánico) tilt_x_ += 0.4f * delta_time; // Velocidad sutil en X tilt_y_ += 0.6f * delta_time; // Velocidad sutil en Y if (idle_timer_ >= next_idle_time_) { // Iniciar voltereta is_flipping_ = true; flip_timer_ = 0.0f; idle_timer_ = 0.0f; // Elegir eje aleatorio (0=X, 1=Y, 2=ambos) flip_axis_ = rand() % 3; // Próximo tiempo idle aleatorio (según modo LOGO o MANUAL) float idle_min = is_logo_mode_ ? PNG_IDLE_TIME_MIN_LOGO : PNG_IDLE_TIME_MIN; float idle_max = is_logo_mode_ ? PNG_IDLE_TIME_MAX_LOGO : PNG_IDLE_TIME_MAX; next_idle_time_ = idle_min + (rand() % 1000) / 1000.0f * (idle_max - idle_min); } } else { // Estado FLIP: voltereta en curso flip_timer_ += delta_time; // Rotar según eje elegido if (flip_axis_ == 0 || flip_axis_ == 2) { angle_x_ += PNG_FLIP_SPEED * delta_time; } if (flip_axis_ == 1 || flip_axis_ == 2) { angle_y_ += PNG_FLIP_SPEED * delta_time; } // Terminar voltereta if (flip_timer_ >= PNG_FLIP_DURATION) { is_flipping_ = false; // Resetear ángulos a 0 (volver de frente) angle_x_ = 0.0f; angle_y_ = 0.0f; } } // Detectar transición de flip (de true a false) para incrementar contador if (was_flipping_last_frame_ && !is_flipping_) { flip_count_++; // Flip completado } was_flipping_last_frame_ = is_flipping_; } void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const { // Usar SIEMPRE el vector optimizado (resultado final de generatePoints) const std::vector& points = optimized_points_; if (points.empty()) { x = y = z = 0.0f; return; } // ENFOQUE A: Extrusión 2D // Cada punto 2D se replica en múltiples capas Z int num_2d_points = static_cast(points.size()); int point_2d_index = index % num_2d_points; int layer_index = (index / num_2d_points) % num_layers_; // Obtener coordenadas 2D del píxel Point2D p = points[point_2d_index]; // Centrar y escalar float x_base = (p.x - center_offset_x_) * scale_factor_; float y_base = (p.y - center_offset_y_) * scale_factor_; // Calcular Z según capa (distribuir uniformemente en profundidad) float z_base = 0.0f; if (num_layers_ > 1) { float layer_step = extrusion_depth_ / static_cast(num_layers_ - 1); z_base = -extrusion_depth_ * 0.5f + layer_index * layer_step; } // Añadir pivoteo sutil en estado IDLE // Calcular tamaño del logo en pantalla para normalizar correctamente float logo_width = image_width_ * scale_factor_; float logo_height = image_height_ * scale_factor_; float logo_size = std::max(logo_width, logo_height); // Normalizar coordenadas a rango [-1, 1] float u = x_base / (logo_size * 0.5f); float v = y_base / (logo_size * 0.5f); // Calcular pivoteo (amplitudes más grandes) float tilt_amount_x = sinf(tilt_x_) * 0.15f; // 15% float tilt_amount_y = sinf(tilt_y_) * 0.1f; // 10% // Aplicar pivoteo proporcional al tamaño del logo float z_tilt = (u * tilt_amount_y + v * tilt_amount_x) * logo_size; z_base += z_tilt; // Añadir pivoteo sutil a la profundidad // Aplicar rotación en eje Y (horizontal) float cos_y = cosf(angle_y_); float sin_y = sinf(angle_y_); float x_rot_y = x_base * cos_y - z_base * sin_y; float z_rot_y = x_base * sin_y + z_base * cos_y; // Aplicar rotación en eje X (vertical) float cos_x = cosf(angle_x_); float sin_x = sinf(angle_x_); float y_rot = y_base * cos_x - z_rot_y * sin_x; float z_rot = y_base * sin_x + z_rot_y * cos_x; // Retornar coordenadas finales x = x_rot_y; y = y_rot; // Cuando está de frente (sin rotación), usar Z con pivoteo sutil if (angle_x_ == 0.0f && angle_y_ == 0.0f) { // De frente: usar z_base que incluye el pivoteo sutil z = z_base; } else { z = z_rot; } } float PNGShape::getScaleFactor(float screen_height) const { // Escala dinámica según resolución return PNG_SIZE_FACTOR; } // Sistema de convergencia: notificar a la figura sobre el % de pelotas en posición void PNGShape::setConvergence(float convergence) { current_convergence_ = convergence; // Umbral de convergencia constexpr float CONVERGENCE_THRESHOLD = 0.4f; // Activar threshold cuando convergencia supera el umbral if (!convergence_threshold_reached_ && convergence >= CONVERGENCE_THRESHOLD) { convergence_threshold_reached_ = true; } // Desactivar threshold cuando convergencia cae por debajo del umbral if (convergence < CONVERGENCE_THRESHOLD) { convergence_threshold_reached_ = false; } } // Obtener progreso del flip actual (0.0 = inicio del flip, 1.0 = fin del flip) float PNGShape::getFlipProgress() const { if (!is_flipping_) { return 0.0f; // No está flipping, progreso = 0 } // Calcular progreso normalizado (0.0 - 1.0) return flip_timer_ / PNG_FLIP_DURATION; }