#include "png_shape.h" #include "../defines.h" #include "../external/stb_image.h" #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 } } 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 puntos según el enfoque generateExtrudedPoints(); // Debug: mostrar cantidad de puntos 2D detectados size_t num_2d_points = PNG_USE_EDGES_ONLY ? edge_points_.size() : filled_points_.size(); size_t total_3d_points = num_2d_points * num_layers_; std::cout << "[PNG_SHAPE] Detectados " << num_2d_points << " puntos 2D × " << num_layers_ << " capas = " << total_3d_points << " puntos 3D totales\n"; std::cout << "[PNG_SHAPE] Pelotas disponibles: " << num_points << "\n"; std::cout << "[PNG_SHAPE] Ratio: " << (float)num_points / (float)total_3d_points << " pelotas/punto\n"; // 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; } void PNGShape::update(float delta_time, float screen_width, float screen_height) { if (!is_flipping_) { // Estado IDLE: texto de frente idle_timer_ += delta_time; 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 next_idle_time_ = PNG_IDLE_TIME_MIN + (rand() % 1000) / 1000.0f * (PNG_IDLE_TIME_MAX - PNG_IDLE_TIME_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; } } } void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const { // Seleccionar puntos según configuración const std::vector& points = PNG_USE_EDGES_ONLY ? edge_points_ : filled_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; } // 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), forzar Z positivo (primer plano brillante) if (angle_x_ == 0.0f && angle_y_ == 0.0f) { // De frente: todo en primer plano (Z máximo) z = extrusion_depth_ * 0.5f; // Máximo Z = más brillante } else { z = z_rot; } } float PNGShape::getScaleFactor(float screen_height) const { // Escala dinámica según resolución return PNG_SIZE_FACTOR; }