diff --git a/ROADMAP.md b/ROADMAP.md index b8edc0f..9db0939 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -22,7 +22,9 @@ ### Sistemas de Presentación - ✅ Transiciones LERP entre temas (0.5s suaves) -- ✅ Hot-swap de sprites con tecla N (ball.png ↔ ball_small.png) +- ✅ Carga dinámica de texturas desde data/balls/ +- ✅ Hot-swap de sprites con tecla N (cicla entre todas las texturas) +- ✅ PNG_SHAPE (O) - Logo "JAILGAMES" con rotación legible --- @@ -127,6 +129,13 @@ - [ ] Campos magnéticos (atracción/repulsión) - [ ] Turbulencia +### Shapes PNG +- [ ] **Voxelización 3D para PNG_SHAPE** (Enfoque B) + - Actualmente: Extrusión 2D simple (píxeles → capas Z) + - Futuro: Voxelización real con detección de interior/exterior + - Permite formas 3D más complejas desde imágenes + - Rotación volumétrica en vez de extrusión plana + ### Interactividad - [ ] Mouse: click para aplicar fuerzas - [ ] Mouse: drag para crear campos @@ -136,6 +145,20 @@ ## Historial de Cambios +### 2025-10-04 (Sesión 5) - PNG Shape + Texturas Dinámicas +- ✅ **PNG_SHAPE implementado** - Tecla O para activar logo "JAILGAMES" +- ✅ Carga de PNG 1-bit con stb_image +- ✅ Extrusión 2D (Enfoque A) - píxeles → capas Z +- ✅ Detección de bordes vs relleno completo (configurable) +- ✅ Tamaño 80% pantalla (como otras figuras) +- ✅ Rotación "legible" - De frente con volteretas ocasionales (3-8s idle) +- ✅ Excluido de DEMO/DEMO_LITE (logo especial) +- ✅ **Sistema de texturas dinámicas** - Carga automática desde data/balls/ +- ✅ Tecla N cicla entre todas las texturas PNG encontradas +- ✅ Orden alfabético con normal.png primero por defecto +- ✅ Display dinámico del nombre de textura (uppercase) +- 📝 Preparado para voxelización 3D (Enfoque B) en futuro + ### 2025-10-04 (Sesión 4) - Modo DEMO - ✅ **Implementado Modo DEMO (auto-play)** - Tecla D para toggle - ✅ Sistema de acciones aleatorias cada 3-8 segundos (configurable) diff --git a/data/ball.png b/data/balls/normal.png similarity index 100% rename from data/ball.png rename to data/balls/normal.png diff --git a/data/ball_small.png b/data/balls/small.png similarity index 100% rename from data/ball_small.png rename to data/balls/small.png diff --git a/data/balls/tiny.png b/data/balls/tiny.png new file mode 100644 index 0000000..70b3157 Binary files /dev/null and b/data/balls/tiny.png differ diff --git a/data/shapes/jailgames.png b/data/shapes/jailgames.png new file mode 100644 index 0000000..619f9d4 Binary files /dev/null and b/data/shapes/jailgames.png differ diff --git a/source/ball.cpp b/source/ball.cpp index d17ce8c..be8acc3 100644 --- a/source/ball.cpp +++ b/source/ball.cpp @@ -303,8 +303,10 @@ void Ball::enableRotoBallAttraction(bool enable) { } } -// Aplicar fuerza de resorte hacia punto objetivo en esfera rotante -void Ball::applyRotoBallForce(float target_x, float target_y, float sphere_radius, float deltaTime) { +// Aplicar fuerza de resorte hacia punto objetivo en figuras 3D +void Ball::applyRotoBallForce(float target_x, float target_y, float sphere_radius, float deltaTime, + float spring_k_base, float damping_base_base, float damping_near_base, + float near_threshold_base, float max_force_base) { if (!rotoball_attraction_active_) return; // Calcular factor de escala basado en el radio (radio base = 80px) @@ -313,11 +315,11 @@ void Ball::applyRotoBallForce(float target_x, float target_y, float sphere_radiu float scale = sphere_radius / BASE_RADIUS; // Escalar constantes de física proporcionalmente - float spring_k = ROTOBALL_SPRING_K * scale; - float damping_base = ROTOBALL_DAMPING_BASE * scale; - float damping_near = ROTOBALL_DAMPING_NEAR * scale; - float near_threshold = ROTOBALL_NEAR_THRESHOLD * scale; - float max_force = ROTOBALL_MAX_FORCE * scale; + float spring_k = spring_k_base * scale; + float damping_base = damping_base_base * scale; + float damping_near = damping_near_base * scale; + float near_threshold = near_threshold_base * scale; + float max_force = max_force_base * scale; // Calcular vector diferencia (dirección hacia el target) float diff_x = target_x - pos_.x; diff --git a/source/ball.h b/source/ball.h index ac9689d..2f1bcc0 100644 --- a/source/ball.h +++ b/source/ball.h @@ -89,7 +89,12 @@ class Ball { void setDepthScale(float scale); float getDepthScale() const { return depth_scale_; } - // Sistema de atracción física hacia esfera RotoBall + // Sistema de atracción física hacia figuras 3D void enableRotoBallAttraction(bool enable); - void applyRotoBallForce(float target_x, float target_y, float sphere_radius, float deltaTime); + void applyRotoBallForce(float target_x, float target_y, float sphere_radius, float deltaTime, + float spring_k = ROTOBALL_SPRING_K, + float damping_base = ROTOBALL_DAMPING_BASE, + float damping_near = ROTOBALL_DAMPING_NEAR, + float near_threshold = ROTOBALL_NEAR_THRESHOLD, + float max_force = ROTOBALL_MAX_FORCE); }; \ No newline at end of file diff --git a/source/defines.h b/source/defines.h index 90023d2..823a33c 100644 --- a/source/defines.h +++ b/source/defines.h @@ -66,12 +66,13 @@ enum class ShapeType { NONE, // Sin figura (modo física pura) SPHERE, // Esfera Fibonacci (antiguo RotoBall) CUBE, // Cubo rotante - HELIX, // Espiral 3D (futuro) - TORUS, // Toroide/donut (futuro) - WAVE_GRID, // Malla ondeante (futuro) - CYLINDER, // Cilindro rotante (futuro) - ICOSAHEDRON, // Icosaedro D20 (futuro) - ATOM // Átomo con órbitas (futuro) + HELIX, // Espiral 3D + TORUS, // Toroide/donut + WAVE_GRID, // Malla ondeante + CYLINDER, // Cilindro rotante + ICOSAHEDRON, // Icosaedro D20 + ATOM, // Átomo con órbitas + PNG_SHAPE // Forma cargada desde PNG 1-bit }; // Enum para modo de simulación @@ -95,13 +96,21 @@ constexpr float ROTOBALL_TRANSITION_TIME = 1.5f; // Tiempo de transición (seg constexpr int ROTOBALL_MIN_BRIGHTNESS = 50; // Brillo mínimo (fondo, 0-255) constexpr int ROTOBALL_MAX_BRIGHTNESS = 255; // Brillo máximo (frente, 0-255) -// Física de atracción para figuras 3D (sistema de resorte compartido) +// Física de atracción para figuras 3D (sistema de resorte) +// ROTOBALL: Figura esfera rotante especial (modo C) constexpr float ROTOBALL_SPRING_K = 300.0f; // Constante de rigidez del resorte (N/m) constexpr float ROTOBALL_DAMPING_BASE = 35.0f; // Amortiguación base (amortiguamiento crítico ≈ 2*√k*m) constexpr float ROTOBALL_DAMPING_NEAR = 80.0f; // Amortiguación cerca del punto (absorción rápida) constexpr float ROTOBALL_NEAR_THRESHOLD = 5.0f; // Distancia "cerca" en píxeles constexpr float ROTOBALL_MAX_FORCE = 1000.0f; // Fuerza máxima aplicable (evita explosiones) +// SHAPE: Figuras 3D normales (Q/W/E/R/T/Y/U/I/O) - Mayor pegajosidad +constexpr float SHAPE_SPRING_K = 800.0f; // Rigidez alta (pelotas más "pegadas") +constexpr float SHAPE_DAMPING_BASE = 60.0f; // Amortiguación alta (menos rebote) +constexpr float SHAPE_DAMPING_NEAR = 150.0f; // Absorción muy rápida al llegar +constexpr float SHAPE_NEAR_THRESHOLD = 8.0f; // Umbral "cerca" más amplio +constexpr float SHAPE_MAX_FORCE = 2000.0f; // Permite fuerzas más fuertes + // Configuración del Cubo (cubo 3D rotante) constexpr float CUBE_SIZE_FACTOR = 0.25f; // Tamaño como proporción de altura (60/240 = 0.25) constexpr float CUBE_ROTATION_SPEED_X = 0.5f; // Velocidad rotación eje X (rad/s) @@ -147,6 +156,17 @@ constexpr float ATOM_NUM_ORBITS = 3; // Número de órbitas constexpr float ATOM_ORBIT_ROTATION_SPEED = 2.0f; // Velocidad de electrones (rad/s) constexpr float ATOM_ROTATION_SPEED_Y = 0.5f; // Velocidad rotación global (rad/s) +// Configuración de PNG Shape (forma desde imagen PNG 1-bit) +constexpr float PNG_SIZE_FACTOR = 0.8f; // Tamaño como proporción de altura (80% pantalla) +constexpr float PNG_EXTRUSION_DEPTH_FACTOR = 0.12f; // Profundidad de extrusión (compacta) +constexpr int PNG_NUM_EXTRUSION_LAYERS = 15; // Capas de extrusión (más capas = más pegajosidad) +constexpr bool PNG_USE_EDGES_ONLY = false; // true = solo bordes, false = relleno completo +// Rotación "legible" (texto de frente con volteretas ocasionales) +constexpr float PNG_IDLE_TIME_MIN = 3.0f; // Tiempo mínimo de frente (segundos) +constexpr float PNG_IDLE_TIME_MAX = 8.0f; // Tiempo máximo de frente (segundos) +constexpr float PNG_FLIP_SPEED = 3.0f; // Velocidad voltereta (rad/s) +constexpr float PNG_FLIP_DURATION = 1.5f; // Duración voltereta (segundos) + // Control manual de escala de figuras 3D (Numpad +/-) constexpr float SHAPE_SCALE_MIN = 0.3f; // Escala mínima (30%) constexpr float SHAPE_SCALE_MAX = 3.0f; // Escala máxima (300%) diff --git a/source/engine.cpp b/source/engine.cpp index f4695a3..ce9cf70 100644 --- a/source/engine.cpp +++ b/source/engine.cpp @@ -32,6 +32,7 @@ #include "shapes/cylinder_shape.h" // for CylinderShape #include "shapes/icosahedron_shape.h" // for IcosahedronShape #include "shapes/atom_shape.h" // for AtomShape +#include "shapes/png_shape.h" // for PNGShape // Función auxiliar para obtener la ruta del directorio del ejecutable std::string getExecutableDirectory() { @@ -80,18 +81,54 @@ bool Engine::initialize() { // Inicializar otros componentes si SDL se inicializó correctamente if (success) { - // Cargar todas las texturas disponibles + // Cargar todas las texturas disponibles desde data/balls/ std::string exe_dir = getExecutableDirectory(); + std::string balls_dir = exe_dir + "/data/balls"; - // Textura 0: ball.png (10x10) - std::string texture_path_normal = exe_dir + "/data/ball.png"; - textures_.push_back(std::make_shared(renderer_, texture_path_normal)); + // Buscar todas las texturas PNG en data/balls/ + namespace fs = std::filesystem; + if (fs::exists(balls_dir) && fs::is_directory(balls_dir)) { + std::vector> texture_files; // (nombre, path) - // Textura 1: ball_small.png (6x6) - std::string texture_path_small = exe_dir + "/data/ball_small.png"; - textures_.push_back(std::make_shared(renderer_, texture_path_small)); + for (const auto& entry : fs::directory_iterator(balls_dir)) { + if (entry.is_regular_file() && entry.path().extension() == ".png") { + std::string filename = entry.path().stem().string(); // Sin extensión + std::string fullpath = entry.path().string(); + texture_files.push_back({filename, fullpath}); + } + } - // Establecer textura inicial (índice 0) + // Ordenar alfabéticamente (normal.png será primero) + std::sort(texture_files.begin(), texture_files.end()); + + // Cargar texturas en orden (con normal.png primero si existe) + int normal_index = -1; + for (size_t i = 0; i < texture_files.size(); i++) { + if (texture_files[i].first == "normal") { + normal_index = static_cast(i); + break; + } + } + + // Poner normal.png primero + if (normal_index > 0) { + std::swap(texture_files[0], texture_files[normal_index]); + } + + // Cargar todas las texturas + for (const auto& [name, path] : texture_files) { + textures_.push_back(std::make_shared(renderer_, path)); + texture_names_.push_back(name); + } + } + + // Fallback si no hay texturas (no debería pasar) + if (textures_.empty()) { + std::cerr << "ERROR: No se encontraron texturas en data/balls/" << std::endl; + success = false; + } + + // Establecer textura inicial (índice 0 = normal.png) current_texture_index_ = 0; texture_ = textures_[current_texture_index_]; current_ball_size_ = texture_->getWidth(); // Obtener tamaño dinámicamente @@ -315,6 +352,10 @@ void Engine::handleEvents() { activateShape(ShapeType::ATOM); break; + case SDLK_O: + activateShape(ShapeType::PNG_SHAPE); + break; + // Ciclar temas de color (movido de T a B) case SDLK_B: // Ciclar al siguiente tema con transición suave (LERP) @@ -1288,8 +1329,8 @@ void Engine::performDemoAction(bool is_lite) { if (random_value < accumulated_weight) { ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, - ShapeType::ICOSAHEDRON, ShapeType::ATOM}; - int shape_index = rand() % 8; + ShapeType::ICOSAHEDRON, ShapeType::ATOM, ShapeType::PNG_SHAPE}; + int shape_index = rand() % 9; activateShape(shapes[shape_index]); return; } @@ -1336,8 +1377,8 @@ void Engine::performDemoAction(bool is_lite) { if (random_value < accumulated_weight) { ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, - ShapeType::ICOSAHEDRON, ShapeType::ATOM}; - int shape_index = rand() % 8; + ShapeType::ICOSAHEDRON, ShapeType::ATOM, ShapeType::PNG_SHAPE}; + int shape_index = rand() % 9; activateShape(shapes[shape_index]); return; } @@ -1430,7 +1471,7 @@ void Engine::randomizeOnDemoStart(bool is_lite) { toggleShapeMode(false); // Salir a física sin forzar gravedad } } else { - // Modo figura: elegir figura aleatoria + // Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial) ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM}; @@ -1468,7 +1509,7 @@ void Engine::randomizeOnDemoStart(bool is_lite) { toggleShapeMode(false); } } else { - // Modo figura: elegir figura aleatoria + // Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial) ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM}; @@ -1576,7 +1617,10 @@ void Engine::switchTexture() { // Mostrar texto informativo (solo si NO estamos en modo demo) if (!demo_mode_enabled_ && !demo_lite_enabled_) { - std::string texture_name = (current_texture_index_ == 0) ? "NORMAL" : "SMALL"; + // Obtener nombre de textura (uppercase) + std::string texture_name = texture_names_[current_texture_index_]; + std::transform(texture_name.begin(), texture_name.end(), texture_name.begin(), ::toupper); + text_ = "SPRITE: " + texture_name; text_pos_ = (current_screen_width_ - static_cast(text_.length() * 8)) / 2; show_text_ = true; @@ -1653,6 +1697,9 @@ void Engine::activateShape(ShapeType type) { case ShapeType::ATOM: active_shape_ = std::make_unique(); break; + case ShapeType::PNG_SHAPE: + active_shape_ = std::make_unique("data/shapes/jailgames.png"); + break; default: active_shape_ = std::make_unique(); // Fallback break; @@ -1714,9 +1761,11 @@ void Engine::updateShape() { float target_y = center_y + y_3d; // Aplicar fuerza de atracción física hacia el punto rotado - // Pasar el tamaño de la figura para escalar fuerzas + // Usar constantes SHAPE (mayor pegajosidad que ROTOBALL) float shape_size = scale_factor * 80.0f; // 80px = radio base - balls_[i]->applyRotoBallForce(target_x, target_y, shape_size, delta_time_); + balls_[i]->applyRotoBallForce(target_x, target_y, shape_size, delta_time_, + SHAPE_SPRING_K, SHAPE_DAMPING_BASE, SHAPE_DAMPING_NEAR, + SHAPE_NEAR_THRESHOLD, SHAPE_MAX_FORCE); // Calcular brillo según profundidad Z para renderizado // Normalizar Z al rango de la figura (asumiendo simetría ±shape_size) diff --git a/source/engine.h b/source/engine.h index 6315f3e..7b6081f 100644 --- a/source/engine.h +++ b/source/engine.h @@ -28,6 +28,7 @@ private: SDL_Renderer* renderer_ = nullptr; std::shared_ptr texture_ = nullptr; // Textura activa actual std::vector> textures_; // Todas las texturas disponibles + std::vector texture_names_; // Nombres de texturas (sin extensión) size_t current_texture_index_ = 0; // Índice de textura activa int current_ball_size_ = 10; // Tamaño actual de pelotas (dinámico, se actualiza desde texture) diff --git a/source/shapes/png_shape.cpp b/source/shapes/png_shape.cpp new file mode 100644 index 0000000..ea6d77d --- /dev/null +++ b/source/shapes/png_shape.cpp @@ -0,0 +1,219 @@ +#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; +} diff --git a/source/shapes/png_shape.h b/source/shapes/png_shape.h new file mode 100644 index 0000000..8eca604 --- /dev/null +++ b/source/shapes/png_shape.h @@ -0,0 +1,58 @@ +#pragma once + +#include "shape.h" +#include + +// Figura: Shape generada desde PNG 1-bit (blanco sobre negro) +// Enfoque A: Extrusión 2D (implementado) +// Enfoque B: Voxelización 3D (preparado para futuro) +class PNGShape : public Shape { +private: + // Datos de la imagen cargada + int image_width_ = 0; + int image_height_ = 0; + std::vector pixel_data_; // Mapa de píxeles blancos (true = blanco) + + // Puntos generados (Enfoque A: Extrusión 2D) + struct Point2D { + float x, y; + }; + std::vector edge_points_; // Contorno (solo bordes) + std::vector filled_points_; // Relleno completo (para Enfoque B) + + // Parámetros de extrusión + float extrusion_depth_ = 0.0f; // Profundidad de extrusión en Z + int num_layers_ = 0; // Capas de extrusión (más capas = más denso) + + // Rotación "legible" (de frente con volteretas ocasionales) + float angle_x_ = 0.0f; + float angle_y_ = 0.0f; + float idle_timer_ = 0.0f; // Timer para tiempo de frente + float flip_timer_ = 0.0f; // Timer para voltereta + float next_idle_time_ = 5.0f; // Próximo tiempo de espera (aleatorio) + bool is_flipping_ = false; // Estado: quieto o voltereta + int flip_axis_ = 0; // Eje de voltereta (0=X, 1=Y, 2=ambos) + + // Dimensiones normalizadas + float scale_factor_ = 1.0f; + float center_offset_x_ = 0.0f; + float center_offset_y_ = 0.0f; + + int num_points_ = 0; // Total de puntos generados (para indexación) + + // Métodos internos + bool loadPNG(const char* path); // Cargar PNG con stb_image + void detectEdges(); // Detectar contorno (Enfoque A) + void floodFill(); // Rellenar interior (Enfoque B - futuro) + void generateExtrudedPoints(); // Generar puntos con extrusión 2D + +public: + // Constructor: recibe path relativo al PNG + PNGShape(const char* png_path = "data/shapes/jailgames.png"); + + void generatePoints(int num_points, float screen_width, float screen_height) override; + void update(float delta_time, float screen_width, float screen_height) override; + void getPoint3D(int index, float& x, float& y, float& z) const override; + const char* getName() const override { return "PNG SHAPE"; } + float getScaleFactor(float screen_height) const override; +};