style: aplicar fixes de clang-tidy (todo excepto uppercase-literal-suffix)

Corregidos ~2570 issues automáticamente con clang-tidy --fix-errors
más ajustes manuales posteriores:

- modernize: designated-initializers, trailing-return-type, use-auto,
  avoid-c-arrays (→ std::array<>), use-ranges, use-emplace,
  deprecated-headers, use-equals-default, pass-by-value,
  return-braced-init-list, use-default-member-init
- readability: math-missing-parentheses, implicit-bool-conversion,
  braces-around-statements, isolate-declaration, use-std-min-max,
  identifier-naming, else-after-return, redundant-casting,
  convert-member-functions-to-static, make-member-function-const,
  static-accessed-through-instance
- performance: avoid-endl, unnecessary-value-param, type-promotion,
  inefficient-vector-operation
- dead code: XOR_KEY (orphan tras eliminar encryptData/decryptData),
  dead stores en engine.cpp y png_shape.cpp
- NOLINT justificado en 10 funciones con alta complejidad cognitiva
  (initialize, render, main, processEvents, update×3, performDemoAction,
  randomizeOnDemoStart, renderDebugHUD, AppLogo::update)

Compilación: gcc -Wall sin warnings. clang-tidy: 0 issues.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 10:52:07 +01:00
parent 4801f287df
commit c9bcce6f9b
71 changed files with 3469 additions and 2838 deletions

View File

@@ -16,6 +16,7 @@ Checks: >
-performance-inefficient-string-concatenation,
-bugprone-integer-division,
-bugprone-easily-swappable-parameters,
-readability-uppercase-literal-suffix,
WarningsAsErrors: '*'
# Solo incluir archivos de tu código fuente

View File

@@ -1,30 +1,31 @@
#include "ball.hpp"
#include <stdlib.h> // for rand
#include <cmath> // for fabs
#include <algorithm>
#include <cmath> // for fabs
#include <cstdlib> // for rand
#include <utility>
#include "defines.hpp" // for Color, SCREEN_HEIGHT, GRAVITY_FORCE
class Texture;
// Función auxiliar para generar pérdida aleatoria en rebotes
float generateBounceVariation() {
auto generateBounceVariation() -> float {
// Genera un valor entre 0 y BOUNCE_RANDOM_LOSS_PERCENT (solo pérdida adicional)
float loss = (rand() % 1000) / 1000.0f * BOUNCE_RANDOM_LOSS_PERCENT;
return 1.0f - loss; // Retorna multiplicador (ej: 0.90 - 1.00 para 10% max pérdida)
}
// Función auxiliar para generar pérdida lateral aleatoria
float generateLateralLoss() {
auto generateLateralLoss() -> float {
// Genera un valor entre 0 y LATERAL_LOSS_PERCENT
float loss = (rand() % 1000) / 1000.0f * LATERAL_LOSS_PERCENT;
return 1.0f - loss; // Retorna multiplicador (ej: 0.98 - 1.0 para 0-2% pérdida)
}
// Constructor
Ball::Ball(float x, float y, float vx, float vy, Color color, std::shared_ptr<Texture> texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir, float mass_factor)
Ball::Ball(float x, float y, float vx, float vy, Color color, const std::shared_ptr<Texture>& texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir, float mass_factor)
: sprite_(std::make_unique<Sprite>(texture)),
pos_({x, y, static_cast<float>(ball_size), static_cast<float>(ball_size)}) {
pos_({.x = x, .y = y, .w = static_cast<float>(ball_size), .h = static_cast<float>(ball_size)}) {
// Convertir velocidades de píxeles/frame a píxeles/segundo (multiplicar por 60)
vx_ = vx * 60.0f;
vy_ = vy * 60.0f;
@@ -36,7 +37,7 @@ Ball::Ball(float x, float y, float vx, float vy, Color color, std::shared_ptr<Te
gravity_force_ = GRAVITY_FORCE * 60.0f * 60.0f;
gravity_mass_factor_ = mass_factor; // Factor de masa individual para esta pelota
gravity_direction_ = gravity_dir;
screen_width_ = screen_width; // Dimensiones del terreno de juego
screen_width_ = screen_width; // Dimensiones del terreno de juego
screen_height_ = screen_height;
on_surface_ = false;
// Coeficiente base IGUAL para todas las pelotas (solo variación por rebote individual)
@@ -54,11 +55,11 @@ Ball::Ball(float x, float y, float vx, float vy, Color color, std::shared_ptr<Te
}
// Actualiza la lógica de la clase
void Ball::update(float deltaTime) {
void Ball::update(float delta_time) { // NOLINT(readability-function-cognitive-complexity)
// Aplica la gravedad según la dirección (píxeles/segundo²)
if (!on_surface_) {
// Aplicar gravedad multiplicada por factor de masa individual
float effective_gravity = gravity_force_ * gravity_mass_factor_ * deltaTime;
float effective_gravity = gravity_force_ * gravity_mass_factor_ * delta_time;
switch (gravity_direction_) {
case GravityDirection::DOWN:
vy_ += effective_gravity;
@@ -77,26 +78,26 @@ void Ball::update(float deltaTime) {
// Actualiza la posición en función de la velocidad (píxeles/segundo)
if (!on_surface_) {
pos_.x += vx_ * deltaTime;
pos_.y += vy_ * deltaTime;
pos_.x += vx_ * delta_time;
pos_.y += vy_ * delta_time;
} else {
// Si está en superficie, mantener posición según dirección de gravedad
switch (gravity_direction_) {
case GravityDirection::DOWN:
pos_.y = screen_height_ - pos_.h;
pos_.x += vx_ * deltaTime; // Seguir moviéndose en X
pos_.x += vx_ * delta_time; // Seguir moviéndose en X
break;
case GravityDirection::UP:
pos_.y = 0;
pos_.x += vx_ * deltaTime; // Seguir moviéndose en X
pos_.x += vx_ * delta_time; // Seguir moviéndose en X
break;
case GravityDirection::LEFT:
pos_.x = 0;
pos_.y += vy_ * deltaTime; // Seguir moviéndose en Y
pos_.y += vy_ * delta_time; // Seguir moviéndose en Y
break;
case GravityDirection::RIGHT:
pos_.x = screen_width_ - pos_.w;
pos_.y += vy_ * deltaTime; // Seguir moviéndose en Y
pos_.y += vy_ * delta_time; // Seguir moviéndose en Y
break;
}
}
@@ -176,7 +177,7 @@ void Ball::update(float deltaTime) {
// Aplica rozamiento al estar en superficie
if (on_surface_) {
// Convertir rozamiento de frame-based a time-based
float friction_factor = pow(0.97f, 60.0f * deltaTime);
float friction_factor = std::pow(0.97f, 60.0f * delta_time);
switch (gravity_direction_) {
case GravityDirection::DOWN:
@@ -246,7 +247,7 @@ void Ball::setGravityDirection(GravityDirection direction) {
// Aplica un pequeño empuje lateral aleatorio
void Ball::applyRandomLateralPush() {
// Generar velocidad lateral aleatoria (nunca 0)
float lateral_speed = GRAVITY_CHANGE_LATERAL_MIN + (rand() % 1000) / 1000.0f * (GRAVITY_CHANGE_LATERAL_MAX - GRAVITY_CHANGE_LATERAL_MIN);
float lateral_speed = GRAVITY_CHANGE_LATERAL_MIN + ((rand() % 1000) / 1000.0f * (GRAVITY_CHANGE_LATERAL_MAX - GRAVITY_CHANGE_LATERAL_MIN));
// Signo aleatorio (+ o -)
int sign = ((rand() % 2) * 2) - 1;
@@ -304,18 +305,18 @@ void Ball::enableShapeAttraction(bool enable) {
}
// Obtener distancia actual al punto objetivo (para calcular convergencia)
float Ball::getDistanceToTarget() const {
auto Ball::getDistanceToTarget() const -> float {
// Siempre calcular distancia (útil para convergencia en LOGO mode)
float dx = target_x_ - pos_.x;
float dy = target_y_ - pos_.y;
return sqrtf(dx * dx + dy * dy);
return sqrtf((dx * dx) + (dy * dy));
}
// Aplicar fuerza de resorte hacia punto objetivo en figuras 3D
void Ball::applyShapeForce(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 (!shape_attraction_active_) return;
void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius, float delta_time, float spring_k_base, float damping_base_base, float damping_near_base, float near_threshold_base, float max_force_base) {
if (!shape_attraction_active_) {
return;
}
// Calcular factor de escala basado en el radio (radio base = 80px)
// Si radius=80 → scale=1.0, si radius=160 → scale=2.0, si radius=360 → scale=4.5
@@ -334,7 +335,7 @@ void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius,
float diff_y = target_y - pos_.y;
// Calcular distancia al punto objetivo
float distance = sqrtf(diff_x * diff_x + diff_y * diff_y);
float distance = sqrtf((diff_x * diff_x) + (diff_y * diff_y));
// Fuerza de resorte (Ley de Hooke: F = -k * x)
float spring_force_x = spring_k * diff_x;
@@ -354,7 +355,7 @@ void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius,
float total_force_y = spring_force_y - damping_force_y;
// Limitar magnitud de fuerza (evitar explosiones numéricas)
float force_magnitude = sqrtf(total_force_x * total_force_x + total_force_y * total_force_y);
float force_magnitude = sqrtf((total_force_x * total_force_x) + (total_force_y * total_force_y));
if (force_magnitude > max_force) {
float scale_limit = max_force / force_magnitude;
total_force_x *= scale_limit;
@@ -363,18 +364,22 @@ void Ball::applyShapeForce(float target_x, float target_y, float sphere_radius,
// Aplicar aceleración (F = ma, asumiendo m = 1 para simplificar)
// a = F/m, pero m=1, así que a = F
vx_ += total_force_x * deltaTime;
vy_ += total_force_y * deltaTime;
vx_ += total_force_x * delta_time;
vy_ += total_force_y * delta_time;
// Actualizar posición con física normal (velocidad integrada)
pos_.x += vx_ * deltaTime;
pos_.y += vy_ * deltaTime;
pos_.x += vx_ * delta_time;
pos_.y += vy_ * delta_time;
// Mantener pelotas dentro de los límites de pantalla
if (pos_.x < 0) pos_.x = 0;
if (pos_.x + pos_.w > screen_width_) pos_.x = screen_width_ - pos_.w;
if (pos_.y < 0) pos_.y = 0;
if (pos_.y + pos_.h > screen_height_) pos_.y = screen_height_ - pos_.h;
pos_.x = std::max<float>(pos_.x, 0);
if (pos_.x + pos_.w > screen_width_) {
pos_.x = screen_width_ - pos_.w;
}
pos_.y = std::max<float>(pos_.y, 0);
if (pos_.y + pos_.h > screen_height_) {
pos_.y = screen_height_ - pos_.h;
}
// Actualizar sprite para renderizado
sprite_->setPos({pos_.x, pos_.y});
@@ -393,5 +398,5 @@ void Ball::updateSize(int new_size) {
void Ball::setTexture(std::shared_ptr<Texture> texture) {
// Actualizar textura del sprite
sprite_->setTexture(texture);
sprite_->setTexture(std::move(texture));
}

View File

@@ -10,34 +10,34 @@ class Texture;
class Ball {
private:
std::unique_ptr<Sprite> sprite_; // Sprite para pintar la clase
SDL_FRect pos_; // Posición y tamaño de la pelota
float vx_, vy_; // Velocidad
float gravity_force_; // Gravedad base
float gravity_mass_factor_; // Factor de masa individual (0.7-1.3, afecta gravedad)
GravityDirection gravity_direction_; // Direcci\u00f3n de la gravedad
int screen_width_; // Ancho del terreno de juego
int screen_height_; // Alto del terreno de juego
Color color_; // Color de la pelota
bool on_surface_; // Indica si la pelota est\u00e1 en la superficie (suelo/techo/pared)
float loss_; // Coeficiente de rebote. Pérdida de energía en cada rebote
std::unique_ptr<Sprite> sprite_; // Sprite para pintar la clase
SDL_FRect pos_; // Posición y tamaño de la pelota
float vx_, vy_; // Velocidad
float gravity_force_; // Gravedad base
float gravity_mass_factor_; // Factor de masa individual (0.7-1.3, afecta gravedad)
GravityDirection gravity_direction_; // Direcci\u00f3n de la gravedad
int screen_width_; // Ancho del terreno de juego
int screen_height_; // Alto del terreno de juego
Color color_; // Color de la pelota
bool on_surface_; // Indica si la pelota est\u00e1 en la superficie (suelo/techo/pared)
float loss_; // Coeficiente de rebote. Pérdida de energía en cada rebote
// Datos para modo Shape (figuras 3D)
float pos_3d_x_, pos_3d_y_, pos_3d_z_; // Posición 3D en la figura
float target_x_, target_y_; // Posición destino 2D (proyección)
float depth_brightness_; // Brillo según profundidad Z (0.0-1.0)
float depth_scale_; // Escala según profundidad Z (0.5-1.5)
bool shape_attraction_active_; // ¿Está siendo atraída hacia la figura?
float target_x_, target_y_; // Posición destino 2D (proyección)
float depth_brightness_; // Brillo según profundidad Z (0.0-1.0)
float depth_scale_; // Escala según profundidad Z (0.5-1.5)
bool shape_attraction_active_; // ¿Está siendo atraída hacia la figura?
public:
// Constructor
Ball(float x, float y, float vx, float vy, Color color, std::shared_ptr<Texture> texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir = GravityDirection::DOWN, float mass_factor = 1.0f);
Ball(float x, float y, float vx, float vy, Color color, const std::shared_ptr<Texture>& texture, int screen_width, int screen_height, int ball_size, GravityDirection gravity_dir = GravityDirection::DOWN, float mass_factor = 1.0f);
// Destructor
~Ball() = default;
// Actualiza la lógica de la clase
void update(float deltaTime);
void update(float delta_time);
// Pinta la clase
void render();
@@ -72,11 +72,20 @@ class Ball {
bool isOnSurface() const { return on_surface_; }
// Getters/Setters para velocidad (usado por BoidManager)
void getVelocity(float& vx, float& vy) const { vx = vx_; vy = vy_; }
void setVelocity(float vx, float vy) { vx_ = vx; vy_ = vy; }
void getVelocity(float& vx, float& vy) const {
vx = vx_;
vy = vy_;
}
void setVelocity(float vx, float vy) {
vx_ = vx;
vy_ = vy;
}
// Setter para posición simple (usado por BoidManager)
void setPosition(float x, float y) { pos_.x = x; pos_.y = y; }
void setPosition(float x, float y) {
pos_.x = x;
pos_.y = y;
}
// Getters/Setters para batch rendering
SDL_FRect getPosition() const { return pos_; }
@@ -84,7 +93,7 @@ class Ball {
void setColor(const Color& color) { color_ = color; }
// Sistema de cambio de sprite dinámico
void updateSize(int new_size); // Actualizar tamaño de hitbox
void updateSize(int new_size); // Actualizar tamaño de hitbox
void setTexture(std::shared_ptr<Texture> texture); // Cambiar textura del sprite
// Funciones para modo Shape (figuras 3D)
@@ -99,10 +108,5 @@ class Ball {
// Sistema de atracción física hacia figuras 3D
void enableShapeAttraction(bool enable);
float getDistanceToTarget() const; // Distancia actual al punto objetivo
void applyShapeForce(float target_x, float target_y, float sphere_radius, float deltaTime,
float spring_k = SHAPE_SPRING_K,
float damping_base = SHAPE_DAMPING_BASE,
float damping_near = SHAPE_DAMPING_NEAR,
float near_threshold = SHAPE_NEAR_THRESHOLD,
float max_force = SHAPE_MAX_FORCE);
void applyShapeForce(float target_x, float target_y, float sphere_radius, float delta_time, float spring_k_base = SHAPE_SPRING_K, float damping_base_base = SHAPE_DAMPING_BASE, float damping_near_base = SHAPE_DAMPING_NEAR, float near_threshold_base = SHAPE_NEAR_THRESHOLD, float max_force_base = SHAPE_MAX_FORCE);
};

View File

@@ -3,39 +3,38 @@
#include <algorithm> // for std::min, std::max
#include <cmath> // for sqrt, atan2
#include "ball.hpp" // for Ball
#include "engine.hpp" // for Engine (si se necesita)
#include "scene/scene_manager.hpp" // for SceneManager
#include "state/state_manager.hpp" // for StateManager
#include "ui/ui_manager.hpp" // for UIManager
#include "ball.hpp" // for Ball
#include "engine.hpp" // for Engine (si se necesita)
#include "scene/scene_manager.hpp" // for SceneManager
#include "state/state_manager.hpp" // for StateManager
#include "ui/ui_manager.hpp" // for UIManager
BoidManager::BoidManager()
: engine_(nullptr)
, scene_mgr_(nullptr)
, ui_mgr_(nullptr)
, state_mgr_(nullptr)
, screen_width_(0)
, screen_height_(0)
, boids_active_(false)
, spatial_grid_(800, 600, BOID_GRID_CELL_SIZE) // Tamaño por defecto, se actualiza en initialize()
, separation_radius_(BOID_SEPARATION_RADIUS)
, alignment_radius_(BOID_ALIGNMENT_RADIUS)
, cohesion_radius_(BOID_COHESION_RADIUS)
, separation_weight_(BOID_SEPARATION_WEIGHT)
, alignment_weight_(BOID_ALIGNMENT_WEIGHT)
, cohesion_weight_(BOID_COHESION_WEIGHT)
, max_speed_(BOID_MAX_SPEED)
, min_speed_(BOID_MIN_SPEED)
, max_force_(BOID_MAX_FORCE)
, boundary_margin_(BOID_BOUNDARY_MARGIN)
, boundary_weight_(BOID_BOUNDARY_WEIGHT) {
: engine_(nullptr),
scene_mgr_(nullptr),
ui_mgr_(nullptr),
state_mgr_(nullptr),
screen_width_(0),
screen_height_(0),
boids_active_(false),
spatial_grid_(800, 600, BOID_GRID_CELL_SIZE) // Tamaño por defecto, se actualiza en initialize()
,
separation_radius_(BOID_SEPARATION_RADIUS),
alignment_radius_(BOID_ALIGNMENT_RADIUS),
cohesion_radius_(BOID_COHESION_RADIUS),
separation_weight_(BOID_SEPARATION_WEIGHT),
alignment_weight_(BOID_ALIGNMENT_WEIGHT),
cohesion_weight_(BOID_COHESION_WEIGHT),
max_speed_(BOID_MAX_SPEED),
min_speed_(BOID_MIN_SPEED),
max_force_(BOID_MAX_FORCE),
boundary_margin_(BOID_BOUNDARY_MARGIN),
boundary_weight_(BOID_BOUNDARY_WEIGHT) {
}
BoidManager::~BoidManager() {
}
BoidManager::~BoidManager() = default;
void BoidManager::initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr,
StateManager* state_mgr, int screen_width, int screen_height) {
void BoidManager::initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, StateManager* state_mgr, int screen_width, int screen_height) {
engine_ = engine;
scene_mgr_ = scene_mgr;
ui_mgr_ = ui_mgr;
@@ -65,7 +64,8 @@ void BoidManager::activateBoids() {
auto& balls = scene_mgr_->getBallsMutable();
for (auto& ball : balls) {
// Dar velocidad inicial aleatoria si está quieto
float vx, vy;
float vx;
float vy;
ball->getVelocity(vx, vy);
if (vx == 0.0f && vy == 0.0f) {
// Velocidad aleatoria entre -60 y +60 px/s (time-based)
@@ -76,13 +76,15 @@ void BoidManager::activateBoids() {
}
// Mostrar notificación (solo si NO estamos en modo demo o logo)
if (state_mgr_ && ui_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
if ((state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
ui_mgr_->showNotification("Modo boids");
}
}
void BoidManager::deactivateBoids(bool force_gravity_on) {
if (!boids_active_) return;
if (!boids_active_) {
return;
}
boids_active_ = false;
@@ -92,7 +94,7 @@ void BoidManager::deactivateBoids(bool force_gravity_on) {
}
// Mostrar notificación (solo si NO estamos en modo demo o logo)
if (state_mgr_ && ui_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
if ((state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
ui_mgr_->showNotification("Modo física");
}
}
@@ -106,7 +108,9 @@ void BoidManager::toggleBoidsMode(bool force_gravity_on) {
}
void BoidManager::update(float delta_time) {
if (!boids_active_) return;
if (!boids_active_) {
return;
}
auto& balls = scene_mgr_->getBallsMutable();
@@ -114,8 +118,8 @@ void BoidManager::update(float delta_time) {
spatial_grid_.clear();
for (auto& ball : balls) {
SDL_FRect pos = ball->getPosition();
float center_x = pos.x + pos.w / 2.0f;
float center_y = pos.y + pos.h / 2.0f;
float center_x = pos.x + (pos.w / 2.0f);
float center_y = pos.y + (pos.h / 2.0f);
spatial_grid_.insert(ball.get(), center_x, center_y);
}
@@ -131,7 +135,8 @@ void BoidManager::update(float delta_time) {
// Actualizar posiciones con velocidades resultantes (time-based)
for (auto& ball : balls) {
float vx, vy;
float vx;
float vy;
ball->getVelocity(vx, vy);
SDL_FRect pos = ball->getPosition();
@@ -153,22 +158,24 @@ void BoidManager::applySeparation(Ball* boid, float delta_time) {
int count = 0;
SDL_FRect pos = boid->getPosition();
float center_x = pos.x + pos.w / 2.0f;
float center_y = pos.y + pos.h / 2.0f;
float center_x = pos.x + (pos.w / 2.0f);
float center_y = pos.y + (pos.h / 2.0f);
// FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n))
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, separation_radius_);
for (Ball* other : neighbors) {
if (other == boid) continue; // Ignorar a sí mismo
if (other == boid) {
continue; // Ignorar a sí mismo
}
SDL_FRect other_pos = other->getPosition();
float other_x = other_pos.x + other_pos.w / 2.0f;
float other_y = other_pos.y + other_pos.h / 2.0f;
float other_x = other_pos.x + (other_pos.w / 2.0f);
float other_y = other_pos.y + (other_pos.h / 2.0f);
float dx = center_x - other_x;
float dy = center_y - other_y;
float distance = std::sqrt(dx * dx + dy * dy);
float distance = std::sqrt((dx * dx) + (dy * dy));
if (distance > 0.0f && distance < separation_radius_) {
// FASE 1.3: Separación más fuerte cuando más cerca (inversamente proporcional a distancia)
@@ -186,7 +193,8 @@ void BoidManager::applySeparation(Ball* boid, float delta_time) {
steer_y /= count;
// Aplicar fuerza de separación
float vx, vy;
float vx;
float vy;
boid->getVelocity(vx, vy);
vx += steer_x * separation_weight_ * delta_time;
vy += steer_y * separation_weight_ * delta_time;
@@ -201,25 +209,28 @@ void BoidManager::applyAlignment(Ball* boid, float delta_time) {
int count = 0;
SDL_FRect pos = boid->getPosition();
float center_x = pos.x + pos.w / 2.0f;
float center_y = pos.y + pos.h / 2.0f;
float center_x = pos.x + (pos.w / 2.0f);
float center_y = pos.y + (pos.h / 2.0f);
// FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n))
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, alignment_radius_);
for (Ball* other : neighbors) {
if (other == boid) continue;
if (other == boid) {
continue;
}
SDL_FRect other_pos = other->getPosition();
float other_x = other_pos.x + other_pos.w / 2.0f;
float other_y = other_pos.y + other_pos.h / 2.0f;
float other_x = other_pos.x + (other_pos.w / 2.0f);
float other_y = other_pos.y + (other_pos.h / 2.0f);
float dx = center_x - other_x;
float dy = center_y - other_y;
float distance = std::sqrt(dx * dx + dy * dy);
float distance = std::sqrt((dx * dx) + (dy * dy));
if (distance < alignment_radius_) {
float other_vx, other_vy;
float other_vx;
float other_vy;
other->getVelocity(other_vx, other_vy);
avg_vx += other_vx;
avg_vy += other_vy;
@@ -233,13 +244,14 @@ void BoidManager::applyAlignment(Ball* boid, float delta_time) {
avg_vy /= count;
// Steering hacia la velocidad promedio
float vx, vy;
float vx;
float vy;
boid->getVelocity(vx, vy);
float steer_x = (avg_vx - vx) * alignment_weight_ * delta_time;
float steer_y = (avg_vy - vy) * alignment_weight_ * delta_time;
// Limitar fuerza máxima de steering
float steer_mag = std::sqrt(steer_x * steer_x + steer_y * steer_y);
float steer_mag = std::sqrt((steer_x * steer_x) + (steer_y * steer_y));
if (steer_mag > max_force_) {
steer_x = (steer_x / steer_mag) * max_force_;
steer_y = (steer_y / steer_mag) * max_force_;
@@ -258,22 +270,24 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
int count = 0;
SDL_FRect pos = boid->getPosition();
float center_x = pos.x + pos.w / 2.0f;
float center_y = pos.y + pos.h / 2.0f;
float center_x = pos.x + (pos.w / 2.0f);
float center_y = pos.y + (pos.h / 2.0f);
// FASE 2: Usar spatial grid para buscar solo vecinos cercanos (O(1) en lugar de O(n))
auto neighbors = spatial_grid_.queryRadius(center_x, center_y, cohesion_radius_);
for (Ball* other : neighbors) {
if (other == boid) continue;
if (other == boid) {
continue;
}
SDL_FRect other_pos = other->getPosition();
float other_x = other_pos.x + other_pos.w / 2.0f;
float other_y = other_pos.y + other_pos.h / 2.0f;
float other_x = other_pos.x + (other_pos.w / 2.0f);
float other_y = other_pos.y + (other_pos.h / 2.0f);
float dx = center_x - other_x;
float dy = center_y - other_y;
float distance = std::sqrt(dx * dx + dy * dy);
float distance = std::sqrt((dx * dx) + (dy * dy));
if (distance < cohesion_radius_) {
center_of_mass_x += other_x;
@@ -290,7 +304,7 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
// FASE 1.4: Normalizar dirección hacia el centro (CRÍTICO - antes no estaba normalizado!)
float dx_to_center = center_of_mass_x - center_x;
float dy_to_center = center_of_mass_y - center_y;
float distance_to_center = std::sqrt(dx_to_center * dx_to_center + dy_to_center * dy_to_center);
float distance_to_center = std::sqrt((dx_to_center * dx_to_center) + (dy_to_center * dy_to_center));
// Solo aplicar si hay distancia al centro (evitar división por cero)
if (distance_to_center > 0.1f) {
@@ -299,13 +313,14 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
float steer_y = (dy_to_center / distance_to_center) * cohesion_weight_ * delta_time;
// Limitar fuerza máxima de steering
float steer_mag = std::sqrt(steer_x * steer_x + steer_y * steer_y);
float steer_mag = std::sqrt((steer_x * steer_x) + (steer_y * steer_y));
if (steer_mag > max_force_) {
steer_x = (steer_x / steer_mag) * max_force_;
steer_y = (steer_y / steer_mag) * max_force_;
}
float vx, vy;
float vx;
float vy;
boid->getVelocity(vx, vy);
vx += steer_x;
vy += steer_y;
@@ -314,12 +329,12 @@ void BoidManager::applyCohesion(Ball* boid, float delta_time) {
}
}
void BoidManager::applyBoundaries(Ball* boid) {
void BoidManager::applyBoundaries(Ball* boid) const {
// NUEVA IMPLEMENTACIÓN: Bordes como obstáculos (repulsión en lugar de wrapping)
// Cuando un boid se acerca a un borde, se aplica una fuerza alejándolo
SDL_FRect pos = boid->getPosition();
float center_x = pos.x + pos.w / 2.0f;
float center_y = pos.y + pos.h / 2.0f;
float center_x = pos.x + (pos.w / 2.0f);
float center_y = pos.y + (pos.h / 2.0f);
float steer_x = 0.0f;
float steer_y = 0.0f;
@@ -363,11 +378,12 @@ void BoidManager::applyBoundaries(Ball* boid) {
// Aplicar fuerza de repulsión si hay alguna
if (steer_x != 0.0f || steer_y != 0.0f) {
float vx, vy;
float vx;
float vy;
boid->getVelocity(vx, vy);
// Normalizar fuerza de repulsión (para que todas las direcciones tengan la misma intensidad)
float steer_mag = std::sqrt(steer_x * steer_x + steer_y * steer_y);
float steer_mag = std::sqrt((steer_x * steer_x) + (steer_y * steer_y));
if (steer_mag > 0.0f) {
steer_x /= steer_mag;
steer_y /= steer_mag;
@@ -381,12 +397,13 @@ void BoidManager::applyBoundaries(Ball* boid) {
}
}
void BoidManager::limitSpeed(Ball* boid) {
void BoidManager::limitSpeed(Ball* boid) const {
// Limitar velocidad máxima del boid
float vx, vy;
float vx;
float vy;
boid->getVelocity(vx, vy);
float speed = std::sqrt(vx * vx + vy * vy);
float speed = std::sqrt((vx * vx) + (vy * vy));
// Limitar velocidad máxima
if (speed > max_speed_) {

View File

@@ -46,8 +46,7 @@ class BoidManager {
* @param screen_width Ancho de pantalla actual
* @param screen_height Alto de pantalla actual
*/
void initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr,
StateManager* state_mgr, int screen_width, int screen_height);
void initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, StateManager* state_mgr, int screen_width, int screen_height);
/**
* @brief Actualiza el tamaño de pantalla (llamado en resize/fullscreen)
@@ -105,22 +104,22 @@ class BoidManager {
// === Parámetros ajustables en runtime (inicializados con valores de defines.h) ===
// Permite modificar comportamiento sin recompilar (para tweaking/debug visual)
float separation_radius_; // Radio de separación (evitar colisiones)
float alignment_radius_; // Radio de alineación (matching de velocidad)
float cohesion_radius_; // Radio de cohesión (centro de masa)
float separation_weight_; // Peso fuerza de separación (aceleración px/s²)
float alignment_weight_; // Peso fuerza de alineación (steering proporcional)
float cohesion_weight_; // Peso fuerza de cohesión (aceleración px/s²)
float max_speed_; // Velocidad máxima (px/s)
float min_speed_; // Velocidad mínima (px/s)
float max_force_; // Fuerza máxima de steering (px/s)
float boundary_margin_; // Margen para repulsión de bordes (px)
float boundary_weight_; // Peso fuerza de repulsión de bordes (aceleración px/s²)
float separation_radius_; // Radio de separación (evitar colisiones)
float alignment_radius_; // Radio de alineación (matching de velocidad)
float cohesion_radius_; // Radio de cohesión (centro de masa)
float separation_weight_; // Peso fuerza de separación (aceleración px/s²)
float alignment_weight_; // Peso fuerza de alineación (steering proporcional)
float cohesion_weight_; // Peso fuerza de cohesión (aceleración px/s²)
float max_speed_; // Velocidad máxima (px/s)
float min_speed_; // Velocidad mínima (px/s)
float max_force_; // Fuerza máxima de steering (px/s)
float boundary_margin_; // Margen para repulsión de bordes (px)
float boundary_weight_; // Peso fuerza de repulsión de bordes (aceleración px/s²)
// Métodos privados para las reglas de Reynolds
void applySeparation(Ball* boid, float delta_time);
void applyAlignment(Ball* boid, float delta_time);
void applyCohesion(Ball* boid, float delta_time);
void applyBoundaries(Ball* boid); // Repulsión de bordes (ya no wrapping)
void limitSpeed(Ball* boid); // Limitar velocidad máxima
void applyBoundaries(Ball* boid) const; // Repulsión de bordes (ya no wrapping)
void limitSpeed(Ball* boid) const; // Limitar velocidad máxima
};

View File

@@ -6,9 +6,9 @@
#include "ball.hpp" // for Ball
SpatialGrid::SpatialGrid(int world_width, int world_height, float cell_size)
: world_width_(world_width)
, world_height_(world_height)
, cell_size_(cell_size) {
: world_width_(world_width),
world_height_(world_height),
cell_size_(cell_size) {
// Calcular número de celdas en cada dimensión
grid_cols_ = static_cast<int>(std::ceil(world_width / cell_size));
grid_rows_ = static_cast<int>(std::ceil(world_height / cell_size));
@@ -21,7 +21,8 @@ void SpatialGrid::clear() {
void SpatialGrid::insert(Ball* ball, float x, float y) {
// Obtener coordenadas de celda
int cell_x, cell_y;
int cell_x;
int cell_y;
getCellCoords(x, y, cell_x, cell_y);
// Generar hash key y añadir a la celda
@@ -29,11 +30,14 @@ void SpatialGrid::insert(Ball* ball, float x, float y) {
cells_[key].push_back(ball);
}
std::vector<Ball*> SpatialGrid::queryRadius(float x, float y, float radius) {
auto SpatialGrid::queryRadius(float x, float y, float radius) -> std::vector<Ball*> {
std::vector<Ball*> results;
// Calcular rango de celdas a revisar (AABB del círculo de búsqueda)
int min_cell_x, min_cell_y, max_cell_x, max_cell_y;
int min_cell_x;
int min_cell_y;
int max_cell_x;
int max_cell_y;
getCellCoords(x - radius, y - radius, min_cell_x, min_cell_y);
getCellCoords(x + radius, y + radius, max_cell_x, max_cell_y);
@@ -82,8 +86,8 @@ void SpatialGrid::getCellCoords(float x, float y, int& cell_x, int& cell_y) cons
cell_y = static_cast<int>(std::floor(y / cell_size_));
}
int SpatialGrid::getCellKey(int cell_x, int cell_y) const {
auto SpatialGrid::getCellKey(int cell_x, int cell_y) const -> int {
// Hash espacial 2D → 1D usando codificación por filas
// Formula: key = y * ancho + x (similar a array 2D aplanado)
return cell_y * grid_cols_ + cell_x;
return (cell_y * grid_cols_) + cell_x;
}

View File

@@ -30,42 +30,42 @@ class Ball; // Forward declaration
// ============================================================================
class SpatialGrid {
public:
// Constructor: especificar dimensiones del mundo y tamaño de celda
SpatialGrid(int world_width, int world_height, float cell_size);
public:
// Constructor: especificar dimensiones del mundo y tamaño de celda
SpatialGrid(int world_width, int world_height, float cell_size);
// Limpiar todas las celdas (llamar al inicio de cada frame)
void clear();
// Limpiar todas las celdas (llamar al inicio de cada frame)
void clear();
// Insertar objeto en el grid según su posición (x, y)
void insert(Ball* ball, float x, float y);
// Insertar objeto en el grid según su posición (x, y)
void insert(Ball* ball, float x, float y);
// Buscar todos los objetos dentro del radio especificado desde (x, y)
// Devuelve vector de punteros a Ball (puede contener duplicados si ball está en múltiples celdas)
std::vector<Ball*> queryRadius(float x, float y, float radius);
// Buscar todos los objetos dentro del radio especificado desde (x, y)
// Devuelve vector de punteros a Ball (puede contener duplicados si ball está en múltiples celdas)
std::vector<Ball*> queryRadius(float x, float y, float radius);
// Actualizar dimensiones del mundo (útil para cambios de resolución F4)
void updateWorldSize(int world_width, int world_height);
// Actualizar dimensiones del mundo (útil para cambios de resolución F4)
void updateWorldSize(int world_width, int world_height);
private:
// Convertir coordenadas (x, y) a índice de celda (cell_x, cell_y)
void getCellCoords(float x, float y, int& cell_x, int& cell_y) const;
private:
// Convertir coordenadas (x, y) a índice de celda (cell_x, cell_y)
void getCellCoords(float x, float y, int& cell_x, int& cell_y) const;
// Convertir (cell_x, cell_y) a hash key único para el mapa
int getCellKey(int cell_x, int cell_y) const;
// Convertir (cell_x, cell_y) a hash key único para el mapa
int getCellKey(int cell_x, int cell_y) const;
// Dimensiones del mundo (ancho/alto en píxeles)
int world_width_;
int world_height_;
// Dimensiones del mundo (ancho/alto en píxeles)
int world_width_;
int world_height_;
// Tamaño de cada celda (en píxeles)
float cell_size_;
// Tamaño de cada celda (en píxeles)
float cell_size_;
// Número de celdas en cada dimensión
int grid_cols_;
int grid_rows_;
// Número de celdas en cada dimensión
int grid_cols_;
int grid_rows_;
// Estructura de datos: hash map de cell_key → vector de Ball*
// Usamos unordered_map para O(1) lookup
std::unordered_map<int, std::vector<Ball*>> cells_;
// Estructura de datos: hash map de cell_key → vector de Ball*
// Usamos unordered_map para O(1) lookup
std::unordered_map<int, std::vector<Ball*>> cells_;
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,32 @@
#pragma once
#include <SDL3/SDL_events.h> // for SDL_Event
#include <SDL3/SDL_render.h> // for SDL_Renderer (ui_renderer_ software renderer)
#include <SDL3/SDL_stdinc.h> // for Uint64
#include <SDL3/SDL_surface.h> // for SDL_Surface (ui_surface_)
#include <SDL3/SDL_video.h> // for SDL_Window
#include <SDL3/SDL_events.h> // for SDL_Event
#include <SDL3/SDL_render.h> // for SDL_Renderer (ui_renderer_ software renderer)
#include <SDL3/SDL_stdinc.h> // for Uint64
#include <SDL3/SDL_surface.h> // for SDL_Surface (ui_surface_)
#include <SDL3/SDL_video.h> // for SDL_Window
#include <array> // for array
#include <memory> // for unique_ptr, shared_ptr
#include <string> // for string
#include <vector> // for vector
#include "ui/app_logo.hpp" // for AppLogo
#include "ball.hpp" // for Ball
#include "boids_mgr/boid_manager.hpp" // for BoidManager
#include "defines.hpp" // for GravityDirection, ColorTheme, ShapeType
#include "external/texture.hpp" // for Texture
#include "gpu/gpu_ball_buffer.hpp" // for GpuBallBuffer, BallGPUData
#include "gpu/gpu_context.hpp" // for GpuContext
#include "gpu/gpu_pipeline.hpp" // for GpuPipeline
#include "gpu/gpu_sprite_batch.hpp" // for GpuSpriteBatch
#include "gpu/gpu_texture.hpp" // for GpuTexture
#include "input/input_handler.hpp" // for InputHandler
#include "scene/scene_manager.hpp" // for SceneManager
#include "shapes_mgr/shape_manager.hpp" // for ShapeManager
#include "state/state_manager.hpp" // for StateManager
#include "theme_manager.hpp" // for ThemeManager
#include "ui/ui_manager.hpp" // for UIManager
#include "ball.hpp" // for Ball
#include "boids_mgr/boid_manager.hpp" // for BoidManager
#include "defines.hpp" // for GravityDirection, ColorTheme, ShapeType
#include "external/texture.hpp" // for Texture
#include "gpu/gpu_ball_buffer.hpp" // for GpuBallBuffer, BallGPUData
#include "gpu/gpu_context.hpp" // for GpuContext
#include "gpu/gpu_pipeline.hpp" // for GpuPipeline
#include "gpu/gpu_sprite_batch.hpp" // for GpuSpriteBatch
#include "gpu/gpu_texture.hpp" // for GpuTexture
#include "input/input_handler.hpp" // for InputHandler
#include "scene/scene_manager.hpp" // for SceneManager
#include "shapes_mgr/shape_manager.hpp" // for ShapeManager
#include "state/state_manager.hpp" // for StateManager
#include "theme_manager.hpp" // for ThemeManager
#include "ui/app_logo.hpp" // for AppLogo
#include "ui/ui_manager.hpp" // for UIManager
class Engine {
public:
@@ -97,8 +97,8 @@ class Engine {
// Escenario custom (tecla 9, --custom-balls)
void setCustomScenario(int balls);
bool isCustomScenarioEnabled() const { return custom_scenario_enabled_; }
bool isCustomAutoAvailable() const { return custom_auto_available_; }
int getCustomScenarioBalls() const { return custom_scenario_balls_; }
bool isCustomAutoAvailable() const { return custom_auto_available_; }
int getCustomScenarioBalls() const { return custom_scenario_balls_; }
// Control manual del benchmark (--skip-benchmark, --max-balls)
void setSkipBenchmark();
@@ -113,10 +113,10 @@ class Engine {
void toggleLogoMode();
// === Métodos públicos para StateManager (automatización DEMO/LOGO sin notificación) ===
void enterShapeMode(ShapeType type); // Activar figura (sin notificación)
void exitShapeMode(bool force_gravity = true); // Volver a física (sin notificación)
void switchTextureSilent(); // Cambiar textura (sin notificación)
void setTextureByIndex(size_t index); // Restaurar textura específica
void enterShapeMode(ShapeType type); // Activar figura (sin notificación)
void exitShapeMode(bool force_gravity = true); // Volver a física (sin notificación)
void switchTextureSilent(); // Cambiar textura (sin notificación)
void setTextureByIndex(size_t index); // Restaurar textura específica
// === Getters públicos para UIManager (Debug HUD) ===
bool getVSyncEnabled() const { return vsync_enabled_; }
@@ -133,11 +133,11 @@ class Engine {
int getBaseScreenHeight() const { return base_screen_height_; }
int getMaxAutoScenario() const { return max_auto_scenario_; }
size_t getCurrentTextureIndex() const { return current_texture_index_; }
bool isPostFXEnabled() const { return postfx_enabled_; }
int getPostFXMode() const { return postfx_effect_mode_; }
float getPostFXVignette() const { return postfx_uniforms_.vignette_strength; }
float getPostFXChroma() const { return postfx_uniforms_.chroma_strength; }
float getPostFXScanline() const { return postfx_uniforms_.scanline_strength; }
bool isPostFXEnabled() const { return postfx_enabled_; }
int getPostFXMode() const { return postfx_effect_mode_; }
float getPostFXVignette() const { return postfx_uniforms_.vignette_strength; }
float getPostFXChroma() const { return postfx_uniforms_.chroma_strength; }
float getPostFXScanline() const { return postfx_uniforms_.scanline_strength; }
private:
// === Componentes del sistema (Composición) ===
@@ -153,23 +153,23 @@ class Engine {
SDL_Window* window_ = nullptr;
// === SDL_GPU rendering pipeline ===
std::unique_ptr<GpuContext> gpu_ctx_; // Device + swapchain
std::unique_ptr<GpuPipeline> gpu_pipeline_; // Sprite + ball + postfx pipelines
std::unique_ptr<GpuSpriteBatch> sprite_batch_; // Per-frame vertex/index batch (bg + shape + UI)
std::unique_ptr<GpuBallBuffer> gpu_ball_buffer_; // Instanced ball instance data (PHYSICS/BOIDS)
std::vector<BallGPUData> ball_gpu_data_; // CPU-side staging vector (reused each frame)
std::unique_ptr<GpuTexture> offscreen_tex_; // Offscreen render target (Pass 1)
std::unique_ptr<GpuTexture> white_tex_; // 1×1 white (background gradient)
std::unique_ptr<GpuTexture> ui_tex_; // UI text overlay texture
std::unique_ptr<GpuContext> gpu_ctx_; // Device + swapchain
std::unique_ptr<GpuPipeline> gpu_pipeline_; // Sprite + ball + postfx pipelines
std::unique_ptr<GpuSpriteBatch> sprite_batch_; // Per-frame vertex/index batch (bg + shape + UI)
std::unique_ptr<GpuBallBuffer> gpu_ball_buffer_; // Instanced ball instance data (PHYSICS/BOIDS)
std::vector<BallGPUData> ball_gpu_data_; // CPU-side staging vector (reused each frame)
std::unique_ptr<GpuTexture> offscreen_tex_; // Offscreen render target (Pass 1)
std::unique_ptr<GpuTexture> white_tex_; // 1×1 white (background gradient)
std::unique_ptr<GpuTexture> ui_tex_; // UI text overlay texture
// GPU sprite textures (one per ball skin, parallel to textures_/texture_names_)
std::unique_ptr<GpuTexture> gpu_texture_; // Active GPU sprite texture
std::unique_ptr<GpuTexture> gpu_texture_; // Active GPU sprite texture
std::vector<std::unique_ptr<GpuTexture>> gpu_textures_; // All GPU sprite textures
// === SDL_Renderer (software, for UI text via SDL3_ttf) ===
// Renders to ui_surface_, then uploaded as gpu texture overlay.
SDL_Renderer* ui_renderer_ = nullptr;
SDL_Surface* ui_surface_ = nullptr;
SDL_Surface* ui_surface_ = nullptr;
// Legacy Texture objects — kept for ball physics sizing and AppLogo
std::shared_ptr<Texture> texture_ = nullptr; // Textura activa actual
@@ -190,7 +190,7 @@ class Engine {
int postfx_effect_mode_ = 3;
bool postfx_enabled_ = false;
float postfx_override_vignette_ = -1.f; // -1 = sin override
float postfx_override_chroma_ = -1.f;
float postfx_override_chroma_ = -1.f;
// Sistema de escala de ventana
float current_window_scale_ = 1.0f;
@@ -228,10 +228,10 @@ class Engine {
int max_auto_scenario_ = 5;
// Escenario custom (--custom-balls)
int custom_scenario_balls_ = 0;
int custom_scenario_balls_ = 0;
bool custom_scenario_enabled_ = false;
bool custom_auto_available_ = false;
bool skip_benchmark_ = false;
bool custom_auto_available_ = false;
bool skip_benchmark_ = false;
// Bucket sort per z-ordering (SHAPE mode)
static constexpr int DEPTH_SORT_BUCKETS = 256;
@@ -273,9 +273,8 @@ class Engine {
bool isScenarioAllowedForBoids(int scenario_id) const;
// GPU helpers
bool loadGpuSpriteTexture(size_t index); // Upload one sprite texture to GPU
void recreateOffscreenTexture(); // Recreate when resolution changes
void renderUIToSurface(); // Render text/UI to ui_surface_
bool loadGpuSpriteTexture(size_t index); // Upload one sprite texture to GPU
void recreateOffscreenTexture(); // Recreate when resolution changes
void renderUIToSurface(); // Render text/UI to ui_surface_
void uploadUISurface(SDL_GPUCommandBuffer* cmd_buf); // Upload ui_surface_ → ui_tex_
};

View File

@@ -1,18 +1,19 @@
#include "gpu_ball_buffer.hpp"
#include <SDL3/SDL_log.h>
#include <algorithm> // std::min
#include <cstring> // memcpy
bool GpuBallBuffer::init(SDL_GPUDevice* device) {
auto GpuBallBuffer::init(SDL_GPUDevice* device) -> bool {
Uint32 buf_size = static_cast<Uint32>(MAX_BALLS) * sizeof(BallGPUData);
// GPU vertex buffer (instance-rate data read by the ball instanced shader)
SDL_GPUBufferCreateInfo buf_info = {};
buf_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
buf_info.size = buf_size;
buf_info.size = buf_size;
gpu_buf_ = SDL_CreateGPUBuffer(device, &buf_info);
if (!gpu_buf_) {
if (gpu_buf_ == nullptr) {
SDL_Log("GpuBallBuffer: GPU buffer creation failed: %s", SDL_GetError());
return false;
}
@@ -20,34 +21,45 @@ bool GpuBallBuffer::init(SDL_GPUDevice* device) {
// Transfer buffer (upload staging, cycled every frame)
SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = buf_size;
tb_info.size = buf_size;
transfer_buf_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
if (!transfer_buf_) {
if (transfer_buf_ == nullptr) {
SDL_Log("GpuBallBuffer: transfer buffer creation failed: %s", SDL_GetError());
return false;
}
SDL_Log("GpuBallBuffer: initialized (capacity %d balls, %.1f MB VRAM)",
MAX_BALLS, buf_size / (1024.0f * 1024.0f));
MAX_BALLS,
buf_size / (1024.0f * 1024.0f));
return true;
}
void GpuBallBuffer::destroy(SDL_GPUDevice* device) {
if (!device) return;
if (transfer_buf_) { SDL_ReleaseGPUTransferBuffer(device, transfer_buf_); transfer_buf_ = nullptr; }
if (gpu_buf_) { SDL_ReleaseGPUBuffer(device, gpu_buf_); gpu_buf_ = nullptr; }
if (device == nullptr) {
return;
}
if (transfer_buf_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(device, transfer_buf_);
transfer_buf_ = nullptr;
}
if (gpu_buf_ != nullptr) {
SDL_ReleaseGPUBuffer(device, gpu_buf_);
gpu_buf_ = nullptr;
}
count_ = 0;
}
bool GpuBallBuffer::upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd,
const BallGPUData* data, int count) {
if (!data || count <= 0) { count_ = 0; return false; }
auto GpuBallBuffer::upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd, const BallGPUData* data, int count) -> bool {
if ((data == nullptr) || count <= 0) {
count_ = 0;
return false;
}
count = std::min(count, MAX_BALLS);
Uint32 upload_size = static_cast<Uint32>(count) * sizeof(BallGPUData);
void* ptr = SDL_MapGPUTransferBuffer(device, transfer_buf_, true /* cycle */);
if (!ptr) {
if (ptr == nullptr) {
SDL_Log("GpuBallBuffer: transfer buffer map failed: %s", SDL_GetError());
return false;
}
@@ -55,8 +67,8 @@ bool GpuBallBuffer::upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd,
SDL_UnmapGPUTransferBuffer(device, transfer_buf_);
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd);
SDL_GPUTransferBufferLocation src = { transfer_buf_, 0 };
SDL_GPUBufferRegion dst = { gpu_buf_, 0, upload_size };
SDL_GPUTransferBufferLocation src = {transfer_buf_, 0};
SDL_GPUBufferRegion dst = {gpu_buf_, 0, upload_size};
SDL_UploadToGPUBuffer(copy, &src, &dst, true /* cycle */);
SDL_EndGPUCopyPass(copy);

View File

@@ -1,6 +1,7 @@
#pragma once
#include <SDL3/SDL_gpu.h>
#include <cstdint>
// ---------------------------------------------------------------------------
@@ -12,9 +13,9 @@
// r,g,b,a: RGBA in [0,1]
// ---------------------------------------------------------------------------
struct BallGPUData {
float cx, cy; // NDC center
float hw, hh; // NDC half-size (positive)
float r, g, b, a; // RGBA color [0,1]
float cx, cy; // NDC center
float hw, hh; // NDC half-size (positive)
float r, g, b, a; // RGBA color [0,1]
};
static_assert(sizeof(BallGPUData) == 32, "BallGPUData must be 32 bytes");
@@ -26,22 +27,21 @@ static_assert(sizeof(BallGPUData) == 32, "BallGPUData must be 32 bytes");
// // Then in render pass: bind buffer, SDL_DrawGPUPrimitives(pass, 6, count, 0, 0)
// ============================================================================
class GpuBallBuffer {
public:
static constexpr int MAX_BALLS = 500000;
public:
static constexpr int MAX_BALLS = 500000;
bool init(SDL_GPUDevice* device);
void destroy(SDL_GPUDevice* device);
bool init(SDL_GPUDevice* device);
void destroy(SDL_GPUDevice* device);
// Upload ball array to GPU via an internal copy pass.
// count is clamped to MAX_BALLS. Returns false on error or empty input.
bool upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd,
const BallGPUData* data, int count);
// Upload ball array to GPU via an internal copy pass.
// count is clamped to MAX_BALLS. Returns false on error or empty input.
bool upload(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd, const BallGPUData* data, int count);
SDL_GPUBuffer* buffer() const { return gpu_buf_; }
int count() const { return count_; }
SDL_GPUBuffer* buffer() const { return gpu_buf_; }
int count() const { return count_; }
private:
SDL_GPUBuffer* gpu_buf_ = nullptr;
SDL_GPUTransferBuffer* transfer_buf_ = nullptr;
int count_ = 0;
private:
SDL_GPUBuffer* gpu_buf_ = nullptr;
SDL_GPUTransferBuffer* transfer_buf_ = nullptr;
int count_ = 0;
};

View File

@@ -1,9 +1,10 @@
#include "gpu_context.hpp"
#include <SDL3/SDL_log.h>
#include <iostream>
bool GpuContext::init(SDL_Window* window) {
auto GpuContext::init(SDL_Window* window) -> bool {
window_ = window;
// Create GPU device: Metal on Apple, Vulkan elsewhere
@@ -13,15 +14,15 @@ bool GpuContext::init(SDL_Window* window) {
SDL_GPUShaderFormat preferred = SDL_GPU_SHADERFORMAT_SPIRV;
#endif
device_ = SDL_CreateGPUDevice(preferred, false, nullptr);
if (!device_) {
std::cerr << "GpuContext: SDL_CreateGPUDevice failed: " << SDL_GetError() << std::endl;
if (device_ == nullptr) {
std::cerr << "GpuContext: SDL_CreateGPUDevice failed: " << SDL_GetError() << '\n';
return false;
}
std::cout << "GpuContext: driver = " << SDL_GetGPUDeviceDriver(device_) << std::endl;
std::cout << "GpuContext: driver = " << SDL_GetGPUDeviceDriver(device_) << '\n';
// Claim the window so the GPU device owns its swapchain
if (!SDL_ClaimWindowForGPUDevice(device_, window_)) {
std::cerr << "GpuContext: SDL_ClaimWindowForGPUDevice failed: " << SDL_GetError() << std::endl;
std::cerr << "GpuContext: SDL_ClaimWindowForGPUDevice failed: " << SDL_GetError() << '\n';
SDL_DestroyGPUDevice(device_);
device_ = nullptr;
return false;
@@ -29,17 +30,15 @@ bool GpuContext::init(SDL_Window* window) {
// Query swapchain format (Metal: typically B8G8R8A8_UNORM or R8G8B8A8_UNORM)
swapchain_format_ = SDL_GetGPUSwapchainTextureFormat(device_, window_);
std::cout << "GpuContext: swapchain format = " << static_cast<int>(swapchain_format_) << std::endl;
std::cout << "GpuContext: swapchain format = " << static_cast<int>(swapchain_format_) << '\n';
// Default: VSync ON
SDL_SetGPUSwapchainParameters(device_, window_,
SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
SDL_GPU_PRESENTMODE_VSYNC);
SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, SDL_GPU_PRESENTMODE_VSYNC);
return true;
}
void GpuContext::destroy() {
if (device_) {
if (device_ != nullptr) {
SDL_WaitForGPUIdle(device_);
SDL_ReleaseWindowFromGPUDevice(device_, window_);
SDL_DestroyGPUDevice(device_);
@@ -48,16 +47,17 @@ void GpuContext::destroy() {
window_ = nullptr;
}
SDL_GPUCommandBuffer* GpuContext::acquireCommandBuffer() {
auto GpuContext::acquireCommandBuffer() -> SDL_GPUCommandBuffer* {
SDL_GPUCommandBuffer* cmd = SDL_AcquireGPUCommandBuffer(device_);
if (!cmd) {
if (cmd == nullptr) {
SDL_Log("GpuContext: SDL_AcquireGPUCommandBuffer failed: %s", SDL_GetError());
}
return cmd;
}
SDL_GPUTexture* GpuContext::acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
Uint32* out_w, Uint32* out_h) {
auto GpuContext::acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
Uint32* out_w,
Uint32* out_h) -> SDL_GPUTexture* {
SDL_GPUTexture* tex = nullptr;
if (!SDL_AcquireGPUSwapchainTexture(cmd_buf, window_, &tex, out_w, out_h)) {
SDL_Log("GpuContext: SDL_AcquireGPUSwapchainTexture failed: %s", SDL_GetError());
@@ -71,10 +71,8 @@ void GpuContext::submit(SDL_GPUCommandBuffer* cmd_buf) {
SDL_SubmitGPUCommandBuffer(cmd_buf);
}
bool GpuContext::setVSync(bool enabled) {
auto GpuContext::setVSync(bool enabled) -> bool {
SDL_GPUPresentMode mode = enabled ? SDL_GPU_PRESENTMODE_VSYNC
: SDL_GPU_PRESENTMODE_IMMEDIATE;
return SDL_SetGPUSwapchainParameters(device_, window_,
SDL_GPU_SWAPCHAINCOMPOSITION_SDR,
mode);
return SDL_SetGPUSwapchainParameters(device_, window_, SDL_GPU_SWAPCHAINCOMPOSITION_SDR, mode);
}

View File

@@ -8,26 +8,27 @@
// Replaces SDL_Renderer as the main rendering backend.
// ============================================================================
class GpuContext {
public:
bool init(SDL_Window* window);
void destroy();
public:
bool init(SDL_Window* window);
void destroy();
SDL_GPUDevice* device() const { return device_; }
SDL_Window* window() const { return window_; }
SDL_GPUTextureFormat swapchainFormat() const { return swapchain_format_; }
SDL_GPUDevice* device() const { return device_; }
SDL_Window* window() const { return window_; }
SDL_GPUTextureFormat swapchainFormat() const { return swapchain_format_; }
// Per-frame helpers
SDL_GPUCommandBuffer* acquireCommandBuffer();
// Returns nullptr if window is minimized (swapchain not available).
SDL_GPUTexture* acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
Uint32* out_w, Uint32* out_h);
void submit(SDL_GPUCommandBuffer* cmd_buf);
// Per-frame helpers
SDL_GPUCommandBuffer* acquireCommandBuffer();
// Returns nullptr if window is minimized (swapchain not available).
SDL_GPUTexture* acquireSwapchainTexture(SDL_GPUCommandBuffer* cmd_buf,
Uint32* out_w,
Uint32* out_h);
static void submit(SDL_GPUCommandBuffer* cmd_buf);
// VSync control (call after init)
bool setVSync(bool enabled);
// VSync control (call after init)
bool setVSync(bool enabled);
private:
SDL_GPUDevice* device_ = nullptr;
SDL_Window* window_ = nullptr;
SDL_GPUTextureFormat swapchain_format_ = SDL_GPU_TEXTUREFORMAT_INVALID;
private:
SDL_GPUDevice* device_ = nullptr;
SDL_Window* window_ = nullptr;
SDL_GPUTextureFormat swapchain_format_ = SDL_GPU_TEXTUREFORMAT_INVALID;
};

View File

@@ -1,18 +1,21 @@
#include "gpu_pipeline.hpp"
#include "gpu_sprite_batch.hpp" // for GpuVertex layout
#include "gpu_ball_buffer.hpp" // for BallGPUData layout
#include <SDL3/SDL_log.h>
#include <array> // for std::array
#include <cstddef> // offsetof
#include <cstring> // strlen
#include "gpu_ball_buffer.hpp" // for BallGPUData layout
#include "gpu_sprite_batch.hpp" // for GpuVertex layout
#ifndef __APPLE__
// Generated at build time by CMake + glslc (see cmake/spv_to_header.cmake)
#include "sprite_vert_spv.h"
#include "sprite_frag_spv.h"
#include "postfx_vert_spv.h"
#include "postfx_frag_spv.h"
#include "ball_vert_spv.h"
#include "postfx_frag_spv.h"
#include "postfx_vert_spv.h"
#include "sprite_frag_spv.h"
#include "sprite_vert_spv.h"
#endif
#ifdef __APPLE__
@@ -198,15 +201,15 @@ vertex BallVOut ball_instanced_vs(BallInstance inst [[stage_in]],
return out;
}
)";
#endif // __APPLE__
#endif // __APPLE__
// ============================================================================
// GpuPipeline implementation
// ============================================================================
bool GpuPipeline::init(SDL_GPUDevice* device,
SDL_GPUTextureFormat target_format,
SDL_GPUTextureFormat offscreen_format) {
auto GpuPipeline::init(SDL_GPUDevice* device,
SDL_GPUTextureFormat target_format,
SDL_GPUTextureFormat offscreen_format) -> bool {
SDL_GPUShaderFormat supported = SDL_GetGPUShaderFormats(device);
#ifdef __APPLE__
if (!(supported & SDL_GPU_SHADERFORMAT_MSL)) {
@@ -214,7 +217,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
return false;
}
#else
if (!(supported & SDL_GPU_SHADERFORMAT_SPIRV)) {
if ((supported & SDL_GPU_SHADERFORMAT_SPIRV) == 0u) {
SDL_Log("GpuPipeline: SPIRV not supported (format mask=%u)", supported);
return false;
}
@@ -224,81 +227,81 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
// Sprite pipeline
// ----------------------------------------------------------------
#ifdef __APPLE__
SDL_GPUShader* sprite_vert = createShader(device, kSpriteVertMSL, "sprite_vs",
SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* sprite_frag = createShader(device, kSpriteFragMSL, "sprite_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
SDL_GPUShader* sprite_vert = createShader(device, kSpriteVertMSL, "sprite_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* sprite_frag = createShader(device, kSpriteFragMSL, "sprite_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
#else
SDL_GPUShader* sprite_vert = createShaderSPIRV(device, ksprite_vert_spv, ksprite_vert_spv_size,
"main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* sprite_frag = createShaderSPIRV(device, ksprite_frag_spv, ksprite_frag_spv_size,
"main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
SDL_GPUShader* sprite_vert = createShaderSPIRV(device, ksprite_vert_spv, ksprite_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* sprite_frag = createShaderSPIRV(device, ksprite_frag_spv, ksprite_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
#endif
if (!sprite_vert || !sprite_frag) {
if ((sprite_vert == nullptr) || (sprite_frag == nullptr)) {
SDL_Log("GpuPipeline: failed to create sprite shaders");
if (sprite_vert) SDL_ReleaseGPUShader(device, sprite_vert);
if (sprite_frag) SDL_ReleaseGPUShader(device, sprite_frag);
if (sprite_vert != nullptr) {
SDL_ReleaseGPUShader(device, sprite_vert);
}
if (sprite_frag != nullptr) {
SDL_ReleaseGPUShader(device, sprite_frag);
}
return false;
}
// Vertex input: GpuVertex layout
SDL_GPUVertexBufferDescription vb_desc = {};
vb_desc.slot = 0;
vb_desc.pitch = sizeof(GpuVertex);
vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
vb_desc.slot = 0;
vb_desc.pitch = sizeof(GpuVertex);
vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_VERTEX;
vb_desc.instance_step_rate = 0;
SDL_GPUVertexAttribute attrs[3] = {};
attrs[0].location = 0;
std::array<SDL_GPUVertexAttribute, 3> attrs = {};
attrs[0].location = 0;
attrs[0].buffer_slot = 0;
attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
attrs[0].offset = static_cast<Uint32>(offsetof(GpuVertex, x));
attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
attrs[0].offset = static_cast<Uint32>(offsetof(GpuVertex, x));
attrs[1].location = 1;
attrs[1].location = 1;
attrs[1].buffer_slot = 0;
attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
attrs[1].offset = static_cast<Uint32>(offsetof(GpuVertex, u));
attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
attrs[1].offset = static_cast<Uint32>(offsetof(GpuVertex, u));
attrs[2].location = 2;
attrs[2].location = 2;
attrs[2].buffer_slot = 0;
attrs[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
attrs[2].offset = static_cast<Uint32>(offsetof(GpuVertex, r));
attrs[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
attrs[2].offset = static_cast<Uint32>(offsetof(GpuVertex, r));
SDL_GPUVertexInputState vertex_input = {};
vertex_input.vertex_buffer_descriptions = &vb_desc;
vertex_input.num_vertex_buffers = 1;
vertex_input.vertex_attributes = attrs;
vertex_input.num_vertex_attributes = 3;
vertex_input.num_vertex_buffers = 1;
vertex_input.vertex_attributes = attrs.data();
vertex_input.num_vertex_attributes = 3;
// Alpha blend state (SRC_ALPHA, ONE_MINUS_SRC_ALPHA)
SDL_GPUColorTargetBlendState blend = {};
blend.enable_blend = true;
blend.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
blend.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
blend.color_blend_op = SDL_GPU_BLENDOP_ADD;
blend.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE;
blend.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
blend.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
blend.enable_color_write_mask = false; // write all channels
blend.enable_blend = true;
blend.src_color_blendfactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA;
blend.dst_color_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
blend.color_blend_op = SDL_GPU_BLENDOP_ADD;
blend.src_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE;
blend.dst_alpha_blendfactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
blend.alpha_blend_op = SDL_GPU_BLENDOP_ADD;
blend.enable_color_write_mask = false; // write all channels
SDL_GPUColorTargetDescription color_target_desc = {};
color_target_desc.format = offscreen_format;
color_target_desc.format = offscreen_format;
color_target_desc.blend_state = blend;
SDL_GPUGraphicsPipelineCreateInfo sprite_pipe_info = {};
sprite_pipe_info.vertex_shader = sprite_vert;
sprite_pipe_info.fragment_shader = sprite_frag;
sprite_pipe_info.vertex_shader = sprite_vert;
sprite_pipe_info.fragment_shader = sprite_frag;
sprite_pipe_info.vertex_input_state = vertex_input;
sprite_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
sprite_pipe_info.target_info.num_color_targets = 1;
sprite_pipe_info.target_info.color_target_descriptions = &color_target_desc;
sprite_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
sprite_pipe_info.target_info.num_color_targets = 1;
sprite_pipe_info.target_info.color_target_descriptions = &color_target_desc;
sprite_pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &sprite_pipe_info);
SDL_ReleaseGPUShader(device, sprite_vert);
SDL_ReleaseGPUShader(device, sprite_frag);
if (!sprite_pipeline_) {
if (sprite_pipeline_ == nullptr) {
SDL_Log("GpuPipeline: sprite pipeline creation failed: %s", SDL_GetError());
return false;
}
@@ -310,59 +313,59 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
// Targets: offscreen (same as sprite pipeline)
// ----------------------------------------------------------------
#ifdef __APPLE__
SDL_GPUShader* ball_vert = createShader(device, kBallInstancedVertMSL, "ball_instanced_vs",
SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* ball_frag = createShader(device, kSpriteFragMSL, "sprite_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
SDL_GPUShader* ball_vert = createShader(device, kBallInstancedVertMSL, "ball_instanced_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* ball_frag = createShader(device, kSpriteFragMSL, "sprite_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
#else
SDL_GPUShader* ball_vert = createShaderSPIRV(device, kball_vert_spv, kball_vert_spv_size,
"main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* ball_frag = createShaderSPIRV(device, ksprite_frag_spv, ksprite_frag_spv_size,
"main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
SDL_GPUShader* ball_vert = createShaderSPIRV(device, kball_vert_spv, kball_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* ball_frag = createShaderSPIRV(device, ksprite_frag_spv, ksprite_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 0);
#endif
if (!ball_vert || !ball_frag) {
if ((ball_vert == nullptr) || (ball_frag == nullptr)) {
SDL_Log("GpuPipeline: failed to create ball instanced shaders");
if (ball_vert) SDL_ReleaseGPUShader(device, ball_vert);
if (ball_frag) SDL_ReleaseGPUShader(device, ball_frag);
if (ball_vert != nullptr) {
SDL_ReleaseGPUShader(device, ball_vert);
}
if (ball_frag != nullptr) {
SDL_ReleaseGPUShader(device, ball_frag);
}
return false;
}
// Vertex input: BallGPUData as per-instance data (step rate = 1 instance)
SDL_GPUVertexBufferDescription ball_vb_desc = {};
ball_vb_desc.slot = 0;
ball_vb_desc.pitch = sizeof(BallGPUData);
ball_vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_INSTANCE;
ball_vb_desc.slot = 0;
ball_vb_desc.pitch = sizeof(BallGPUData);
ball_vb_desc.input_rate = SDL_GPU_VERTEXINPUTRATE_INSTANCE;
ball_vb_desc.instance_step_rate = 1;
SDL_GPUVertexAttribute ball_attrs[3] = {};
std::array<SDL_GPUVertexAttribute, 3> ball_attrs = {};
// attr 0: center (float2) at offset 0
ball_attrs[0].location = 0;
ball_attrs[0].location = 0;
ball_attrs[0].buffer_slot = 0;
ball_attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
ball_attrs[0].offset = static_cast<Uint32>(offsetof(BallGPUData, cx));
ball_attrs[0].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
ball_attrs[0].offset = static_cast<Uint32>(offsetof(BallGPUData, cx));
// attr 1: half-size (float2) at offset 8
ball_attrs[1].location = 1;
ball_attrs[1].location = 1;
ball_attrs[1].buffer_slot = 0;
ball_attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
ball_attrs[1].offset = static_cast<Uint32>(offsetof(BallGPUData, hw));
ball_attrs[1].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2;
ball_attrs[1].offset = static_cast<Uint32>(offsetof(BallGPUData, hw));
// attr 2: color (float4) at offset 16
ball_attrs[2].location = 2;
ball_attrs[2].location = 2;
ball_attrs[2].buffer_slot = 0;
ball_attrs[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
ball_attrs[2].offset = static_cast<Uint32>(offsetof(BallGPUData, r));
ball_attrs[2].format = SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4;
ball_attrs[2].offset = static_cast<Uint32>(offsetof(BallGPUData, r));
SDL_GPUVertexInputState ball_vertex_input = {};
ball_vertex_input.vertex_buffer_descriptions = &ball_vb_desc;
ball_vertex_input.num_vertex_buffers = 1;
ball_vertex_input.vertex_attributes = ball_attrs;
ball_vertex_input.num_vertex_attributes = 3;
ball_vertex_input.num_vertex_buffers = 1;
ball_vertex_input.vertex_attributes = ball_attrs.data();
ball_vertex_input.num_vertex_attributes = 3;
SDL_GPUGraphicsPipelineCreateInfo ball_pipe_info = {};
ball_pipe_info.vertex_shader = ball_vert;
ball_pipe_info.fragment_shader = ball_frag;
ball_pipe_info.vertex_shader = ball_vert;
ball_pipe_info.fragment_shader = ball_frag;
ball_pipe_info.vertex_input_state = ball_vertex_input;
ball_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
ball_pipe_info.target_info.num_color_targets = 1;
ball_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
ball_pipe_info.target_info.num_color_targets = 1;
ball_pipe_info.target_info.color_target_descriptions = &color_target_desc;
ball_pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &ball_pipe_info);
@@ -370,7 +373,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
SDL_ReleaseGPUShader(device, ball_vert);
SDL_ReleaseGPUShader(device, ball_frag);
if (!ball_pipeline_) {
if (ball_pipeline_ == nullptr) {
SDL_Log("GpuPipeline: ball instanced pipeline creation failed: %s", SDL_GetError());
return false;
}
@@ -389,20 +392,20 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
// PostFX pipeline
// ----------------------------------------------------------------
#ifdef __APPLE__
SDL_GPUShader* postfx_vert = createShader(device, kPostFXVertMSL, "postfx_vs",
SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* postfx_frag = createShader(device, kPostFXFragMSL, "postfx_fs",
SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
SDL_GPUShader* postfx_vert = createShader(device, kPostFXVertMSL, "postfx_vs", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* postfx_frag = createShader(device, kPostFXFragMSL, "postfx_fs", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#else
SDL_GPUShader* postfx_vert = createShaderSPIRV(device, kpostfx_vert_spv, kpostfx_vert_spv_size,
"main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* postfx_frag = createShaderSPIRV(device, kpostfx_frag_spv, kpostfx_frag_spv_size,
"main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
SDL_GPUShader* postfx_vert = createShaderSPIRV(device, kpostfx_vert_spv, kpostfx_vert_spv_size, "main", SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
SDL_GPUShader* postfx_frag = createShaderSPIRV(device, kpostfx_frag_spv, kpostfx_frag_spv_size, "main", SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
#endif
if (!postfx_vert || !postfx_frag) {
if ((postfx_vert == nullptr) || (postfx_frag == nullptr)) {
SDL_Log("GpuPipeline: failed to create postfx shaders");
if (postfx_vert) SDL_ReleaseGPUShader(device, postfx_vert);
if (postfx_frag) SDL_ReleaseGPUShader(device, postfx_frag);
if (postfx_vert != nullptr) {
SDL_ReleaseGPUShader(device, postfx_vert);
}
if (postfx_frag != nullptr) {
SDL_ReleaseGPUShader(device, postfx_frag);
}
return false;
}
@@ -412,17 +415,17 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
no_blend.enable_color_write_mask = false;
SDL_GPUColorTargetDescription postfx_target_desc = {};
postfx_target_desc.format = target_format;
postfx_target_desc.format = target_format;
postfx_target_desc.blend_state = no_blend;
SDL_GPUVertexInputState no_input = {};
SDL_GPUGraphicsPipelineCreateInfo postfx_pipe_info = {};
postfx_pipe_info.vertex_shader = postfx_vert;
postfx_pipe_info.fragment_shader = postfx_frag;
postfx_pipe_info.vertex_shader = postfx_vert;
postfx_pipe_info.fragment_shader = postfx_frag;
postfx_pipe_info.vertex_input_state = no_input;
postfx_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
postfx_pipe_info.target_info.num_color_targets = 1;
postfx_pipe_info.primitive_type = SDL_GPU_PRIMITIVETYPE_TRIANGLELIST;
postfx_pipe_info.target_info.num_color_targets = 1;
postfx_pipe_info.target_info.color_target_descriptions = &postfx_target_desc;
postfx_pipeline_ = SDL_CreateGPUGraphicsPipeline(device, &postfx_pipe_info);
@@ -430,7 +433,7 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
SDL_ReleaseGPUShader(device, postfx_vert);
SDL_ReleaseGPUShader(device, postfx_frag);
if (!postfx_pipeline_) {
if (postfx_pipeline_ == nullptr) {
SDL_Log("GpuPipeline: postfx pipeline creation failed: %s", SDL_GetError());
return false;
}
@@ -440,55 +443,65 @@ bool GpuPipeline::init(SDL_GPUDevice* device,
}
void GpuPipeline::destroy(SDL_GPUDevice* device) {
if (sprite_pipeline_) { SDL_ReleaseGPUGraphicsPipeline(device, sprite_pipeline_); sprite_pipeline_ = nullptr; }
if (ball_pipeline_) { SDL_ReleaseGPUGraphicsPipeline(device, ball_pipeline_); ball_pipeline_ = nullptr; }
if (postfx_pipeline_) { SDL_ReleaseGPUGraphicsPipeline(device, postfx_pipeline_); postfx_pipeline_ = nullptr; }
if (sprite_pipeline_ != nullptr) {
SDL_ReleaseGPUGraphicsPipeline(device, sprite_pipeline_);
sprite_pipeline_ = nullptr;
}
if (ball_pipeline_ != nullptr) {
SDL_ReleaseGPUGraphicsPipeline(device, ball_pipeline_);
ball_pipeline_ = nullptr;
}
if (postfx_pipeline_ != nullptr) {
SDL_ReleaseGPUGraphicsPipeline(device, postfx_pipeline_);
postfx_pipeline_ = nullptr;
}
}
SDL_GPUShader* GpuPipeline::createShaderSPIRV(SDL_GPUDevice* device,
const uint8_t* spv_code,
size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers,
Uint32 num_storage_buffers) {
auto GpuPipeline::createShaderSPIRV(SDL_GPUDevice* device,
const uint8_t* spv_code,
size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers,
Uint32 num_storage_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info = {};
info.code = spv_code;
info.code_size = spv_size;
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
info.stage = stage;
info.num_samplers = num_samplers;
info.code = spv_code;
info.code_size = spv_size;
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_SPIRV;
info.stage = stage;
info.num_samplers = num_samplers;
info.num_storage_textures = 0;
info.num_storage_buffers = num_storage_buffers;
info.num_uniform_buffers = num_uniform_buffers;
info.num_storage_buffers = num_storage_buffers;
info.num_uniform_buffers = num_uniform_buffers;
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
if (!shader)
if (shader == nullptr) {
SDL_Log("GpuPipeline: SPIRV shader '%s' failed: %s", entrypoint, SDL_GetError());
}
return shader;
}
SDL_GPUShader* GpuPipeline::createShader(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers,
Uint32 num_storage_buffers) {
auto GpuPipeline::createShader(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers,
Uint32 num_storage_buffers) -> SDL_GPUShader* {
SDL_GPUShaderCreateInfo info = {};
info.code = reinterpret_cast<const Uint8*>(msl_source);
info.code_size = static_cast<size_t>(strlen(msl_source) + 1);
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_MSL;
info.stage = stage;
info.num_samplers = num_samplers;
info.code = reinterpret_cast<const Uint8*>(msl_source);
info.code_size = static_cast<size_t>(strlen(msl_source) + 1);
info.entrypoint = entrypoint;
info.format = SDL_GPU_SHADERFORMAT_MSL;
info.stage = stage;
info.num_samplers = num_samplers;
info.num_storage_textures = 0;
info.num_storage_buffers = num_storage_buffers;
info.num_uniform_buffers = num_uniform_buffers;
info.num_storage_buffers = num_storage_buffers;
info.num_uniform_buffers = num_uniform_buffers;
SDL_GPUShader* shader = SDL_CreateGPUShader(device, &info);
if (!shader) {
if (shader == nullptr) {
SDL_Log("GpuPipeline: shader '%s' failed: %s", entrypoint, SDL_GetError());
}
return shader;

View File

@@ -8,10 +8,10 @@
// MSL binding: constant PostFXUniforms& u [[buffer(0)]]
// ============================================================================
struct PostFXUniforms {
float vignette_strength; // 0 = none, 0.8 = default subtle
float chroma_strength; // 0 = off, 0.2 = default chromatic aberration
float scanline_strength; // 0 = off, 1 = full scanlines
float screen_height; // logical render target height (px), for resolution-independent scanlines
float vignette_strength; // 0 = none, 0.8 = default subtle
float chroma_strength; // 0 = off, 0.2 = default chromatic aberration
float scanline_strength; // 0 = off, 1 = full scanlines
float screen_height; // logical render target height (px), for resolution-independent scanlines
};
// ============================================================================
@@ -27,37 +27,37 @@ struct PostFXUniforms {
// Accepts PostFXUniforms via fragment uniform buffer slot 0.
// ============================================================================
class GpuPipeline {
public:
// target_format: pass SDL_GetGPUSwapchainTextureFormat() result.
// offscreen_format: format of the offscreen render target.
bool init(SDL_GPUDevice* device,
SDL_GPUTextureFormat target_format,
SDL_GPUTextureFormat offscreen_format);
void destroy(SDL_GPUDevice* device);
public:
// target_format: pass SDL_GetGPUSwapchainTextureFormat() result.
// offscreen_format: format of the offscreen render target.
bool init(SDL_GPUDevice* device,
SDL_GPUTextureFormat target_format,
SDL_GPUTextureFormat offscreen_format);
void destroy(SDL_GPUDevice* device);
SDL_GPUGraphicsPipeline* spritePipeline() const { return sprite_pipeline_; }
SDL_GPUGraphicsPipeline* ballPipeline() const { return ball_pipeline_; }
SDL_GPUGraphicsPipeline* postfxPipeline() const { return postfx_pipeline_; }
SDL_GPUGraphicsPipeline* spritePipeline() const { return sprite_pipeline_; }
SDL_GPUGraphicsPipeline* ballPipeline() const { return ball_pipeline_; }
SDL_GPUGraphicsPipeline* postfxPipeline() const { return postfx_pipeline_; }
private:
SDL_GPUShader* createShader(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers,
Uint32 num_storage_buffers = 0);
private:
static SDL_GPUShader* createShader(SDL_GPUDevice* device,
const char* msl_source,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers,
Uint32 num_storage_buffers = 0);
SDL_GPUShader* createShaderSPIRV(SDL_GPUDevice* device,
const uint8_t* spv_code,
size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers,
Uint32 num_storage_buffers = 0);
static SDL_GPUShader* createShaderSPIRV(SDL_GPUDevice* device,
const uint8_t* spv_code,
size_t spv_size,
const char* entrypoint,
SDL_GPUShaderStage stage,
Uint32 num_samplers,
Uint32 num_uniform_buffers,
Uint32 num_storage_buffers = 0);
SDL_GPUGraphicsPipeline* sprite_pipeline_ = nullptr;
SDL_GPUGraphicsPipeline* ball_pipeline_ = nullptr;
SDL_GPUGraphicsPipeline* postfx_pipeline_ = nullptr;
SDL_GPUGraphicsPipeline* sprite_pipeline_ = nullptr;
SDL_GPUGraphicsPipeline* ball_pipeline_ = nullptr;
SDL_GPUGraphicsPipeline* postfx_pipeline_ = nullptr;
};

View File

@@ -1,28 +1,29 @@
#include "gpu_sprite_batch.hpp"
#include <SDL3/SDL_log.h>
#include <cstring> // memcpy
// ---------------------------------------------------------------------------
// Public interface
// ---------------------------------------------------------------------------
bool GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) {
auto GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) -> bool {
max_sprites_ = max_sprites;
// Pre-allocate GPU buffers large enough for (max_sprites_ + 2) quads.
// The +2 reserves one slot for the background quad and one for the fullscreen overlay.
Uint32 max_verts = static_cast<Uint32>(max_sprites_ + 2) * 4;
Uint32 max_verts = static_cast<Uint32>(max_sprites_ + 2) * 4;
Uint32 max_indices = static_cast<Uint32>(max_sprites_ + 2) * 6;
Uint32 vb_size = max_verts * sizeof(GpuVertex);
Uint32 vb_size = max_verts * sizeof(GpuVertex);
Uint32 ib_size = max_indices * sizeof(uint32_t);
// Vertex buffer
SDL_GPUBufferCreateInfo vb_info = {};
vb_info.usage = SDL_GPU_BUFFERUSAGE_VERTEX;
vb_info.size = vb_size;
vb_info.size = vb_size;
vertex_buf_ = SDL_CreateGPUBuffer(device, &vb_info);
if (!vertex_buf_) {
if (vertex_buf_ == nullptr) {
SDL_Log("GpuSpriteBatch: vertex buffer creation failed: %s", SDL_GetError());
return false;
}
@@ -30,9 +31,9 @@ bool GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) {
// Index buffer
SDL_GPUBufferCreateInfo ib_info = {};
ib_info.usage = SDL_GPU_BUFFERUSAGE_INDEX;
ib_info.size = ib_size;
ib_info.size = ib_size;
index_buf_ = SDL_CreateGPUBuffer(device, &ib_info);
if (!index_buf_) {
if (index_buf_ == nullptr) {
SDL_Log("GpuSpriteBatch: index buffer creation failed: %s", SDL_GetError());
return false;
}
@@ -41,16 +42,16 @@ bool GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) {
SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = vb_size;
tb_info.size = vb_size;
vertex_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
if (!vertex_transfer_) {
if (vertex_transfer_ == nullptr) {
SDL_Log("GpuSpriteBatch: vertex transfer buffer failed: %s", SDL_GetError());
return false;
}
tb_info.size = ib_size;
tb_info.size = ib_size;
index_transfer_ = SDL_CreateGPUTransferBuffer(device, &tb_info);
if (!index_transfer_) {
if (index_transfer_ == nullptr) {
SDL_Log("GpuSpriteBatch: index transfer buffer failed: %s", SDL_GetError());
return false;
}
@@ -61,67 +62,84 @@ bool GpuSpriteBatch::init(SDL_GPUDevice* device, int max_sprites) {
}
void GpuSpriteBatch::destroy(SDL_GPUDevice* device) {
if (!device) return;
if (vertex_transfer_) { SDL_ReleaseGPUTransferBuffer(device, vertex_transfer_); vertex_transfer_ = nullptr; }
if (index_transfer_) { SDL_ReleaseGPUTransferBuffer(device, index_transfer_); index_transfer_ = nullptr; }
if (vertex_buf_) { SDL_ReleaseGPUBuffer(device, vertex_buf_); vertex_buf_ = nullptr; }
if (index_buf_) { SDL_ReleaseGPUBuffer(device, index_buf_); index_buf_ = nullptr; }
if (device == nullptr) {
return;
}
if (vertex_transfer_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(device, vertex_transfer_);
vertex_transfer_ = nullptr;
}
if (index_transfer_ != nullptr) {
SDL_ReleaseGPUTransferBuffer(device, index_transfer_);
index_transfer_ = nullptr;
}
if (vertex_buf_ != nullptr) {
SDL_ReleaseGPUBuffer(device, vertex_buf_);
vertex_buf_ = nullptr;
}
if (index_buf_ != nullptr) {
SDL_ReleaseGPUBuffer(device, index_buf_);
index_buf_ = nullptr;
}
}
void GpuSpriteBatch::beginFrame() {
vertices_.clear();
indices_.clear();
bg_index_count_ = 0;
sprite_index_offset_ = 0;
sprite_index_count_ = 0;
bg_index_count_ = 0;
sprite_index_offset_ = 0;
sprite_index_count_ = 0;
overlay_index_offset_ = 0;
overlay_index_count_ = 0;
overlay_index_count_ = 0;
}
void GpuSpriteBatch::addBackground(float screen_w, float screen_h,
float top_r, float top_g, float top_b,
float bot_r, float bot_g, float bot_b) {
void GpuSpriteBatch::addBackground(float screen_w, float screen_h, float top_r, float top_g, float top_b, float bot_r, float bot_g, float bot_b) {
// Background is the full screen quad, corners:
// TL(-1, 1) TR(1, 1) → top color
// BL(-1,-1) BR(1,-1) → bottom color
// We push it as 4 separate vertices (different colors per row).
uint32_t vi = static_cast<uint32_t>(vertices_.size());
auto vi = static_cast<uint32_t>(vertices_.size());
// Top-left
vertices_.push_back({ -1.0f, 1.0f, 0.0f, 0.0f, top_r, top_g, top_b, 1.0f });
vertices_.push_back({-1.0f, 1.0f, 0.0f, 0.0f, top_r, top_g, top_b, 1.0f});
// Top-right
vertices_.push_back({ 1.0f, 1.0f, 1.0f, 0.0f, top_r, top_g, top_b, 1.0f });
vertices_.push_back({1.0f, 1.0f, 1.0f, 0.0f, top_r, top_g, top_b, 1.0f});
// Bottom-right
vertices_.push_back({ 1.0f, -1.0f, 1.0f, 1.0f, bot_r, bot_g, bot_b, 1.0f });
vertices_.push_back({1.0f, -1.0f, 1.0f, 1.0f, bot_r, bot_g, bot_b, 1.0f});
// Bottom-left
vertices_.push_back({ -1.0f, -1.0f, 0.0f, 1.0f, bot_r, bot_g, bot_b, 1.0f });
vertices_.push_back({-1.0f, -1.0f, 0.0f, 1.0f, bot_r, bot_g, bot_b, 1.0f});
// Two triangles: TL-TR-BR, BR-BL-TL
indices_.push_back(vi + 0); indices_.push_back(vi + 1); indices_.push_back(vi + 2);
indices_.push_back(vi + 2); indices_.push_back(vi + 3); indices_.push_back(vi + 0);
indices_.push_back(vi + 0);
indices_.push_back(vi + 1);
indices_.push_back(vi + 2);
indices_.push_back(vi + 2);
indices_.push_back(vi + 3);
indices_.push_back(vi + 0);
bg_index_count_ = 6;
bg_index_count_ = 6;
sprite_index_offset_ = 6;
(void)screen_w; (void)screen_h; // unused — bg always covers full NDC
(void)screen_w;
(void)screen_h; // unused — bg always covers full NDC
}
void GpuSpriteBatch::addSprite(float x, float y, float w, float h,
float r, float g, float b, float a,
float scale,
float screen_w, float screen_h) {
void GpuSpriteBatch::addSprite(float x, float y, float w, float h, float r, float g, float b, float a, float scale, float screen_w, float screen_h) {
// Apply scale around the sprite centre
float scaled_w = w * scale;
float scaled_h = h * scale;
float offset_x = (w - scaled_w) * 0.5f;
float offset_y = (h - scaled_h) * 0.5f;
float scaled_w = w * scale;
float scaled_h = h * scale;
float offset_x = (w - scaled_w) * 0.5f;
float offset_y = (h - scaled_h) * 0.5f;
float px0 = x + offset_x;
float py0 = y + offset_y;
float px1 = px0 + scaled_w;
float py1 = py0 + scaled_h;
float ndx0, ndy0, ndx1, ndy1;
float ndx0;
float ndy0;
float ndx1;
float ndy1;
toNDC(px0, py0, screen_w, screen_h, ndx0, ndy0);
toNDC(px1, py1, screen_w, screen_h, ndx1, ndy1);
@@ -133,42 +151,54 @@ void GpuSpriteBatch::addFullscreenOverlay() {
// El overlay es un slot reservado fuera del espacio de max_sprites_, igual que el background.
// Escribe directamente sin pasar por el guard de pushQuad().
overlay_index_offset_ = static_cast<int>(indices_.size());
uint32_t vi = static_cast<uint32_t>(vertices_.size());
vertices_.push_back({ -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f });
vertices_.push_back({ 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f });
vertices_.push_back({ 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f });
vertices_.push_back({ -1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f });
indices_.push_back(vi + 0); indices_.push_back(vi + 1); indices_.push_back(vi + 2);
indices_.push_back(vi + 2); indices_.push_back(vi + 3); indices_.push_back(vi + 0);
auto vi = static_cast<uint32_t>(vertices_.size());
vertices_.push_back({-1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f});
vertices_.push_back({1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f});
vertices_.push_back({1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f});
vertices_.push_back({-1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f});
indices_.push_back(vi + 0);
indices_.push_back(vi + 1);
indices_.push_back(vi + 2);
indices_.push_back(vi + 2);
indices_.push_back(vi + 3);
indices_.push_back(vi + 0);
overlay_index_count_ = 6;
}
bool GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf) {
if (vertices_.empty()) return false;
auto GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf) -> bool {
if (vertices_.empty()) {
return false;
}
Uint32 vb_size = static_cast<Uint32>(vertices_.size() * sizeof(GpuVertex));
Uint32 ib_size = static_cast<Uint32>(indices_.size() * sizeof(uint32_t));
auto vb_size = static_cast<Uint32>(vertices_.size() * sizeof(GpuVertex));
auto ib_size = static_cast<Uint32>(indices_.size() * sizeof(uint32_t));
// Map → write → unmap transfer buffers
void* vp = SDL_MapGPUTransferBuffer(device, vertex_transfer_, true /* cycle */);
if (!vp) { SDL_Log("GpuSpriteBatch: vertex map failed"); return false; }
if (vp == nullptr) {
SDL_Log("GpuSpriteBatch: vertex map failed");
return false;
}
memcpy(vp, vertices_.data(), vb_size);
SDL_UnmapGPUTransferBuffer(device, vertex_transfer_);
void* ip = SDL_MapGPUTransferBuffer(device, index_transfer_, true /* cycle */);
if (!ip) { SDL_Log("GpuSpriteBatch: index map failed"); return false; }
if (ip == nullptr) {
SDL_Log("GpuSpriteBatch: index map failed");
return false;
}
memcpy(ip, indices_.data(), ib_size);
SDL_UnmapGPUTransferBuffer(device, index_transfer_);
// Upload via copy pass
SDL_GPUCopyPass* copy = SDL_BeginGPUCopyPass(cmd_buf);
SDL_GPUTransferBufferLocation v_src = { vertex_transfer_, 0 };
SDL_GPUBufferRegion v_dst = { vertex_buf_, 0, vb_size };
SDL_GPUTransferBufferLocation v_src = {vertex_transfer_, 0};
SDL_GPUBufferRegion v_dst = {vertex_buf_, 0, vb_size};
SDL_UploadToGPUBuffer(copy, &v_src, &v_dst, true /* cycle */);
SDL_GPUTransferBufferLocation i_src = { index_transfer_, 0 };
SDL_GPUBufferRegion i_dst = { index_buf_, 0, ib_size };
SDL_GPUTransferBufferLocation i_src = {index_transfer_, 0};
SDL_GPUBufferRegion i_dst = {index_buf_, 0, ib_size};
SDL_UploadToGPUBuffer(copy, &i_src, &i_dst, true /* cycle */);
SDL_EndGPUCopyPass(copy);
@@ -179,26 +209,28 @@ bool GpuSpriteBatch::uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cm
// Private helpers
// ---------------------------------------------------------------------------
void GpuSpriteBatch::toNDC(float px, float py,
float screen_w, float screen_h,
float& ndx, float& ndy) const {
void GpuSpriteBatch::toNDC(float px, float py, float screen_w, float screen_h, float& ndx, float& ndy) {
ndx = (px / screen_w) * 2.0f - 1.0f;
ndy = 1.0f - (py / screen_h) * 2.0f;
}
void GpuSpriteBatch::pushQuad(float ndx0, float ndy0, float ndx1, float ndy1,
float u0, float v0, float u1, float v1,
float r, float g, float b, float a) {
void GpuSpriteBatch::pushQuad(float ndx0, float ndy0, float ndx1, float ndy1, float u0, float v0, float u1, float v1, float r, float g, float b, float a) {
// +1 reserva el slot del background que ya entró sin pasar por este guard.
if (vertices_.size() + 4 > static_cast<size_t>(max_sprites_ + 1) * 4) return;
uint32_t vi = static_cast<uint32_t>(vertices_.size());
if (vertices_.size() + 4 > static_cast<size_t>(max_sprites_ + 1) * 4) {
return;
}
auto vi = static_cast<uint32_t>(vertices_.size());
// TL, TR, BR, BL
vertices_.push_back({ ndx0, ndy0, u0, v0, r, g, b, a });
vertices_.push_back({ ndx1, ndy0, u1, v0, r, g, b, a });
vertices_.push_back({ ndx1, ndy1, u1, v1, r, g, b, a });
vertices_.push_back({ ndx0, ndy1, u0, v1, r, g, b, a });
vertices_.push_back({ndx0, ndy0, u0, v0, r, g, b, a});
vertices_.push_back({ndx1, ndy0, u1, v0, r, g, b, a});
vertices_.push_back({ndx1, ndy1, u1, v1, r, g, b, a});
vertices_.push_back({ndx0, ndy1, u0, v1, r, g, b, a});
indices_.push_back(vi + 0); indices_.push_back(vi + 1); indices_.push_back(vi + 2);
indices_.push_back(vi + 2); indices_.push_back(vi + 3); indices_.push_back(vi + 0);
indices_.push_back(vi + 0);
indices_.push_back(vi + 1);
indices_.push_back(vi + 2);
indices_.push_back(vi + 2);
indices_.push_back(vi + 3);
indices_.push_back(vi + 0);
}

View File

@@ -1,17 +1,18 @@
#pragma once
#include <SDL3/SDL_gpu.h>
#include <vector>
#include <cstdint>
#include <vector>
// ---------------------------------------------------------------------------
// GpuVertex — 8-float vertex layout sent to the GPU.
// Position is in NDC (pre-transformed on CPU), UV in [0,1], color in [0,1].
// ---------------------------------------------------------------------------
struct GpuVertex {
float x, y; // NDC position (1..1)
float u, v; // Texture coords (0..1)
float r, g, b, a; // RGBA color (0..1)
float x, y; // NDC position (1..1)
float u, v; // Texture coords (0..1)
float r, g, b, a; // RGBA color (0..1)
};
// ============================================================================
@@ -25,64 +26,56 @@ struct GpuVertex {
// // Then in render pass: bind buffers, draw bg with white tex, draw sprites.
// ============================================================================
class GpuSpriteBatch {
public:
// Default maximum sprites (background + UI overlay each count as one sprite)
static constexpr int DEFAULT_MAX_SPRITES = 200000;
public:
// Default maximum sprites (background + UI overlay each count as one sprite)
static constexpr int DEFAULT_MAX_SPRITES = 200000;
bool init(SDL_GPUDevice* device, int max_sprites = DEFAULT_MAX_SPRITES);
void destroy(SDL_GPUDevice* device);
bool init(SDL_GPUDevice* device, int max_sprites = DEFAULT_MAX_SPRITES);
void destroy(SDL_GPUDevice* device);
void beginFrame();
void beginFrame();
// Add the full-screen background gradient quad.
// top_* and bot_* are RGB in [0,1].
void addBackground(float screen_w, float screen_h,
float top_r, float top_g, float top_b,
float bot_r, float bot_g, float bot_b);
// Add the full-screen background gradient quad.
// top_* and bot_* are RGB in [0,1].
void addBackground(float screen_w, float screen_h, float top_r, float top_g, float top_b, float bot_r, float bot_g, float bot_b);
// Add a sprite quad (pixel coordinates).
// scale: uniform scale around the quad centre.
void addSprite(float x, float y, float w, float h,
float r, float g, float b, float a,
float scale,
float screen_w, float screen_h);
// Add a sprite quad (pixel coordinates).
// scale: uniform scale around the quad centre.
void addSprite(float x, float y, float w, float h, float r, float g, float b, float a, float scale, float screen_w, float screen_h);
// Add a full-screen overlay quad (e.g. UI surface, NDC 1..1).
void addFullscreenOverlay();
// Add a full-screen overlay quad (e.g. UI surface, NDC 1..1).
void addFullscreenOverlay();
// Upload CPU vectors to GPU buffers via a copy pass.
// Returns false if the batch is empty.
bool uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf);
// Upload CPU vectors to GPU buffers via a copy pass.
// Returns false if the batch is empty.
bool uploadBatch(SDL_GPUDevice* device, SDL_GPUCommandBuffer* cmd_buf);
SDL_GPUBuffer* vertexBuffer() const { return vertex_buf_; }
SDL_GPUBuffer* indexBuffer() const { return index_buf_; }
int bgIndexCount() const { return bg_index_count_; }
int overlayIndexOffset() const { return overlay_index_offset_; }
int overlayIndexCount() const { return overlay_index_count_; }
int spriteIndexOffset() const { return sprite_index_offset_; }
int spriteIndexCount() const { return sprite_index_count_; }
bool isEmpty() const { return vertices_.empty(); }
SDL_GPUBuffer* vertexBuffer() const { return vertex_buf_; }
SDL_GPUBuffer* indexBuffer() const { return index_buf_; }
int bgIndexCount() const { return bg_index_count_; }
int overlayIndexOffset() const { return overlay_index_offset_; }
int overlayIndexCount() const { return overlay_index_count_; }
int spriteIndexOffset() const { return sprite_index_offset_; }
int spriteIndexCount() const { return sprite_index_count_; }
bool isEmpty() const { return vertices_.empty(); }
private:
void toNDC(float px, float py, float screen_w, float screen_h,
float& ndx, float& ndy) const;
void pushQuad(float ndx0, float ndy0, float ndx1, float ndy1,
float u0, float v0, float u1, float v1,
float r, float g, float b, float a);
private:
static void toNDC(float px, float py, float screen_w, float screen_h, float& ndx, float& ndy);
void pushQuad(float ndx0, float ndy0, float ndx1, float ndy1, float u0, float v0, float u1, float v1, float r, float g, float b, float a);
std::vector<GpuVertex> vertices_;
std::vector<uint32_t> indices_;
std::vector<GpuVertex> vertices_;
std::vector<uint32_t> indices_;
SDL_GPUBuffer* vertex_buf_ = nullptr;
SDL_GPUBuffer* index_buf_ = nullptr;
SDL_GPUTransferBuffer* vertex_transfer_ = nullptr;
SDL_GPUTransferBuffer* index_transfer_ = nullptr;
SDL_GPUBuffer* vertex_buf_ = nullptr;
SDL_GPUBuffer* index_buf_ = nullptr;
SDL_GPUTransferBuffer* vertex_transfer_ = nullptr;
SDL_GPUTransferBuffer* index_transfer_ = nullptr;
int bg_index_count_ = 0;
int sprite_index_offset_ = 0;
int sprite_index_count_ = 0;
int overlay_index_offset_ = 0;
int overlay_index_count_ = 0;
int bg_index_count_ = 0;
int sprite_index_offset_ = 0;
int sprite_index_count_ = 0;
int overlay_index_offset_ = 0;
int overlay_index_count_ = 0;
int max_sprites_ = DEFAULT_MAX_SPRITES;
int max_sprites_ = DEFAULT_MAX_SPRITES;
};

View File

@@ -2,6 +2,8 @@
#include <SDL3/SDL_log.h>
#include <SDL3/SDL_pixels.h>
#include <array> // for std::array
#include <cstring> // memcpy
#include <string>
@@ -13,7 +15,7 @@
// Public interface
// ---------------------------------------------------------------------------
bool GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) {
auto GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) -> bool {
unsigned char* resource_data = nullptr;
size_t resource_size = 0;
@@ -22,15 +24,22 @@ bool GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) {
return false;
}
int w = 0, h = 0, orig = 0;
int w = 0;
int h = 0;
int orig = 0;
unsigned char* pixels = stbi_load_from_memory(
resource_data, static_cast<int>(resource_size),
&w, &h, &orig, STBI_rgb_alpha);
resource_data,
static_cast<int>(resource_size),
&w,
&h,
&orig,
STBI_rgb_alpha);
delete[] resource_data;
if (!pixels) {
if (pixels == nullptr) {
SDL_Log("GpuTexture: stbi decode failed for '%s': %s",
file_path.c_str(), stbi_failure_reason());
file_path.c_str(),
stbi_failure_reason());
return false;
}
@@ -44,15 +53,17 @@ bool GpuTexture::fromFile(SDL_GPUDevice* device, const std::string& file_path) {
return ok;
}
bool GpuTexture::fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest) {
if (!surface) return false;
auto GpuTexture::fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest) -> bool {
if (surface == nullptr) {
return false;
}
// Ensure RGBA32 format
SDL_Surface* rgba = surface;
bool need_free = false;
if (surface->format != SDL_PIXELFORMAT_RGBA32) {
rgba = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
if (!rgba) {
if (rgba == nullptr) {
SDL_Log("GpuTexture: SDL_ConvertSurface failed: %s", SDL_GetError());
return false;
}
@@ -60,55 +71,65 @@ bool GpuTexture::fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool n
}
destroy(device);
bool ok = uploadPixels(device, rgba->pixels, rgba->w, rgba->h,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
if (ok) ok = createSampler(device, nearest);
bool ok = uploadPixels(device, rgba->pixels, rgba->w, rgba->h, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
if (ok) {
ok = createSampler(device, nearest);
}
if (need_free) SDL_DestroySurface(rgba);
if (need_free) {
SDL_DestroySurface(rgba);
}
return ok;
}
bool GpuTexture::createRenderTarget(SDL_GPUDevice* device, int w, int h,
SDL_GPUTextureFormat format) {
auto GpuTexture::createRenderTarget(SDL_GPUDevice* device, int w, int h, SDL_GPUTextureFormat format) -> bool {
destroy(device);
SDL_GPUTextureCreateInfo info = {};
info.type = SDL_GPU_TEXTURETYPE_2D;
info.format = format;
info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET
| SDL_GPU_TEXTUREUSAGE_SAMPLER;
info.width = static_cast<Uint32>(w);
info.height = static_cast<Uint32>(h);
info.type = SDL_GPU_TEXTURETYPE_2D;
info.format = format;
info.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER;
info.width = static_cast<Uint32>(w);
info.height = static_cast<Uint32>(h);
info.layer_count_or_depth = 1;
info.num_levels = 1;
info.sample_count = SDL_GPU_SAMPLECOUNT_1;
info.num_levels = 1;
info.sample_count = SDL_GPU_SAMPLECOUNT_1;
texture_ = SDL_CreateGPUTexture(device, &info);
if (!texture_) {
if (texture_ == nullptr) {
SDL_Log("GpuTexture: createRenderTarget failed: %s", SDL_GetError());
return false;
}
width_ = w;
width_ = w;
height_ = h;
// Render targets are sampled with linear filter (postfx reads them)
return createSampler(device, false);
}
bool GpuTexture::createWhite(SDL_GPUDevice* device) {
auto GpuTexture::createWhite(SDL_GPUDevice* device) -> bool {
destroy(device);
// 1×1 white RGBA pixel
const Uint8 white[4] = {255, 255, 255, 255};
bool ok = uploadPixels(device, white, 1, 1,
SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
if (ok) ok = createSampler(device, true);
constexpr std::array<Uint8, 4> WHITE = {255, 255, 255, 255};
bool ok = uploadPixels(device, WHITE.data(), 1, 1, SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM);
if (ok) {
ok = createSampler(device, true);
}
return ok;
}
void GpuTexture::destroy(SDL_GPUDevice* device) {
if (!device) return;
if (sampler_) { SDL_ReleaseGPUSampler(device, sampler_); sampler_ = nullptr; }
if (texture_) { SDL_ReleaseGPUTexture(device, texture_); texture_ = nullptr; }
if (device == nullptr) {
return;
}
if (sampler_ != nullptr) {
SDL_ReleaseGPUSampler(device, sampler_);
sampler_ = nullptr;
}
if (texture_ != nullptr) {
SDL_ReleaseGPUTexture(device, texture_);
texture_ = nullptr;
}
width_ = height_ = 0;
}
@@ -116,34 +137,33 @@ void GpuTexture::destroy(SDL_GPUDevice* device) {
// Private helpers
// ---------------------------------------------------------------------------
bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels,
int w, int h, SDL_GPUTextureFormat format) {
auto GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels, int w, int h, SDL_GPUTextureFormat format) -> bool {
// Create GPU texture
SDL_GPUTextureCreateInfo tex_info = {};
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
tex_info.format = format;
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
tex_info.width = static_cast<Uint32>(w);
tex_info.height = static_cast<Uint32>(h);
tex_info.type = SDL_GPU_TEXTURETYPE_2D;
tex_info.format = format;
tex_info.usage = SDL_GPU_TEXTUREUSAGE_SAMPLER;
tex_info.width = static_cast<Uint32>(w);
tex_info.height = static_cast<Uint32>(h);
tex_info.layer_count_or_depth = 1;
tex_info.num_levels = 1;
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
tex_info.num_levels = 1;
tex_info.sample_count = SDL_GPU_SAMPLECOUNT_1;
texture_ = SDL_CreateGPUTexture(device, &tex_info);
if (!texture_) {
if (texture_ == nullptr) {
SDL_Log("GpuTexture: SDL_CreateGPUTexture failed: %s", SDL_GetError());
return false;
}
// Create transfer buffer and upload pixels
Uint32 data_size = static_cast<Uint32>(w * h * 4); // RGBA = 4 bytes/pixel
auto data_size = static_cast<Uint32>(w * h * 4); // RGBA = 4 bytes/pixel
SDL_GPUTransferBufferCreateInfo tb_info = {};
tb_info.usage = SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD;
tb_info.size = data_size;
tb_info.size = data_size;
SDL_GPUTransferBuffer* transfer = SDL_CreateGPUTransferBuffer(device, &tb_info);
if (!transfer) {
if (transfer == nullptr) {
SDL_Log("GpuTexture: transfer buffer creation failed: %s", SDL_GetError());
SDL_ReleaseGPUTexture(device, texture_);
texture_ = nullptr;
@@ -151,7 +171,7 @@ bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels,
}
void* mapped = SDL_MapGPUTransferBuffer(device, transfer, false);
if (!mapped) {
if (mapped == nullptr) {
SDL_Log("GpuTexture: map failed: %s", SDL_GetError());
SDL_ReleaseGPUTransferBuffer(device, transfer);
SDL_ReleaseGPUTexture(device, texture_);
@@ -167,14 +187,14 @@ bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels,
SDL_GPUTextureTransferInfo src = {};
src.transfer_buffer = transfer;
src.offset = 0;
src.pixels_per_row = static_cast<Uint32>(w);
src.rows_per_layer = static_cast<Uint32>(h);
src.offset = 0;
src.pixels_per_row = static_cast<Uint32>(w);
src.rows_per_layer = static_cast<Uint32>(h);
SDL_GPUTextureRegion dst = {};
dst.texture = texture_;
dst.texture = texture_;
dst.mip_level = 0;
dst.layer = 0;
dst.layer = 0;
dst.x = dst.y = dst.z = 0;
dst.w = static_cast<Uint32>(w);
dst.h = static_cast<Uint32>(h);
@@ -185,22 +205,22 @@ bool GpuTexture::uploadPixels(SDL_GPUDevice* device, const void* pixels,
SDL_SubmitGPUCommandBuffer(cmd);
SDL_ReleaseGPUTransferBuffer(device, transfer);
width_ = w;
width_ = w;
height_ = h;
return true;
}
bool GpuTexture::createSampler(SDL_GPUDevice* device, bool nearest) {
auto GpuTexture::createSampler(SDL_GPUDevice* device, bool nearest) -> bool {
SDL_GPUSamplerCreateInfo info = {};
info.min_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR;
info.mag_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR;
info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
info.min_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR;
info.mag_filter = nearest ? SDL_GPU_FILTER_NEAREST : SDL_GPU_FILTER_LINEAR;
info.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_NEAREST;
info.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
info.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
info.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE;
sampler_ = SDL_CreateGPUSampler(device, &info);
if (!sampler_) {
if (sampler_ == nullptr) {
SDL_Log("GpuTexture: SDL_CreateGPUSampler failed: %s", SDL_GetError());
return false;
}

View File

@@ -2,6 +2,7 @@
#include <SDL3/SDL_gpu.h>
#include <SDL3/SDL_surface.h>
#include <string>
// ============================================================================
@@ -9,40 +10,38 @@
// Handles sprite textures, render targets, and the 1×1 white utility texture.
// ============================================================================
class GpuTexture {
public:
GpuTexture() = default;
~GpuTexture() = default;
public:
GpuTexture() = default;
~GpuTexture() = default;
// Load from resource path (pack or disk) using stb_image.
bool fromFile(SDL_GPUDevice* device, const std::string& file_path);
// Load from resource path (pack or disk) using stb_image.
bool fromFile(SDL_GPUDevice* device, const std::string& file_path);
// Upload pixel data from an SDL_Surface to a new GPU texture + sampler.
// Uses nearest-neighbor filter for sprite pixel-perfect look.
bool fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest = true);
// Upload pixel data from an SDL_Surface to a new GPU texture + sampler.
// Uses nearest-neighbor filter for sprite pixel-perfect look.
bool fromSurface(SDL_GPUDevice* device, SDL_Surface* surface, bool nearest = true);
// Create an offscreen render target (COLOR_TARGET | SAMPLER usage).
bool createRenderTarget(SDL_GPUDevice* device, int w, int h,
SDL_GPUTextureFormat format);
// Create an offscreen render target (COLOR_TARGET | SAMPLER usage).
bool createRenderTarget(SDL_GPUDevice* device, int w, int h, SDL_GPUTextureFormat format);
// Create a 1×1 opaque white texture (used for untextured geometry).
bool createWhite(SDL_GPUDevice* device);
// Create a 1×1 opaque white texture (used for untextured geometry).
bool createWhite(SDL_GPUDevice* device);
// Release GPU resources.
void destroy(SDL_GPUDevice* device);
// Release GPU resources.
void destroy(SDL_GPUDevice* device);
SDL_GPUTexture* texture() const { return texture_; }
SDL_GPUSampler* sampler() const { return sampler_; }
int width() const { return width_; }
int height() const { return height_; }
bool isValid() const { return texture_ != nullptr; }
SDL_GPUTexture* texture() const { return texture_; }
SDL_GPUSampler* sampler() const { return sampler_; }
int width() const { return width_; }
int height() const { return height_; }
bool isValid() const { return texture_ != nullptr; }
private:
bool uploadPixels(SDL_GPUDevice* device, const void* pixels,
int w, int h, SDL_GPUTextureFormat format);
bool createSampler(SDL_GPUDevice* device, bool nearest);
private:
bool uploadPixels(SDL_GPUDevice* device, const void* pixels, int w, int h, SDL_GPUTextureFormat format);
bool createSampler(SDL_GPUDevice* device, bool nearest);
SDL_GPUTexture* texture_ = nullptr;
SDL_GPUSampler* sampler_ = nullptr;
int width_ = 0;
int height_ = 0;
SDL_GPUTexture* texture_ = nullptr;
SDL_GPUSampler* sampler_ = nullptr;
int width_ = 0;
int height_ = 0;
};

View File

@@ -1,13 +1,14 @@
#include "input_handler.hpp"
#include <SDL3/SDL_keycode.h> // for SDL_Keycode
#include <string> // for std::string, std::to_string
#include <string> // for std::string, std::to_string
#include "defines.hpp" // for KIOSK_NOTIFICATION_TEXT
#include "engine.hpp" // for Engine
#include "external/mouse.hpp" // for Mouse namespace
bool InputHandler::processEvents(Engine& engine) {
auto InputHandler::processEvents(Engine& engine) -> bool { // NOLINT(readability-function-cognitive-complexity)
SDL_Event event;
while (SDL_PollEvent(&event)) {
// Procesar eventos de ratón (auto-ocultar cursor)
@@ -19,7 +20,7 @@ bool InputHandler::processEvents(Engine& engine) {
}
// Procesar eventos de teclado
if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat == 0) {
if (event.type == SDL_EVENT_KEY_DOWN && static_cast<int>(event.key.repeat) == 0) {
switch (event.key.key) {
case SDLK_ESCAPE:
if (engine.isKioskMode()) {
@@ -105,23 +106,21 @@ bool InputHandler::processEvents(Engine& engine) {
// Toggle Modo Boids (comportamiento de enjambre)
case SDLK_B:
engine.toggleBoidsMode();
engine.toggleBoidsMode();
break;
// Ciclar temas de color (movido de B a C)
case SDLK_C:
{
// Detectar si Shift está presionado
SDL_Keymod modstate = SDL_GetModState();
if (modstate & SDL_KMOD_SHIFT) {
// Shift+C: Ciclar hacia atrás (tema anterior)
engine.cycleTheme(false);
} else {
// C solo: Ciclar hacia adelante (tema siguiente)
engine.cycleTheme(true);
}
case SDLK_C: {
// Detectar si Shift está presionado
SDL_Keymod modstate = SDL_GetModState();
if ((modstate & SDL_KMOD_SHIFT) != 0u) {
// Shift+C: Ciclar hacia atrás (tema anterior)
engine.cycleTheme(false);
} else {
// C solo: Ciclar hacia adelante (tema siguiente)
engine.cycleTheme(true);
}
break;
} break;
// Temas de colores con teclado numérico (con transición suave)
case SDLK_KP_1:
@@ -233,25 +232,37 @@ bool InputHandler::processEvents(Engine& engine) {
// Controles de zoom dinámico (solo si no estamos en fullscreen)
case SDLK_F1:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
else engine.handleZoomOut();
if (engine.isKioskMode()) {
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
} else {
engine.handleZoomOut();
}
break;
case SDLK_F2:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
else engine.handleZoomIn();
if (engine.isKioskMode()) {
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
} else {
engine.handleZoomIn();
}
break;
// Control de pantalla completa
case SDLK_F3:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
else engine.toggleFullscreen();
if (engine.isKioskMode()) {
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
} else {
engine.toggleFullscreen();
}
break;
// Modo real fullscreen (cambia resolución interna)
case SDLK_F4:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
else engine.toggleRealFullscreen();
if (engine.isKioskMode()) {
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
} else {
engine.toggleRealFullscreen();
}
break;
// Toggle PostFX activo/inactivo
@@ -266,19 +277,25 @@ bool InputHandler::processEvents(Engine& engine) {
// Redimensionar campo de juego (tamaño lógico + físico)
case SDLK_F7:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
else engine.fieldSizeDown();
if (engine.isKioskMode()) {
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
} else {
engine.fieldSizeDown();
}
break;
case SDLK_F8:
if (engine.isKioskMode()) engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
else engine.fieldSizeUp();
if (engine.isKioskMode()) {
engine.showNotificationForAction(KIOSK_NOTIFICATION_TEXT);
} else {
engine.fieldSizeUp();
}
break;
// Toggle Modo DEMO COMPLETO (auto-play) o Pausar tema dinámico (Shift+D)
case SDLK_D:
// Shift+D = Pausar tema dinámico
if (event.key.mod & SDL_KMOD_SHIFT) {
if ((event.key.mod & SDL_KMOD_SHIFT) != 0u) {
engine.pauseDynamicTheme();
} else {
// D sin Shift = Toggle DEMO ↔ SANDBOX

View File

@@ -24,7 +24,7 @@ class InputHandler {
* @param engine Referencia al engine para ejecutar acciones
* @return true si se debe salir de la aplicación (ESC o cerrar ventana), false en caso contrario
*/
bool processEvents(Engine& engine);
static bool processEvents(Engine& engine);
private:
// Sin estado interno por ahora - el InputHandler es stateless

View File

@@ -1,8 +1,9 @@
#include <iostream>
#include <cstring>
#include <iostream>
#include <string>
#include "engine.hpp"
#include "defines.hpp"
#include "engine.hpp"
#include "resource_manager.hpp"
// getExecutableDirectory() ya está definido en defines.h como inline
@@ -38,7 +39,7 @@ void printHelp() {
std::cout << "Nota: Si resolución > pantalla, se usa default. Zoom se ajusta automáticamente.\n";
}
int main(int argc, char* argv[]) {
auto main(int argc, char* argv[]) -> int { // NOLINT(readability-function-cognitive-complexity)
int width = 0;
int height = 0;
int zoom = 0;
@@ -58,7 +59,8 @@ int main(int argc, char* argv[]) {
if (strcmp(argv[i], "--help") == 0) {
printHelp();
return 0;
} else if (strcmp(argv[i], "-w") == 0 || strcmp(argv[i], "--width") == 0) {
}
if (strcmp(argv[i], "-w") == 0 || strcmp(argv[i], "--width") == 0) {
if (i + 1 < argc) {
width = atoi(argv[++i]);
if (width < 320) {
@@ -189,25 +191,29 @@ int main(int argc, char* argv[]) {
Engine engine;
if (custom_balls > 0)
if (custom_balls > 0) {
engine.setCustomScenario(custom_balls); // pre-init: asigna campos antes del benchmark
}
if (max_balls_override > 0)
if (max_balls_override > 0) {
engine.setMaxBallsOverride(max_balls_override);
else if (skip_benchmark)
} else if (skip_benchmark) {
engine.setSkipBenchmark();
}
if (initial_postfx >= 0)
if (initial_postfx >= 0) {
engine.setInitialPostFX(initial_postfx);
}
if (override_vignette >= 0.f || override_chroma >= 0.f) {
if (initial_postfx < 0)
if (initial_postfx < 0) {
engine.setInitialPostFX(0);
}
engine.setPostFXParamOverrides(override_vignette, override_chroma);
}
if (!engine.initialize(width, height, zoom, fullscreen, initial_mode)) {
std::cout << "¡Error al inicializar el engine!" << std::endl;
std::cout << "¡Error al inicializar el engine!" << '\n';
return -1;
}

View File

@@ -1,15 +1,16 @@
#include "resource_manager.hpp"
#include "resource_pack.hpp"
#include <iostream>
#include <fstream>
#include <cstring>
#include <fstream>
#include <iostream>
#include "resource_pack.hpp"
// Inicializar estáticos
ResourcePack* ResourceManager::resourcePack_ = nullptr;
std::map<std::string, std::vector<unsigned char>> ResourceManager::cache_;
bool ResourceManager::init(const std::string& packFilePath) {
auto ResourceManager::init(const std::string& pack_file_path) -> bool {
// Si ya estaba inicializado, liberar primero
if (resourcePack_ != nullptr) {
delete resourcePack_;
@@ -18,15 +19,15 @@ bool ResourceManager::init(const std::string& packFilePath) {
// Intentar cargar el pack
resourcePack_ = new ResourcePack();
if (!resourcePack_->loadPack(packFilePath)) {
if (!resourcePack_->loadPack(pack_file_path)) {
// Si falla, borrar instancia (usará fallback a disco)
delete resourcePack_;
resourcePack_ = nullptr;
std::cout << "resources.pack no encontrado - usando carpeta data/" << std::endl;
std::cout << "resources.pack no encontrado - usando carpeta data/" << '\n';
return false;
}
std::cout << "resources.pack cargado (" << resourcePack_->getResourceCount() << " recursos)" << std::endl;
std::cout << "resources.pack cargado (" << resourcePack_->getResourceCount() << " recursos)" << '\n';
return true;
}
@@ -38,12 +39,12 @@ void ResourceManager::shutdown() {
}
}
bool ResourceManager::loadResource(const std::string& resourcePath, unsigned char*& data, size_t& size) {
auto ResourceManager::loadResource(const std::string& resource_path, unsigned char*& data, size_t& size) -> bool {
data = nullptr;
size = 0;
// 1. Consultar caché en RAM (sin I/O)
auto it = cache_.find(resourcePath);
auto it = cache_.find(resource_path);
if (it != cache_.end()) {
size = it->second.size();
data = new unsigned char[size];
@@ -53,20 +54,20 @@ bool ResourceManager::loadResource(const std::string& resourcePath, unsigned cha
// 2. Intentar cargar desde pack (si está disponible)
if (resourcePack_ != nullptr) {
ResourcePack::ResourceData packData = resourcePack_->loadResource(resourcePath);
if (packData.data != nullptr) {
cache_[resourcePath] = std::vector<unsigned char>(packData.data, packData.data + packData.size);
data = packData.data;
size = packData.size;
ResourcePack::ResourceData pack_data = resourcePack_->loadResource(resource_path);
if (pack_data.data != nullptr) {
cache_[resource_path] = std::vector<unsigned char>(pack_data.data, pack_data.data + pack_data.size);
data = pack_data.data;
size = pack_data.size;
return true;
}
}
// 3. Fallback: cargar desde disco
std::ifstream file(resourcePath, std::ios::binary | std::ios::ate);
std::ifstream file(resource_path, std::ios::binary | std::ios::ate);
if (!file) {
std::string dataPath = "data/" + resourcePath;
file.open(dataPath, std::ios::binary | std::ios::ate);
std::string data_path = "data/" + resource_path;
file.open(data_path, std::ios::binary | std::ios::ate);
if (!file) { return false; }
}
size = static_cast<size_t>(file.tellg());
@@ -76,22 +77,22 @@ bool ResourceManager::loadResource(const std::string& resourcePath, unsigned cha
file.close();
// Guardar en caché
cache_[resourcePath] = std::vector<unsigned char>(data, data + size);
cache_[resource_path] = std::vector<unsigned char>(data, data + size);
return true;
}
bool ResourceManager::isPackLoaded() {
auto ResourceManager::isPackLoaded() -> bool {
return resourcePack_ != nullptr;
}
std::vector<std::string> ResourceManager::getResourceList() {
auto ResourceManager::getResourceList() -> std::vector<std::string> {
if (resourcePack_ != nullptr) {
return resourcePack_->getResourceList();
}
return std::vector<std::string>(); // Vacío si no hay pack
return {}; // Vacío si no hay pack
}
size_t ResourceManager::getResourceCount() {
auto ResourceManager::getResourceCount() -> size_t {
if (resourcePack_ != nullptr) {
return resourcePack_->getResourceCount();
}

View File

@@ -25,62 +25,62 @@ class ResourcePack;
* }
*/
class ResourceManager {
public:
/**
* Inicializa el sistema de recursos empaquetados
* Debe llamarse una única vez al inicio del programa
*
* @param packFilePath Ruta al archivo .pack (ej: "resources.pack")
* @return true si el pack se cargó correctamente, false si no existe (fallback a disco)
*/
static bool init(const std::string& packFilePath);
public:
/**
* Inicializa el sistema de recursos empaquetados
* Debe llamarse una única vez al inicio del programa
*
* @param packFilePath Ruta al archivo .pack (ej: "resources.pack")
* @return true si el pack se cargó correctamente, false si no existe (fallback a disco)
*/
static bool init(const std::string& pack_file_path);
/**
* Libera el sistema de recursos
* Opcional - se llama automáticamente al cerrar el programa
*/
static void shutdown();
/**
* Libera el sistema de recursos
* Opcional - se llama automáticamente al cerrar el programa
*/
static void shutdown();
/**
* Carga un recurso desde el pack (o disco si no existe pack)
*
* @param resourcePath Ruta relativa del recurso (ej: "textures/ball.png")
* @param data [out] Puntero donde se almacenará el buffer (debe liberar con delete[])
* @param size [out] Tamaño del buffer en bytes
* @return true si se cargó correctamente, false si falla
*/
static bool loadResource(const std::string& resourcePath, unsigned char*& data, size_t& size);
/**
* Carga un recurso desde el pack (o disco si no existe pack)
*
* @param resourcePath Ruta relativa del recurso (ej: "textures/ball.png")
* @param data [out] Puntero donde se almacenará el buffer (debe liberar con delete[])
* @param size [out] Tamaño del buffer en bytes
* @return true si se cargó correctamente, false si falla
*/
static bool loadResource(const std::string& resource_path, unsigned char*& data, size_t& size);
/**
* Verifica si el pack está cargado
* @return true si hay un pack cargado, false si se usa disco
*/
static bool isPackLoaded();
/**
* Verifica si el pack está cargado
* @return true si hay un pack cargado, false si se usa disco
*/
static bool isPackLoaded();
/**
* Obtiene la lista de recursos disponibles en el pack
* @return Vector con las rutas de todos los recursos, vacío si no hay pack
*/
static std::vector<std::string> getResourceList();
/**
* Obtiene la lista de recursos disponibles en el pack
* @return Vector con las rutas de todos los recursos, vacío si no hay pack
*/
static std::vector<std::string> getResourceList();
/**
* Obtiene el número de recursos en el pack
* @return Número de recursos, 0 si no hay pack
*/
static size_t getResourceCount();
/**
* Obtiene el número de recursos en el pack
* @return Número de recursos, 0 si no hay pack
*/
static size_t getResourceCount();
private:
// Constructor privado (singleton)
ResourceManager() = default;
~ResourceManager() = default;
private:
// Constructor privado (singleton)
ResourceManager() = default;
~ResourceManager() = default;
// Deshabilitar copia y asignación
ResourceManager(const ResourceManager&) = delete;
ResourceManager& operator=(const ResourceManager&) = delete;
// Deshabilitar copia y asignación
ResourceManager(const ResourceManager&) = delete;
ResourceManager& operator=(const ResourceManager&) = delete;
// Instancia del pack (nullptr si no está cargado)
static ResourcePack* resourcePack_;
// Instancia del pack (nullptr si no está cargado)
static ResourcePack* resourcePack_;
// Caché en RAM para evitar I/O repetido en el bucle principal
static std::map<std::string, std::vector<unsigned char>> cache_;
// Caché en RAM para evitar I/O repetido en el bucle principal
static std::map<std::string, std::vector<unsigned char>> cache_;
};

View File

@@ -7,10 +7,8 @@
namespace fs = std::filesystem;
// Clave XOR para ofuscación simple (puede cambiarse)
constexpr uint8_t XOR_KEY = 0x5A;
ResourcePack::ResourcePack() : isLoaded_(false) {}
ResourcePack::ResourcePack()
: isLoaded_(false) {}
ResourcePack::~ResourcePack() {
clear();
@@ -20,54 +18,54 @@ ResourcePack::~ResourcePack() {
// EMPAQUETADO (herramienta pack_resources)
// ============================================================================
bool ResourcePack::addDirectory(const std::string& dirPath, const std::string& prefix) {
if (!fs::exists(dirPath) || !fs::is_directory(dirPath)) {
std::cerr << "Error: Directorio no existe: " << dirPath << std::endl;
auto ResourcePack::addDirectory(const std::string& dir_path, const std::string& prefix) -> bool {
if (!fs::exists(dir_path) || !fs::is_directory(dir_path)) {
std::cerr << "Error: Directorio no existe: " << dir_path << '\n';
return false;
}
for (const auto& entry : fs::recursive_directory_iterator(dirPath)) {
for (const auto& entry : fs::recursive_directory_iterator(dir_path)) {
if (entry.is_regular_file()) {
// Construir ruta relativa desde data/ (ej: "data/ball.png" → "ball.png")
std::string relativePath = fs::relative(entry.path(), dirPath).string();
std::string fullPath = prefix.empty() ? relativePath : prefix + "/" + relativePath;
fullPath = normalizePath(fullPath);
std::string relative_path = fs::relative(entry.path(), dir_path).string();
std::string full_path = prefix.empty() ? relative_path : prefix + "/" + relative_path;
full_path = normalizePath(full_path);
// Leer archivo completo
std::ifstream file(entry.path(), std::ios::binary);
if (!file) {
std::cerr << "Error: No se pudo abrir: " << entry.path() << std::endl;
std::cerr << "Error: No se pudo abrir: " << entry.path() << '\n';
continue;
}
file.seekg(0, std::ios::end);
size_t fileSize = file.tellg();
size_t file_size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<unsigned char> buffer(fileSize);
file.read(reinterpret_cast<char*>(buffer.data()), fileSize);
std::vector<unsigned char> buffer(file_size);
file.read(reinterpret_cast<char*>(buffer.data()), file_size);
file.close();
// Crear entrada de recurso
ResourceEntry resource;
resource.path = fullPath;
resource.path = full_path;
resource.offset = 0; // Se calculará al guardar
resource.size = static_cast<uint32_t>(fileSize);
resource.checksum = calculateChecksum(buffer.data(), fileSize);
resource.size = static_cast<uint32_t>(file_size);
resource.checksum = calculateChecksum(buffer.data(), file_size);
resources_[fullPath] = resource;
resources_[full_path] = resource;
std::cout << " Añadido: " << fullPath << " (" << fileSize << " bytes)" << std::endl;
std::cout << " Añadido: " << full_path << " (" << file_size << " bytes)" << '\n';
}
}
return !resources_.empty();
}
bool ResourcePack::savePack(const std::string& packFilePath) {
std::ofstream packFile(packFilePath, std::ios::binary);
if (!packFile) {
std::cerr << "Error: No se pudo crear pack: " << packFilePath << std::endl;
auto ResourcePack::savePack(const std::string& pack_file_path) -> bool {
std::ofstream pack_file(pack_file_path, std::ios::binary);
if (!pack_file) {
std::cerr << "Error: No se pudo crear pack: " << pack_file_path << '\n';
return false;
}
@@ -76,39 +74,39 @@ bool ResourcePack::savePack(const std::string& packFilePath) {
std::memcpy(header.magic, "VBE3", 4);
header.version = 1;
header.fileCount = static_cast<uint32_t>(resources_.size());
packFile.write(reinterpret_cast<const char*>(&header), sizeof(PackHeader));
pack_file.write(reinterpret_cast<const char*>(&header), sizeof(PackHeader));
// 2. Calcular offsets (después del header + índice)
uint32_t currentOffset = sizeof(PackHeader);
uint32_t current_offset = sizeof(PackHeader);
// Calcular tamaño del índice (cada entrada: uint32_t pathLen + path + 3*uint32_t)
for (const auto& [path, entry] : resources_) {
currentOffset += sizeof(uint32_t); // pathLen
currentOffset += static_cast<uint32_t>(path.size()); // path
currentOffset += sizeof(uint32_t) * 3; // offset, size, checksum
current_offset += sizeof(uint32_t); // pathLen
current_offset += static_cast<uint32_t>(path.size()); // path
current_offset += sizeof(uint32_t) * 3; // offset, size, checksum
}
// 3. Escribir índice
for (auto& [path, entry] : resources_) {
entry.offset = currentOffset;
entry.offset = current_offset;
uint32_t pathLen = static_cast<uint32_t>(path.size());
packFile.write(reinterpret_cast<const char*>(&pathLen), sizeof(uint32_t));
packFile.write(path.c_str(), pathLen);
packFile.write(reinterpret_cast<const char*>(&entry.offset), sizeof(uint32_t));
packFile.write(reinterpret_cast<const char*>(&entry.size), sizeof(uint32_t));
packFile.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(uint32_t));
auto path_len = static_cast<uint32_t>(path.size());
pack_file.write(reinterpret_cast<const char*>(&path_len), sizeof(uint32_t));
pack_file.write(path.c_str(), path_len);
pack_file.write(reinterpret_cast<const char*>(&entry.offset), sizeof(uint32_t));
pack_file.write(reinterpret_cast<const char*>(&entry.size), sizeof(uint32_t));
pack_file.write(reinterpret_cast<const char*>(&entry.checksum), sizeof(uint32_t));
currentOffset += entry.size;
current_offset += entry.size;
}
// 4. Escribir datos de archivos (sin encriptar en pack, se encripta al cargar)
for (const auto& [path, entry] : resources_) {
// Encontrar archivo original en disco
fs::path originalPath = fs::current_path() / "data" / path;
std::ifstream file(originalPath, std::ios::binary);
fs::path original_path = fs::current_path() / "data" / path;
std::ifstream file(original_path, std::ios::binary);
if (!file) {
std::cerr << "Error: No se pudo re-leer: " << originalPath << std::endl;
std::cerr << "Error: No se pudo re-leer: " << original_path << '\n';
continue;
}
@@ -116,10 +114,10 @@ bool ResourcePack::savePack(const std::string& packFilePath) {
file.read(reinterpret_cast<char*>(buffer.data()), entry.size);
file.close();
packFile.write(reinterpret_cast<const char*>(buffer.data()), entry.size);
pack_file.write(reinterpret_cast<const char*>(buffer.data()), entry.size);
}
packFile.close();
pack_file.close();
return true;
}
@@ -127,10 +125,10 @@ bool ResourcePack::savePack(const std::string& packFilePath) {
// DESEMPAQUETADO (juego)
// ============================================================================
bool ResourcePack::loadPack(const std::string& packFilePath) {
auto ResourcePack::loadPack(const std::string& pack_file_path) -> bool {
clear();
packFile_.open(packFilePath, std::ios::binary);
packFile_.open(pack_file_path, std::ios::binary);
if (!packFile_) {
return false;
}
@@ -140,13 +138,13 @@ bool ResourcePack::loadPack(const std::string& packFilePath) {
packFile_.read(reinterpret_cast<char*>(&header), sizeof(PackHeader));
if (std::memcmp(header.magic, "VBE3", 4) != 0) {
std::cerr << "Error: Pack inválido (magic incorrecto)" << std::endl;
std::cerr << "Error: Pack inválido (magic incorrecto)" << '\n';
packFile_.close();
return false;
}
if (header.version != 1) {
std::cerr << "Error: Versión de pack no soportada: " << header.version << std::endl;
std::cerr << "Error: Versión de pack no soportada: " << header.version << '\n';
packFile_.close();
return false;
}
@@ -155,12 +153,12 @@ bool ResourcePack::loadPack(const std::string& packFilePath) {
for (uint32_t i = 0; i < header.fileCount; i++) {
ResourceEntry entry;
uint32_t pathLen;
packFile_.read(reinterpret_cast<char*>(&pathLen), sizeof(uint32_t));
uint32_t path_len;
packFile_.read(reinterpret_cast<char*>(&path_len), sizeof(uint32_t));
std::vector<char> pathBuffer(pathLen + 1, '\0');
packFile_.read(pathBuffer.data(), pathLen);
entry.path = std::string(pathBuffer.data());
std::vector<char> path_buffer(path_len + 1, '\0');
packFile_.read(path_buffer.data(), path_len);
entry.path = std::string(path_buffer.data());
packFile_.read(reinterpret_cast<char*>(&entry.offset), sizeof(uint32_t));
packFile_.read(reinterpret_cast<char*>(&entry.size), sizeof(uint32_t));
@@ -173,15 +171,15 @@ bool ResourcePack::loadPack(const std::string& packFilePath) {
return true;
}
ResourcePack::ResourceData ResourcePack::loadResource(const std::string& resourcePath) {
ResourceData result = {nullptr, 0};
auto ResourcePack::loadResource(const std::string& resource_path) -> ResourcePack::ResourceData {
ResourceData result = {.data = nullptr, .size = 0};
if (!isLoaded_) {
return result;
}
std::string normalizedPath = normalizePath(resourcePath);
auto it = resources_.find(normalizedPath);
std::string normalized_path = normalizePath(resource_path);
auto it = resources_.find(normalized_path);
if (it == resources_.end()) {
return result;
}
@@ -197,7 +195,7 @@ ResourcePack::ResourceData ResourcePack::loadResource(const std::string& resourc
// Verificar checksum
uint32_t checksum = calculateChecksum(result.data, entry.size);
if (checksum != entry.checksum) {
std::cerr << "Warning: Checksum incorrecto para: " << resourcePath << std::endl;
std::cerr << "Warning: Checksum incorrecto para: " << resource_path << '\n';
}
return result;
@@ -207,15 +205,16 @@ ResourcePack::ResourceData ResourcePack::loadResource(const std::string& resourc
// UTILIDADES
// ============================================================================
std::vector<std::string> ResourcePack::getResourceList() const {
auto ResourcePack::getResourceList() const -> std::vector<std::string> {
std::vector<std::string> list;
list.reserve(resources_.size());
for (const auto& [path, entry] : resources_) {
list.push_back(path);
}
return list;
}
size_t ResourcePack::getResourceCount() const {
auto ResourcePack::getResourceCount() const -> size_t {
return resources_.size();
}
@@ -231,7 +230,7 @@ void ResourcePack::clear() {
// FUNCIONES AUXILIARES
// ============================================================================
uint32_t ResourcePack::calculateChecksum(const unsigned char* data, size_t size) {
auto ResourcePack::calculateChecksum(const unsigned char* data, size_t size) -> uint32_t {
uint32_t checksum = 0;
for (size_t i = 0; i < size; i++) {
checksum ^= static_cast<uint32_t>(data[i]);
@@ -240,11 +239,11 @@ uint32_t ResourcePack::calculateChecksum(const unsigned char* data, size_t size)
return checksum;
}
std::string ResourcePack::normalizePath(const std::string& path) {
auto ResourcePack::normalizePath(const std::string& path) -> std::string {
std::string normalized = path;
// Reemplazar \ por /
std::replace(normalized.begin(), normalized.end(), '\\', '/');
std::ranges::replace(normalized, '\\', '/');
// Buscar "data/" en cualquier parte del path y extraer lo que viene después
size_t data_pos = normalized.find("data/");

View File

@@ -13,51 +13,51 @@
* único y ofuscado. Proporciona fallback automático a carpeta data/ si no existe pack.
*/
class ResourcePack {
public:
ResourcePack();
~ResourcePack();
public:
ResourcePack();
~ResourcePack();
// Empaquetado (usado por herramienta pack_resources)
bool addDirectory(const std::string& dirPath, const std::string& prefix = "");
bool savePack(const std::string& packFilePath);
// Empaquetado (usado por herramienta pack_resources)
bool addDirectory(const std::string& dir_path, const std::string& prefix = "");
bool savePack(const std::string& pack_file_path);
// Desempaquetado (usado por el juego)
bool loadPack(const std::string& packFilePath);
// Desempaquetado (usado por el juego)
bool loadPack(const std::string& pack_file_path);
// Carga de recursos individuales
struct ResourceData {
unsigned char* data;
size_t size;
};
ResourceData loadResource(const std::string& resourcePath);
// Carga de recursos individuales
struct ResourceData {
unsigned char* data;
size_t size;
};
ResourceData loadResource(const std::string& resource_path);
// Utilidades
std::vector<std::string> getResourceList() const;
size_t getResourceCount() const;
void clear();
// Utilidades
std::vector<std::string> getResourceList() const;
size_t getResourceCount() const;
void clear();
private:
// Header del pack (12 bytes)
struct PackHeader {
char magic[4]; // "VBE3"
uint32_t version; // Versión del formato (1)
uint32_t fileCount; // Número de archivos empaquetados
};
private:
// Header del pack (12 bytes)
struct PackHeader {
char magic[4]; // "VBE3"
uint32_t version; // Versión del formato (1)
uint32_t fileCount; // Número de archivos empaquetados
};
// Índice de un recurso (variable length)
struct ResourceEntry {
std::string path; // Ruta relativa del recurso
uint32_t offset; // Offset en el archivo pack
uint32_t size; // Tamaño en bytes
uint32_t checksum; // Checksum simple (XOR de bytes)
};
// Índice de un recurso (variable length)
struct ResourceEntry {
std::string path; // Ruta relativa del recurso
uint32_t offset; // Offset en el archivo pack
uint32_t size; // Tamaño en bytes
uint32_t checksum; // Checksum simple (XOR de bytes)
};
// Datos internos
std::map<std::string, ResourceEntry> resources_;
std::ifstream packFile_;
bool isLoaded_;
// Datos internos
std::map<std::string, ResourceEntry> resources_;
std::ifstream packFile_;
bool isLoaded_;
// Funciones auxiliares
uint32_t calculateChecksum(const unsigned char* data, size_t size);
std::string normalizePath(const std::string& path);
// Funciones auxiliares
static uint32_t calculateChecksum(const unsigned char* data, size_t size);
static std::string normalizePath(const std::string& path);
};

View File

@@ -1,24 +1,25 @@
#include "scene_manager.hpp"
#include <cstdlib> // for rand
#include <utility>
#include "defines.hpp" // for BALL_COUNT_SCENARIOS, GRAVITY_MASS_MIN, etc
#include "external/texture.hpp" // for Texture
#include "theme_manager.hpp" // for ThemeManager
#include "defines.hpp" // for BALL_COUNT_SCENARIOS, GRAVITY_MASS_MIN, etc
#include "external/texture.hpp" // for Texture
#include "theme_manager.hpp" // for ThemeManager
SceneManager::SceneManager(int screen_width, int screen_height)
: current_gravity_(GravityDirection::DOWN)
, scenario_(0)
, screen_width_(screen_width)
, screen_height_(screen_height)
, current_ball_size_(10)
, texture_(nullptr)
, theme_manager_(nullptr) {
: current_gravity_(GravityDirection::DOWN),
scenario_(0),
screen_width_(screen_width),
screen_height_(screen_height),
current_ball_size_(10),
texture_(nullptr),
theme_manager_(nullptr) {
}
void SceneManager::initialize(int scenario, std::shared_ptr<Texture> texture, ThemeManager* theme_manager) {
scenario_ = scenario;
texture_ = texture;
texture_ = std::move(texture);
theme_manager_ = theme_manager;
current_ball_size_ = texture_->getWidth();
@@ -48,28 +49,31 @@ void SceneManager::changeScenario(int scenario_id, SimulationMode mode) {
? custom_ball_count_
: BALL_COUNT_SCENARIOS[scenario_id];
for (int i = 0; i < ball_count; ++i) {
float X, Y, VX, VY;
float x;
float y;
float vx;
float vy;
// Inicialización según SimulationMode (RULES.md líneas 23-26)
switch (mode) {
case SimulationMode::PHYSICS: {
// PHYSICS: Parte superior, 75% distribución central en X
const int SIGN = ((rand() % 2) * 2) - 1;
const int margin = static_cast<int>(screen_width_ * BALL_SPAWN_MARGIN);
const int spawn_zone_width = screen_width_ - (2 * margin);
X = (rand() % spawn_zone_width) + margin;
Y = 0.0f; // Parte superior
VX = (((rand() % 20) + 10) * 0.1f) * SIGN;
VY = ((rand() % 60) - 30) * 0.1f;
const int MARGIN = static_cast<int>(screen_width_ * BALL_SPAWN_MARGIN);
const int SPAWN_ZONE_WIDTH = screen_width_ - (2 * MARGIN);
x = (rand() % SPAWN_ZONE_WIDTH) + MARGIN;
y = 0.0f; // Parte superior
vx = (((rand() % 20) + 10) * 0.1f) * SIGN;
vy = ((rand() % 60) - 30) * 0.1f;
break;
}
case SimulationMode::SHAPE: {
// SHAPE: Centro de pantalla, sin velocidad inicial
X = screen_width_ / 2.0f;
Y = screen_height_ / 2.0f; // Centro vertical
VX = 0.0f;
VY = 0.0f;
x = screen_width_ / 2.0f;
y = screen_height_ / 2.0f; // Centro vertical
vx = 0.0f;
vy = 0.0f;
break;
}
@@ -77,48 +81,57 @@ void SceneManager::changeScenario(int scenario_id, SimulationMode mode) {
// BOIDS: Posiciones aleatorias, velocidades aleatorias
const int SIGN_X = ((rand() % 2) * 2) - 1;
const int SIGN_Y = ((rand() % 2) * 2) - 1;
X = static_cast<float>(rand() % screen_width_);
Y = static_cast<float>(rand() % screen_height_); // Posición Y aleatoria
VX = (((rand() % 40) + 10) * 0.1f) * SIGN_X; // 1.0 - 5.0 px/frame
VY = (((rand() % 40) + 10) * 0.1f) * SIGN_Y;
x = static_cast<float>(rand() % screen_width_);
y = static_cast<float>(rand() % screen_height_); // Posición Y aleatoria
vx = (((rand() % 40) + 10) * 0.1f) * SIGN_X; // 1.0 - 5.0 px/frame
vy = (((rand() % 40) + 10) * 0.1f) * SIGN_Y;
break;
}
default:
// Fallback a PHYSICS por seguridad
const int SIGN = ((rand() % 2) * 2) - 1;
const int margin = static_cast<int>(screen_width_ * BALL_SPAWN_MARGIN);
const int spawn_zone_width = screen_width_ - (2 * margin);
X = (rand() % spawn_zone_width) + margin;
Y = 0.0f; // Parte superior
VX = (((rand() % 20) + 10) * 0.1f) * SIGN;
VY = ((rand() % 60) - 30) * 0.1f;
const int MARGIN = static_cast<int>(screen_width_ * BALL_SPAWN_MARGIN);
const int SPAWN_ZONE_WIDTH = screen_width_ - (2 * MARGIN);
x = (rand() % SPAWN_ZONE_WIDTH) + MARGIN;
y = 0.0f; // Parte superior
vx = (((rand() % 20) + 10) * 0.1f) * SIGN;
vy = ((rand() % 60) - 30) * 0.1f;
break;
}
// Seleccionar color de la paleta del tema actual (delegado a ThemeManager)
int random_index = rand();
Color COLOR = theme_manager_->getInitialBallColor(random_index);
Color color = theme_manager_->getInitialBallColor(random_index);
// Generar factor de masa aleatorio (0.7 = ligera, 1.3 = pesada)
float mass_factor = GRAVITY_MASS_MIN + (rand() % 1000) / 1000.0f * (GRAVITY_MASS_MAX - GRAVITY_MASS_MIN);
float mass_factor = GRAVITY_MASS_MIN + ((rand() % 1000) / 1000.0f * (GRAVITY_MASS_MAX - GRAVITY_MASS_MIN));
balls_.emplace_back(std::make_unique<Ball>(
X, Y, VX, VY, COLOR, texture_,
screen_width_, screen_height_, current_ball_size_,
current_gravity_, mass_factor
));
x,
y,
vx,
vy,
color,
texture_,
screen_width_,
screen_height_,
current_ball_size_,
current_gravity_,
mass_factor));
}
}
void SceneManager::updateBallTexture(std::shared_ptr<Texture> new_texture, int new_ball_size) {
if (balls_.empty()) return;
if (balls_.empty()) {
return;
}
// Guardar tamaño antiguo
int old_size = current_ball_size_;
// Actualizar textura y tamaño
texture_ = new_texture;
texture_ = std::move(new_texture);
current_ball_size_ = new_ball_size;
// Actualizar texturas de todas las pelotas
@@ -136,7 +149,8 @@ void SceneManager::pushBallsAwayFromGravity() {
const float LATERAL = (((rand() % 20) + 10) * 0.1f) * SIGNO;
const float MAIN = ((rand() % 40) * 0.1f) + 5;
float vx = 0, vy = 0;
float vx = 0;
float vy = 0;
switch (current_gravity_) {
case GravityDirection::DOWN: // Impulsar ARRIBA
vx = LATERAL;

View File

@@ -1,7 +1,10 @@
#include "atom_shape.hpp"
#include "defines.hpp"
#include <algorithm>
#include <cmath>
#include "defines.hpp"
void AtomShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points;
nucleus_radius_ = screen_height * ATOM_NUCLEUS_RADIUS_FACTOR;
@@ -25,15 +28,15 @@ void AtomShape::getPoint3D(int index, float& x, float& y, float& z) const {
int num_orbits = static_cast<int>(ATOM_NUM_ORBITS);
// Calcular cuántos puntos para núcleo vs órbitas
int nucleus_points = (num_points_ < 10) ? 1 : (num_points_ / 10); // 10% para núcleo
if (nucleus_points < 1) nucleus_points = 1;
int nucleus_points = (num_points_ < 10) ? 1 : (num_points_ / 10); // 10% para núcleo
nucleus_points = std::max(nucleus_points, 1);
// Si estamos en el núcleo
if (index < nucleus_points) {
// Distribuir puntos en esfera pequeña (núcleo)
float t = static_cast<float>(index) / static_cast<float>(nucleus_points);
float phi = acosf(1.0f - 2.0f * t);
float theta = PI * 2.0f * t * 3.61803398875f; // Golden ratio
float phi = acosf(1.0f - (2.0f * t));
float theta = PI * 2.0f * t * 3.61803398875f; // Golden ratio
float x_nuc = nucleus_radius_ * cosf(theta) * sinf(phi);
float y_nuc = nucleus_radius_ * sinf(theta) * sinf(phi);
@@ -51,16 +54,18 @@ void AtomShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Puntos restantes: distribuir en órbitas
int orbit_points = num_points_ - nucleus_points;
int points_per_orbit = orbit_points / num_orbits;
if (points_per_orbit < 1) points_per_orbit = 1;
points_per_orbit = std::max(points_per_orbit, 1);
int orbit_index = (index - nucleus_points) / points_per_orbit;
if (orbit_index >= num_orbits) orbit_index = num_orbits - 1;
if (orbit_index >= num_orbits) {
orbit_index = num_orbits - 1;
}
int point_in_orbit = (index - nucleus_points) % points_per_orbit;
// Ángulo del electrón en su órbita
float electron_angle = (static_cast<float>(point_in_orbit) / static_cast<float>(points_per_orbit)) * 2.0f * PI;
electron_angle += orbit_phase_; // Añadir rotación animada
electron_angle += orbit_phase_; // Añadir rotación animada
// Inclinación del plano orbital (cada órbita en ángulo diferente)
float orbit_tilt = (static_cast<float>(orbit_index) / static_cast<float>(num_orbits)) * PI;
@@ -73,21 +78,21 @@ void AtomShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Inclinar el plano orbital (rotación en eje X local)
float cos_tilt = cosf(orbit_tilt);
float sin_tilt = sinf(orbit_tilt);
float y_tilted = y_local * cos_tilt - z_local * sin_tilt;
float z_tilted = y_local * sin_tilt + z_local * cos_tilt;
float y_tilted = (y_local * cos_tilt) - (z_local * sin_tilt);
float z_tilted = (y_local * sin_tilt) + (z_local * cos_tilt);
// Aplicar rotación global del átomo (eje Y)
float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_);
float x_rot = x_local * cos_y - z_tilted * sin_y;
float z_rot = x_local * sin_y + z_tilted * cos_y;
float x_rot = (x_local * cos_y) - (z_tilted * sin_y);
float z_rot = (x_local * sin_y) + (z_tilted * cos_y);
x = x_rot;
y = y_tilted;
z = z_rot;
}
float AtomShape::getScaleFactor(float screen_height) const {
auto AtomShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional al radio de órbita
// Radio órbita base = 72px (0.30 * 240px en resolución 320x240)
const float BASE_RADIUS = 72.0f;

View File

@@ -6,17 +6,17 @@
// Comportamiento: Núcleo estático + electrones orbitando en planos inclinados
// Efecto: Modelo atómico clásico Bohr
class AtomShape : public Shape {
private:
float angle_y_ = 0.0f; // Ángulo de rotación global en eje Y (rad)
float orbit_phase_ = 0.0f; // Fase de rotación de electrones (rad)
float nucleus_radius_ = 0.0f; // Radio del núcleo central (píxeles)
float orbit_radius_ = 0.0f; // Radio de las órbitas (píxeles)
int num_points_ = 0; // Cantidad total de puntos
private:
float angle_y_ = 0.0f; // Ángulo de rotación global en eje Y (rad)
float orbit_phase_ = 0.0f; // Fase de rotación de electrones (rad)
float nucleus_radius_ = 0.0f; // Radio del núcleo central (píxeles)
float orbit_radius_ = 0.0f; // Radio de las órbitas (píxeles)
int num_points_ = 0; // Cantidad total de puntos
public:
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 "ATOM"; }
float getScaleFactor(float screen_height) const override;
public:
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 "ATOM"; }
float getScaleFactor(float screen_height) const override;
};

View File

@@ -1,7 +1,10 @@
#include "cube_shape.hpp"
#include "defines.hpp"
#include <algorithm>
#include <cmath>
#include "defines.hpp"
void CubeShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points;
size_ = screen_height * CUBE_SIZE_FACTOR;
@@ -52,23 +55,23 @@ void CubeShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Aplicar rotación en eje Z
float cos_z = cosf(angle_z_);
float sin_z = sinf(angle_z_);
float x_rot_z = x_base * cos_z - y_base * sin_z;
float y_rot_z = x_base * sin_z + y_base * cos_z;
float x_rot_z = (x_base * cos_z) - (y_base * sin_z);
float y_rot_z = (x_base * sin_z) + (y_base * cos_z);
float z_rot_z = z_base;
// Aplicar rotación en eje Y
float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_);
float x_rot_y = x_rot_z * cos_y + z_rot_z * sin_y;
float x_rot_y = (x_rot_z * cos_y) + (z_rot_z * sin_y);
float y_rot_y = y_rot_z;
float z_rot_y = -x_rot_z * sin_y + z_rot_z * cos_y;
float z_rot_y = (-x_rot_z * sin_y) + (z_rot_z * cos_y);
// Aplicar rotación en eje X
float cos_x = cosf(angle_x_);
float sin_x = sinf(angle_x_);
float x_final = x_rot_y;
float y_final = y_rot_y * cos_x - z_rot_y * sin_x;
float z_final = y_rot_y * sin_x + z_rot_y * cos_x;
float y_final = (y_rot_y * cos_x) - (z_rot_y * sin_x);
float z_final = (y_rot_y * sin_x) + (z_rot_y * cos_x);
// Retornar coordenadas finales rotadas
x = x_final;
@@ -76,7 +79,7 @@ void CubeShape::getPoint3D(int index, float& x, float& y, float& z) const {
z = z_final;
}
float CubeShape::getScaleFactor(float screen_height) const {
auto CubeShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional al tamaño del cubo
// Tamaño base = 60px (resolución 320x240, factor 0.25)
const float BASE_SIZE = 60.0f;
@@ -105,12 +108,24 @@ void CubeShape::generateVerticesAndCenters() {
// 2. Añadir 6 centros de caras
// Caras: X=±size (Y,Z varían), Y=±size (X,Z varían), Z=±size (X,Y varían)
base_x_.push_back(size_); base_y_.push_back(0); base_z_.push_back(0); // +X
base_x_.push_back(-size_); base_y_.push_back(0); base_z_.push_back(0); // -X
base_x_.push_back(0); base_y_.push_back(size_); base_z_.push_back(0); // +Y
base_x_.push_back(0); base_y_.push_back(-size_);base_z_.push_back(0); // -Y
base_x_.push_back(0); base_y_.push_back(0); base_z_.push_back(size_); // +Z
base_x_.push_back(0); base_y_.push_back(0); base_z_.push_back(-size_); // -Z
base_x_.push_back(size_);
base_y_.push_back(0);
base_z_.push_back(0); // +X
base_x_.push_back(-size_);
base_y_.push_back(0);
base_z_.push_back(0); // -X
base_x_.push_back(0);
base_y_.push_back(size_);
base_z_.push_back(0); // +Y
base_x_.push_back(0);
base_y_.push_back(-size_);
base_z_.push_back(0); // -Y
base_x_.push_back(0);
base_y_.push_back(0);
base_z_.push_back(size_); // +Z
base_x_.push_back(0);
base_y_.push_back(0);
base_z_.push_back(-size_); // -Z
// 3. Añadir 12 centros de aristas
// Aristas paralelas a X (4), Y (4), Z (4)
@@ -143,16 +158,16 @@ void CubeShape::generateVerticesAndCenters() {
void CubeShape::generateVolumetricGrid() {
// Calcular dimensión del grid cúbico: N³ ≈ num_points
int grid_dim = static_cast<int>(ceilf(cbrtf(static_cast<float>(num_points_))));
if (grid_dim < 3) grid_dim = 3; // Mínimo grid 3x3x3
grid_dim = std::max(grid_dim, 3); // Mínimo grid 3x3x3
float step = (2.0f * size_) / (grid_dim - 1); // Espacio entre puntos
for (int ix = 0; ix < grid_dim; ix++) {
for (int iy = 0; iy < grid_dim; iy++) {
for (int iz = 0; iz < grid_dim; iz++) {
float x = -size_ + ix * step;
float y = -size_ + iy * step;
float z = -size_ + iz * step;
float x = -size_ + (ix * step);
float y = -size_ + (iy * step);
float z = -size_ + (iz * step);
base_x_.push_back(x);
base_y_.push_back(y);

View File

@@ -1,8 +1,9 @@
#pragma once
#include "shape.hpp"
#include <vector>
#include "shape.hpp"
// Figura: Cubo 3D rotante
// Distribución:
// - 1-8 pelotas: Solo vértices (8 puntos)
@@ -10,28 +11,28 @@
// - 27+ pelotas: Grid volumétrico 3D uniforme
// Comportamiento: Rotación simultánea en ejes X, Y, Z (efecto Rubik)
class CubeShape : public Shape {
private:
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
float size_ = 0.0f; // Mitad del lado del cubo (píxeles)
int num_points_ = 0; // Cantidad de puntos generados
private:
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
float size_ = 0.0f; // Mitad del lado del cubo (píxeles)
int num_points_ = 0; // Cantidad de puntos generados
// Posiciones base 3D (sin rotar) - se calculan en generatePoints()
std::vector<float> base_x_;
std::vector<float> base_y_;
std::vector<float> base_z_;
// Posiciones base 3D (sin rotar) - se calculan en generatePoints()
std::vector<float> base_x_;
std::vector<float> base_y_;
std::vector<float> base_z_;
public:
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 "CUBE"; }
float getScaleFactor(float screen_height) const override;
public:
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 "CUBE"; }
float getScaleFactor(float screen_height) const override;
private:
// Métodos auxiliares para distribución de puntos
void generateVertices(); // 8 vértices
void generateVerticesAndCenters(); // 26 puntos (vértices + caras + aristas)
void generateVolumetricGrid(); // Grid 3D para muchas pelotas
private:
// Métodos auxiliares para distribución de puntos
void generateVertices(); // 8 vértices
void generateVerticesAndCenters(); // 26 puntos (vértices + caras + aristas)
void generateVolumetricGrid(); // Grid 3D para muchas pelotas
};

View File

@@ -1,8 +1,11 @@
#include "cylinder_shape.hpp"
#include "defines.hpp"
#include <algorithm>
#include <cmath>
#include <cstdlib> // Para rand()
#include "defines.hpp"
void CylinderShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points;
radius_ = screen_height * CYLINDER_RADIUS_FACTOR;
@@ -37,7 +40,7 @@ void CylinderShape::update(float delta_time, float screen_width, float screen_he
float t = tumble_progress;
float ease = t < 0.5f
? 2.0f * t * t
: 1.0f - (-2.0f * t + 2.0f) * (-2.0f * t + 2.0f) / 2.0f;
: 1.0f - ((-2.0f * t + 2.0f) * (-2.0f * t + 2.0f) / 2.0f);
angle_x_ = ease * tumble_target_;
}
} else {
@@ -58,10 +61,10 @@ void CylinderShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Calcular número de anillos (altura) y puntos por anillo (circunferencia)
int num_rings = static_cast<int>(sqrtf(static_cast<float>(num_points_) * 0.5f));
if (num_rings < 2) num_rings = 2;
num_rings = std::max(num_rings, 2);
int points_per_ring = num_points_ / num_rings;
if (points_per_ring < 3) points_per_ring = 3;
points_per_ring = std::max(points_per_ring, 3);
// Obtener parámetros u (ángulo) y v (altura) del índice
int ring = index / points_per_ring;
@@ -80,8 +83,10 @@ void CylinderShape::getPoint3D(int index, float& x, float& y, float& z) const {
float u = (static_cast<float>(point_in_ring) / static_cast<float>(points_per_ring)) * 2.0f * PI;
// Parámetro v (altura normalizada): [-1, 1]
float v = (static_cast<float>(ring) / static_cast<float>(num_rings - 1)) * 2.0f - 1.0f;
if (num_rings == 1) v = 0.0f;
float v = ((static_cast<float>(ring) / static_cast<float>(num_rings - 1)) * 2.0f) - 1.0f;
if (num_rings == 1) {
v = 0.0f;
}
// Ecuaciones paramétricas del cilindro
// x = radius * cos(u)
@@ -94,14 +99,14 @@ void CylinderShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Aplicar rotación en eje Y (principal, siempre activa)
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;
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 (tumbling ocasional)
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;
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 con ambas rotaciones
x = x_rot_y;
@@ -109,7 +114,7 @@ void CylinderShape::getPoint3D(int index, float& x, float& y, float& z) const {
z = z_rot;
}
float CylinderShape::getScaleFactor(float screen_height) const {
auto CylinderShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional a la dimensión mayor (altura)
// Altura base = 120px (0.5 * 240px en resolución 320x240)
const float BASE_HEIGHT = 120.0f;

View File

@@ -6,23 +6,23 @@
// Comportamiento: Superficie cilíndrica con rotación en eje Y + tumbling ocasional en X/Z
// Ecuaciones: x = r*cos(u), y = v, z = r*sin(u)
class CylinderShape : public Shape {
private:
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (tumbling ocasional)
float radius_ = 0.0f; // Radio del cilindro (píxeles)
float height_ = 0.0f; // Altura del cilindro (píxeles)
int num_points_ = 0; // Cantidad de puntos generados
private:
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (tumbling ocasional)
float radius_ = 0.0f; // Radio del cilindro (píxeles)
float height_ = 0.0f; // Altura del cilindro (píxeles)
int num_points_ = 0; // Cantidad de puntos generados
// Sistema de tumbling ocasional
float tumble_timer_ = 0.0f; // Temporizador para próximo tumble
float tumble_duration_ = 0.0f; // Duración del tumble actual
bool is_tumbling_ = false; // ¿Estamos en modo tumble?
float tumble_target_ = 0.0f; // Ángulo objetivo del tumble
// Sistema de tumbling ocasional
float tumble_timer_ = 0.0f; // Temporizador para próximo tumble
float tumble_duration_ = 0.0f; // Duración del tumble actual
bool is_tumbling_ = false; // ¿Estamos en modo tumble?
float tumble_target_ = 0.0f; // Ángulo objetivo del tumble
public:
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 "CYLINDER"; }
float getScaleFactor(float screen_height) const override;
public:
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 "CYLINDER"; }
float getScaleFactor(float screen_height) const override;
};

View File

@@ -1,7 +1,9 @@
#include "helix_shape.hpp"
#include "defines.hpp"
#include <cmath>
#include "defines.hpp"
void HelixShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points;
radius_ = screen_height * HELIX_RADIUS_FACTOR;
@@ -41,8 +43,8 @@ void HelixShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Aplicar rotación en eje Y (horizontal)
float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_);
float x_rot = x_base * cos_y - z_base * sin_y;
float z_rot = x_base * sin_y + z_base * cos_y;
float x_rot = (x_base * cos_y) - (z_base * sin_y);
float z_rot = (x_base * sin_y) + (z_base * cos_y);
// Retornar coordenadas finales
x = x_rot;
@@ -50,7 +52,7 @@ void HelixShape::getPoint3D(int index, float& x, float& y, float& z) const {
z = z_rot;
}
float HelixShape::getScaleFactor(float screen_height) const {
auto HelixShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional a la dimensión mayor (altura total)
// Altura base = 180px para 3 vueltas con pitch=0.25 en 240px de altura (180 = 240 * 0.25 * 3)
const float BASE_HEIGHT = 180.0f;

View File

@@ -6,18 +6,18 @@
// Comportamiento: Rotación en eje Y + animación de fase vertical
// Ecuaciones: x = r*cos(t), y = pitch*t + phase, z = r*sin(t)
class HelixShape : public Shape {
private:
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
float phase_offset_ = 0.0f; // Offset de fase para animación vertical (rad)
float radius_ = 0.0f; // Radio de la espiral (píxeles)
float pitch_ = 0.0f; // Separación vertical entre vueltas (píxeles)
float total_height_ = 0.0f; // Altura total de la espiral (píxeles)
int num_points_ = 0; // Cantidad de puntos generados
private:
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
float phase_offset_ = 0.0f; // Offset de fase para animación vertical (rad)
float radius_ = 0.0f; // Radio de la espiral (píxeles)
float pitch_ = 0.0f; // Separación vertical entre vueltas (píxeles)
float total_height_ = 0.0f; // Altura total de la espiral (píxeles)
int num_points_ = 0; // Cantidad de puntos generados
public:
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 "HELIX"; }
float getScaleFactor(float screen_height) const override;
public:
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 "HELIX"; }
float getScaleFactor(float screen_height) const override;
};

View File

@@ -1,8 +1,12 @@
#include "icosahedron_shape.hpp"
#include "defines.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <vector>
#include "defines.hpp"
void IcosahedronShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points;
radius_ = screen_height * ICOSAHEDRON_RADIUS_FACTOR;
@@ -21,37 +25,36 @@ void IcosahedronShape::update(float delta_time, float screen_width, float screen
void IcosahedronShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Proporción áurea (golden ratio)
const float phi = (1.0f + sqrtf(5.0f)) / 2.0f;
const float PHI = (1.0f + sqrtf(5.0f)) / 2.0f;
// 12 vértices del icosaedro regular normalizado
// Basados en 3 rectángulos áureos ortogonales
static const float vertices[12][3] = {
const std::array<std::array<float, 3>, 12> VERTICES = {{
// Rectángulo XY
{-1.0f, phi, 0.0f},
{ 1.0f, phi, 0.0f},
{-1.0f, -phi, 0.0f},
{ 1.0f, -phi, 0.0f},
{-1.0f, PHI, 0.0f},
{1.0f, PHI, 0.0f},
{-1.0f, -PHI, 0.0f},
{1.0f, -PHI, 0.0f},
// Rectángulo YZ
{ 0.0f, -1.0f, phi},
{ 0.0f, 1.0f, phi},
{ 0.0f, -1.0f, -phi},
{ 0.0f, 1.0f, -phi},
{0.0f, -1.0f, PHI},
{0.0f, 1.0f, PHI},
{0.0f, -1.0f, -PHI},
{0.0f, 1.0f, -PHI},
// Rectángulo ZX
{ phi, 0.0f, -1.0f},
{ phi, 0.0f, 1.0f},
{-phi, 0.0f, -1.0f},
{-phi, 0.0f, 1.0f}
};
{PHI, 0.0f, -1.0f},
{PHI, 0.0f, 1.0f},
{-PHI, 0.0f, -1.0f},
{-PHI, 0.0f, 1.0f}}};
// Normalizar para esfera circunscrita
const float normalization = sqrtf(1.0f + phi * phi);
const float NORMALIZATION = sqrtf(1.0f + (PHI * PHI));
// Si tenemos 12 o menos puntos, usar solo vértices
if (num_points_ <= 12) {
int vertex_index = index % 12;
float x_base = vertices[vertex_index][0] / normalization * radius_;
float y_base = vertices[vertex_index][1] / normalization * radius_;
float z_base = vertices[vertex_index][2] / normalization * radius_;
float x_base = VERTICES[vertex_index][0] / NORMALIZATION * radius_;
float y_base = VERTICES[vertex_index][1] / NORMALIZATION * radius_;
float z_base = VERTICES[vertex_index][2] / NORMALIZATION * radius_;
// Aplicar rotaciones
applyRotations(x_base, y_base, z_base, x, y, z);
@@ -62,9 +65,9 @@ void IcosahedronShape::getPoint3D(int index, float& x, float& y, float& z) const
// Distribuir puntos entre vértices (primero) y caras (después)
if (index < 12) {
// Primeros 12 puntos: vértices del icosaedro
float x_base = vertices[index][0] / normalization * radius_;
float y_base = vertices[index][1] / normalization * radius_;
float z_base = vertices[index][2] / normalization * radius_;
float x_base = VERTICES[index][0] / NORMALIZATION * radius_;
float y_base = VERTICES[index][1] / NORMALIZATION * radius_;
float z_base = VERTICES[index][2] / NORMALIZATION * radius_;
applyRotations(x_base, y_base, z_base, x, y, z);
return;
}
@@ -73,38 +76,55 @@ void IcosahedronShape::getPoint3D(int index, float& x, float& y, float& z) const
// El icosaedro tiene 20 caras triangulares
int remaining_points = index - 12;
int points_per_face = (num_points_ - 12) / 20;
if (points_per_face < 1) points_per_face = 1;
points_per_face = std::max(points_per_face, 1);
int face_index = remaining_points / points_per_face;
if (face_index >= 20) face_index = 19;
if (face_index >= 20) {
face_index = 19;
}
int point_in_face = remaining_points % points_per_face;
// Definir algunas caras del icosaedro (usando índices de vértices)
// Solo necesitamos generar puntos, no renderizar caras completas
static const int faces[20][3] = {
{0, 11, 5}, {0, 5, 1}, {0, 1, 7}, {0, 7, 10}, {0, 10, 11},
{1, 5, 9}, {5, 11, 4}, {11, 10, 2}, {10, 7, 6}, {7, 1, 8},
{3, 9, 4}, {3, 4, 2}, {3, 2, 6}, {3, 6, 8}, {3, 8, 9},
{4, 9, 5}, {2, 4, 11}, {6, 2, 10}, {8, 6, 7}, {9, 8, 1}
};
static constexpr std::array<std::array<int, 3>, 20> FACES = {{
{0, 11, 5},
{0, 5, 1},
{0, 1, 7},
{0, 7, 10},
{0, 10, 11},
{1, 5, 9},
{5, 11, 4},
{11, 10, 2},
{10, 7, 6},
{7, 1, 8},
{3, 9, 4},
{3, 4, 2},
{3, 2, 6},
{3, 6, 8},
{3, 8, 9},
{4, 9, 5},
{2, 4, 11},
{6, 2, 10},
{8, 6, 7},
{9, 8, 1}}};
// Obtener vértices de la cara
int v0 = faces[face_index][0];
int v1 = faces[face_index][1];
int v2 = faces[face_index][2];
int v0 = FACES[face_index][0];
int v1 = FACES[face_index][1];
int v2 = FACES[face_index][2];
// Interpolar dentro del triángulo usando coordenadas baricéntricas simples
float t = static_cast<float>(point_in_face) / static_cast<float>(points_per_face + 1);
float u = sqrtf(t);
float v = t - u;
float x_interp = vertices[v0][0] * (1.0f - u - v) + vertices[v1][0] * u + vertices[v2][0] * v;
float y_interp = vertices[v0][1] * (1.0f - u - v) + vertices[v1][1] * u + vertices[v2][1] * v;
float z_interp = vertices[v0][2] * (1.0f - u - v) + vertices[v1][2] * u + vertices[v2][2] * v;
float x_interp = (VERTICES[v0][0] * (1.0f - u - v)) + (VERTICES[v1][0] * u) + (VERTICES[v2][0] * v);
float y_interp = (VERTICES[v0][1] * (1.0f - u - v)) + (VERTICES[v1][1] * u) + (VERTICES[v2][1] * v);
float z_interp = (VERTICES[v0][2] * (1.0f - u - v)) + (VERTICES[v1][2] * u) + (VERTICES[v2][2] * v);
// Proyectar a la esfera
float len = sqrtf(x_interp * x_interp + y_interp * y_interp + z_interp * z_interp);
float len = sqrtf((x_interp * x_interp) + (y_interp * y_interp) + (z_interp * z_interp));
if (len > 0.0001f) {
x_interp /= len;
y_interp /= len;
@@ -122,27 +142,27 @@ void IcosahedronShape::applyRotations(float x_in, float y_in, float z_in, float&
// Aplicar rotación en eje X
float cos_x = cosf(angle_x_);
float sin_x = sinf(angle_x_);
float y_rot_x = y_in * cos_x - z_in * sin_x;
float z_rot_x = y_in * sin_x + z_in * cos_x;
float y_rot_x = (y_in * cos_x) - (z_in * sin_x);
float z_rot_x = (y_in * sin_x) + (z_in * cos_x);
// Aplicar rotación en eje Y
float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_);
float x_rot_y = x_in * cos_y - z_rot_x * sin_y;
float z_rot_y = x_in * sin_y + z_rot_x * cos_y;
float x_rot_y = (x_in * cos_y) - (z_rot_x * sin_y);
float z_rot_y = (x_in * sin_y) + (z_rot_x * cos_y);
// Aplicar rotación en eje Z
float cos_z = cosf(angle_z_);
float sin_z = sinf(angle_z_);
float x_final = x_rot_y * cos_z - y_rot_x * sin_z;
float y_final = x_rot_y * sin_z + y_rot_x * cos_z;
float x_final = (x_rot_y * cos_z) - (y_rot_x * sin_z);
float y_final = (x_rot_y * sin_z) + (y_rot_x * cos_z);
x_out = x_final;
y_out = y_final;
z_out = z_rot_y;
}
float IcosahedronShape::getScaleFactor(float screen_height) const {
auto IcosahedronShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional al radio
// Radio base = 72px (0.30 * 240px en resolución 320x240)
const float BASE_RADIUS = 72.0f;

View File

@@ -6,20 +6,20 @@
// Comportamiento: 12 vértices distribuidos uniformemente con rotación triple
// Geometría: Basado en proporción áurea (golden ratio)
class IcosahedronShape : public Shape {
private:
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
float radius_ = 0.0f; // Radio de la esfera circunscrita (píxeles)
int num_points_ = 0; // Cantidad de puntos generados
private:
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
float radius_ = 0.0f; // Radio de la esfera circunscrita (píxeles)
int num_points_ = 0; // Cantidad de puntos generados
// Helper para aplicar rotaciones triple XYZ
void applyRotations(float x_in, float y_in, float z_in, float& x_out, float& y_out, float& z_out) const;
// Helper para aplicar rotaciones triple XYZ
void applyRotations(float x_in, float y_in, float z_in, float& x_out, float& y_out, float& z_out) const;
public:
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 "ICOSAHEDRON"; }
float getScaleFactor(float screen_height) const override;
public:
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 "ICOSAHEDRON"; }
float getScaleFactor(float screen_height) const override;
};

View File

@@ -1,7 +1,9 @@
#include "lissajous_shape.hpp"
#include "defines.hpp"
#include <cmath>
#include "defines.hpp"
void LissajousShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points;
amplitude_ = screen_height * LISSAJOUS_SIZE_FACTOR;
@@ -33,21 +35,21 @@ void LissajousShape::getPoint3D(int index, float& x, float& y, float& z) const {
// x(t) = A * sin(freq_x * t + phase_x)
// y(t) = A * sin(freq_y * t)
// z(t) = A * sin(freq_z * t + phase_z)
float x_local = amplitude_ * sinf(freq_x_ * t + phase_x_);
float x_local = amplitude_ * sinf((freq_x_ * t) + phase_x_);
float y_local = amplitude_ * sinf(freq_y_ * t);
float z_local = amplitude_ * sinf(freq_z_ * t + phase_z_);
float z_local = amplitude_ * sinf((freq_z_ * t) + phase_z_);
// Aplicar rotación global en eje X
float cos_x = cosf(rotation_x_);
float sin_x = sinf(rotation_x_);
float y_rot = y_local * cos_x - z_local * sin_x;
float z_rot = y_local * sin_x + z_local * cos_x;
float y_rot = (y_local * cos_x) - (z_local * sin_x);
float z_rot = (y_local * sin_x) + (z_local * cos_x);
// Aplicar rotación global en eje Y
float cos_y = cosf(rotation_y_);
float sin_y = sinf(rotation_y_);
float x_final = x_local * cos_y - z_rot * sin_y;
float z_final = x_local * sin_y + z_rot * cos_y;
float x_final = (x_local * cos_y) - (z_rot * sin_y);
float z_final = (x_local * sin_y) + (z_rot * cos_y);
// Retornar coordenadas rotadas
x = x_final;
@@ -55,7 +57,7 @@ void LissajousShape::getPoint3D(int index, float& x, float& y, float& z) const {
z = z_final;
}
float LissajousShape::getScaleFactor(float screen_height) const {
auto LissajousShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional a la amplitud de la curva
// Amplitud base = 84px (0.35 * 240px en resolución 320x240)
const float BASE_SIZE = 84.0f;

View File

@@ -6,21 +6,21 @@
// Comportamiento: Curva paramétrica 3D con rotación global y animación de fase
// Ecuaciones: x(t) = A*sin(freq_x*t + phase_x), y(t) = A*sin(freq_y*t), z(t) = A*sin(freq_z*t + phase_z)
class LissajousShape : public Shape {
private:
float freq_x_ = 0.0f; // Frecuencia en eje X
float freq_y_ = 0.0f; // Frecuencia en eje Y
float freq_z_ = 0.0f; // Frecuencia en eje Z
float phase_x_ = 0.0f; // Desfase X (animado)
float phase_z_ = 0.0f; // Desfase Z (animado)
float rotation_x_ = 0.0f; // Rotación global en eje X (rad)
float rotation_y_ = 0.0f; // Rotación global en eje Y (rad)
float amplitude_ = 0.0f; // Amplitud de la curva (píxeles)
int num_points_ = 0; // Cantidad total de puntos
private:
float freq_x_ = 0.0f; // Frecuencia en eje X
float freq_y_ = 0.0f; // Frecuencia en eje Y
float freq_z_ = 0.0f; // Frecuencia en eje Z
float phase_x_ = 0.0f; // Desfase X (animado)
float phase_z_ = 0.0f; // Desfase Z (animado)
float rotation_x_ = 0.0f; // Rotación global en eje X (rad)
float rotation_y_ = 0.0f; // Rotación global en eje Y (rad)
float amplitude_ = 0.0f; // Amplitud de la curva (píxeles)
int num_points_ = 0; // Cantidad total de puntos
public:
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 "LISSAJOUS"; }
float getScaleFactor(float screen_height) const override;
public:
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 "LISSAJOUS"; }
float getScaleFactor(float screen_height) const override;
};

View File

@@ -1,16 +1,18 @@
#include "png_shape.hpp"
#include <algorithm>
#include <cmath>
#include <iostream>
#include <map>
#include "defines.hpp"
#include "external/stb_image.h"
#include "resource_manager.hpp"
#include <cmath>
#include <algorithm>
#include <iostream>
#include <map>
PNGShape::PNGShape(const char* png_path) {
// Cargar PNG desde path
if (!loadPNG(png_path)) {
std::cerr << "[PNGShape] Usando fallback 10x10" << std::endl;
std::cerr << "[PNGShape] Usando fallback 10x10" << '\n';
// Fallback: generar un cuadrado simple si falla la carga
image_width_ = 10;
image_height_ = 10;
@@ -21,7 +23,7 @@ PNGShape::PNGShape(const char* png_path) {
next_idle_time_ = PNG_IDLE_TIME_MIN + (rand() % 1000) / 1000.0f * (PNG_IDLE_TIME_MAX - PNG_IDLE_TIME_MIN);
}
bool PNGShape::loadPNG(const char* resource_key) {
auto PNGShape::loadPNG(const char* resource_key) -> bool {
{
std::string fn = std::string(resource_key);
fn = fn.substr(fn.find_last_of("\\/") + 1);
@@ -30,15 +32,16 @@ bool PNGShape::loadPNG(const char* resource_key) {
unsigned char* file_data = nullptr;
size_t file_size = 0;
if (!ResourceManager::loadResource(resource_key, file_data, file_size)) {
std::cerr << "[PNGShape] ERROR: recurso no encontrado: " << resource_key << std::endl;
std::cerr << "[PNGShape] ERROR: recurso no encontrado: " << resource_key << '\n';
return false;
}
int width, height, channels;
unsigned char* pixels = stbi_load_from_memory(file_data, static_cast<int>(file_size),
&width, &height, &channels, 1);
int width;
int height;
int channels;
unsigned char* pixels = stbi_load_from_memory(file_data, static_cast<int>(file_size), &width, &height, &channels, 1);
delete[] file_data;
if (!pixels) {
std::cerr << "[PNGShape] ERROR al decodificar PNG: " << stbi_failure_reason() << std::endl;
if (pixels == nullptr) {
std::cerr << "[PNGShape] ERROR al decodificar PNG: " << stbi_failure_reason() << '\n';
return false;
}
image_width_ = width;
@@ -57,9 +60,11 @@ void PNGShape::detectEdges() {
// 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;
int idx = (y * image_width_) + x;
if (!pixel_data_[idx]) continue; // Solo píxeles blancos
if (!pixel_data_[idx]) {
continue; // Solo píxeles blancos
}
// Verificar vecinos (arriba, abajo, izq, der)
bool is_edge = false;
@@ -68,10 +73,10 @@ void PNGShape::detectEdges() {
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
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;
}
}
@@ -90,7 +95,7 @@ void PNGShape::floodFill() {
for (int y = 0; y < image_height_; y++) {
for (int x = 0; x < image_width_; x++) {
int idx = y * image_width_ + x;
int idx = (y * image_width_) + x;
if (pixel_data_[idx]) {
filled_points_.push_back({static_cast<float>(x), static_cast<float>(y)});
}
@@ -114,8 +119,8 @@ void PNGShape::generatePoints(int num_points, float screen_width, float screen_h
num_layers_ = PNG_NUM_EXTRUSION_LAYERS;
// Generar AMBOS conjuntos de puntos (relleno Y bordes)
floodFill(); // Generar filled_points_
detectEdges(); // Generar edge_points_
floodFill(); // Generar filled_points_
detectEdges(); // Generar edge_points_
// Guardar copias originales (las funciones de filtrado modifican los vectores)
std::vector<Point2D> filled_points_original = filled_points_;
@@ -123,7 +128,7 @@ void PNGShape::generatePoints(int num_points, float screen_width, float screen_h
// Conjunto de puntos ACTIVO (será modificado por filtros)
std::vector<Point2D> active_points_data;
std::string mode_name = "";
std::string mode_name;
// === SISTEMA DE DISTRIBUCIÓN ADAPTATIVA ===
// Estrategia: Optimizar según número de pelotas disponibles
@@ -196,8 +201,6 @@ void PNGShape::generatePoints(int num_points, float screen_width, float screen_h
std::vector<Point2D> 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";
}
}
@@ -216,7 +219,7 @@ void PNGShape::generatePoints(int num_points, float screen_width, float screen_h
// Extraer filas alternas de puntos (FUNCIÓN PURA: no modifica parámetros)
// Recibe vector original y devuelve nuevo vector filtrado
std::vector<PNGShape::Point2D> PNGShape::extractAlternateRows(const std::vector<Point2D>& source, int row_skip) {
auto PNGShape::extractAlternateRows(const std::vector<Point2D>& source, int row_skip) -> std::vector<PNGShape::Point2D> {
std::vector<Point2D> result;
if (row_skip <= 1 || source.empty()) {
@@ -243,7 +246,7 @@ std::vector<PNGShape::Point2D> PNGShape::extractAlternateRows(const std::vector<
}
// Extraer vértices y esquinas (FUNCIÓN PURA: devuelve nuevo vector)
std::vector<PNGShape::Point2D> PNGShape::extractCornerVertices(const std::vector<Point2D>& source) {
auto PNGShape::extractCornerVertices(const std::vector<Point2D>& source) -> std::vector<PNGShape::Point2D> {
std::vector<Point2D> result;
if (source.empty()) {
@@ -267,9 +270,9 @@ std::vector<PNGShape::Point2D> PNGShape::extractCornerVertices(const std::vector
// Generar puntos en extremos de cada fila
for (const auto& [row_y, extremes] : row_extremes) {
result.push_back({extremes.first, static_cast<float>(row_y)}); // Extremo izquierdo
if (extremes.second != extremes.first) { // Solo añadir derecho si es diferente
result.push_back({extremes.second, static_cast<float>(row_y)}); // Extremo derecho
result.push_back({extremes.first, static_cast<float>(row_y)}); // Extremo izquierdo
if (extremes.second != extremes.first) { // Solo añadir derecho si es diferente
result.push_back({extremes.second, static_cast<float>(row_y)}); // Extremo derecho
}
}
@@ -376,8 +379,8 @@ void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const {
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%
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;
@@ -386,14 +389,14 @@ void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const {
// 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;
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;
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;
@@ -408,7 +411,7 @@ void PNGShape::getPoint3D(int index, float& x, float& y, float& z) const {
}
}
float PNGShape::getScaleFactor(float screen_height) const {
auto PNGShape::getScaleFactor(float screen_height) const -> float {
// Escala dinámica según resolución
return PNG_SIZE_FACTOR;
}
@@ -432,7 +435,7 @@ void PNGShape::setConvergence(float convergence) {
}
// Obtener progreso del flip actual (0.0 = inicio del flip, 1.0 = fin del flip)
float PNGShape::getFlipProgress() const {
auto PNGShape::getFlipProgress() const -> float {
if (!is_flipping_) {
return 0.0f; // No está flipping, progreso = 0
}

View File

@@ -1,104 +1,108 @@
#pragma once
#include "shape.hpp"
#include "defines.hpp" // Para PNG_IDLE_TIME_MIN/MAX constantes
#include <cstdlib> // Para rand()
#include <vector>
#include <cstdlib> // Para rand()
#include "defines.hpp" // Para PNG_IDLE_TIME_MIN/MAX constantes
#include "shape.hpp"
// 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<bool> pixel_data_; // Mapa de píxeles blancos (true = blanco)
private:
// Datos de la imagen cargada
int image_width_ = 0;
int image_height_ = 0;
std::vector<bool> pixel_data_; // Mapa de píxeles blancos (true = blanco)
// Puntos generados (Enfoque A: Extrusión 2D)
struct Point2D {
float x, y;
};
std::vector<Point2D> edge_points_; // Contorno (solo bordes) - ORIGINAL sin optimizar
std::vector<Point2D> filled_points_; // Relleno completo - ORIGINAL sin optimizar
std::vector<Point2D> optimized_points_; // Puntos finales optimizados (usado por getPoint3D)
// Puntos generados (Enfoque A: Extrusión 2D)
struct Point2D {
float x, y;
};
std::vector<Point2D> edge_points_; // Contorno (solo bordes) - ORIGINAL sin optimizar
std::vector<Point2D> filled_points_; // Relleno completo - ORIGINAL sin optimizar
std::vector<Point2D> optimized_points_; // Puntos finales optimizados (usado por getPoint3D)
// 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)
// 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)
// 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)
// Pivoteo sutil en estado IDLE
float tilt_x_ = 0.0f; // Oscilación sutil en eje X
float tilt_y_ = 0.0f; // Oscilación sutil en eje Y
// Pivoteo sutil en estado IDLE
float tilt_x_ = 0.0f; // Oscilación sutil en eje X
float tilt_y_ = 0.0f; // Oscilación sutil en eje Y
// Modo LOGO (intervalos de flip más largos)
bool is_logo_mode_ = false; // true = usar intervalos LOGO (más lentos)
// Modo LOGO (intervalos de flip más largos)
bool is_logo_mode_ = false; // true = usar intervalos LOGO (más lentos)
// Sistema de convergencia (solo relevante en modo LOGO)
float current_convergence_ = 0.0f; // Porcentaje actual de convergencia (0.0-1.0)
bool convergence_threshold_reached_ = false; // true si ha alcanzado umbral mínimo (80%)
// Sistema de convergencia (solo relevante en modo LOGO)
float current_convergence_ = 0.0f; // Porcentaje actual de convergencia (0.0-1.0)
bool convergence_threshold_reached_ = false; // true si ha alcanzado umbral mínimo (80%)
// Sistema de tracking de flips (para modo LOGO - espera de flips)
int flip_count_ = 0; // Contador de flips completados (reset al entrar a LOGO)
bool was_flipping_last_frame_ = false; // Estado previo para detectar transiciones
// Sistema de tracking de flips (para modo LOGO - espera de flips)
int flip_count_ = 0; // Contador de flips completados (reset al entrar a LOGO)
bool was_flipping_last_frame_ = false; // Estado previo para detectar transiciones
// Dimensiones normalizadas
float scale_factor_ = 1.0f;
float center_offset_x_ = 0.0f;
float center_offset_y_ = 0.0f;
// 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)
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
// Métodos internos
bool loadPNG(const char* resource_key); // 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
// Métodos de distribución adaptativa (funciones puras, no modifican parámetros)
std::vector<Point2D> extractAlternateRows(const std::vector<Point2D>& source, int row_skip); // Extraer filas alternas
std::vector<Point2D> extractCornerVertices(const std::vector<Point2D>& source); // Extraer vértices/esquinas
// Métodos de distribución adaptativa (funciones puras, no modifican parámetros)
static std::vector<Point2D> extractAlternateRows(const std::vector<Point2D>& source, int row_skip); // Extraer filas alternas
static std::vector<Point2D> extractCornerVertices(const std::vector<Point2D>& source); // Extraer vértices/esquinas
public:
// Constructor: recibe path relativo al PNG
PNGShape(const char* png_path = "data/shapes/jailgames.png");
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;
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;
// Consultar estado de flip
bool isFlipping() const { return is_flipping_; }
// Consultar estado de flip
bool isFlipping() const { return is_flipping_; }
// Obtener progreso del flip actual (0.0 = inicio, 1.0 = fin)
float getFlipProgress() const;
// Obtener progreso del flip actual (0.0 = inicio, 1.0 = fin)
float getFlipProgress() const;
// Obtener número de flips completados (para modo LOGO)
int getFlipCount() const { return flip_count_; }
// Obtener número de flips completados (para modo LOGO)
int getFlipCount() const { return flip_count_; }
// Resetear contador de flips (llamar al entrar a LOGO MODE)
void resetFlipCount() { flip_count_ = 0; was_flipping_last_frame_ = false; }
// Resetear contador de flips (llamar al entrar a LOGO MODE)
void resetFlipCount() {
flip_count_ = 0;
was_flipping_last_frame_ = false;
}
// Control de modo LOGO (flip intervals más largos)
void setLogoMode(bool enable) {
is_logo_mode_ = enable;
// Recalcular next_idle_time_ con el rango apropiado
float idle_min = enable ? PNG_IDLE_TIME_MIN_LOGO : PNG_IDLE_TIME_MIN;
float idle_max = enable ? PNG_IDLE_TIME_MAX_LOGO : PNG_IDLE_TIME_MAX;
next_idle_time_ = idle_min + (rand() % 1000) / 1000.0f * (idle_max - idle_min);
}
// Control de modo LOGO (flip intervals más largos)
void setLogoMode(bool enable) {
is_logo_mode_ = enable;
// Recalcular next_idle_time_ con el rango apropiado
float idle_min = enable ? PNG_IDLE_TIME_MIN_LOGO : PNG_IDLE_TIME_MIN;
float idle_max = enable ? PNG_IDLE_TIME_MAX_LOGO : PNG_IDLE_TIME_MAX;
next_idle_time_ = idle_min + (rand() % 1000) / 1000.0f * (idle_max - idle_min);
}
// Sistema de convergencia (override de Shape::setConvergence)
void setConvergence(float convergence) override;
// Sistema de convergencia (override de Shape::setConvergence)
void setConvergence(float convergence) override;
};

View File

@@ -2,34 +2,34 @@
// Interfaz abstracta para todas las figuras 3D
class Shape {
public:
virtual ~Shape() = default;
public:
virtual ~Shape() = default;
// Generar distribución inicial de puntos en la figura
// num_points: cantidad de pelotas a distribuir
// screen_width/height: dimensiones del área de juego (para escalar)
virtual void generatePoints(int num_points, float screen_width, float screen_height) = 0;
// Generar distribución inicial de puntos en la figura
// num_points: cantidad de pelotas a distribuir
// screen_width/height: dimensiones del área de juego (para escalar)
virtual void generatePoints(int num_points, float screen_width, float screen_height) = 0;
// Actualizar animación de la figura (rotación, deformación, etc.)
// delta_time: tiempo transcurrido desde último frame
// screen_width/height: dimensiones actuales (puede cambiar con F4)
virtual void update(float delta_time, float screen_width, float screen_height) = 0;
// Actualizar animación de la figura (rotación, deformación, etc.)
// delta_time: tiempo transcurrido desde último frame
// screen_width/height: dimensiones actuales (puede cambiar con F4)
virtual void update(float delta_time, float screen_width, float screen_height) = 0;
// Obtener posición 3D del punto i después de transformaciones (rotación, etc.)
// index: índice del punto (0 a num_points-1)
// x, y, z: coordenadas 3D en espacio mundo (centradas en 0,0,0)
virtual void getPoint3D(int index, float& x, float& y, float& z) const = 0;
// Obtener posición 3D del punto i después de transformaciones (rotación, etc.)
// index: índice del punto (0 a num_points-1)
// x, y, z: coordenadas 3D en espacio mundo (centradas en 0,0,0)
virtual void getPoint3D(int index, float& x, float& y, float& z) const = 0;
// Obtener nombre de la figura para debug display
virtual const char* getName() const = 0;
// Obtener nombre de la figura para debug display
virtual const char* getName() const = 0;
// Obtener factor de escala para ajustar física según tamaño de figura
// screen_height: altura actual de pantalla
// Retorna: factor multiplicador para constantes de física (spring_k, damping, etc.)
virtual float getScaleFactor(float screen_height) const = 0;
// Obtener factor de escala para ajustar física según tamaño de figura
// screen_height: altura actual de pantalla
// Retorna: factor multiplicador para constantes de física (spring_k, damping, etc.)
virtual float getScaleFactor(float screen_height) const = 0;
// Notificar a la figura sobre el porcentaje de convergencia (pelotas cerca del objetivo)
// convergence: valor de 0.0 (0%) a 1.0 (100%) indicando cuántas pelotas están en posición
// Default: no-op (la mayoría de figuras no necesitan esta información)
virtual void setConvergence(float convergence) {}
// Notificar a la figura sobre el porcentaje de convergencia (pelotas cerca del objetivo)
// convergence: valor de 0.0 (0%) a 1.0 (100%) indicando cuántas pelotas están en posición
// Default: no-op (la mayoría de figuras no necesitan esta información)
virtual void setConvergence(float convergence) {}
};

View File

@@ -1,7 +1,9 @@
#include "sphere_shape.hpp"
#include "defines.hpp"
#include <cmath>
#include "defines.hpp"
void SphereShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points;
radius_ = screen_height * ROTOBALL_RADIUS_FACTOR;
@@ -19,12 +21,12 @@ void SphereShape::update(float delta_time, float screen_width, float screen_heig
void SphereShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Algoritmo Fibonacci Sphere para distribución uniforme
const float golden_ratio = (1.0f + sqrtf(5.0f)) / 2.0f;
const float angle_increment = PI * 2.0f * golden_ratio;
const float GOLDEN_RATIO = (1.0f + sqrtf(5.0f)) / 2.0f;
const float ANGLE_INCREMENT = PI * 2.0f * GOLDEN_RATIO;
float t = static_cast<float>(index) / static_cast<float>(num_points_);
float phi = acosf(1.0f - 2.0f * t); // Latitud
float theta = angle_increment * static_cast<float>(index); // Longitud
float phi = acosf(1.0f - (2.0f * t)); // Latitud
float theta = ANGLE_INCREMENT * static_cast<float>(index); // Longitud
// Convertir coordenadas esféricas a cartesianas
float x_base = cosf(theta) * sinf(phi) * radius_;
@@ -34,14 +36,14 @@ void SphereShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Aplicar rotación en eje Y
float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_);
float x_rot = x_base * cos_y - z_base * sin_y;
float z_rot = x_base * sin_y + z_base * cos_y;
float x_rot = (x_base * cos_y) - (z_base * sin_y);
float z_rot = (x_base * sin_y) + (z_base * cos_y);
// Aplicar rotación en eje X
float cos_x = cosf(angle_x_);
float sin_x = sinf(angle_x_);
float y_rot = y_base * cos_x - z_rot * sin_x;
float z_final = y_base * sin_x + z_rot * cos_x;
float y_rot = (y_base * cos_x) - (z_rot * sin_x);
float z_final = (y_base * sin_x) + (z_rot * cos_x);
// Retornar coordenadas finales rotadas
x = x_rot;
@@ -49,7 +51,7 @@ void SphereShape::getPoint3D(int index, float& x, float& y, float& z) const {
z = z_final;
}
float SphereShape::getScaleFactor(float screen_height) const {
auto SphereShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional al radio
// Radio base = 80px (resolución 320x240)
const float BASE_RADIUS = 80.0f;

View File

@@ -6,16 +6,16 @@
// Comportamiento: Rotación dual en ejes X e Y
// Uso anterior: RotoBall
class SphereShape : public Shape {
private:
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
float radius_ = 0.0f; // Radio de la esfera (píxeles)
int num_points_ = 0; // Cantidad de puntos generados
private:
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
float radius_ = 0.0f; // Radio de la esfera (píxeles)
int num_points_ = 0; // Cantidad de puntos generados
public:
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 "SPHERE"; }
float getScaleFactor(float screen_height) const override;
public:
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 "SPHERE"; }
float getScaleFactor(float screen_height) const override;
};

View File

@@ -1,7 +1,10 @@
#include "torus_shape.hpp"
#include "defines.hpp"
#include <algorithm>
#include <cmath>
#include "defines.hpp"
void TorusShape::generatePoints(int num_points, float screen_width, float screen_height) {
num_points_ = num_points;
major_radius_ = screen_height * TORUS_MAJOR_RADIUS_FACTOR;
@@ -26,10 +29,10 @@ void TorusShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Calcular número aproximado de anillos y puntos por anillo
int num_rings = static_cast<int>(sqrtf(static_cast<float>(num_points_) * 0.5f));
if (num_rings < 2) num_rings = 2;
num_rings = std::max(num_rings, 2);
int points_per_ring = num_points_ / num_rings;
if (points_per_ring < 3) points_per_ring = 3;
points_per_ring = std::max(points_per_ring, 3);
// Obtener parámetros u y v del índice
int ring = index / points_per_ring;
@@ -57,7 +60,7 @@ void TorusShape::getPoint3D(int index, float& x, float& y, float& z) const {
float cos_u = cosf(u);
float sin_u = sinf(u);
float radius_at_v = major_radius_ + minor_radius_ * cos_v;
float radius_at_v = major_radius_ + (minor_radius_ * cos_v);
float x_base = radius_at_v * cos_u;
float y_base = radius_at_v * sin_u;
@@ -66,20 +69,20 @@ void TorusShape::getPoint3D(int index, float& x, float& y, float& z) const {
// Aplicar rotación en eje X
float cos_x = cosf(angle_x_);
float sin_x = sinf(angle_x_);
float y_rot_x = y_base * cos_x - z_base * sin_x;
float z_rot_x = y_base * sin_x + z_base * cos_x;
float y_rot_x = (y_base * cos_x) - (z_base * sin_x);
float z_rot_x = (y_base * sin_x) + (z_base * cos_x);
// Aplicar rotación en eje Y
float cos_y = cosf(angle_y_);
float sin_y = sinf(angle_y_);
float x_rot_y = x_base * cos_y - z_rot_x * sin_y;
float z_rot_y = x_base * sin_y + z_rot_x * cos_y;
float x_rot_y = (x_base * cos_y) - (z_rot_x * sin_y);
float z_rot_y = (x_base * sin_y) + (z_rot_x * cos_y);
// Aplicar rotación en eje Z
float cos_z = cosf(angle_z_);
float sin_z = sinf(angle_z_);
float x_final = x_rot_y * cos_z - y_rot_x * sin_z;
float y_final = x_rot_y * sin_z + y_rot_x * cos_z;
float x_final = (x_rot_y * cos_z) - (y_rot_x * sin_z);
float y_final = (x_rot_y * sin_z) + (y_rot_x * cos_z);
// Retornar coordenadas finales rotadas
x = x_final;
@@ -87,7 +90,7 @@ void TorusShape::getPoint3D(int index, float& x, float& y, float& z) const {
z = z_rot_y;
}
float TorusShape::getScaleFactor(float screen_height) const {
auto TorusShape::getScaleFactor(float screen_height) const -> float {
// Factor de escala para física: proporcional al radio mayor
// Radio mayor base = 60px (0.25 * 240px en resolución 320x240)
const float BASE_RADIUS = 60.0f;

View File

@@ -6,18 +6,18 @@
// Comportamiento: Superficie toroidal con rotación triple (X, Y, Z)
// Ecuaciones: x = (R + r*cos(v))*cos(u), y = (R + r*cos(v))*sin(u), z = r*sin(v)
class TorusShape : public Shape {
private:
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
float major_radius_ = 0.0f; // Radio mayor R (del centro al tubo)
float minor_radius_ = 0.0f; // Radio menor r (grosor del tubo)
int num_points_ = 0; // Cantidad de puntos generados
private:
float angle_x_ = 0.0f; // Ángulo de rotación en eje X (rad)
float angle_y_ = 0.0f; // Ángulo de rotación en eje Y (rad)
float angle_z_ = 0.0f; // Ángulo de rotación en eje Z (rad)
float major_radius_ = 0.0f; // Radio mayor R (del centro al tubo)
float minor_radius_ = 0.0f; // Radio menor r (grosor del tubo)
int num_points_ = 0; // Cantidad de puntos generados
public:
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 "TORUS"; }
float getScaleFactor(float screen_height) const override;
public:
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 "TORUS"; }
float getScaleFactor(float screen_height) const override;
};

View File

@@ -1,15 +1,15 @@
#include "shape_manager.hpp"
#include <algorithm> // for std::min, std::max, std::transform
#include <cctype> // for ::tolower
#include <cstdlib> // for rand
#include <string> // for std::string
#include <algorithm> // for std::min, std::max, std::transform
#include <cctype> // for ::tolower
#include <cstdlib> // for rand
#include <string> // for std::string
#include "ball.hpp" // for Ball
#include "defines.hpp" // for constantes
#include "scene/scene_manager.hpp" // for SceneManager
#include "state/state_manager.hpp" // for StateManager
#include "ui/ui_manager.hpp" // for UIManager
#include "ball.hpp" // for Ball
#include "defines.hpp" // for constantes
#include "scene/scene_manager.hpp" // for SceneManager
#include "state/state_manager.hpp" // for StateManager
#include "ui/ui_manager.hpp" // for UIManager
// Includes de todas las shapes (necesario para creación polimórfica)
#include "shapes/atom_shape.hpp"
@@ -23,26 +23,24 @@
#include "shapes/torus_shape.hpp"
ShapeManager::ShapeManager()
: engine_(nullptr)
, scene_mgr_(nullptr)
, ui_mgr_(nullptr)
, state_mgr_(nullptr)
, current_mode_(SimulationMode::PHYSICS)
, current_shape_type_(ShapeType::SPHERE)
, last_shape_type_(ShapeType::SPHERE)
, active_shape_(nullptr)
, shape_scale_factor_(1.0f)
, depth_zoom_enabled_(true)
, screen_width_(0)
, screen_height_(0)
, shape_convergence_(0.0f) {
: engine_(nullptr),
scene_mgr_(nullptr),
ui_mgr_(nullptr),
state_mgr_(nullptr),
current_mode_(SimulationMode::PHYSICS),
current_shape_type_(ShapeType::SPHERE),
last_shape_type_(ShapeType::SPHERE),
active_shape_(nullptr),
shape_scale_factor_(1.0f),
depth_zoom_enabled_(true),
screen_width_(0),
screen_height_(0),
shape_convergence_(0.0f) {
}
ShapeManager::~ShapeManager() {
}
ShapeManager::~ShapeManager() = default;
void ShapeManager::initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr,
StateManager* state_mgr, int screen_width, int screen_height) {
void ShapeManager::initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, StateManager* state_mgr, int screen_width, int screen_height) {
engine_ = engine;
scene_mgr_ = scene_mgr;
ui_mgr_ = ui_mgr;
@@ -66,17 +64,17 @@ void ShapeManager::toggleShapeMode(bool force_gravity_on_exit) {
activateShapeInternal(last_shape_type_);
// Si estamos en modo LOGO y la figura es PNG_SHAPE, restaurar configuración LOGO
if (state_mgr_ && state_mgr_->getCurrentMode() == AppMode::LOGO && last_shape_type_ == ShapeType::PNG_SHAPE) {
if ((state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::LOGO && last_shape_type_ == ShapeType::PNG_SHAPE) {
if (active_shape_) {
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
if (png_shape) {
auto* png_shape = dynamic_cast<PNGShape*>(active_shape_.get());
if (png_shape != nullptr) {
png_shape->setLogoMode(true);
}
}
}
// Si estamos en LOGO MODE, resetear convergencia al entrar
if (state_mgr_ && state_mgr_->getCurrentMode() == AppMode::LOGO) {
if ((state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::LOGO) {
shape_convergence_ = 0.0f;
}
} else {
@@ -93,7 +91,7 @@ void ShapeManager::toggleShapeMode(bool force_gravity_on_exit) {
}
// Mostrar notificación (solo si NO estamos en modo demo o logo)
if (state_mgr_ && ui_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
if ((state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
ui_mgr_->showNotification("Modo física");
}
}
@@ -113,8 +111,8 @@ void ShapeManager::handleShapeScaleChange(bool increase) {
clampShapeScale();
// Mostrar notificación si está en modo SANDBOX
if (ui_mgr_ && state_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
std::string notification = "Escala " + std::to_string(static_cast<int>(shape_scale_factor_ * 100.0f + 0.5f)) + "%";
if ((ui_mgr_ != nullptr) && (state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
std::string notification = "Escala " + std::to_string(static_cast<int>((shape_scale_factor_ * 100.0f) + 0.5f)) + "%";
ui_mgr_->showNotification(notification);
}
}
@@ -125,7 +123,7 @@ void ShapeManager::resetShapeScale() {
shape_scale_factor_ = SHAPE_SCALE_DEFAULT;
// Mostrar notificación si está en modo SANDBOX
if (ui_mgr_ && state_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
if ((ui_mgr_ != nullptr) && (state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
ui_mgr_->showNotification("Escala 100%");
}
}
@@ -136,14 +134,16 @@ void ShapeManager::toggleDepthZoom() {
depth_zoom_enabled_ = !depth_zoom_enabled_;
// Mostrar notificación si está en modo SANDBOX
if (ui_mgr_ && state_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
if ((ui_mgr_ != nullptr) && (state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
ui_mgr_->showNotification(depth_zoom_enabled_ ? "Profundidad on" : "Profundidad off");
}
}
}
void ShapeManager::update(float delta_time) {
if (!active_shape_ || current_mode_ != SimulationMode::SHAPE) return;
if (!active_shape_ || current_mode_ != SimulationMode::SHAPE) {
return;
}
// Actualizar animación de la figura
active_shape_->update(delta_time, static_cast<float>(screen_width_), static_cast<float>(screen_height_));
@@ -161,7 +161,9 @@ void ShapeManager::update(float delta_time) {
// Actualizar cada pelota con física de atracción
for (size_t i = 0; i < balls.size(); i++) {
// Obtener posición 3D rotada del punto i
float x_3d, y_3d, z_3d;
float x_3d;
float y_3d;
float z_3d;
active_shape_->getPoint3D(static_cast<int>(i), x_3d, y_3d, z_3d);
// Aplicar escala manual a las coordenadas 3D
@@ -179,9 +181,7 @@ void ShapeManager::update(float delta_time) {
// Aplicar fuerza de atracción física hacia el punto rotado
// Usar constantes SHAPE (mayor pegajosidad que ROTOBALL)
float shape_size = scale_factor * 80.0f; // 80px = radio base
balls[i]->applyShapeForce(target_x, target_y, shape_size, delta_time,
SHAPE_SPRING_K, SHAPE_DAMPING_BASE, SHAPE_DAMPING_NEAR,
SHAPE_NEAR_THRESHOLD, SHAPE_MAX_FORCE);
balls[i]->applyShapeForce(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)
@@ -191,12 +191,12 @@ void ShapeManager::update(float delta_time) {
// Calcular escala según profundidad Z (perspectiva) - solo si está activado
// 0.0 (fondo) → 0.5x, 0.5 (medio) → 1.0x, 1.0 (frente) → 1.5x
float depth_scale = depth_zoom_enabled_ ? (0.5f + z_normalized * 1.0f) : 1.0f;
float depth_scale = depth_zoom_enabled_ ? (0.5f + (z_normalized * 1.0f)) : 1.0f;
balls[i]->setDepthScale(depth_scale);
}
// Calcular convergencia en LOGO MODE (% de pelotas cerca de su objetivo)
if (state_mgr_ && state_mgr_->getCurrentMode() == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) {
if ((state_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::LOGO && current_mode_ == SimulationMode::SHAPE) {
int balls_near = 0;
float distance_threshold = LOGO_CONVERGENCE_DISTANCE; // 20px fijo (más permisivo)
@@ -215,7 +215,9 @@ void ShapeManager::update(float delta_time) {
}
void ShapeManager::generateShape() {
if (!active_shape_) return;
if (!active_shape_) {
return;
}
int num_points = static_cast<int>(scene_mgr_->getBallCount());
active_shape_->generatePoints(num_points, static_cast<float>(screen_width_), static_cast<float>(screen_height_));
@@ -277,9 +279,9 @@ void ShapeManager::activateShapeInternal(ShapeType type) {
scene_mgr_->enableShapeAttractionAll(true);
// Mostrar notificación con nombre de figura (solo si NO estamos en modo demo o logo)
if (active_shape_ && state_mgr_ && ui_mgr_ && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
if (active_shape_ && (state_mgr_ != nullptr) && (ui_mgr_ != nullptr) && state_mgr_->getCurrentMode() == AppMode::SANDBOX) {
std::string shape_name = active_shape_->getName();
std::transform(shape_name.begin(), shape_name.end(), shape_name.begin(), ::tolower);
std::ranges::transform(shape_name, shape_name.begin(), ::tolower);
std::string notification = std::string("Modo ") + shape_name;
ui_mgr_->showNotification(notification);
}

View File

@@ -46,8 +46,7 @@ class ShapeManager {
* @param screen_width Ancho lógico de pantalla
* @param screen_height Alto lógico de pantalla
*/
void initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr,
StateManager* state_mgr, int screen_width, int screen_height);
void initialize(Engine* engine, SceneManager* scene_mgr, UIManager* ui_mgr, StateManager* state_mgr, int screen_width, int screen_height);
/**
* @brief Toggle entre modo PHYSICS y SHAPE
@@ -147,10 +146,10 @@ class ShapeManager {
private:
// === Referencias a otros componentes ===
Engine* engine_; // Callback al Engine (legacy - temporal)
SceneManager* scene_mgr_; // Acceso a bolas y física
UIManager* ui_mgr_; // Notificaciones
StateManager* state_mgr_; // Verificación de modo actual
Engine* engine_; // Callback al Engine (legacy - temporal)
SceneManager* scene_mgr_; // Acceso a bolas y física
UIManager* ui_mgr_; // Notificaciones
StateManager* state_mgr_; // Verificación de modo actual
// === Estado de figuras 3D ===
SimulationMode current_mode_;

View File

@@ -1,39 +1,37 @@
#include "state_manager.hpp"
#include <algorithm> // for std::min
#include <array> // for std::array
#include <cstdlib> // for rand
#include <vector> // for std::vector
#include "defines.hpp" // for constantes DEMO/LOGO
#include "engine.hpp" // for Engine (enter/exitShapeMode, texture)
#include "scene/scene_manager.hpp" // for SceneManager
#include "shapes_mgr/shape_manager.hpp" // for ShapeManager
#include "shapes/png_shape.hpp" // for PNGShape flip detection
#include "theme_manager.hpp" // for ThemeManager
#include "defines.hpp" // for constantes DEMO/LOGO
#include "engine.hpp" // for Engine (enter/exitShapeMode, texture)
#include "scene/scene_manager.hpp" // for SceneManager
#include "shapes/png_shape.hpp" // for PNGShape flip detection
#include "shapes_mgr/shape_manager.hpp" // for ShapeManager
#include "theme_manager.hpp" // for ThemeManager
StateManager::StateManager()
: engine_(nullptr)
, scene_mgr_(nullptr)
, theme_mgr_(nullptr)
, shape_mgr_(nullptr)
, current_app_mode_(AppMode::SANDBOX)
, previous_app_mode_(AppMode::SANDBOX)
, demo_timer_(0.0f)
, demo_next_action_time_(0.0f)
, logo_convergence_threshold_(0.90f)
, logo_min_time_(3.0f)
, logo_max_time_(5.0f)
, logo_waiting_for_flip_(false)
, logo_target_flip_number_(0)
, logo_target_flip_percentage_(0.0f)
, logo_current_flip_count_(0)
, logo_entered_manually_(false)
, logo_previous_theme_(0)
, logo_previous_texture_index_(0)
, logo_previous_shape_scale_(1.0f) {
}
StateManager::~StateManager() {
: engine_(nullptr),
scene_mgr_(nullptr),
theme_mgr_(nullptr),
shape_mgr_(nullptr),
current_app_mode_(AppMode::SANDBOX),
previous_app_mode_(AppMode::SANDBOX),
demo_timer_(0.0f),
demo_next_action_time_(0.0f),
logo_convergence_threshold_(0.90f),
logo_min_time_(3.0f),
logo_max_time_(5.0f),
logo_waiting_for_flip_(false),
logo_target_flip_number_(0),
logo_target_flip_percentage_(0.0f),
logo_current_flip_count_(0),
logo_entered_manually_(false),
logo_previous_theme_(0),
logo_previous_texture_index_(0),
logo_previous_shape_scale_(1.0f) {
}
void StateManager::initialize(Engine* engine, SceneManager* scene_mgr, ThemeManager* theme_mgr, ShapeManager* shape_mgr) {
@@ -53,8 +51,10 @@ void StateManager::setLogoPreviousState(int theme, size_t texture_index, float s
// ACTUALIZACIÓN DE ESTADOS
// ===========================================================================
void StateManager::update(float delta_time, float shape_convergence, Shape* active_shape) {
if (current_app_mode_ == AppMode::SANDBOX) return;
void StateManager::update(float delta_time, float shape_convergence, Shape* active_shape) { // NOLINT(readability-function-cognitive-complexity)
if (current_app_mode_ == AppMode::SANDBOX) {
return;
}
demo_timer_ += delta_time;
@@ -63,14 +63,12 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
if (current_app_mode_ == AppMode::LOGO) {
if (logo_waiting_for_flip_) {
// CAMINO B: Esperando a que ocurran flips
PNGShape* png_shape = dynamic_cast<PNGShape*>(active_shape);
auto* png_shape = dynamic_cast<PNGShape*>(active_shape);
if (png_shape) {
if (png_shape != nullptr) {
int current_flip_count = png_shape->getFlipCount();
if (current_flip_count > logo_current_flip_count_) {
logo_current_flip_count_ = current_flip_count;
}
logo_current_flip_count_ = std::max(current_flip_count, logo_current_flip_count_);
if (logo_current_flip_count_ + 1 >= logo_target_flip_number_) {
if (png_shape->isFlipping()) {
@@ -93,7 +91,9 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
should_trigger = demo_timer_ >= demo_next_action_time_;
}
if (!should_trigger) return;
if (!should_trigger) {
return;
}
if (current_app_mode_ == AppMode::LOGO) {
// LOGO MODE: Sistema de acciones variadas con gravedad dinámica
@@ -104,7 +104,7 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
if (logo_waiting_for_flip_) {
// Ya estábamos esperando flips → hacer el cambio SHAPE → PHYSICS
if (action < 50) {
engine_->exitShapeMode(true); // Con gravedad ON
engine_->exitShapeMode(true); // Con gravedad ON
} else {
engine_->exitShapeMode(false); // Con gravedad OFF
}
@@ -122,15 +122,15 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
logo_target_flip_percentage_ = LOGO_FLIP_TRIGGER_MIN + (rand() % 1000) / 1000.0f * (LOGO_FLIP_TRIGGER_MAX - LOGO_FLIP_TRIGGER_MIN);
logo_current_flip_count_ = 0;
PNGShape* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
if (png_shape) {
auto* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
if (png_shape != nullptr) {
png_shape->resetFlipCount();
}
// No hacer nada más — esperar a que ocurran los flips
} else {
// CAMINO A (50%): Cambio inmediato
if (action < 50) {
engine_->exitShapeMode(true); // SHAPE → PHYSICS con gravedad ON
engine_->exitShapeMode(true); // SHAPE → PHYSICS con gravedad ON
} else {
engine_->exitShapeMode(false); // SHAPE → PHYSICS con gravedad OFF
}
@@ -158,7 +158,7 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
scene_mgr_->forceBallsGravityOff();
} else {
// 16%: Cambiar dirección de gravedad
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
auto new_direction = static_cast<GravityDirection>(rand() % 4);
scene_mgr_->changeGravityDirection(new_direction);
scene_mgr_->forceBallsGravityOn();
}
@@ -186,7 +186,9 @@ void StateManager::update(float delta_time, float shape_convergence, Shape* acti
}
void StateManager::setState(AppMode new_mode, int current_screen_width, int current_screen_height) {
if (current_app_mode_ == new_mode) return;
if (current_app_mode_ == new_mode) {
return;
}
if (current_app_mode_ == AppMode::LOGO && new_mode != AppMode::LOGO) {
previous_app_mode_ = new_mode;
@@ -201,7 +203,8 @@ void StateManager::setState(AppMode new_mode, int current_screen_width, int curr
demo_timer_ = 0.0f;
if (new_mode == AppMode::DEMO || new_mode == AppMode::DEMO_LITE || new_mode == AppMode::LOGO) {
float min_interval, max_interval;
float min_interval;
float max_interval;
if (new_mode == AppMode::LOGO) {
float resolution_scale = current_screen_height / 720.0f;
@@ -250,8 +253,10 @@ void StateManager::toggleLogoMode(int current_screen_width, int current_screen_h
// ACCIONES DE DEMO
// ===========================================================================
void StateManager::performDemoAction(bool is_lite) {
if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return;
void StateManager::performDemoAction(bool is_lite) { // NOLINT(readability-function-cognitive-complexity)
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
return;
}
// ============================================
// SALTO AUTOMÁTICO A LOGO MODE (Easter Egg)
@@ -278,18 +283,18 @@ void StateManager::performDemoAction(bool is_lite) {
// ACCIONES NORMALES DE DEMO/DEMO_LITE
// ============================================
int TOTAL_WEIGHT;
int total_weight;
int random_value;
int accumulated_weight = 0;
if (is_lite) {
TOTAL_WEIGHT = DEMO_LITE_WEIGHT_GRAVITY_DIR + DEMO_LITE_WEIGHT_GRAVITY_TOGGLE + DEMO_LITE_WEIGHT_SHAPE + DEMO_LITE_WEIGHT_TOGGLE_PHYSICS + DEMO_LITE_WEIGHT_IMPULSE;
random_value = rand() % TOTAL_WEIGHT;
total_weight = DEMO_LITE_WEIGHT_GRAVITY_DIR + DEMO_LITE_WEIGHT_GRAVITY_TOGGLE + DEMO_LITE_WEIGHT_SHAPE + DEMO_LITE_WEIGHT_TOGGLE_PHYSICS + DEMO_LITE_WEIGHT_IMPULSE;
random_value = rand() % total_weight;
// Cambiar dirección gravedad (25%)
accumulated_weight += DEMO_LITE_WEIGHT_GRAVITY_DIR;
if (random_value < accumulated_weight) {
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
auto new_direction = static_cast<GravityDirection>(rand() % 4);
scene_mgr_->changeGravityDirection(new_direction);
return;
}
@@ -304,8 +309,8 @@ void StateManager::performDemoAction(bool is_lite) {
// Activar figura 3D (25%) - PNG_SHAPE excluido
accumulated_weight += DEMO_LITE_WEIGHT_SHAPE;
if (random_value < accumulated_weight) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(shapes[rand() % 8]);
constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(SHAPES[rand() % 8]);
return;
}
@@ -324,13 +329,13 @@ void StateManager::performDemoAction(bool is_lite) {
}
} else {
TOTAL_WEIGHT = DEMO_WEIGHT_GRAVITY_DIR + DEMO_WEIGHT_GRAVITY_TOGGLE + DEMO_WEIGHT_SHAPE + DEMO_WEIGHT_TOGGLE_PHYSICS + DEMO_WEIGHT_REGENERATE_SHAPE + DEMO_WEIGHT_THEME + DEMO_WEIGHT_SCENARIO + DEMO_WEIGHT_IMPULSE + DEMO_WEIGHT_DEPTH_ZOOM + DEMO_WEIGHT_SHAPE_SCALE + DEMO_WEIGHT_SPRITE;
random_value = rand() % TOTAL_WEIGHT;
total_weight = DEMO_WEIGHT_GRAVITY_DIR + DEMO_WEIGHT_GRAVITY_TOGGLE + DEMO_WEIGHT_SHAPE + DEMO_WEIGHT_TOGGLE_PHYSICS + DEMO_WEIGHT_REGENERATE_SHAPE + DEMO_WEIGHT_THEME + DEMO_WEIGHT_SCENARIO + DEMO_WEIGHT_IMPULSE + DEMO_WEIGHT_DEPTH_ZOOM + DEMO_WEIGHT_SHAPE_SCALE + DEMO_WEIGHT_SPRITE;
random_value = rand() % total_weight;
// Cambiar dirección gravedad (10%)
accumulated_weight += DEMO_WEIGHT_GRAVITY_DIR;
if (random_value < accumulated_weight) {
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
auto new_direction = static_cast<GravityDirection>(rand() % 4);
scene_mgr_->changeGravityDirection(new_direction);
return;
}
@@ -345,8 +350,8 @@ void StateManager::performDemoAction(bool is_lite) {
// Activar figura 3D (20%) - PNG_SHAPE excluido
accumulated_weight += DEMO_WEIGHT_SHAPE;
if (random_value < accumulated_weight) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(shapes[rand() % 8]);
constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(SHAPES[rand() % 8]);
return;
}
@@ -378,10 +383,12 @@ void StateManager::performDemoAction(bool is_lite) {
if (random_value < accumulated_weight) {
int auto_max = std::min(engine_->getMaxAutoScenario(), DEMO_AUTO_MAX_SCENARIO);
std::vector<int> candidates;
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i)
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) {
candidates.push_back(i);
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable())
}
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable()) {
candidates.push_back(CUSTOM_SCENARIO_IDX);
}
int new_scenario = candidates[rand() % candidates.size()];
SimulationMode current_sim_mode = shape_mgr_->getCurrentMode();
scene_mgr_->changeScenario(new_scenario, current_sim_mode);
@@ -439,15 +446,15 @@ void StateManager::performDemoAction(bool is_lite) {
// RANDOMIZACIÓN AL INICIAR DEMO
// ===========================================================================
void StateManager::randomizeOnDemoStart(bool is_lite) {
if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return;
void StateManager::randomizeOnDemoStart(bool is_lite) { // NOLINT(readability-function-cognitive-complexity)
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
return;
}
// Si venimos de LOGO con PNG_SHAPE, cambiar figura obligatoriamente
if (shape_mgr_->getCurrentShapeType() == ShapeType::PNG_SHAPE) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX,
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(shapes[rand() % 8]);
constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(SHAPES[rand() % 8]);
}
if (is_lite) {
@@ -457,11 +464,11 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
engine_->exitShapeMode(false);
}
} else {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(shapes[rand() % 8]);
constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(SHAPES[rand() % 8]);
}
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
auto new_direction = static_cast<GravityDirection>(rand() % 4);
scene_mgr_->changeGravityDirection(new_direction);
if (rand() % 2 == 0) {
toggleGravityOnOff();
@@ -476,14 +483,14 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
engine_->exitShapeMode(false);
}
} else {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
ShapeType selected_shape = shapes[rand() % 8];
constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
ShapeType selected_shape = SHAPES[rand() % 8];
// Randomizar profundidad y escala ANTES de activar la figura
if (rand() % 2 == 0) {
shape_mgr_->setDepthZoomEnabled(!shape_mgr_->isDepthZoomEnabled());
}
shape_mgr_->setShapeScaleFactor(0.5f + (rand() % 1500) / 1000.0f);
shape_mgr_->setShapeScaleFactor(0.5f + ((rand() % 1500) / 1000.0f));
engine_->enterShapeMode(selected_shape);
}
@@ -491,10 +498,12 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
// 2. Escenario
int auto_max = std::min(engine_->getMaxAutoScenario(), DEMO_AUTO_MAX_SCENARIO);
std::vector<int> candidates;
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i)
for (int i = DEMO_AUTO_MIN_SCENARIO; i <= auto_max; ++i) {
candidates.push_back(i);
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable())
}
if (engine_->isCustomScenarioEnabled() && engine_->isCustomAutoAvailable()) {
candidates.push_back(CUSTOM_SCENARIO_IDX);
}
int new_scenario = candidates[rand() % candidates.size()];
SimulationMode current_sim_mode = shape_mgr_->getCurrentMode();
scene_mgr_->changeScenario(new_scenario, current_sim_mode);
@@ -513,7 +522,7 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
}
// 5. Gravedad
GravityDirection new_direction = static_cast<GravityDirection>(rand() % 4);
auto new_direction = static_cast<GravityDirection>(rand() % 4);
scene_mgr_->changeGravityDirection(new_direction);
if (rand() % 3 == 0) {
toggleGravityOnOff();
@@ -526,10 +535,12 @@ void StateManager::randomizeOnDemoStart(bool is_lite) {
// ===========================================================================
void StateManager::toggleGravityOnOff() {
if (!scene_mgr_) return;
if (scene_mgr_ == nullptr) {
return;
}
bool gravity_enabled = scene_mgr_->hasBalls() &&
(scene_mgr_->getFirstBall()->getGravityForce() > 0.0f);
(scene_mgr_->getFirstBall()->getGravityForce() > 0.0f);
if (gravity_enabled) {
scene_mgr_->forceBallsGravityOff();
@@ -543,7 +554,9 @@ void StateManager::toggleGravityOnOff() {
// ===========================================================================
void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int current_screen_height, size_t ball_count) {
if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return;
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
return;
}
logo_entered_manually_ = !from_demo;
@@ -585,8 +598,8 @@ void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int c
}
// Cambiar a tema aleatorio entre: MONOCHROME, LAVENDER, CRIMSON, ESMERALDA
int logo_themes[] = {5, 6, 7, 8};
theme_mgr_->switchToTheme(logo_themes[rand() % 4]);
constexpr std::array<int, 4> LOGO_THEMES = {5, 6, 7, 8};
theme_mgr_->switchToTheme(LOGO_THEMES[rand() % 4]);
// Establecer escala a 120%
shape_mgr_->setShapeScaleFactor(LOGO_MODE_SHAPE_SCALE);
@@ -595,8 +608,8 @@ void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int c
engine_->enterShapeMode(ShapeType::PNG_SHAPE);
// Configurar PNG_SHAPE en modo LOGO
PNGShape* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
if (png_shape) {
auto* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
if (png_shape != nullptr) {
png_shape->setLogoMode(true);
png_shape->resetFlipCount();
}
@@ -607,8 +620,12 @@ void StateManager::enterLogoMode(bool from_demo, int current_screen_width, int c
// ===========================================================================
void StateManager::exitLogoMode(bool return_to_demo) {
if (current_app_mode_ != AppMode::LOGO) return;
if (!engine_ || !scene_mgr_ || !theme_mgr_ || !shape_mgr_) return;
if (current_app_mode_ != AppMode::LOGO) {
return;
}
if ((engine_ == nullptr) || (scene_mgr_ == nullptr) || (theme_mgr_ == nullptr) || (shape_mgr_ == nullptr)) {
return;
}
logo_entered_manually_ = false;
@@ -624,17 +641,15 @@ void StateManager::exitLogoMode(bool return_to_demo) {
}
// Desactivar modo LOGO en PNG_SHAPE
PNGShape* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
if (png_shape) {
auto* png_shape = dynamic_cast<PNGShape*>(shape_mgr_->getActiveShape());
if (png_shape != nullptr) {
png_shape->setLogoMode(false);
}
// Si la figura activa es PNG_SHAPE, cambiar a otra figura aleatoria
if (shape_mgr_->getCurrentShapeType() == ShapeType::PNG_SHAPE) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX,
ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER,
ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(shapes[rand() % 8]);
constexpr std::array<ShapeType, 8> SHAPES = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
engine_->enterShapeMode(SHAPES[rand() % 8]);
}
if (!return_to_demo) {

View File

@@ -1,7 +1,8 @@
#pragma once
#include <SDL3/SDL_stdinc.h> // for Uint64
#include <cstddef> // for size_t
#include <cstddef> // for size_t
#include "defines.hpp" // for AppMode, ShapeType, GravityDirection
@@ -37,7 +38,7 @@ class StateManager {
/**
* @brief Destructor
*/
~StateManager();
~StateManager() = default;
/**
* @brief Inicializa el StateManager con referencias a los subsistemas necesarios

View File

@@ -1,24 +1,32 @@
#include "textrenderer.hpp"
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <iostream>
#include "resource_manager.hpp"
TextRenderer::TextRenderer() : renderer_(nullptr), font_(nullptr), font_size_(0), use_antialiasing_(true), font_data_buffer_(nullptr) {
TextRenderer::TextRenderer()
: renderer_(nullptr),
font_(nullptr),
font_size_(0),
use_antialiasing_(true),
font_data_buffer_(nullptr) {
}
TextRenderer::~TextRenderer() {
cleanup();
}
bool TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_size, bool use_antialiasing) {
auto TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_size, bool use_antialiasing) -> bool {
renderer_ = renderer;
font_size_ = font_size;
use_antialiasing_ = use_antialiasing;
font_path_ = font_path; // Guardar ruta para reinitialize()
// Inicializar SDL_ttf si no está inicializado
if (!TTF_WasInit()) {
if (TTF_WasInit() == 0) {
if (!TTF_Init()) {
SDL_Log("Error al inicializar SDL_ttf: %s", SDL_GetError());
return false;
@@ -26,34 +34,33 @@ bool TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_
}
// Intentar cargar la fuente desde ResourceManager (pack o disco)
unsigned char* fontData = nullptr;
size_t fontDataSize = 0;
unsigned char* font_data = nullptr;
size_t font_data_size = 0;
if (ResourceManager::loadResource(font_path, fontData, fontDataSize)) {
if (ResourceManager::loadResource(font_path, font_data, font_data_size)) {
// Crear SDL_IOStream desde memoria
SDL_IOStream* fontIO = SDL_IOFromConstMem(fontData, static_cast<size_t>(fontDataSize));
if (fontIO != nullptr) {
SDL_IOStream* font_io = SDL_IOFromConstMem(font_data, font_data_size);
if (font_io != nullptr) {
// Cargar fuente desde IOStream
font_ = TTF_OpenFontIO(fontIO, true, font_size); // true = cerrar stream automáticamente
font_ = TTF_OpenFontIO(font_io, true, font_size); // true = cerrar stream automáticamente
if (font_ == nullptr) {
SDL_Log("Error al cargar fuente desde memoria '%s': %s", font_path, SDL_GetError());
delete[] fontData; // Liberar solo si falla la carga
delete[] font_data; // Liberar solo si falla la carga
return false;
}
// CRÍTICO: NO eliminar fontData aquí - SDL_ttf necesita estos datos en memoria
// mientras la fuente esté abierta. Se liberará en cleanup()
font_data_buffer_ = fontData;
font_data_buffer_ = font_data;
{
std::string fn = std::string(font_path);
fn = fn.substr(fn.find_last_of("\\/") + 1);
std::cout << "[Fuente] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
}
return true;
} else {
delete[] fontData;
}
delete[] font_data;
}
// Fallback final: intentar cargar directamente desde disco (por si falla ResourceManager)
@@ -66,7 +73,7 @@ bool TextRenderer::init(SDL_Renderer* renderer, const char* font_path, int font_
return true;
}
bool TextRenderer::reinitialize(int new_font_size) {
auto TextRenderer::reinitialize(int new_font_size) -> bool {
// Verificar que tenemos todo lo necesario
if (renderer_ == nullptr || font_path_.empty()) {
SDL_Log("Error: TextRenderer no inicializado correctamente para reinitialize()");
@@ -89,39 +96,42 @@ bool TextRenderer::reinitialize(int new_font_size) {
}
// Intentar cargar la fuente desde ResourceManager con el nuevo tamaño
unsigned char* fontData = nullptr;
size_t fontDataSize = 0;
unsigned char* font_data = nullptr;
size_t font_data_size = 0;
if (ResourceManager::loadResource(font_path_, fontData, fontDataSize)) {
SDL_IOStream* fontIO = SDL_IOFromConstMem(fontData, static_cast<size_t>(fontDataSize));
if (fontIO != nullptr) {
font_ = TTF_OpenFontIO(fontIO, true, new_font_size);
if (ResourceManager::loadResource(font_path_, font_data, font_data_size)) {
SDL_IOStream* font_io = SDL_IOFromConstMem(font_data, font_data_size);
if (font_io != nullptr) {
font_ = TTF_OpenFontIO(font_io, true, new_font_size);
if (font_ == nullptr) {
SDL_Log("Error al recargar fuente '%s' con tamaño %d: %s",
font_path_.c_str(), new_font_size, SDL_GetError());
delete[] fontData; // Liberar solo si falla
font_path_.c_str(),
new_font_size,
SDL_GetError());
delete[] font_data; // Liberar solo si falla
return false;
}
// Mantener buffer en memoria (NO eliminar)
font_data_buffer_ = fontData;
font_data_buffer_ = font_data;
font_size_ = new_font_size;
{
std::string fn = font_path_.substr(font_path_.find_last_of("\\/") + 1);
std::cout << "[Fuente] " << fn << " (" << (ResourceManager::isPackLoaded() ? "pack" : "disco") << ")\n";
}
return true;
} else {
delete[] fontData;
}
delete[] font_data;
}
// Fallback: cargar directamente desde disco
font_ = TTF_OpenFont(font_path_.c_str(), new_font_size);
if (font_ == nullptr) {
SDL_Log("Error al recargar fuente '%s' con tamaño %d: %s",
font_path_.c_str(), new_font_size, SDL_GetError());
font_path_.c_str(),
new_font_size,
SDL_GetError());
return false;
}
@@ -233,7 +243,8 @@ void TextRenderer::printPhysical(int logical_x, int logical_y, const char* text,
dest_rect.h = static_cast<float>(text_surface->h);
// Deshabilitar temporalmente presentación lógica para renderizar en píxeles físicos
int logical_w = 0, logical_h = 0;
int logical_w = 0;
int logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode);
@@ -301,7 +312,8 @@ void TextRenderer::printAbsolute(int physical_x, int physical_y, const char* tex
dest_rect.h = static_cast<float>(text_surface->h);
// Deshabilitar temporalmente presentación lógica para renderizar en píxeles físicos
int logical_w = 0, logical_h = 0;
int logical_w = 0;
int logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode);
@@ -332,7 +344,7 @@ void TextRenderer::printAbsoluteShadowed(int physical_x, int physical_y, const s
printAbsoluteShadowed(physical_x, physical_y, text.c_str());
}
int TextRenderer::getTextWidth(const char* text) {
auto TextRenderer::getTextWidth(const char* text) -> int {
if (!isInitialized() || text == nullptr) {
return 0;
}
@@ -345,7 +357,7 @@ int TextRenderer::getTextWidth(const char* text) {
return width;
}
int TextRenderer::getTextWidthPhysical(const char* text) {
auto TextRenderer::getTextWidthPhysical(const char* text) -> int {
// Retorna el ancho REAL en píxeles físicos (sin escalado lógico)
// Idéntico a getTextWidth() pero semánticamente diferente:
// - Este método se usa cuando se necesita el ancho REAL de la fuente
@@ -362,7 +374,7 @@ int TextRenderer::getTextWidthPhysical(const char* text) {
return width; // Ancho real de la textura generada por TTF
}
int TextRenderer::getTextHeight() {
auto TextRenderer::getTextHeight() -> int {
if (!isInitialized()) {
return 0;
}
@@ -370,7 +382,7 @@ int TextRenderer::getTextHeight() {
return TTF_GetFontHeight(font_);
}
int TextRenderer::getGlyphHeight() {
auto TextRenderer::getGlyphHeight() -> int {
if (!isInitialized()) {
return 0;
}

View File

@@ -2,60 +2,61 @@
#include <SDL3/SDL.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <string>
class TextRenderer {
public:
TextRenderer();
~TextRenderer();
public:
TextRenderer();
~TextRenderer();
// Inicializa el renderizador de texto con una fuente
bool init(SDL_Renderer* renderer, const char* font_path, int font_size, bool use_antialiasing = true);
// Inicializa el renderizador de texto con una fuente
bool init(SDL_Renderer* renderer, const char* font_path, int font_size, bool use_antialiasing = true);
// Reinicializa el renderizador con un nuevo tamaño de fuente
bool reinitialize(int new_font_size);
// Reinicializa el renderizador con un nuevo tamaño de fuente
bool reinitialize(int new_font_size);
// Libera recursos
void cleanup();
// Libera recursos
void cleanup();
// Renderiza texto en la posición especificada con color RGB
void print(int x, int y, const char* text, uint8_t r, uint8_t g, uint8_t b);
void print(int x, int y, const std::string& text, uint8_t r, uint8_t g, uint8_t b);
// Renderiza texto en la posición especificada con color RGB
void print(int x, int y, const char* text, uint8_t r, uint8_t g, uint8_t b);
void print(int x, int y, const std::string& text, uint8_t r, uint8_t g, uint8_t b);
// Renderiza texto en coordenadas lógicas, pero convierte a físicas para tamaño absoluto
void printPhysical(int logical_x, int logical_y, const char* text, uint8_t r, uint8_t g, uint8_t b, float scale_x, float scale_y);
void printPhysical(int logical_x, int logical_y, const std::string& text, uint8_t r, uint8_t g, uint8_t b, float scale_x, float scale_y);
// Renderiza texto en coordenadas lógicas, pero convierte a físicas para tamaño absoluto
void printPhysical(int logical_x, int logical_y, const char* text, uint8_t r, uint8_t g, uint8_t b, float scale_x, float scale_y);
void printPhysical(int logical_x, int logical_y, const std::string& text, uint8_t r, uint8_t g, uint8_t b, float scale_x, float scale_y);
// Renderiza texto en coordenadas físicas absolutas (tamaño fijo independiente de resolución)
// NOTA: Este método usa el tamaño de fuente tal cual fue cargado, sin escalado
void printAbsolute(int physical_x, int physical_y, const char* text, SDL_Color color);
void printAbsolute(int physical_x, int physical_y, const std::string& text, SDL_Color color);
// Renderiza texto en coordenadas físicas absolutas (tamaño fijo independiente de resolución)
// NOTA: Este método usa el tamaño de fuente tal cual fue cargado, sin escalado
void printAbsolute(int physical_x, int physical_y, const char* text, SDL_Color color);
void printAbsolute(int physical_x, int physical_y, const std::string& text, SDL_Color color);
// Renderiza texto con sombra negra (+1px offset) para máxima legibilidad sobre cualquier fondo
void printAbsoluteShadowed(int physical_x, int physical_y, const char* text);
void printAbsoluteShadowed(int physical_x, int physical_y, const std::string& text);
// Renderiza texto con sombra negra (+1px offset) para máxima legibilidad sobre cualquier fondo
void printAbsoluteShadowed(int physical_x, int physical_y, const char* text);
void printAbsoluteShadowed(int physical_x, int physical_y, const std::string& text);
// Obtiene el ancho de un texto renderizado (en píxeles lógicos para compatibilidad)
int getTextWidth(const char* text);
// Obtiene el ancho de un texto renderizado (en píxeles lógicos para compatibilidad)
int getTextWidth(const char* text);
// Obtiene el ancho de un texto en píxeles FÍSICOS reales (sin escalado)
// Útil para notificaciones y elementos UI de tamaño fijo
int getTextWidthPhysical(const char* text);
// Obtiene el ancho de un texto en píxeles FÍSICOS reales (sin escalado)
// Útil para notificaciones y elementos UI de tamaño fijo
int getTextWidthPhysical(const char* text);
// Obtiene la altura de la fuente (incluye line_gap)
int getTextHeight();
// Obtiene la altura de la fuente (incluye line_gap)
int getTextHeight();
// Obtiene la altura real del glifo (ascender + |descendente|, sin line_gap)
int getGlyphHeight();
// Obtiene la altura real del glifo (ascender + |descendente|, sin line_gap)
int getGlyphHeight();
// Verifica si está inicializado correctamente
bool isInitialized() const { return font_ != nullptr && renderer_ != nullptr; }
// Verifica si está inicializado correctamente
bool isInitialized() const { return font_ != nullptr && renderer_ != nullptr; }
private:
SDL_Renderer* renderer_;
TTF_Font* font_;
int font_size_;
bool use_antialiasing_;
std::string font_path_; // Almacenar ruta para reinitialize()
unsigned char* font_data_buffer_; // Buffer de datos de fuente (mantener en memoria mientras esté abierta)
private:
SDL_Renderer* renderer_;
TTF_Font* font_;
int font_size_;
bool use_antialiasing_;
std::string font_path_; // Almacenar ruta para reinitialize()
unsigned char* font_data_buffer_; // Buffer de datos de fuente (mantener en memoria mientras esté abierta)
};

File diff suppressed because it is too large Load Diff

View File

@@ -3,10 +3,10 @@
#include <memory> // for unique_ptr
#include <vector> // for vector
#include "ball.hpp" // for Ball class
#include "defines.hpp" // for Color, ColorTheme
#include "themes/theme.hpp" // for Theme interface
#include "themes/theme_snapshot.hpp" // for ThemeSnapshot
#include "ball.hpp" // for Ball class
#include "defines.hpp" // for Color, ColorTheme
#include "themes/theme.hpp" // for Theme interface
#include "themes/theme_snapshot.hpp" // for ThemeSnapshot
/**
* ThemeManager: Gestiona el sistema de temas visuales (unificado, estáticos y dinámicos)
@@ -42,7 +42,7 @@ class ThemeManager {
~ThemeManager() = default;
// Inicialización
void initialize(); // Inicializa 15 temas unificados (9 estáticos + 6 dinámicos)
void initialize(); // Inicializa 15 temas unificados (9 estáticos + 6 dinámicos)
void setMaxBallCount(int n) { max_ball_count_ = n; } // Máximo real (escenario 8 o custom si mayor)
// Interfaz unificada (PHASE 2 + PHASE 3)
@@ -53,9 +53,8 @@ class ThemeManager {
void pauseDynamic(); // Toggle pausa de animación (Shift+D, solo dinámicos)
// Queries de colores (usado en rendering)
Color getInterpolatedColor(size_t ball_index) const; // Obtiene color interpolado para pelota
void getBackgroundColors(float& top_r, float& top_g, float& top_b,
float& bottom_r, float& bottom_g, float& bottom_b) const; // Obtiene colores de fondo degradado
Color getInterpolatedColor(size_t ball_index) const; // Obtiene color interpolado para pelota
void getBackgroundColors(float& top_r, float& top_g, float& top_b, float& bottom_r, float& bottom_g, float& bottom_b) const; // Obtiene colores de fondo degradado
// Queries de estado (para debug display y lógica)
int getCurrentThemeIndex() const { return current_theme_index_; }
@@ -89,13 +88,13 @@ class ThemeManager {
// ========================================
// Estado de transición
bool transitioning_ = false; // ¿Hay transición LERP activa?
float transition_progress_ = 0.0f; // Progreso 0.0-1.0 (0.0 = origen, 1.0 = destino)
bool transitioning_ = false; // ¿Hay transición LERP activa?
float transition_progress_ = 0.0f; // Progreso 0.0-1.0 (0.0 = origen, 1.0 = destino)
float transition_duration_ = THEME_TRANSITION_DURATION; // Duración en segundos (configurable en defines.h)
// Índices de temas involucrados en transición
int source_theme_index_ = 0; // Tema origen (del que venimos)
int target_theme_index_ = 0; // Tema destino (al que vamos)
int source_theme_index_ = 0; // Tema origen (del que venimos)
int target_theme_index_ = 0; // Tema destino (al que vamos)
// Snapshot del tema origen (capturado al iniciar transición)
std::unique_ptr<ThemeSnapshot> source_snapshot_; // nullptr si no hay transición
@@ -112,6 +111,6 @@ class ThemeManager {
void initializeDynamicThemes(); // Crea 6 temas dinámicos (índices 9-14)
// Sistema de transición LERP (PHASE 3)
std::unique_ptr<ThemeSnapshot> captureCurrentSnapshot() const; // Captura snapshot del tema actual
std::unique_ptr<ThemeSnapshot> captureCurrentSnapshot() const; // Captura snapshot del tema actual
float lerp(float a, float b, float t) const { return a + (b - a) * t; } // Interpolación lineal
};

View File

@@ -1,19 +1,15 @@
#include "dynamic_theme.hpp"
#include <algorithm> // for std::min
DynamicTheme::DynamicTheme(const char* name_en, const char* name_es,
int text_r, int text_g, int text_b,
std::vector<DynamicThemeKeyframe> keyframes,
bool loop)
DynamicTheme::DynamicTheme(const char* name_en, const char* name_es, int text_r, int text_g, int text_b, std::vector<DynamicThemeKeyframe> keyframes, bool loop)
: name_en_(name_en),
name_es_(name_es),
text_r_(text_r), text_g_(text_g), text_b_(text_b),
text_r_(text_r),
text_g_(text_g),
text_b_(text_b),
keyframes_(std::move(keyframes)),
loop_(loop),
current_keyframe_index_(0),
target_keyframe_index_(1),
transition_progress_(0.0f),
paused_(false) {
loop_(loop) {
// Validación: mínimo 2 keyframes
if (keyframes_.size() < 2) {
// Fallback: duplicar primer keyframe si solo hay 1
@@ -29,7 +25,9 @@ DynamicTheme::DynamicTheme(const char* name_en, const char* name_es,
}
void DynamicTheme::update(float delta_time) {
if (paused_) return; // No actualizar si está pausado
if (paused_) {
return; // No actualizar si está pausado
}
// Obtener duración del keyframe objetivo
float duration = keyframes_[target_keyframe_index_].duration;
@@ -74,14 +72,14 @@ void DynamicTheme::advanceToNextKeyframe() {
transition_progress_ = 0.0f;
}
Color DynamicTheme::getBallColor(size_t ball_index, float progress) const {
auto DynamicTheme::getBallColor(size_t ball_index, float progress) const -> Color {
// Obtener keyframes actual y objetivo
const auto& current_kf = keyframes_[current_keyframe_index_];
const auto& target_kf = keyframes_[target_keyframe_index_];
// Si paletas vacías, retornar blanco
if (current_kf.ball_colors.empty() || target_kf.ball_colors.empty()) {
return {255, 255, 255};
return {.r = 255, .g = 255, .b = 255};
}
// Obtener colores de ambos keyframes (con wrap)
@@ -95,15 +93,18 @@ Color DynamicTheme::getBallColor(size_t ball_index, float progress) const {
// (progress parámetro será usado en PHASE 3 para LERP externo)
float t = transition_progress_;
return {
static_cast<int>(lerp(c1.r, c2.r, t)),
static_cast<int>(lerp(c1.g, c2.g, t)),
static_cast<int>(lerp(c1.b, c2.b, t))
};
.r = static_cast<int>(lerp(c1.r, c2.r, t)),
.g = static_cast<int>(lerp(c1.g, c2.g, t)),
.b = static_cast<int>(lerp(c1.b, c2.b, t))};
}
void DynamicTheme::getBackgroundColors(float progress,
float& tr, float& tg, float& tb,
float& br, float& bg, float& bb) const {
float& tr,
float& tg,
float& tb,
float& br,
float& bg,
float& bb) const {
// Obtener keyframes actual y objetivo
const auto& current_kf = keyframes_[current_keyframe_index_];
const auto& target_kf = keyframes_[target_keyframe_index_];

View File

@@ -1,8 +1,9 @@
#pragma once
#include "theme.hpp"
#include <string>
#include "theme.hpp"
// Forward declaration (estructura definida en defines.h)
struct DynamicThemeKeyframe;
@@ -30,10 +31,7 @@ class DynamicTheme : public Theme {
* @param keyframes: Vector de keyframes (mínimo 2)
* @param loop: ¿Volver al inicio al terminar? (siempre true en esta app)
*/
DynamicTheme(const char* name_en, const char* name_es,
int text_r, int text_g, int text_b,
std::vector<DynamicThemeKeyframe> keyframes,
bool loop = true);
DynamicTheme(const char* name_en, const char* name_es, int text_r, int text_g, int text_b, std::vector<DynamicThemeKeyframe> keyframes, bool loop = true);
~DynamicTheme() override = default;
@@ -56,8 +54,12 @@ class DynamicTheme : public Theme {
Color getBallColor(size_t ball_index, float progress) const override;
void getBackgroundColors(float progress,
float& tr, float& tg, float& tb,
float& br, float& bg, float& bb) const override;
float& tr,
float& tg,
float& tb,
float& br,
float& bg,
float& bb) const override;
// ========================================
// ANIMACIÓN (soporte completo)
@@ -90,10 +92,10 @@ class DynamicTheme : public Theme {
// ESTADO DE ANIMACIÓN
// ========================================
size_t current_keyframe_index_ = 0; // Keyframe actual
size_t target_keyframe_index_ = 1; // Próximo keyframe
float transition_progress_ = 0.0f; // Progreso 0.0-1.0 hacia target
bool paused_ = false; // Pausa manual con Shift+D
size_t current_keyframe_index_ = 0; // Keyframe actual
size_t target_keyframe_index_ = 1; // Próximo keyframe
float transition_progress_ = 0.0f; // Progreso 0.0-1.0 hacia target
bool paused_ = false; // Pausa manual con Shift+D
// ========================================
// UTILIDADES PRIVADAS

View File

@@ -1,32 +1,39 @@
#include "static_theme.hpp"
StaticTheme::StaticTheme(const char* name_en, const char* name_es,
int text_r, int text_g, int text_b,
int notif_bg_r, int notif_bg_g, int notif_bg_b,
float bg_top_r, float bg_top_g, float bg_top_b,
float bg_bottom_r, float bg_bottom_g, float bg_bottom_b,
std::vector<Color> ball_colors)
StaticTheme::StaticTheme(const char* name_en, const char* name_es, int text_r, int text_g, int text_b, int notif_bg_r, int notif_bg_g, int notif_bg_b, float bg_top_r, float bg_top_g, float bg_top_b, float bg_bottom_r, float bg_bottom_g, float bg_bottom_b, std::vector<Color> ball_colors)
: name_en_(name_en),
name_es_(name_es),
text_r_(text_r), text_g_(text_g), text_b_(text_b),
notif_bg_r_(notif_bg_r), notif_bg_g_(notif_bg_g), notif_bg_b_(notif_bg_b),
bg_top_r_(bg_top_r), bg_top_g_(bg_top_g), bg_top_b_(bg_top_b),
bg_bottom_r_(bg_bottom_r), bg_bottom_g_(bg_bottom_g), bg_bottom_b_(bg_bottom_b),
text_r_(text_r),
text_g_(text_g),
text_b_(text_b),
notif_bg_r_(notif_bg_r),
notif_bg_g_(notif_bg_g),
notif_bg_b_(notif_bg_b),
bg_top_r_(bg_top_r),
bg_top_g_(bg_top_g),
bg_top_b_(bg_top_b),
bg_bottom_r_(bg_bottom_r),
bg_bottom_g_(bg_bottom_g),
bg_bottom_b_(bg_bottom_b),
ball_colors_(std::move(ball_colors)) {
}
Color StaticTheme::getBallColor(size_t ball_index, float progress) const {
auto StaticTheme::getBallColor(size_t ball_index, float progress) const -> Color {
// Tema estático: siempre retorna color de paleta según índice
// (progress se ignora aquí, pero será usado en PHASE 3 para LERP externo)
if (ball_colors_.empty()) {
return {255, 255, 255}; // Blanco por defecto si paleta vacía
return {.r = 255, .g = 255, .b = 255}; // Blanco por defecto si paleta vacía
}
return ball_colors_[ball_index % ball_colors_.size()];
}
void StaticTheme::getBackgroundColors(float progress,
float& tr, float& tg, float& tb,
float& br, float& bg, float& bb) const {
float& tr,
float& tg,
float& tb,
float& br,
float& bg,
float& bb) const {
// Tema estático: siempre retorna colores de fondo fijos
// (progress se ignora aquí, pero será usado en PHASE 3 para LERP externo)
tr = bg_top_r_;

View File

@@ -1,8 +1,9 @@
#pragma once
#include "theme.hpp"
#include <string>
#include "theme.hpp"
/**
* StaticTheme: Tema estático con 1 keyframe (sin animación)
*
@@ -28,12 +29,7 @@ class StaticTheme : public Theme {
* @param bg_bottom_r, bg_bottom_g, bg_bottom_b: Color inferior de fondo
* @param ball_colors: Paleta de colores para pelotas
*/
StaticTheme(const char* name_en, const char* name_es,
int text_r, int text_g, int text_b,
int notif_bg_r, int notif_bg_g, int notif_bg_b,
float bg_top_r, float bg_top_g, float bg_top_b,
float bg_bottom_r, float bg_bottom_g, float bg_bottom_b,
std::vector<Color> ball_colors);
StaticTheme(const char* name_en, const char* name_es, int text_r, int text_g, int text_b, int notif_bg_r, int notif_bg_g, int notif_bg_b, float bg_top_r, float bg_top_g, float bg_top_b, float bg_bottom_r, float bg_bottom_g, float bg_bottom_b, std::vector<Color> ball_colors);
~StaticTheme() override = default;
@@ -60,8 +56,12 @@ class StaticTheme : public Theme {
Color getBallColor(size_t ball_index, float progress) const override;
void getBackgroundColors(float progress,
float& tr, float& tg, float& tb,
float& br, float& bg, float& bb) const override;
float& tr,
float& tg,
float& tb,
float& br,
float& bg,
float& bb) const override;
// ========================================
// ANIMACIÓN (sin soporte - tema estático)

View File

@@ -1,6 +1,7 @@
#pragma once
#include <vector>
#include "defines.hpp" // for Color, ThemeKeyframe
/**
@@ -47,8 +48,12 @@ class Theme {
* @param br, bg, bb: Color inferior (out)
*/
virtual void getBackgroundColors(float progress,
float& tr, float& tg, float& tb,
float& br, float& bg, float& bb) const = 0;
float& tr,
float& tg,
float& tb,
float& br,
float& bg,
float& bb) const = 0;
// ========================================
// ANIMACIÓN (solo temas dinámicos)
@@ -58,7 +63,7 @@ class Theme {
* Actualiza progreso de animación interna (solo dinámicos)
* @param delta_time: Tiempo transcurrido desde último frame
*/
virtual void update(float delta_time) { }
virtual void update(float delta_time) {}
/**
* ¿Este tema necesita update() cada frame?
@@ -75,7 +80,7 @@ class Theme {
/**
* Reinicia progreso de animación a 0.0 (usado al activar tema)
*/
virtual void resetProgress() { }
virtual void resetProgress() {}
// ========================================
// PAUSA (solo temas dinámicos)
@@ -90,5 +95,5 @@ class Theme {
/**
* Toggle pausa de animación (solo dinámicos, tecla Shift+D)
*/
virtual void togglePause() { }
virtual void togglePause() {}
};

View File

@@ -2,6 +2,7 @@
#include <string>
#include <vector>
#include "defines.hpp" // for Color
/**
@@ -24,23 +25,23 @@
* - Nombres del tema (para debug display durante transición)
*/
struct ThemeSnapshot {
// Colores de fondo degradado
float bg_top_r, bg_top_g, bg_top_b;
float bg_bottom_r, bg_bottom_g, bg_bottom_b;
// Colores de fondo degradado
float bg_top_r, bg_top_g, bg_top_b;
float bg_bottom_r, bg_bottom_g, bg_bottom_b;
// Color de texto UI
int text_color_r, text_color_g, text_color_b;
// Color de texto UI
int text_color_r, text_color_g, text_color_b;
// Color de fondo de notificaciones
int notif_bg_r, notif_bg_g, notif_bg_b;
// Color de fondo de notificaciones
int notif_bg_r, notif_bg_g, notif_bg_b;
// Nombres del tema (para mostrar "SOURCE → TARGET" durante transición)
std::string name_en;
std::string name_es;
// Nombres del tema (para mostrar "SOURCE → TARGET" durante transición)
std::string name_en;
std::string name_es;
// Colores de pelotas capturados (índice = ball_index % ball_colors.size())
// Se capturan suficientes colores para cubrir escenario máximo (50,000 pelotas)
// Nota: Si el tema tiene 8 colores y capturamos 50,000, habrá repetición
// pero permite LERP correcto incluso con muchas pelotas
std::vector<Color> ball_colors;
// Colores de pelotas capturados (índice = ball_index % ball_colors.size())
// Se capturan suficientes colores para cubrir escenario máximo (50,000 pelotas)
// Nota: Si el tema tiene 8 colores y capturamos 50,000, habrá repetición
// pero permite LERP correcto incluso con muchas pelotas
std::vector<Color> ball_colors;
};

View File

@@ -1,31 +1,34 @@
#include "app_logo.hpp"
#include <SDL3/SDL_render.h> // for SDL_DestroyTexture, SDL_RenderGeometry, SDL_SetTextureAlphaMod
#include <cmath> // for powf, sinf, cosf
#include <cstdlib> // for free()
#include <iostream> // for std::cout
#include "logo_scaler.hpp" // for LogoScaler
#include "defines.hpp" // for APPLOGO_HEIGHT_PERCENT, getResourcesDirectory
#include <array> // for std::array
#include <cmath> // for powf, sinf, cosf
#include <cstdlib> // for free()
#include <iostream> // for std::cout
#include <numbers>
#include "defines.hpp" // for APPLOGO_HEIGHT_PERCENT, getResourcesDirectory
#include "logo_scaler.hpp" // for LogoScaler
// ============================================================================
// Destructor - Liberar las 4 texturas SDL
// ============================================================================
AppLogo::~AppLogo() {
if (logo1_base_texture_) {
if (logo1_base_texture_ != nullptr) {
SDL_DestroyTexture(logo1_base_texture_);
logo1_base_texture_ = nullptr;
}
if (logo1_native_texture_) {
if (logo1_native_texture_ != nullptr) {
SDL_DestroyTexture(logo1_native_texture_);
logo1_native_texture_ = nullptr;
}
if (logo2_base_texture_) {
if (logo2_base_texture_ != nullptr) {
SDL_DestroyTexture(logo2_base_texture_);
logo2_base_texture_ = nullptr;
}
if (logo2_native_texture_) {
if (logo2_native_texture_ != nullptr) {
SDL_DestroyTexture(logo2_native_texture_);
logo2_native_texture_ = nullptr;
}
@@ -35,11 +38,23 @@ AppLogo::~AppLogo() {
// Inicialización - Pre-escalar logos a 2 resoluciones (base y nativa)
// ============================================================================
bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_height) {
if (logo1_base_texture_) { SDL_DestroyTexture(logo1_base_texture_); logo1_base_texture_ = nullptr; }
if (logo1_native_texture_) { SDL_DestroyTexture(logo1_native_texture_); logo1_native_texture_ = nullptr; }
if (logo2_base_texture_) { SDL_DestroyTexture(logo2_base_texture_); logo2_base_texture_ = nullptr; }
if (logo2_native_texture_) { SDL_DestroyTexture(logo2_native_texture_); logo2_native_texture_ = nullptr; }
auto AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_height) -> bool {
if (logo1_base_texture_ != nullptr) {
SDL_DestroyTexture(logo1_base_texture_);
logo1_base_texture_ = nullptr;
}
if (logo1_native_texture_ != nullptr) {
SDL_DestroyTexture(logo1_native_texture_);
logo1_native_texture_ = nullptr;
}
if (logo2_base_texture_ != nullptr) {
SDL_DestroyTexture(logo2_base_texture_);
logo2_base_texture_ = nullptr;
}
if (logo2_native_texture_ != nullptr) {
SDL_DestroyTexture(logo2_native_texture_);
logo2_native_texture_ = nullptr;
}
renderer_ = renderer;
base_screen_width_ = screen_width;
@@ -53,7 +68,7 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
// 1. Detectar resolución nativa del monitor
// ========================================================================
if (!LogoScaler::detectNativeResolution(native_screen_width_, native_screen_height_)) {
std::cout << "No se pudo detectar resolución nativa, usando solo base" << std::endl;
std::cout << "No se pudo detectar resolución nativa, usando solo base" << '\n';
// Fallback: usar resolución base como nativa
native_screen_width_ = screen_width;
native_screen_height_ = screen_height;
@@ -76,20 +91,21 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
0, // width calculado automáticamente por aspect ratio
logo_base_target_height,
logo1_base_width_,
logo1_base_height_
);
logo1_base_height_);
if (logo1_base_data == nullptr) {
std::cout << "Error: No se pudo escalar logo1 (base)" << std::endl;
std::cout << "Error: No se pudo escalar logo1 (base)" << '\n';
return false;
}
logo1_base_texture_ = LogoScaler::createTextureFromBuffer(
renderer, logo1_base_data, logo1_base_width_, logo1_base_height_
);
renderer,
logo1_base_data,
logo1_base_width_,
logo1_base_height_);
free(logo1_base_data); // Liberar buffer temporal
if (logo1_base_texture_ == nullptr) {
std::cout << "Error: No se pudo crear textura logo1 (base)" << std::endl;
std::cout << "Error: No se pudo crear textura logo1 (base)" << '\n';
return false;
}
@@ -102,20 +118,21 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
0, // width calculado automáticamente
logo_native_target_height,
logo1_native_width_,
logo1_native_height_
);
logo1_native_height_);
if (logo1_native_data == nullptr) {
std::cout << "Error: No se pudo escalar logo1 (nativa)" << std::endl;
std::cout << "Error: No se pudo escalar logo1 (nativa)" << '\n';
return false;
}
logo1_native_texture_ = LogoScaler::createTextureFromBuffer(
renderer, logo1_native_data, logo1_native_width_, logo1_native_height_
);
renderer,
logo1_native_data,
logo1_native_width_,
logo1_native_height_);
free(logo1_native_data);
if (logo1_native_texture_ == nullptr) {
std::cout << "Error: No se pudo crear textura logo1 (nativa)" << std::endl;
std::cout << "Error: No se pudo crear textura logo1 (nativa)" << '\n';
return false;
}
@@ -132,20 +149,21 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
0,
logo_base_target_height,
logo2_base_width_,
logo2_base_height_
);
logo2_base_height_);
if (logo2_base_data == nullptr) {
std::cout << "Error: No se pudo escalar logo2 (base)" << std::endl;
std::cout << "Error: No se pudo escalar logo2 (base)" << '\n';
return false;
}
logo2_base_texture_ = LogoScaler::createTextureFromBuffer(
renderer, logo2_base_data, logo2_base_width_, logo2_base_height_
);
renderer,
logo2_base_data,
logo2_base_width_,
logo2_base_height_);
free(logo2_base_data);
if (logo2_base_texture_ == nullptr) {
std::cout << "Error: No se pudo crear textura logo2 (base)" << std::endl;
std::cout << "Error: No se pudo crear textura logo2 (base)" << '\n';
return false;
}
@@ -157,20 +175,21 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
0,
logo_native_target_height,
logo2_native_width_,
logo2_native_height_
);
logo2_native_height_);
if (logo2_native_data == nullptr) {
std::cout << "Error: No se pudo escalar logo2 (nativa)" << std::endl;
std::cout << "Error: No se pudo escalar logo2 (nativa)" << '\n';
return false;
}
logo2_native_texture_ = LogoScaler::createTextureFromBuffer(
renderer, logo2_native_data, logo2_native_width_, logo2_native_height_
);
renderer,
logo2_native_data,
logo2_native_width_,
logo2_native_height_);
free(logo2_native_data);
if (logo2_native_texture_ == nullptr) {
std::cout << "Error: No se pudo crear textura logo2 (nativa)" << std::endl;
std::cout << "Error: No se pudo crear textura logo2 (nativa)" << '\n';
return false;
}
@@ -191,7 +210,7 @@ bool AppLogo::initialize(SDL_Renderer* renderer, int screen_width, int screen_he
return true;
}
void AppLogo::update(float delta_time, AppMode current_mode) {
void AppLogo::update(float delta_time, AppMode current_mode) { // NOLINT(readability-function-cognitive-complexity)
// Si estamos en SANDBOX, resetear y no hacer nada (logo desactivado)
if (current_mode == AppMode::SANDBOX) {
state_ = AppLogoState::HIDDEN;
@@ -262,63 +281,57 @@ void AppLogo::update(float delta_time, AppMode current_mode) {
logo2_rotation_ = 0.0f;
break;
case AppLogoAnimationType::ELASTIC_STICK:
{
float prog1 = std::min(1.0f, fade_progress_logo1);
float elastic_t1 = easeOutElastic(prog1);
logo1_scale_ = 1.2f - (elastic_t1 * 0.2f);
float squash_t1 = easeOutBack(prog1);
logo1_squash_y_ = 0.6f + (squash_t1 * 0.4f);
logo1_stretch_x_ = 1.0f + (1.0f - logo1_squash_y_) * 0.5f;
logo1_rotation_ = 0.0f;
case AppLogoAnimationType::ELASTIC_STICK: {
float prog1 = std::min(1.0f, fade_progress_logo1);
float elastic_t1 = easeOutElastic(prog1);
logo1_scale_ = 1.2f - (elastic_t1 * 0.2f);
float squash_t1 = easeOutBack(prog1);
logo1_squash_y_ = 0.6f + (squash_t1 * 0.4f);
logo1_stretch_x_ = 1.0f + (1.0f - logo1_squash_y_) * 0.5f;
logo1_rotation_ = 0.0f;
float prog2 = std::min(1.0f, fade_progress_logo2);
float elastic_t2 = easeOutElastic(prog2);
logo2_scale_ = 1.2f - (elastic_t2 * 0.2f);
float squash_t2 = easeOutBack(prog2);
logo2_squash_y_ = 0.6f + (squash_t2 * 0.4f);
logo2_stretch_x_ = 1.0f + (1.0f - logo2_squash_y_) * 0.5f;
logo2_rotation_ = 0.0f;
}
break;
float prog2 = std::min(1.0f, fade_progress_logo2);
float elastic_t2 = easeOutElastic(prog2);
logo2_scale_ = 1.2f - (elastic_t2 * 0.2f);
float squash_t2 = easeOutBack(prog2);
logo2_squash_y_ = 0.6f + (squash_t2 * 0.4f);
logo2_stretch_x_ = 1.0f + (1.0f - logo2_squash_y_) * 0.5f;
logo2_rotation_ = 0.0f;
} break;
case AppLogoAnimationType::ROTATE_SPIRAL:
{
float prog1 = std::min(1.0f, fade_progress_logo1);
float ease_t1 = easeInOutQuad(prog1);
logo1_scale_ = 0.3f + (ease_t1 * 0.7f);
logo1_rotation_ = (1.0f - prog1) * 6.28f;
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
case AppLogoAnimationType::ROTATE_SPIRAL: {
float prog1 = std::min(1.0f, fade_progress_logo1);
float ease_t1 = easeInOutQuad(prog1);
logo1_scale_ = 0.3f + (ease_t1 * 0.7f);
logo1_rotation_ = (1.0f - prog1) * 6.28f;
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
float prog2 = std::min(1.0f, fade_progress_logo2);
float ease_t2 = easeInOutQuad(prog2);
logo2_scale_ = 0.3f + (ease_t2 * 0.7f);
logo2_rotation_ = (1.0f - prog2) * 6.28f;
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
}
break;
float prog2 = std::min(1.0f, fade_progress_logo2);
float ease_t2 = easeInOutQuad(prog2);
logo2_scale_ = 0.3f + (ease_t2 * 0.7f);
logo2_rotation_ = (1.0f - prog2) * 6.28f;
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
} break;
case AppLogoAnimationType::BOUNCE_SQUASH:
{
float prog1 = std::min(1.0f, fade_progress_logo1);
float bounce_t1 = easeOutBounce(prog1);
logo1_scale_ = 1.0f;
float squash_amount1 = (1.0f - bounce_t1) * 0.3f;
logo1_squash_y_ = 1.0f - squash_amount1;
logo1_stretch_x_ = 1.0f + squash_amount1 * 0.5f;
logo1_rotation_ = 0.0f;
case AppLogoAnimationType::BOUNCE_SQUASH: {
float prog1 = std::min(1.0f, fade_progress_logo1);
float bounce_t1 = easeOutBounce(prog1);
logo1_scale_ = 1.0f;
float squash_amount1 = (1.0f - bounce_t1) * 0.3f;
logo1_squash_y_ = 1.0f - squash_amount1;
logo1_stretch_x_ = 1.0f + squash_amount1 * 0.5f;
logo1_rotation_ = 0.0f;
float prog2 = std::min(1.0f, fade_progress_logo2);
float bounce_t2 = easeOutBounce(prog2);
logo2_scale_ = 1.0f;
float squash_amount2 = (1.0f - bounce_t2) * 0.3f;
logo2_squash_y_ = 1.0f - squash_amount2;
logo2_stretch_x_ = 1.0f + squash_amount2 * 0.5f;
logo2_rotation_ = 0.0f;
}
break;
float prog2 = std::min(1.0f, fade_progress_logo2);
float bounce_t2 = easeOutBounce(prog2);
logo2_scale_ = 1.0f;
float squash_amount2 = (1.0f - bounce_t2) * 0.3f;
logo2_squash_y_ = 1.0f - squash_amount2;
logo2_stretch_x_ = 1.0f + squash_amount2 * 0.5f;
logo2_rotation_ = 0.0f;
} break;
}
}
}
@@ -379,69 +392,63 @@ void AppLogo::update(float delta_time, AppMode current_mode) {
logo2_rotation_ = 0.0f;
break;
case AppLogoAnimationType::ELASTIC_STICK:
{
float prog1 = std::min(1.0f, fade_progress_logo1);
logo1_scale_ = 1.0f + (prog1 * prog1 * 0.2f);
logo1_squash_y_ = 1.0f + (prog1 * 0.3f);
logo1_stretch_x_ = 1.0f - (prog1 * 0.2f);
logo1_rotation_ = prog1 * 0.1f;
case AppLogoAnimationType::ELASTIC_STICK: {
float prog1 = std::min(1.0f, fade_progress_logo1);
logo1_scale_ = 1.0f + (prog1 * prog1 * 0.2f);
logo1_squash_y_ = 1.0f + (prog1 * 0.3f);
logo1_stretch_x_ = 1.0f - (prog1 * 0.2f);
logo1_rotation_ = prog1 * 0.1f;
float prog2 = std::min(1.0f, fade_progress_logo2);
logo2_scale_ = 1.0f + (prog2 * prog2 * 0.2f);
logo2_squash_y_ = 1.0f + (prog2 * 0.3f);
logo2_stretch_x_ = 1.0f - (prog2 * 0.2f);
logo2_rotation_ = prog2 * 0.1f;
float prog2 = std::min(1.0f, fade_progress_logo2);
logo2_scale_ = 1.0f + (prog2 * prog2 * 0.2f);
logo2_squash_y_ = 1.0f + (prog2 * 0.3f);
logo2_stretch_x_ = 1.0f - (prog2 * 0.2f);
logo2_rotation_ = prog2 * 0.1f;
} break;
case AppLogoAnimationType::ROTATE_SPIRAL: {
float prog1 = std::min(1.0f, fade_progress_logo1);
float ease_t1 = easeInOutQuad(prog1);
logo1_scale_ = 1.0f - (ease_t1 * 0.7f);
logo1_rotation_ = prog1 * 6.28f;
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
float prog2 = std::min(1.0f, fade_progress_logo2);
float ease_t2 = easeInOutQuad(prog2);
logo2_scale_ = 1.0f - (ease_t2 * 0.7f);
logo2_rotation_ = prog2 * 6.28f;
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
} break;
case AppLogoAnimationType::BOUNCE_SQUASH: {
float prog1 = std::min(1.0f, fade_progress_logo1);
if (prog1 < 0.2f) {
float squash_t = prog1 / 0.2f;
logo1_squash_y_ = 1.0f - (squash_t * 0.3f);
logo1_stretch_x_ = 1.0f + (squash_t * 0.2f);
} else {
float jump_t = (prog1 - 0.2f) / 0.8f;
logo1_squash_y_ = 0.7f + (jump_t * 0.5f);
logo1_stretch_x_ = 1.2f - (jump_t * 0.2f);
}
break;
logo1_scale_ = 1.0f + (prog1 * 0.3f);
logo1_rotation_ = 0.0f;
case AppLogoAnimationType::ROTATE_SPIRAL:
{
float prog1 = std::min(1.0f, fade_progress_logo1);
float ease_t1 = easeInOutQuad(prog1);
logo1_scale_ = 1.0f - (ease_t1 * 0.7f);
logo1_rotation_ = prog1 * 6.28f;
logo1_squash_y_ = 1.0f;
logo1_stretch_x_ = 1.0f;
float prog2 = std::min(1.0f, fade_progress_logo2);
float ease_t2 = easeInOutQuad(prog2);
logo2_scale_ = 1.0f - (ease_t2 * 0.7f);
logo2_rotation_ = prog2 * 6.28f;
logo2_squash_y_ = 1.0f;
logo2_stretch_x_ = 1.0f;
float prog2 = std::min(1.0f, fade_progress_logo2);
if (prog2 < 0.2f) {
float squash_t = prog2 / 0.2f;
logo2_squash_y_ = 1.0f - (squash_t * 0.3f);
logo2_stretch_x_ = 1.0f + (squash_t * 0.2f);
} else {
float jump_t = (prog2 - 0.2f) / 0.8f;
logo2_squash_y_ = 0.7f + (jump_t * 0.5f);
logo2_stretch_x_ = 1.2f - (jump_t * 0.2f);
}
break;
case AppLogoAnimationType::BOUNCE_SQUASH:
{
float prog1 = std::min(1.0f, fade_progress_logo1);
if (prog1 < 0.2f) {
float squash_t = prog1 / 0.2f;
logo1_squash_y_ = 1.0f - (squash_t * 0.3f);
logo1_stretch_x_ = 1.0f + (squash_t * 0.2f);
} else {
float jump_t = (prog1 - 0.2f) / 0.8f;
logo1_squash_y_ = 0.7f + (jump_t * 0.5f);
logo1_stretch_x_ = 1.2f - (jump_t * 0.2f);
}
logo1_scale_ = 1.0f + (prog1 * 0.3f);
logo1_rotation_ = 0.0f;
float prog2 = std::min(1.0f, fade_progress_logo2);
if (prog2 < 0.2f) {
float squash_t = prog2 / 0.2f;
logo2_squash_y_ = 1.0f - (squash_t * 0.3f);
logo2_stretch_x_ = 1.0f + (squash_t * 0.2f);
} else {
float jump_t = (prog2 - 0.2f) / 0.8f;
logo2_squash_y_ = 0.7f + (jump_t * 0.5f);
logo2_stretch_x_ = 1.2f - (jump_t * 0.2f);
}
logo2_scale_ = 1.0f + (prog2 * 0.3f);
logo2_rotation_ = 0.0f;
}
break;
logo2_scale_ = 1.0f + (prog2 * 0.3f);
logo2_rotation_ = 0.0f;
} break;
}
}
}
@@ -477,7 +484,7 @@ void AppLogo::updateScreenSize(int screen_width, int screen_height) {
logo2_current_width_ = logo2_native_width_;
logo2_current_height_ = logo2_native_height_;
std::cout << "AppLogo: Cambiado a texturas NATIVAS" << std::endl;
std::cout << "AppLogo: Cambiado a texturas NATIVAS" << '\n';
} else {
// Cambiar a texturas base (ventana redimensionable)
logo1_current_texture_ = logo1_base_texture_;
@@ -488,7 +495,7 @@ void AppLogo::updateScreenSize(int screen_width, int screen_height) {
logo2_current_width_ = logo2_base_width_;
logo2_current_height_ = logo2_base_height_;
std::cout << "AppLogo: Cambiado a texturas BASE" << std::endl;
std::cout << "AppLogo: Cambiado a texturas BASE" << '\n';
}
// Nota: No es necesario recalcular escalas porque las texturas están pre-escaladas
@@ -499,57 +506,61 @@ void AppLogo::updateScreenSize(int screen_width, int screen_height) {
// Funciones de easing para animaciones
// ============================================================================
float AppLogo::easeOutElastic(float t) {
auto AppLogo::easeOutElastic(float t) -> float {
// Elastic easing out: bounce elástico al final
const float c4 = (2.0f * 3.14159f) / 3.0f;
const float C4 = (2.0f * std::numbers::pi_v<float>) / 3.0f;
if (t == 0.0f) return 0.0f;
if (t == 1.0f) return 1.0f;
return powf(2.0f, -10.0f * t) * sinf((t * 10.0f - 0.75f) * c4) + 1.0f;
}
float AppLogo::easeOutBack(float t) {
// Back easing out: overshoot suave al final
const float c1 = 1.70158f;
const float c3 = c1 + 1.0f;
return 1.0f + c3 * powf(t - 1.0f, 3.0f) + c1 * powf(t - 1.0f, 2.0f);
}
float AppLogo::easeOutBounce(float t) {
// Bounce easing out: rebotes decrecientes (para BOUNCE_SQUASH)
const float n1 = 7.5625f;
const float d1 = 2.75f;
if (t < 1.0f / d1) {
return n1 * t * t;
} else if (t < 2.0f / d1) {
t -= 1.5f / d1;
return n1 * t * t + 0.75f;
} else if (t < 2.5f / d1) {
t -= 2.25f / d1;
return n1 * t * t + 0.9375f;
} else {
t -= 2.625f / d1;
return n1 * t * t + 0.984375f;
if (t == 0.0f) {
return 0.0f;
}
if (t == 1.0f) {
return 1.0f;
}
return (powf(2.0f, -10.0f * t) * sinf((t * 10.0f - 0.75f) * C4)) + 1.0f;
}
float AppLogo::easeInOutQuad(float t) {
auto AppLogo::easeOutBack(float t) -> float {
// Back easing out: overshoot suave al final
const float C1 = 1.70158f;
const float C3 = C1 + 1.0f;
return 1.0f + (C3 * powf(t - 1.0f, 3.0f)) + (C1 * powf(t - 1.0f, 2.0f));
}
auto AppLogo::easeOutBounce(float t) -> float {
// Bounce easing out: rebotes decrecientes (para BOUNCE_SQUASH)
const float N1 = 7.5625f;
const float D1 = 2.75f;
if (t < 1.0f / D1) {
return N1 * t * t;
}
if (t < 2.0f / D1) {
t -= 1.5f / D1;
return (N1 * t * t) + 0.75f;
}
if (t < 2.5f / D1) {
t -= 2.25f / D1;
return (N1 * t * t) + 0.9375f;
}
t -= 2.625f / D1;
return (N1 * t * t) + 0.984375f;
}
auto AppLogo::easeInOutQuad(float t) -> float {
// Quadratic easing in/out: aceleración suave (para ROTATE_SPIRAL)
if (t < 0.5f) {
return 2.0f * t * t;
} else {
return 1.0f - powf(-2.0f * t + 2.0f, 2.0f) / 2.0f;
}
return 1.0f - (powf((-2.0f * t) + 2.0f, 2.0f) / 2.0f);
}
// ============================================================================
// Función auxiliar para aleatorización
// ============================================================================
AppLogoAnimationType AppLogo::getRandomAnimation() {
auto AppLogo::getRandomAnimation() -> AppLogoAnimationType {
// Generar número aleatorio entre 0 y 3 (4 tipos de animación)
int random_value = rand() % 4;
@@ -571,15 +582,23 @@ AppLogoAnimationType AppLogo::getRandomAnimation() {
// ============================================================================
void AppLogo::renderWithGeometry(int logo_index) {
if (!renderer_) return;
if (renderer_ == nullptr) {
return;
}
// Seleccionar variables según el logo_index (1 = logo1, 2 = logo2)
SDL_Texture* texture;
int base_width, base_height;
float scale, squash_y, stretch_x, rotation;
int base_width;
int base_height;
float scale;
float squash_y;
float stretch_x;
float rotation;
if (logo_index == 1) {
if (!logo1_current_texture_) return;
if (logo1_current_texture_ == nullptr) {
return;
}
texture = logo1_current_texture_;
base_width = logo1_current_width_;
base_height = logo1_current_height_;
@@ -588,7 +607,9 @@ void AppLogo::renderWithGeometry(int logo_index) {
stretch_x = logo1_stretch_x_;
rotation = logo1_rotation_;
} else if (logo_index == 2) {
if (!logo2_current_texture_) return;
if (logo2_current_texture_ == nullptr) {
return;
}
texture = logo2_current_texture_;
base_width = logo2_current_width_;
base_height = logo2_current_height_;
@@ -628,7 +649,7 @@ void AppLogo::renderWithGeometry(int logo_index) {
float sin_rot = sinf(rotation);
// Crear 4 vértices del quad (centrado en center_x, center_y)
SDL_Vertex vertices[4];
std::array<SDL_Vertex, 4> vertices{};
// Offset desde el centro
float half_w = width / 2.0f;
@@ -638,49 +659,49 @@ void AppLogo::renderWithGeometry(int logo_index) {
{
float local_x = -half_w;
float local_y = -half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot;
float rotated_y = local_x * sin_rot + local_y * cos_rot;
vertices[0].position = {center_x + rotated_x, center_y + rotated_y};
vertices[0].tex_coord = {0.0f, 0.0f};
vertices[0].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice
float rotated_x = (local_x * cos_rot) - (local_y * sin_rot);
float rotated_y = (local_x * sin_rot) + (local_y * cos_rot);
vertices[0].position = {.x = center_x + rotated_x, .y = center_y + rotated_y};
vertices[0].tex_coord = {.x = 0.0f, .y = 0.0f};
vertices[0].color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = alpha_normalized}; // Alpha aplicado al vértice
}
// Vértice superior derecho (rotado)
{
float local_x = half_w;
float local_y = -half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot;
float rotated_y = local_x * sin_rot + local_y * cos_rot;
vertices[1].position = {center_x + rotated_x, center_y + rotated_y};
vertices[1].tex_coord = {1.0f, 0.0f};
vertices[1].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice
float rotated_x = (local_x * cos_rot) - (local_y * sin_rot);
float rotated_y = (local_x * sin_rot) + (local_y * cos_rot);
vertices[1].position = {.x = center_x + rotated_x, .y = center_y + rotated_y};
vertices[1].tex_coord = {.x = 1.0f, .y = 0.0f};
vertices[1].color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = alpha_normalized}; // Alpha aplicado al vértice
}
// Vértice inferior derecho (rotado)
{
float local_x = half_w;
float local_y = half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot;
float rotated_y = local_x * sin_rot + local_y * cos_rot;
vertices[2].position = {center_x + rotated_x, center_y + rotated_y};
vertices[2].tex_coord = {1.0f, 1.0f};
vertices[2].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice
float rotated_x = (local_x * cos_rot) - (local_y * sin_rot);
float rotated_y = (local_x * sin_rot) + (local_y * cos_rot);
vertices[2].position = {.x = center_x + rotated_x, .y = center_y + rotated_y};
vertices[2].tex_coord = {.x = 1.0f, .y = 1.0f};
vertices[2].color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = alpha_normalized}; // Alpha aplicado al vértice
}
// Vértice inferior izquierdo (rotado)
{
float local_x = -half_w;
float local_y = half_h;
float rotated_x = local_x * cos_rot - local_y * sin_rot;
float rotated_y = local_x * sin_rot + local_y * cos_rot;
vertices[3].position = {center_x + rotated_x, center_y + rotated_y};
vertices[3].tex_coord = {0.0f, 1.0f};
vertices[3].color = {1.0f, 1.0f, 1.0f, alpha_normalized}; // Alpha aplicado al vértice
float rotated_x = (local_x * cos_rot) - (local_y * sin_rot);
float rotated_y = (local_x * sin_rot) + (local_y * cos_rot);
vertices[3].position = {.x = center_x + rotated_x, .y = center_y + rotated_y};
vertices[3].tex_coord = {.x = 0.0f, .y = 1.0f};
vertices[3].color = {.r = 1.0f, .g = 1.0f, .b = 1.0f, .a = alpha_normalized}; // Alpha aplicado al vértice
}
// Índices para 2 triángulos
int indices[6] = {0, 1, 2, 2, 3, 0};
std::array<int, 6> indices = {0, 1, 2, 2, 3, 0};
// Renderizar con la textura del logo correspondiente
SDL_RenderGeometry(renderer_, texture, vertices, 4, indices, 6);
SDL_RenderGeometry(renderer_, texture, vertices.data(), 4, indices.data(), 6);
}

View File

@@ -11,18 +11,18 @@ class Sprite;
// Estados de la máquina de estados del logo
enum class AppLogoState {
HIDDEN, // Logo oculto, esperando APPLOGO_DISPLAY_INTERVAL
FADE_IN, // Apareciendo (alpha 0 → 255)
VISIBLE, // Completamente visible, esperando APPLOGO_DISPLAY_DURATION
FADE_OUT // Desapareciendo (alpha 255 → 0)
HIDDEN, // Logo oculto, esperando APPLOGO_DISPLAY_INTERVAL
FADE_IN, // Apareciendo (alpha 0 → 255)
VISIBLE, // Completamente visible, esperando APPLOGO_DISPLAY_DURATION
FADE_OUT // Desapareciendo (alpha 255 → 0)
};
// Tipo de animación de entrada/salida
enum class AppLogoAnimationType {
ZOOM_ONLY, // A: Solo zoom simple (120% → 100% → 120%)
ELASTIC_STICK, // B: Zoom + deformación elástica tipo "pegatina"
ROTATE_SPIRAL, // C: Rotación en espiral (entra girando, sale girando)
BOUNCE_SQUASH // D: Rebote con aplastamiento (cae rebotando, salta)
ZOOM_ONLY, // A: Solo zoom simple (120% → 100% → 120%)
ELASTIC_STICK, // B: Zoom + deformación elástica tipo "pegatina"
ROTATE_SPIRAL, // C: Rotación en espiral (entra girando, sale girando)
BOUNCE_SQUASH // D: Rebote con aplastamiento (cae rebotando, salta)
};
class AppLogo {
@@ -46,10 +46,10 @@ class AppLogo {
// ====================================================================
// Texturas pre-escaladas (4 texturas: 2 logos × 2 resoluciones)
// ====================================================================
SDL_Texture* logo1_base_texture_ = nullptr; // Logo1 para resolución base
SDL_Texture* logo1_native_texture_ = nullptr; // Logo1 para resolución nativa (F4)
SDL_Texture* logo2_base_texture_ = nullptr; // Logo2 para resolución base
SDL_Texture* logo2_native_texture_ = nullptr; // Logo2 para resolución nativa (F4)
SDL_Texture* logo1_base_texture_ = nullptr; // Logo1 para resolución base
SDL_Texture* logo1_native_texture_ = nullptr; // Logo1 para resolución nativa (F4)
SDL_Texture* logo2_base_texture_ = nullptr; // Logo2 para resolución base
SDL_Texture* logo2_native_texture_ = nullptr; // Logo2 para resolución nativa (F4)
// Dimensiones pre-calculadas para cada textura
int logo1_base_width_ = 0, logo1_base_height_ = 0;
@@ -64,8 +64,8 @@ class AppLogo {
int logo2_current_width_ = 0, logo2_current_height_ = 0;
// Resoluciones conocidas
int base_screen_width_ = 0, base_screen_height_ = 0; // Resolución inicial
int native_screen_width_ = 0, native_screen_height_ = 0; // Resolución nativa (F4)
int base_screen_width_ = 0, base_screen_height_ = 0; // Resolución inicial
int native_screen_width_ = 0, native_screen_height_ = 0; // Resolución nativa (F4)
// ====================================================================
// Variables COMPARTIDAS (sincronización de ambos logos)
@@ -74,8 +74,8 @@ class AppLogo {
float timer_ = 0.0f; // Contador de tiempo para estado actual
// Alpha INDEPENDIENTE para cada logo (Logo 2 con retraso)
int logo1_alpha_ = 0; // Alpha de Logo 1 (0-255)
int logo2_alpha_ = 0; // Alpha de Logo 2 (0-255, con retraso)
int logo1_alpha_ = 0; // Alpha de Logo 1 (0-255)
int logo2_alpha_ = 0; // Alpha de Logo 2 (0-255, con retraso)
// Animación COMPARTIDA (misma para ambos logos, misma entrada y salida)
AppLogoAnimationType current_animation_ = AppLogoAnimationType::ZOOM_ONLY;
@@ -103,15 +103,15 @@ class AppLogo {
SDL_Renderer* renderer_ = nullptr;
// Métodos privados auxiliares
void updateLogoPosition(); // Centrar ambos logos en pantalla (superpuestos)
void updateLogoPosition(); // Centrar ambos logos en pantalla (superpuestos)
void renderWithGeometry(int logo_index); // Renderizar logo con vértices deformados (1 o 2)
// Funciones de easing
float easeOutElastic(float t); // Elastic bounce out
float easeOutBack(float t); // Overshoot out
float easeOutBounce(float t); // Bounce easing (para BOUNCE_SQUASH)
float easeInOutQuad(float t); // Quadratic easing (para ROTATE_SPIRAL)
static float easeOutElastic(float t); // Elastic bounce out
static float easeOutBack(float t); // Overshoot out
static float easeOutBounce(float t); // Bounce easing (para BOUNCE_SQUASH)
static float easeInOutQuad(float t); // Quadratic easing (para ROTATE_SPIRAL)
// Función auxiliar para elegir animación aleatoria
AppLogoAnimationType getRandomAnimation();
static AppLogoAnimationType getRandomAnimation();
};

View File

@@ -1,6 +1,7 @@
#include "help_overlay.hpp"
#include <algorithm> // for std::min
#include <array> // for std::array
#include "defines.hpp"
#include "text/textrenderer.hpp"
@@ -21,69 +22,69 @@ HelpOverlay::HelpOverlay()
column2_width_(0),
column3_width_(0),
cached_texture_(nullptr),
last_category_color_({0, 0, 0, 255}),
last_content_color_({0, 0, 0, 255}),
last_bg_color_({0, 0, 0, 255}),
last_category_color_({.r = 0, .g = 0, .b = 0, .a = 255}),
last_content_color_({.r = 0, .g = 0, .b = 0, .a = 255}),
last_bg_color_({.r = 0, .g = 0, .b = 0, .a = 255}),
texture_needs_rebuild_(true) {
// Llenar lista de controles (organizados por categoría, equilibrado en 3 columnas)
key_bindings_ = {
// COLUMNA 1: SIMULACIÓN
{"SIMULACIÓN", ""},
{"1-8", "Escenarios (10 a 50.000 pelotas)"},
{"F", "Cambia entre figura y física"},
{"B", "Cambia entre boids y física"},
{"ESPACIO", "Impulso contra la gravedad"},
{"G", "Activar / Desactivar gravedad"},
{"CURSORES", "Dirección de la gravedad"},
{"", ""}, // Separador
{.key = "SIMULACIÓN", .description = ""},
{.key = "1-8", .description = "Escenarios (10 a 50.000 pelotas)"},
{.key = "F", .description = "Cambia entre figura y física"},
{.key = "B", .description = "Cambia entre boids y física"},
{.key = "ESPACIO", .description = "Impulso contra la gravedad"},
{.key = "G", .description = "Activar / Desactivar gravedad"},
{.key = "CURSORES", .description = "Dirección de la gravedad"},
{.key = "", .description = ""}, // Separador
// COLUMNA 1: FIGURAS 3D
{"FIGURAS 3D", ""},
{"Q/W/E/R", "Esfera / Lissajous / Hélice / Toroide"},
{"T/Y/U/I", "Cubo / Cilindro / Icosaedro / Átomo"},
{"Num+/-", "Escalar figura"},
{"Num*", "Reset escala"},
{"Num/", "Activar / Desactivar profundidad"},
{"[new_col]", ""}, // CAMBIO DE COLUMNA -> COLUMNA 2
{.key = "FIGURAS 3D", .description = ""},
{.key = "Q/W/E/R", .description = "Esfera / Lissajous / Hélice / Toroide"},
{.key = "T/Y/U/I", .description = "Cubo / Cilindro / Icosaedro / Átomo"},
{.key = "Num+/-", .description = "Escalar figura"},
{.key = "Num*", .description = "Reset escala"},
{.key = "Num/", .description = "Activar / Desactivar profundidad"},
{.key = "[new_col]", .description = ""}, // CAMBIO DE COLUMNA -> COLUMNA 2
// COLUMNA 2: MODOS
{"MODOS", ""},
{"D", "Activar / Desactivar modo demo"},
{"L", "Activar / Desactivar modo demo lite"},
{"K", "Activar / Desactivar modo logo"},
{"", ""}, // Separador
{.key = "MODOS", .description = ""},
{.key = "D", .description = "Activar / Desactivar modo demo"},
{.key = "L", .description = "Activar / Desactivar modo demo lite"},
{.key = "K", .description = "Activar / Desactivar modo logo"},
{.key = "", .description = ""}, // Separador
// COLUMNA 2: VISUAL
{"VISUAL", ""},
{"C", "Tema siguiente"},
{"Shift+C", "Tema anterior"},
{"NumEnter", "Página de temas"},
{"Shift+D", "Pausar tema dinámico"},
{"N", "Cambiar tamaño de pelota"},
{"X", "Ciclar presets PostFX"},
{"[new_col]", ""}, // CAMBIO DE COLUMNA -> COLUMNA 3
{.key = "VISUAL", .description = ""},
{.key = "C", .description = "Tema siguiente"},
{.key = "Shift+C", .description = "Tema anterior"},
{.key = "NumEnter", .description = "Página de temas"},
{.key = "Shift+D", .description = "Pausar tema dinámico"},
{.key = "N", .description = "Cambiar tamaño de pelota"},
{.key = "X", .description = "Ciclar presets PostFX"},
{.key = "[new_col]", .description = ""}, // CAMBIO DE COLUMNA -> COLUMNA 3
// COLUMNA 3: PANTALLA
{"PANTALLA", ""},
{"F1", "Disminuye ventana"},
{"F2", "Aumenta ventana"},
{"F3", "Pantalla completa"},
{"F4", "Pantalla completa real"},
{"F5", "Activar / Desactivar PostFX"},
{"F6", "Cambia el escalado de pantalla"},
{"V", "Activar / Desactivar V-Sync"},
{"", ""}, // Separador
{.key = "PANTALLA", .description = ""},
{.key = "F1", .description = "Disminuye ventana"},
{.key = "F2", .description = "Aumenta ventana"},
{.key = "F3", .description = "Pantalla completa"},
{.key = "F4", .description = "Pantalla completa real"},
{.key = "F5", .description = "Activar / Desactivar PostFX"},
{.key = "F6", .description = "Cambia el escalado de pantalla"},
{.key = "V", .description = "Activar / Desactivar V-Sync"},
{.key = "", .description = ""}, // Separador
// COLUMNA 3: DEBUG/AYUDA
{"DEBUG / AYUDA", ""},
{"F12", "Activar / Desactivar info debug"},
{"H", "Esta ayuda"},
{"ESC", "Salir"}};
{.key = "DEBUG / AYUDA", .description = ""},
{.key = "F12", .description = "Activar / Desactivar info debug"},
{.key = "H", .description = "Esta ayuda"},
{.key = "ESC", .description = "Salir"}};
}
HelpOverlay::~HelpOverlay() {
// Destruir textura cacheada si existe
if (cached_texture_) {
if (cached_texture_ != nullptr) {
SDL_DestroyTexture(cached_texture_);
cached_texture_ = nullptr;
}
@@ -117,7 +118,9 @@ void HelpOverlay::updatePhysicalWindowSize(int physical_width, int physical_heig
}
void HelpOverlay::reinitializeFontSize(int new_font_size) {
if (!text_renderer_) return;
if (text_renderer_ == nullptr) {
return;
}
// Reinicializar text renderer con nuevo tamaño
text_renderer_->reinitialize(new_font_size);
@@ -136,7 +139,7 @@ void HelpOverlay::updateAll(int font_size, int physical_width, int physical_heig
physical_height_ = physical_height;
// Reinicializar text renderer con nuevo tamaño (si cambió)
if (text_renderer_) {
if (text_renderer_ != nullptr) {
text_renderer_->reinitialize(font_size);
}
@@ -148,7 +151,7 @@ void HelpOverlay::updateAll(int font_size, int physical_width, int physical_heig
}
void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) {
if (!text_renderer_) {
if (text_renderer_ == nullptr) {
max_width = 0;
total_height = 0;
return;
@@ -210,7 +213,7 @@ void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) {
max_width = max_col1_width + max_col2_width + max_col3_width + padding * 2 + col_gap * 2;
// Calcular altura real simulando exactamente lo que hace el render
int col_heights[3] = {0, 0, 0};
std::array<int, 3> col_heights = {0, 0, 0};
current_column = 0;
for (const auto& binding : key_bindings_) {
@@ -220,11 +223,11 @@ void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) {
}
if (binding.key[0] == '\0') {
col_heights[current_column] += line_height; // separador vacío
col_heights[current_column] += line_height; // separador vacío
} else if (binding.description[0] == '\0') {
col_heights[current_column] += line_height; // encabezado
col_heights[current_column] += line_height; // encabezado
} else {
col_heights[current_column] += line_height; // línea normal
col_heights[current_column] += line_height; // línea normal
}
}
@@ -240,7 +243,8 @@ void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) {
void HelpOverlay::calculateBoxDimensions() {
// Calcular dimensiones necesarias según el texto
int text_width, text_height;
int text_width;
int text_height;
calculateTextDimensions(text_width, text_height);
// Aplicar límites máximos: 95% ancho, 90% altura
@@ -253,26 +257,27 @@ void HelpOverlay::calculateBoxDimensions() {
// Centrar en pantalla
box_x_ = (physical_width_ - box_width_) / 2;
box_y_ = (physical_height_ - box_height_) / 2;
}
void HelpOverlay::rebuildCachedTexture() {
if (!renderer_ || !theme_mgr_ || !text_renderer_) return;
if ((renderer_ == nullptr) || (theme_mgr_ == nullptr) || (text_renderer_ == nullptr)) {
return;
}
// Destruir textura anterior si existe
if (cached_texture_) {
if (cached_texture_ != nullptr) {
SDL_DestroyTexture(cached_texture_);
cached_texture_ = nullptr;
}
// Crear nueva textura del tamaño del overlay
cached_texture_ = SDL_CreateTexture(renderer_,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET,
box_width_,
box_height_);
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_TARGET,
box_width_,
box_height_);
if (!cached_texture_) {
if (cached_texture_ == nullptr) {
SDL_Log("Error al crear textura cacheada: %s", SDL_GetError());
return;
}
@@ -294,42 +299,46 @@ void HelpOverlay::rebuildCachedTexture() {
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND);
// Obtener colores actuales del tema
int notif_bg_r, notif_bg_g, notif_bg_b;
int notif_bg_r;
int notif_bg_g;
int notif_bg_b;
theme_mgr_->getCurrentNotificationBackgroundColor(notif_bg_r, notif_bg_g, notif_bg_b);
// Renderizar fondo del overlay a la textura
float alpha = 0.85f;
SDL_Vertex bg_vertices[4];
std::array<SDL_Vertex, 4> bg_vertices{};
float r = notif_bg_r / 255.0f;
float g = notif_bg_g / 255.0f;
float b = notif_bg_b / 255.0f;
// Vértices del fondo (posición relativa 0,0 porque estamos renderizando a textura)
bg_vertices[0].position = {0, 0};
bg_vertices[0].tex_coord = {0.0f, 0.0f};
bg_vertices[0].color = {r, g, b, alpha};
bg_vertices[0].position = {.x = 0, .y = 0};
bg_vertices[0].tex_coord = {.x = 0.0f, .y = 0.0f};
bg_vertices[0].color = {.r = r, .g = g, .b = b, .a = alpha};
bg_vertices[1].position = {static_cast<float>(box_width_), 0};
bg_vertices[1].tex_coord = {1.0f, 0.0f};
bg_vertices[1].color = {r, g, b, alpha};
bg_vertices[1].position = {.x = static_cast<float>(box_width_), .y = 0};
bg_vertices[1].tex_coord = {.x = 1.0f, .y = 0.0f};
bg_vertices[1].color = {.r = r, .g = g, .b = b, .a = alpha};
bg_vertices[2].position = {static_cast<float>(box_width_), static_cast<float>(box_height_)};
bg_vertices[2].tex_coord = {1.0f, 1.0f};
bg_vertices[2].color = {r, g, b, alpha};
bg_vertices[2].position = {.x = static_cast<float>(box_width_), .y = static_cast<float>(box_height_)};
bg_vertices[2].tex_coord = {.x = 1.0f, .y = 1.0f};
bg_vertices[2].color = {.r = r, .g = g, .b = b, .a = alpha};
bg_vertices[3].position = {0, static_cast<float>(box_height_)};
bg_vertices[3].tex_coord = {0.0f, 1.0f};
bg_vertices[3].color = {r, g, b, alpha};
bg_vertices[3].position = {.x = 0, .y = static_cast<float>(box_height_)};
bg_vertices[3].tex_coord = {.x = 0.0f, .y = 1.0f};
bg_vertices[3].color = {.r = r, .g = g, .b = b, .a = alpha};
int bg_indices[6] = {0, 1, 2, 2, 3, 0};
SDL_RenderGeometry(renderer_, nullptr, bg_vertices, 4, bg_indices, 6);
std::array<int, 6> bg_indices = {0, 1, 2, 2, 3, 0};
SDL_RenderGeometry(renderer_, nullptr, bg_vertices.data(), 4, bg_indices.data(), 6);
// Renderizar texto del overlay (ajustando coordenadas para que sean relativas a 0,0)
// Necesito renderizar el texto igual que en renderHelpText() pero con coordenadas ajustadas
// Obtener colores para el texto
int text_r, text_g, text_b;
int text_r;
int text_g;
int text_b;
theme_mgr_->getCurrentThemeTextColor(text_r, text_g, text_b);
SDL_Color category_color = {static_cast<Uint8>(text_r), static_cast<Uint8>(text_g), static_cast<Uint8>(text_b), 255};
@@ -339,7 +348,7 @@ void HelpOverlay::rebuildCachedTexture() {
// Guardar colores actuales para comparación futura
last_category_color_ = category_color;
last_content_color_ = content_color;
last_bg_color_ = {static_cast<Uint8>(notif_bg_r), static_cast<Uint8>(notif_bg_g), static_cast<Uint8>(notif_bg_b), 255};
last_bg_color_ = {.r = static_cast<Uint8>(notif_bg_r), .g = static_cast<Uint8>(notif_bg_g), .b = static_cast<Uint8>(notif_bg_b), .a = 255};
// Configuración de espaciado
int line_height = text_renderer_->getTextHeight();
@@ -347,13 +356,13 @@ void HelpOverlay::rebuildCachedTexture() {
int col_gap = padding * 2;
// Posición X de inicio de cada columna
int col_start[3];
std::array<int, 3> col_start{};
col_start[0] = padding;
col_start[1] = padding + column1_width_ + col_gap;
col_start[2] = padding + column1_width_ + col_gap + column2_width_ + col_gap;
// Ancho de cada columna (para centrado interno)
int col_width[3] = {column1_width_, column2_width_, column3_width_};
std::array<int, 3> col_width = {column1_width_, column2_width_, column3_width_};
int glyph_height = text_renderer_->getGlyphHeight();
int current_y = padding;
@@ -387,7 +396,7 @@ void HelpOverlay::rebuildCachedTexture() {
} else {
// Encabezado de sección — centrado en la columna
int w = text_renderer_->getTextWidthPhysical(binding.key);
text_renderer_->printAbsolute(cx + (cw - w) / 2, current_y, binding.key, category_color);
text_renderer_->printAbsolute(cx + ((cw - w) / 2), current_y, binding.key, category_color);
current_y += line_height;
}
continue;
@@ -397,7 +406,7 @@ void HelpOverlay::rebuildCachedTexture() {
int key_width = text_renderer_->getTextWidthPhysical(binding.key);
int desc_width = text_renderer_->getTextWidthPhysical(binding.description);
int total_width = key_width + 10 + desc_width;
int line_x = cx + (cw - total_width) / 2;
int line_x = cx + ((cw - total_width) / 2);
text_renderer_->printAbsolute(line_x, current_y, binding.key, category_color);
text_renderer_->printAbsolute(line_x + key_width + 10, current_y, binding.description, content_color);
@@ -413,13 +422,19 @@ void HelpOverlay::rebuildCachedTexture() {
}
void HelpOverlay::render(SDL_Renderer* renderer) {
if (!visible_) return;
if (!visible_) {
return;
}
// Obtener colores actuales del tema
int notif_bg_r, notif_bg_g, notif_bg_b;
int notif_bg_r;
int notif_bg_g;
int notif_bg_b;
theme_mgr_->getCurrentNotificationBackgroundColor(notif_bg_r, notif_bg_g, notif_bg_b);
int text_r, text_g, text_b;
int text_r;
int text_g;
int text_b;
theme_mgr_->getCurrentThemeTextColor(text_r, text_g, text_b);
Color ball_color = theme_mgr_->getInterpolatedColor(0);
@@ -433,22 +448,24 @@ void HelpOverlay::render(SDL_Renderer* renderer) {
constexpr int COLOR_CHANGE_THRESHOLD = 5;
bool colors_changed =
(abs(current_bg.r - last_bg_color_.r) > COLOR_CHANGE_THRESHOLD ||
abs(current_bg.g - last_bg_color_.g) > COLOR_CHANGE_THRESHOLD ||
abs(current_bg.b - last_bg_color_.b) > COLOR_CHANGE_THRESHOLD ||
abs(current_category.r - last_category_color_.r) > COLOR_CHANGE_THRESHOLD ||
abs(current_category.g - last_category_color_.g) > COLOR_CHANGE_THRESHOLD ||
abs(current_category.b - last_category_color_.b) > COLOR_CHANGE_THRESHOLD ||
abs(current_content.r - last_content_color_.r) > COLOR_CHANGE_THRESHOLD ||
abs(current_content.g - last_content_color_.g) > COLOR_CHANGE_THRESHOLD ||
abs(current_content.b - last_content_color_.b) > COLOR_CHANGE_THRESHOLD);
abs(current_bg.g - last_bg_color_.g) > COLOR_CHANGE_THRESHOLD ||
abs(current_bg.b - last_bg_color_.b) > COLOR_CHANGE_THRESHOLD ||
abs(current_category.r - last_category_color_.r) > COLOR_CHANGE_THRESHOLD ||
abs(current_category.g - last_category_color_.g) > COLOR_CHANGE_THRESHOLD ||
abs(current_category.b - last_category_color_.b) > COLOR_CHANGE_THRESHOLD ||
abs(current_content.r - last_content_color_.r) > COLOR_CHANGE_THRESHOLD ||
abs(current_content.g - last_content_color_.g) > COLOR_CHANGE_THRESHOLD ||
abs(current_content.b - last_content_color_.b) > COLOR_CHANGE_THRESHOLD);
// Regenerar textura si es necesario (colores cambiaron O flag de rebuild activo)
if (texture_needs_rebuild_ || colors_changed || !cached_texture_) {
if (texture_needs_rebuild_ || colors_changed || (cached_texture_ == nullptr)) {
rebuildCachedTexture();
}
// Si no hay textura cacheada (error), salir
if (!cached_texture_) return;
if (cached_texture_ == nullptr) {
return;
}
// CRÍTICO: Habilitar alpha blending para que la transparencia funcione
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
@@ -460,8 +477,8 @@ void HelpOverlay::render(SDL_Renderer* renderer) {
// Calcular posición centrada dentro del VIEWPORT, no de la pantalla física
// viewport.w y viewport.h son las dimensiones del área visible
// viewport.x y viewport.y son el offset de las barras negras
int centered_x = viewport.x + (viewport.w - box_width_) / 2;
int centered_y = viewport.y + (viewport.h - box_height_) / 2;
int centered_x = viewport.x + ((viewport.w - box_width_) / 2);
int centered_y = viewport.y + ((viewport.h - box_height_) / 2);
// Renderizar la textura cacheada centrada en el viewport
SDL_FRect dest_rect;

View File

@@ -17,89 +17,89 @@ class TextRenderer;
* Toggle on/off con tecla H. La simulación continúa en el fondo.
*/
class HelpOverlay {
public:
HelpOverlay();
~HelpOverlay();
public:
HelpOverlay();
~HelpOverlay();
/**
* @brief Inicializa el overlay con renderer y theme manager
*/
void initialize(SDL_Renderer* renderer, ThemeManager* theme_mgr, int physical_width, int physical_height, int font_size);
/**
* @brief Inicializa el overlay con renderer y theme manager
*/
void initialize(SDL_Renderer* renderer, ThemeManager* theme_mgr, int physical_width, int physical_height, int font_size);
/**
* @brief Renderiza el overlay si está visible
*/
void render(SDL_Renderer* renderer);
/**
* @brief Renderiza el overlay si está visible
*/
void render(SDL_Renderer* renderer);
/**
* @brief Actualiza dimensiones físicas de ventana (zoom, fullscreen, etc.)
*/
void updatePhysicalWindowSize(int physical_width, int physical_height);
/**
* @brief Actualiza dimensiones físicas de ventana (zoom, fullscreen, etc.)
*/
void updatePhysicalWindowSize(int physical_width, int physical_height);
/**
* @brief Reinitializa el tamaño de fuente (cuando cambia el tamaño de ventana)
*/
void reinitializeFontSize(int new_font_size);
/**
* @brief Reinitializa el tamaño de fuente (cuando cambia el tamaño de ventana)
*/
void reinitializeFontSize(int new_font_size);
/**
* @brief Actualiza font size Y dimensiones físicas de forma atómica
* @param font_size Tamaño de fuente actual
* @param physical_width Nueva anchura física
* @param physical_height Nueva altura física
*/
void updateAll(int font_size, int physical_width, int physical_height);
/**
* @brief Actualiza font size Y dimensiones físicas de forma atómica
* @param font_size Tamaño de fuente actual
* @param physical_width Nueva anchura física
* @param physical_height Nueva altura física
*/
void updateAll(int font_size, int physical_width, int physical_height);
/**
* @brief Toggle visibilidad del overlay
*/
void toggle();
/**
* @brief Toggle visibilidad del overlay
*/
void toggle();
/**
* @brief Consulta si el overlay está visible
*/
bool isVisible() const { return visible_; }
/**
* @brief Consulta si el overlay está visible
*/
bool isVisible() const { return visible_; }
private:
SDL_Renderer* renderer_;
ThemeManager* theme_mgr_;
TextRenderer* text_renderer_; // Renderer de texto para la ayuda
int physical_width_;
int physical_height_;
bool visible_;
private:
SDL_Renderer* renderer_;
ThemeManager* theme_mgr_;
TextRenderer* text_renderer_; // Renderer de texto para la ayuda
int physical_width_;
int physical_height_;
bool visible_;
// Dimensiones calculadas del recuadro (anchura dinámica según texto, centrado)
int box_width_;
int box_height_;
int box_x_;
int box_y_;
// Dimensiones calculadas del recuadro (anchura dinámica según texto, centrado)
int box_width_;
int box_height_;
int box_x_;
int box_y_;
// Anchos individuales de cada columna (para evitar solapamiento)
int column1_width_;
int column2_width_;
int column3_width_;
// Anchos individuales de cada columna (para evitar solapamiento)
int column1_width_;
int column2_width_;
int column3_width_;
// Sistema de caché para optimización de rendimiento
SDL_Texture* cached_texture_; // Textura cacheada del overlay completo
SDL_Color last_category_color_; // Último color de categorías renderizado
SDL_Color last_content_color_; // Último color de contenido renderizado
SDL_Color last_bg_color_; // Último color de fondo renderizado
bool texture_needs_rebuild_; // Flag para forzar regeneración de textura
// Sistema de caché para optimización de rendimiento
SDL_Texture* cached_texture_; // Textura cacheada del overlay completo
SDL_Color last_category_color_; // Último color de categorías renderizado
SDL_Color last_content_color_; // Último color de contenido renderizado
SDL_Color last_bg_color_; // Último color de fondo renderizado
bool texture_needs_rebuild_; // Flag para forzar regeneración de textura
// Calcular dimensiones del texto más largo
void calculateTextDimensions(int& max_width, int& total_height);
// Calcular dimensiones del texto más largo
void calculateTextDimensions(int& max_width, int& total_height);
// Calcular dimensiones del recuadro según tamaño de ventana y texto
void calculateBoxDimensions();
// Calcular dimensiones del recuadro según tamaño de ventana y texto
void calculateBoxDimensions();
// Regenerar textura cacheada del overlay
void rebuildCachedTexture();
// Regenerar textura cacheada del overlay
void rebuildCachedTexture();
// Estructura para par tecla-descripción
struct KeyBinding {
const char* key;
const char* description;
};
// Estructura para par tecla-descripción
struct KeyBinding {
const char* key;
const char* description;
};
// Lista de todos los controles (se llena en constructor)
std::vector<KeyBinding> key_bindings_;
// Lista de todos los controles (se llena en constructor)
std::vector<KeyBinding> key_bindings_;
};

View File

@@ -1,25 +1,25 @@
#define STB_IMAGE_RESIZE_IMPLEMENTATION
#include "logo_scaler.hpp"
#include <SDL3/SDL_error.h> // Para SDL_GetError
#include <SDL3/SDL_log.h> // Para SDL_Log
#include <SDL3/SDL_pixels.h> // Para SDL_PixelFormat
#include <SDL3/SDL_render.h> // Para SDL_CreateTexture
#include <SDL3/SDL_surface.h> // Para SDL_CreateSurfaceFrom
#include <SDL3/SDL_video.h> // Para SDL_GetDisplays
#include <SDL3/SDL_error.h> // Para SDL_GetError
#include <SDL3/SDL_log.h> // Para SDL_Log
#include <SDL3/SDL_pixels.h> // Para SDL_PixelFormat
#include <SDL3/SDL_render.h> // Para SDL_CreateTexture
#include <SDL3/SDL_surface.h> // Para SDL_CreateSurfaceFrom
#include <SDL3/SDL_video.h> // Para SDL_GetDisplays
#include <cstdlib> // Para free()
#include <iostream> // Para std::cout
#include "external/stb_image.h" // Para stbi_load, stbi_image_free
#include "external/stb_image_resize2.h" // Para stbir_resize_uint8_srgb
#include "resource_manager.hpp" // Para cargar desde pack
#include "resource_manager.hpp" // Para cargar desde pack
// ============================================================================
// Detectar resolución nativa del monitor principal
// ============================================================================
bool LogoScaler::detectNativeResolution(int& native_width, int& native_height) {
auto LogoScaler::detectNativeResolution(int& native_width, int& native_height) -> bool {
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
@@ -48,22 +48,25 @@ bool LogoScaler::detectNativeResolution(int& native_width, int& native_height) {
// Cargar PNG y escalar al tamaño especificado
// ============================================================================
unsigned char* LogoScaler::loadAndScale(const std::string& path,
int target_width, int target_height,
int& out_width, int& out_height) {
auto LogoScaler::loadAndScale(const std::string& path,
int target_width,
int target_height,
int& out_width,
int& out_height) -> unsigned char* {
// 1. Intentar cargar imagen desde ResourceManager (pack o disco)
int orig_width, orig_height, orig_channels;
int orig_width;
int orig_height;
int orig_channels;
unsigned char* orig_data = nullptr;
// 1a. Cargar desde ResourceManager
unsigned char* resourceData = nullptr;
size_t resourceSize = 0;
unsigned char* resource_data = nullptr;
size_t resource_size = 0;
if (ResourceManager::loadResource(path, resourceData, resourceSize)) {
if (ResourceManager::loadResource(path, resource_data, resource_size)) {
// Descodificar imagen desde memoria usando stb_image
orig_data = stbi_load_from_memory(resourceData, static_cast<int>(resourceSize),
&orig_width, &orig_height, &orig_channels, STBI_rgb_alpha);
delete[] resourceData; // Liberar buffer temporal
orig_data = stbi_load_from_memory(resource_data, static_cast<int>(resource_size), &orig_width, &orig_height, &orig_channels, STBI_rgb_alpha);
delete[] resource_data; // Liberar buffer temporal
}
// 1b. Si falla todo, error
@@ -80,7 +83,7 @@ unsigned char* LogoScaler::loadAndScale(const std::string& path,
out_height = target_height;
// 3. Alocar buffer para imagen escalada (RGBA = 4 bytes por píxel)
unsigned char* scaled_data = static_cast<unsigned char*>(malloc(out_width * out_height * 4));
auto* scaled_data = static_cast<unsigned char*>(malloc(out_width * out_height * 4));
if (scaled_data == nullptr) {
SDL_Log("Error al alocar memoria para imagen escalada");
stbi_image_free(orig_data);
@@ -90,9 +93,15 @@ unsigned char* LogoScaler::loadAndScale(const std::string& path,
// 4. Escalar con stb_image_resize2 (algoritmo Mitchell, espacio sRGB)
// La función devuelve el puntero de salida, o nullptr si falla
unsigned char* result = stbir_resize_uint8_srgb(
orig_data, orig_width, orig_height, 0, // Input
scaled_data, out_width, out_height, 0, // Output
STBIR_RGBA // Formato píxel
orig_data,
orig_width,
orig_height,
0, // Input
scaled_data,
out_width,
out_height,
0, // Output
STBIR_RGBA // Formato píxel
);
// Liberar imagen original (ya no la necesitamos)
@@ -111,9 +120,10 @@ unsigned char* LogoScaler::loadAndScale(const std::string& path,
// Crear textura SDL desde buffer RGBA
// ============================================================================
SDL_Texture* LogoScaler::createTextureFromBuffer(SDL_Renderer* renderer,
unsigned char* data,
int width, int height) {
auto LogoScaler::createTextureFromBuffer(SDL_Renderer* renderer,
unsigned char* data,
int width,
int height) -> SDL_Texture* {
if (renderer == nullptr || data == nullptr || width <= 0 || height <= 0) {
SDL_Log("Parámetros inválidos para createTextureFromBuffer");
return nullptr;
@@ -124,11 +134,11 @@ SDL_Texture* LogoScaler::createTextureFromBuffer(SDL_Renderer* renderer,
SDL_PixelFormat pixel_format = SDL_PIXELFORMAT_RGBA32;
SDL_Surface* surface = SDL_CreateSurfaceFrom(
width, height,
width,
height,
pixel_format,
data,
pitch
);
pitch);
if (surface == nullptr) {
SDL_Log("Error al crear surface: %s", SDL_GetError());

View File

@@ -17,45 +17,48 @@
* de pantalla, eliminando el escalado dinámico de SDL y mejorando calidad visual.
*/
class LogoScaler {
public:
/**
* @brief Detecta la resolución nativa del monitor principal
*
* @param native_width [out] Ancho nativo del display en píxeles
* @param native_height [out] Alto nativo del display en píxeles
* @return true si se pudo detectar, false si hubo error
*/
static bool detectNativeResolution(int& native_width, int& native_height);
public:
/**
* @brief Detecta la resolución nativa del monitor principal
*
* @param native_width [out] Ancho nativo del display en píxeles
* @param native_height [out] Alto nativo del display en píxeles
* @return true si se pudo detectar, false si hubo error
*/
static bool detectNativeResolution(int& native_width, int& native_height);
/**
* @brief Carga un PNG y lo escala al tamaño especificado
*
* Usa stb_image para cargar y stb_image_resize2 para escalar con
* algoritmo Mitchell (balance calidad/velocidad) en espacio sRGB.
*
* @param path Ruta al archivo PNG (ej: "data/logo/logo.png")
* @param target_width Ancho destino en píxeles
* @param target_height Alto destino en píxeles
* @param out_width [out] Ancho real de la imagen escalada
* @param out_height [out] Alto real de la imagen escalada
* @return Buffer RGBA (4 bytes por píxel) o nullptr si falla
* IMPORTANTE: El caller debe liberar con free() cuando termine
*/
static unsigned char* loadAndScale(const std::string& path,
int target_width, int target_height,
int& out_width, int& out_height);
/**
* @brief Carga un PNG y lo escala al tamaño especificado
*
* Usa stb_image para cargar y stb_image_resize2 para escalar con
* algoritmo Mitchell (balance calidad/velocidad) en espacio sRGB.
*
* @param path Ruta al archivo PNG (ej: "data/logo/logo.png")
* @param target_width Ancho destino en píxeles
* @param target_height Alto destino en píxeles
* @param out_width [out] Ancho real de la imagen escalada
* @param out_height [out] Alto real de la imagen escalada
* @return Buffer RGBA (4 bytes por píxel) o nullptr si falla
* IMPORTANTE: El caller debe liberar con free() cuando termine
*/
static unsigned char* loadAndScale(const std::string& path,
int target_width,
int target_height,
int& out_width,
int& out_height);
/**
* @brief Crea una textura SDL desde un buffer RGBA
*
* @param renderer Renderizador SDL activo
* @param data Buffer RGBA (4 bytes por píxel)
* @param width Ancho del buffer en píxeles
* @param height Alto del buffer en píxeles
* @return Textura SDL creada o nullptr si falla
* IMPORTANTE: El caller debe destruir con SDL_DestroyTexture()
*/
static SDL_Texture* createTextureFromBuffer(SDL_Renderer* renderer,
unsigned char* data,
int width, int height);
/**
* @brief Crea una textura SDL desde un buffer RGBA
*
* @param renderer Renderizador SDL activo
* @param data Buffer RGBA (4 bytes por píxel)
* @param width Ancho del buffer en píxeles
* @param height Alto del buffer en píxeles
* @return Textura SDL creada o nullptr si falla
* IMPORTANTE: El caller debe destruir con SDL_DestroyTexture()
*/
static SDL_Texture* createTextureFromBuffer(SDL_Renderer* renderer,
unsigned char* data,
int width,
int height);
};

View File

@@ -1,9 +1,11 @@
#include "notifier.hpp"
#include <SDL3/SDL.h>
#include "defines.hpp"
#include "text/textrenderer.hpp"
#include "theme_manager.hpp"
#include "defines.hpp"
#include "utils/easing_functions.hpp"
#include <SDL3/SDL.h>
// ============================================================================
// HELPER: Obtener viewport en coordenadas físicas (no lógicas)
@@ -11,9 +13,10 @@
// SDL_GetRenderViewport() devuelve coordenadas LÓGICAS cuando hay presentación
// lógica activa. Para obtener coordenadas FÍSICAS, necesitamos deshabilitar
// temporalmente la presentación lógica.
static SDL_Rect getPhysicalViewport(SDL_Renderer* renderer) {
static auto getPhysicalViewport(SDL_Renderer* renderer) -> SDL_Rect {
// Guardar estado actual de presentación lógica
int logical_w = 0, logical_h = 0;
int logical_w = 0;
int logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer, &logical_w, &logical_h, &presentation_mode);
@@ -31,19 +34,19 @@ static SDL_Rect getPhysicalViewport(SDL_Renderer* renderer) {
}
Notifier::Notifier()
: renderer_(nullptr)
, text_renderer_(nullptr)
, theme_manager_(nullptr)
, window_width_(0)
, window_height_(0)
, current_notification_(nullptr) {
: renderer_(nullptr),
text_renderer_(nullptr),
theme_manager_(nullptr),
window_width_(0),
window_height_(0),
current_notification_(nullptr) {
}
Notifier::~Notifier() {
clear();
}
bool Notifier::init(SDL_Renderer* renderer, TextRenderer* text_renderer, ThemeManager* theme_manager, int window_width, int window_height) {
auto Notifier::init(SDL_Renderer* renderer, TextRenderer* text_renderer, ThemeManager* theme_manager, int window_width, int window_height) -> bool {
renderer_ = renderer;
text_renderer_ = text_renderer;
theme_manager_ = theme_manager;
@@ -105,7 +108,7 @@ void Notifier::update(Uint64 current_time) {
// Animación de entrada (NOTIFICATION_SLIDE_TIME ms)
if (elapsed < NOTIFICATION_SLIDE_TIME) {
float progress = static_cast<float>(elapsed) / static_cast<float>(NOTIFICATION_SLIDE_TIME);
float eased = Easing::easeOutBack(progress); // Efecto con ligero overshoot
float eased = Easing::easeOutBack(progress); // Efecto con ligero overshoot
current_notification_->y_offset = -50.0f + (50.0f * eased); // De -50 a 0
} else {
// Transición a VISIBLE
@@ -151,28 +154,30 @@ void Notifier::update(Uint64 current_time) {
}
void Notifier::render() {
if (!current_notification_ || !text_renderer_ || !renderer_ || !theme_manager_) {
if (!current_notification_ || (text_renderer_ == nullptr) || (renderer_ == nullptr) || (theme_manager_ == nullptr)) {
return;
}
// Obtener colores DINÁMICOS desde ThemeManager (incluye LERP automático)
int text_r, text_g, text_b;
int text_r;
int text_g;
int text_b;
theme_manager_->getCurrentThemeTextColor(text_r, text_g, text_b);
SDL_Color text_color = {
static_cast<Uint8>(text_r),
static_cast<Uint8>(text_g),
static_cast<Uint8>(text_b),
static_cast<Uint8>(current_notification_->alpha * 255.0f)
};
static_cast<Uint8>(current_notification_->alpha * 255.0f)};
int bg_r, bg_g, bg_b;
int bg_r;
int bg_g;
int bg_b;
theme_manager_->getCurrentNotificationBackgroundColor(bg_r, bg_g, bg_b);
SDL_Color bg_color = {
static_cast<Uint8>(bg_r),
static_cast<Uint8>(bg_g),
static_cast<Uint8>(bg_b),
255
};
255};
// Calcular dimensiones del texto en píxeles FÍSICOS
// IMPORTANTE: Usar getTextWidthPhysical() en lugar de getTextWidth()
@@ -207,7 +212,7 @@ void Notifier::render() {
}
void Notifier::renderBackground(int x, int y, int width, int height, float alpha, SDL_Color bg_color) {
if (!renderer_) {
if (renderer_ == nullptr) {
return;
}
@@ -225,7 +230,7 @@ void Notifier::renderBackground(int x, int y, int width, int height, float alpha
bg_rect.h = static_cast<float>(height);
// Color del tema con alpha
Uint8 bg_alpha = static_cast<Uint8>(alpha * 255.0f);
auto bg_alpha = static_cast<Uint8>(alpha * 255.0f);
SDL_SetRenderDrawColor(renderer_, bg_color.r, bg_color.g, bg_color.b, bg_alpha);
// Habilitar blending para transparencia
@@ -233,7 +238,8 @@ void Notifier::renderBackground(int x, int y, int width, int height, float alpha
// CRÍTICO: Deshabilitar presentación lógica para renderizar en píxeles físicos absolutos
// (igual que printAbsolute() en TextRenderer)
int logical_w = 0, logical_h = 0;
int logical_w = 0;
int logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer_, &logical_w, &logical_h, &presentation_mode);
@@ -248,7 +254,7 @@ void Notifier::renderBackground(int x, int y, int width, int height, float alpha
SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_NONE);
}
bool Notifier::isActive() const {
auto Notifier::isActive() const -> bool {
return (current_notification_ != nullptr);
}

View File

@@ -1,9 +1,10 @@
#pragma once
#include <SDL3/SDL.h>
#include <string>
#include <queue>
#include <memory>
#include <queue>
#include <string>
// Forward declarations
class TextRenderer;
@@ -20,92 +21,92 @@ class ThemeManager;
* - Texto de tamaño fijo independiente de resolución
*/
class Notifier {
public:
enum class NotificationState {
SLIDING_IN, // Animación de entrada desde arriba
VISIBLE, // Visible estático
FADING_OUT, // Animación de salida (fade)
DONE // Completado, listo para eliminar
};
public:
enum class NotificationState {
SLIDING_IN, // Animación de entrada desde arriba
VISIBLE, // Visible estático
FADING_OUT, // Animación de salida (fade)
DONE // Completado, listo para eliminar
};
struct Notification {
std::string text;
Uint64 created_time;
Uint64 duration;
NotificationState state;
float alpha; // Opacidad 0.0-1.0
float y_offset; // Offset Y para animación slide (píxeles)
// NOTA: Los colores se obtienen dinámicamente desde ThemeManager en render()
};
struct Notification {
std::string text;
Uint64 created_time;
Uint64 duration;
NotificationState state;
float alpha; // Opacidad 0.0-1.0
float y_offset; // Offset Y para animación slide (píxeles)
// NOTA: Los colores se obtienen dinámicamente desde ThemeManager en render()
};
Notifier();
~Notifier();
Notifier();
~Notifier();
/**
* @brief Inicializa el notifier con un TextRenderer y ThemeManager
* @param renderer SDL renderer para dibujar
* @param text_renderer TextRenderer configurado con tamaño absoluto
* @param theme_manager ThemeManager para obtener colores dinámicos con LERP
* @param window_width Ancho de ventana física
* @param window_height Alto de ventana física
* @return true si inicialización exitosa
*/
bool init(SDL_Renderer* renderer, TextRenderer* text_renderer, ThemeManager* theme_manager, int window_width, int window_height);
/**
* @brief Inicializa el notifier con un TextRenderer y ThemeManager
* @param renderer SDL renderer para dibujar
* @param text_renderer TextRenderer configurado con tamaño absoluto
* @param theme_manager ThemeManager para obtener colores dinámicos con LERP
* @param window_width Ancho de ventana física
* @param window_height Alto de ventana física
* @return true si inicialización exitosa
*/
bool init(SDL_Renderer* renderer, TextRenderer* text_renderer, ThemeManager* theme_manager, int window_width, int window_height);
/**
* @brief Actualiza las dimensiones de la ventana (llamar en resize)
* @param window_width Nuevo ancho de ventana física
* @param window_height Nuevo alto de ventana física
*/
void updateWindowSize(int window_width, int window_height);
/**
* @brief Actualiza las dimensiones de la ventana (llamar en resize)
* @param window_width Nuevo ancho de ventana física
* @param window_height Nuevo alto de ventana física
*/
void updateWindowSize(int window_width, int window_height);
/**
* @brief Muestra una nueva notificación
* @param text Texto a mostrar
* @param duration Duración en milisegundos (0 = usar default)
* @note Los colores se obtienen dinámicamente desde ThemeManager cada frame
*/
void show(const std::string& text, Uint64 duration = 0);
/**
* @brief Muestra una nueva notificación
* @param text Texto a mostrar
* @param duration Duración en milisegundos (0 = usar default)
* @note Los colores se obtienen dinámicamente desde ThemeManager cada frame
*/
void show(const std::string& text, Uint64 duration = 0);
/**
* @brief Actualiza las animaciones de notificaciones
* @param current_time Tiempo actual en ms (SDL_GetTicks())
*/
void update(Uint64 current_time);
/**
* @brief Actualiza las animaciones de notificaciones
* @param current_time Tiempo actual en ms (SDL_GetTicks())
*/
void update(Uint64 current_time);
/**
* @brief Renderiza la notificación activa
*/
void render();
/**
* @brief Renderiza la notificación activa
*/
void render();
/**
* @brief Verifica si hay una notificación activa (visible)
* @return true si hay notificación mostrándose
*/
bool isActive() const;
/**
* @brief Verifica si hay una notificación activa (visible)
* @return true si hay notificación mostrándose
*/
bool isActive() const;
/**
* @brief Limpia todas las notificaciones pendientes
*/
void clear();
/**
* @brief Limpia todas las notificaciones pendientes
*/
void clear();
private:
SDL_Renderer* renderer_;
TextRenderer* text_renderer_;
ThemeManager* theme_manager_; // Gestor de temas para obtener colores dinámicos con LERP
int window_width_;
int window_height_;
private:
SDL_Renderer* renderer_;
TextRenderer* text_renderer_;
ThemeManager* theme_manager_; // Gestor de temas para obtener colores dinámicos con LERP
int window_width_;
int window_height_;
std::queue<Notification> notification_queue_;
std::unique_ptr<Notification> current_notification_;
std::queue<Notification> notification_queue_;
std::unique_ptr<Notification> current_notification_;
/**
* @brief Procesa la cola y activa la siguiente notificación si es posible
*/
void processQueue();
/**
* @brief Procesa la cola y activa la siguiente notificación si es posible
*/
void processQueue();
/**
* @brief Dibuja el fondo semitransparente de la notificación
*/
void renderBackground(int x, int y, int width, int height, float alpha, SDL_Color bg_color);
/**
* @brief Dibuja el fondo semitransparente de la notificación
*/
void renderBackground(int x, int y, int width, int height, float alpha, SDL_Color bg_color);
};

View File

@@ -1,18 +1,20 @@
#include "ui_manager.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <array>
#include <string>
#include "ball.hpp" // for Ball
#include "defines.hpp" // for TEXT_DURATION, NOTIFICATION_DURATION, AppMode, SimulationMode
#include "engine.hpp" // for Engine (info de sistema)
#include "scene/scene_manager.hpp" // for SceneManager
#include "shapes/shape.hpp" // for Shape
#include "text/textrenderer.hpp" // for TextRenderer
#include "theme_manager.hpp" // for ThemeManager
#include "notifier.hpp" // for Notifier
#include "help_overlay.hpp" // for HelpOverlay
#include "ball.hpp" // for Ball
#include "defines.hpp" // for TEXT_DURATION, NOTIFICATION_DURATION, AppMode, SimulationMode
#include "engine.hpp" // for Engine (info de sistema)
#include "help_overlay.hpp" // for HelpOverlay
#include "notifier.hpp" // for Notifier
#include "scene/scene_manager.hpp" // for SceneManager
#include "shapes/shape.hpp" // for Shape
#include "text/textrenderer.hpp" // for TextRenderer
#include "theme_manager.hpp" // for ThemeManager
// ============================================================================
// HELPER: Obtener viewport en coordenadas físicas (no lógicas)
@@ -20,9 +22,10 @@
// SDL_GetRenderViewport() devuelve coordenadas LÓGICAS cuando hay presentación
// lógica activa. Para obtener coordenadas FÍSICAS, necesitamos deshabilitar
// temporalmente la presentación lógica.
static SDL_Rect getPhysicalViewport(SDL_Renderer* renderer) {
static auto getPhysicalViewport(SDL_Renderer* renderer) -> SDL_Rect {
// Guardar estado actual de presentación lógica
int logical_w = 0, logical_h = 0;
int logical_w = 0;
int logical_h = 0;
SDL_RendererLogicalPresentation presentation_mode;
SDL_GetRenderLogicalPresentation(renderer, &logical_w, &logical_h, &presentation_mode);
@@ -40,23 +43,23 @@ static SDL_Rect getPhysicalViewport(SDL_Renderer* renderer) {
}
UIManager::UIManager()
: text_renderer_debug_(nullptr)
, text_renderer_notifier_(nullptr)
, notifier_(nullptr)
, help_overlay_(nullptr)
, show_debug_(false)
, fps_last_time_(0)
, fps_frame_count_(0)
, fps_current_(0)
, fps_text_("FPS: 0")
, vsync_text_("VSYNC ON")
, renderer_(nullptr)
, theme_manager_(nullptr)
, physical_window_width_(0)
, physical_window_height_(0)
, logical_window_width_(0)
, logical_window_height_(0)
, current_font_size_(18) { // Tamaño por defecto (medium)
: text_renderer_debug_(nullptr),
text_renderer_notifier_(nullptr),
notifier_(nullptr),
help_overlay_(nullptr),
show_debug_(false),
fps_last_time_(0),
fps_frame_count_(0),
fps_current_(0),
fps_text_("FPS: 0"),
vsync_text_("VSYNC ON"),
renderer_(nullptr),
theme_manager_(nullptr),
physical_window_width_(0),
physical_window_height_(0),
logical_window_width_(0),
logical_window_height_(0),
current_font_size_(18) { // Tamaño por defecto (medium)
}
UIManager::~UIManager() {
@@ -67,13 +70,15 @@ UIManager::~UIManager() {
delete help_overlay_;
}
void UIManager::initialize(SDL_Renderer* renderer, ThemeManager* theme_manager,
int physical_width, int physical_height,
int logical_width, int logical_height) {
delete text_renderer_debug_; text_renderer_debug_ = nullptr;
delete text_renderer_notifier_; text_renderer_notifier_ = nullptr;
delete notifier_; notifier_ = nullptr;
delete help_overlay_; help_overlay_ = nullptr;
void UIManager::initialize(SDL_Renderer* renderer, ThemeManager* theme_manager, int physical_width, int physical_height, int logical_width, int logical_height) {
delete text_renderer_debug_;
text_renderer_debug_ = nullptr;
delete text_renderer_notifier_;
text_renderer_notifier_ = nullptr;
delete notifier_;
notifier_ = nullptr;
delete help_overlay_;
help_overlay_ = nullptr;
renderer_ = renderer;
theme_manager_ = theme_manager;
@@ -95,8 +100,7 @@ void UIManager::initialize(SDL_Renderer* renderer, ThemeManager* theme_manager,
// Crear y configurar sistema de notificaciones
notifier_ = new Notifier();
notifier_->init(renderer, text_renderer_notifier_, theme_manager_,
physical_width, physical_height);
notifier_->init(renderer, text_renderer_notifier_, theme_manager_, physical_width, physical_height);
// Crear y configurar sistema de ayuda (overlay)
help_overlay_ = new HelpOverlay();
@@ -123,30 +127,29 @@ void UIManager::update(Uint64 current_time, float delta_time) {
}
void UIManager::render(SDL_Renderer* renderer,
const Engine* engine,
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
const Shape* active_shape,
float shape_convergence,
int physical_width,
int physical_height,
int current_screen_width) {
const Engine* engine,
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
const Shape* active_shape,
float shape_convergence,
int physical_width,
int physical_height,
int current_screen_width) {
// Actualizar dimensiones físicas (puede cambiar en fullscreen)
physical_window_width_ = physical_width;
physical_window_height_ = physical_height;
// Renderizar debug HUD si está activo
if (show_debug_) {
renderDebugHUD(engine, scene_manager, current_mode, current_app_mode,
active_shape, shape_convergence);
renderDebugHUD(engine, scene_manager, current_mode, current_app_mode, active_shape, shape_convergence);
}
// Renderizar notificaciones (siempre al final, sobre todo lo demás)
notifier_->render();
// Renderizar ayuda (siempre última, sobre todo incluso notificaciones)
if (help_overlay_) {
if (help_overlay_ != nullptr) {
help_overlay_->render(renderer);
}
}
@@ -156,7 +159,7 @@ void UIManager::toggleDebug() {
}
void UIManager::toggleHelp() {
if (help_overlay_) {
if (help_overlay_ != nullptr) {
help_overlay_->toggle();
}
}
@@ -190,16 +193,16 @@ void UIManager::updatePhysicalWindowSize(int width, int height, int logical_heig
current_font_size_ = new_font_size;
// Reinicializar text renderers con nuevo tamaño
if (text_renderer_debug_) {
if (text_renderer_debug_ != nullptr) {
text_renderer_debug_->reinitialize(std::max(9, current_font_size_ - 2));
}
if (text_renderer_notifier_) {
if (text_renderer_notifier_ != nullptr) {
text_renderer_notifier_->reinitialize(current_font_size_);
}
}
// Actualizar help overlay con font size actual Y nuevas dimensiones (atómicamente)
if (help_overlay_) {
if (help_overlay_ != nullptr) {
help_overlay_->updateAll(std::max(9, current_font_size_ - 1), width, height);
}
@@ -209,12 +212,12 @@ void UIManager::updatePhysicalWindowSize(int width, int height, int logical_heig
// === Métodos privados ===
void UIManager::renderDebugHUD(const Engine* engine,
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
const Shape* active_shape,
float shape_convergence) {
void UIManager::renderDebugHUD(const Engine* engine, // NOLINT(readability-function-cognitive-complexity)
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
const Shape* active_shape,
float shape_convergence) {
int line_height = text_renderer_debug_->getTextHeight();
int margin = 8;
SDL_Rect physical_viewport = getPhysicalViewport(renderer_);
@@ -236,7 +239,7 @@ void UIManager::renderDebugHUD(const Engine* engine,
if (current_mode == SimulationMode::PHYSICS) {
simmode_text = "SimMode: PHYSICS";
} else if (current_mode == SimulationMode::SHAPE) {
if (active_shape) {
if (active_shape != nullptr) {
simmode_text = std::string("SimMode: SHAPE (") + active_shape->getName() + ")";
} else {
simmode_text = "SimMode: SHAPE";
@@ -246,7 +249,7 @@ void UIManager::renderDebugHUD(const Engine* engine,
}
std::string sprite_name = engine->getCurrentTextureName();
std::transform(sprite_name.begin(), sprite_name.end(), sprite_name.begin(), ::toupper);
std::ranges::transform(sprite_name, sprite_name.begin(), ::toupper);
std::string sprite_text = "Sprite: " + sprite_name;
size_t ball_count = scene_manager->getBallCount();
@@ -256,7 +259,9 @@ void UIManager::renderDebugHUD(const Engine* engine,
std::string formatted;
int digits = static_cast<int>(count_str.length());
for (int i = 0; i < digits; i++) {
if (i > 0 && (digits - i) % 3 == 0) formatted += ',';
if (i > 0 && (digits - i) % 3 == 0) {
formatted += ',';
}
formatted += count_str[i];
}
balls_text = "Balls: " + formatted;
@@ -275,7 +280,9 @@ void UIManager::renderDebugHUD(const Engine* engine,
std::string formatted;
int digits = static_cast<int>(count_str.length());
for (int i = 0; i < digits; i++) {
if (i > 0 && (digits - i) % 3 == 0) formatted += ',';
if (i > 0 && (digits - i) % 3 == 0) {
formatted += ',';
}
formatted += count_str[i];
}
max_auto_text = "Auto max: " + formatted;
@@ -303,9 +310,9 @@ void UIManager::renderDebugHUD(const Engine* engine,
std::string refresh_text;
int num_displays = 0;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
if (displays && num_displays > 0) {
if ((displays != nullptr) && num_displays > 0) {
const auto* dm = SDL_GetCurrentDisplayMode(displays[0]);
if (dm) {
if (dm != nullptr) {
refresh_text = "Refresh: " + std::to_string(static_cast<int>(dm->refresh_rate)) + " Hz";
} else {
refresh_text = "Refresh: N/A";
@@ -322,9 +329,9 @@ void UIManager::renderDebugHUD(const Engine* engine,
int hh = static_cast<int>(total_secs / 3600);
int mm = static_cast<int>((total_secs % 3600) / 60);
int ss = static_cast<int>(total_secs % 60);
char elapsed_buf[32];
SDL_snprintf(elapsed_buf, sizeof(elapsed_buf), "Elapsed: %02d:%02d:%02d", hh, mm, ss);
std::string elapsed_text(elapsed_buf);
std::array<char, 32> elapsed_buf{};
SDL_snprintf(elapsed_buf.data(), elapsed_buf.size(), "Elapsed: %02d:%02d:%02d", hh, mm, ss);
std::string elapsed_text(elapsed_buf.data());
// --- Construir vector de líneas en orden ---
std::vector<std::string> lines;
@@ -344,17 +351,15 @@ void UIManager::renderDebugHUD(const Engine* engine,
if (!engine->isPostFXEnabled()) {
postfx_text = "PostFX: OFF";
} else {
static constexpr const char* preset_names[4] = {
"Vinyeta", "Scanlines", "Cromatica", "Complet"
};
static constexpr std::array<const char*, 4> PRESET_NAMES = {
"Vinyeta",
"Scanlines",
"Cromatica",
"Complet"};
int mode = engine->getPostFXMode();
char buf[64];
SDL_snprintf(buf, sizeof(buf), "PostFX: %s [V:%.2f C:%.2f S:%.2f]",
preset_names[mode],
engine->getPostFXVignette(),
engine->getPostFXChroma(),
engine->getPostFXScanline());
postfx_text = buf;
std::array<char, 64> buf{};
SDL_snprintf(buf.data(), buf.size(), "PostFX: %s [V:%.2f C:%.2f S:%.2f]", PRESET_NAMES[mode], engine->getPostFXVignette(), engine->getPostFXChroma(), engine->getPostFXScanline());
postfx_text = buf.data();
}
lines.push_back(postfx_text);
lines.push_back(elapsed_text);
@@ -366,7 +371,7 @@ void UIManager::renderDebugHUD(const Engine* engine,
SDL_FRect pos = first_ball->getPosition();
lines.push_back("Pos: (" + std::to_string(static_cast<int>(pos.x)) + ", " + std::to_string(static_cast<int>(pos.y)) + ")");
lines.push_back("Gravity: " + std::to_string(static_cast<int>(first_ball->getGravityForce())));
lines.push_back(first_ball->isOnSurface() ? "Surface: YES" : "Surface: NO");
lines.emplace_back(first_ball->isOnSurface() ? "Surface: YES" : "Surface: NO");
lines.push_back("Loss: " + std::to_string(first_ball->getLossCoefficient()).substr(0, 4));
lines.push_back("Dir: " + gravityDirectionToString(static_cast<int>(scene_manager->getCurrentGravity())));
}
@@ -378,29 +383,34 @@ void UIManager::renderDebugHUD(const Engine* engine,
// --- Render con desbordamiento a segunda columna ---
int max_lines = (physical_viewport.h - 2 * margin) / line_height;
if (max_lines < 1) max_lines = 1;
max_lines = std::max(max_lines, 1);
int col_width = physical_viewport.w / 2;
for (int i = 0; i < static_cast<int>(lines.size()); i++) {
int col = i / max_lines;
int row = i % max_lines;
int x = margin + col * col_width;
int y = margin + row * line_height;
int x = margin + (col * col_width);
int y = margin + (row * line_height);
text_renderer_debug_->printAbsoluteShadowed(x, y, lines[i].c_str());
}
}
std::string UIManager::gravityDirectionToString(int direction) const {
auto UIManager::gravityDirectionToString(int direction) -> std::string {
switch (direction) {
case 0: return "Abajo"; // DOWN
case 1: return "Arriba"; // UP
case 2: return "Izquierda"; // LEFT
case 3: return "Derecha"; // RIGHT
default: return "Desconocida";
case 0:
return "Abajo"; // DOWN
case 1:
return "Arriba"; // UP
case 2:
return "Izquierda"; // LEFT
case 3:
return "Derecha"; // RIGHT
default:
return "Desconocida";
}
}
int UIManager::calculateFontSize(int logical_height) const {
auto UIManager::calculateFontSize(int logical_height) -> int {
// Escalado híbrido basado en ALTURA LÓGICA (resolución interna, sin zoom)
// Esto asegura que el tamaño de fuente sea consistente independientemente del zoom de ventana
// - Proporcional en extremos (muy bajo/alto)
@@ -435,8 +445,8 @@ int UIManager::calculateFontSize(int logical_height) const {
}
// Aplicar límites: mínimo 9px, máximo 72px
if (font_size < 9) font_size = 9;
if (font_size > 72) font_size = 72;
font_size = std::max(font_size, 9);
font_size = std::min(font_size, 72);
return font_size;
}

View File

@@ -1,7 +1,8 @@
#pragma once
#include <SDL3/SDL_stdinc.h> // for Uint64
#include <string> // for std::string
#include <string> // for std::string
// Forward declarations
struct SDL_Renderer;
@@ -49,9 +50,7 @@ class UIManager {
* @param logical_width Ancho lógico (resolución interna)
* @param logical_height Alto lógico (resolución interna)
*/
void initialize(SDL_Renderer* renderer, ThemeManager* theme_manager,
int physical_width, int physical_height,
int logical_width, int logical_height);
void initialize(SDL_Renderer* renderer, ThemeManager* theme_manager, int physical_width, int physical_height, int logical_width, int logical_height);
/**
* @brief Actualiza UI (FPS counter, notificaciones, texto obsoleto)
@@ -74,15 +73,15 @@ class UIManager {
* @param current_screen_width Ancho lógico de pantalla (para texto centrado)
*/
void render(SDL_Renderer* renderer,
const Engine* engine,
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
const Shape* active_shape,
float shape_convergence,
int physical_width,
int physical_height,
int current_screen_width);
const Engine* engine,
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
const Shape* active_shape,
float shape_convergence,
int physical_width,
int physical_height,
int current_screen_width);
/**
* @brief Toggle del debug HUD (tecla F12)
@@ -138,31 +137,31 @@ class UIManager {
* @param shape_convergence % de convergencia en LOGO mode
*/
void renderDebugHUD(const Engine* engine,
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
const Shape* active_shape,
float shape_convergence);
const SceneManager* scene_manager,
SimulationMode current_mode,
AppMode current_app_mode,
const Shape* active_shape,
float shape_convergence);
/**
* @brief Convierte dirección de gravedad a string
* @param direction Dirección como int (cast de GravityDirection)
* @return String en español ("Abajo", "Arriba", etc.)
*/
std::string gravityDirectionToString(int direction) const;
static std::string gravityDirectionToString(int direction);
/**
* @brief Calcula tamaño de fuente apropiado según dimensiones lógicas
* @param logical_height Alto lógico (resolución interna, sin zoom)
* @return Tamaño de fuente (9-72px)
*/
int calculateFontSize(int logical_height) const;
static int calculateFontSize(int logical_height);
// === Recursos de renderizado ===
TextRenderer* text_renderer_debug_; // HUD de debug
TextRenderer* text_renderer_notifier_; // Notificaciones
Notifier* notifier_; // Sistema de notificaciones
HelpOverlay* help_overlay_; // Overlay de ayuda (tecla H)
TextRenderer* text_renderer_debug_; // HUD de debug
TextRenderer* text_renderer_notifier_; // Notificaciones
Notifier* notifier_; // Sistema de notificaciones
HelpOverlay* help_overlay_; // Overlay de ayuda (tecla H)
// === Estado de UI ===
bool show_debug_; // HUD de debug activo (tecla F12)
@@ -175,13 +174,13 @@ class UIManager {
std::string vsync_text_; // Texto "V-Sync: On/Off"
// === Referencias externas ===
SDL_Renderer* renderer_; // Renderizador SDL3 (referencia)
ThemeManager* theme_manager_; // Gestor de temas (para colores)
int physical_window_width_; // Ancho físico de ventana (píxeles reales)
int physical_window_height_; // Alto físico de ventana (píxeles reales)
int logical_window_width_; // Ancho lógico (resolución interna)
int logical_window_height_; // Alto lógico (resolución interna)
SDL_Renderer* renderer_; // Renderizador SDL3 (referencia)
ThemeManager* theme_manager_; // Gestor de temas (para colores)
int physical_window_width_; // Ancho físico de ventana (píxeles reales)
int physical_window_height_; // Alto físico de ventana (píxeles reales)
int logical_window_width_; // Ancho lógico (resolución interna)
int logical_window_height_; // Alto lógico (resolución interna)
// === Sistema de escalado dinámico de texto ===
int current_font_size_; // Tamaño de fuente actual (9-72px)
int current_font_size_; // Tamaño de fuente actual (9-72px)
};

View File

@@ -210,4 +210,4 @@ inline float easeOutCirc(float t) {
return sqrtf(1.0f - powf(t - 1.0f, 2.0f));
}
} // namespace Easing
} // namespace Easing