Reemplazar Wave Grid por Lissajous Curve 3D

Cambiar figura "Wave Grid" (malla ondeante) por curva de Lissajous 3D,
con ecuaciones paramétricas más hipnóticas y resultónas visualmente.

## Cambios Principales

**Archivos renombrados:**
- `wave_grid_shape.h/cpp` → `lissajous_shape.h/cpp`
- Clase `WaveGridShape` → `LissajousShape`

**Ecuaciones implementadas:**
- x(t) = A * sin(3t + φx)  - Frecuencia 3 en X
- y(t) = A * sin(2t)       - Frecuencia 2 en Y
- z(t) = A * sin(t + φz)   - Frecuencia 1 en Z
- Ratio 3:2:1 produce patrón de "trenza elegante"

**Animación:**
- Rotación global dual (ejes X/Y)
- Animación de fase continua (morphing)
- Más dinámica y orgánica que Wave Grid

**defines.h:**
- `WAVE_GRID_*` → `LISSAJOUS_*` constantes
- `ShapeType::WAVE_GRID` → `ShapeType::LISSAJOUS`

**engine.cpp:**
- Actualizado include y instanciación
- Arrays de figuras DEMO actualizados
- Tecla W ahora activa Lissajous

## Resultado

Curva 3D paramétrica hipnótica con patrón entrelazado,
rotación continua y morphing de fase. Más espectacular
que el grid ondeante anterior. 🌀

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-07 12:31:38 +02:00
parent c55d6de687
commit 6cb3c2eef9
8 changed files with 233 additions and 232 deletions

View File

@@ -30,10 +30,10 @@
#include "shapes/cylinder_shape.h" // for CylinderShape
#include "shapes/helix_shape.h" // for HelixShape
#include "shapes/icosahedron_shape.h" // for IcosahedronShape
#include "shapes/lissajous_shape.h" // for LissajousShape
#include "shapes/png_shape.h" // for PNGShape
#include "shapes/sphere_shape.h" // for SphereShape
#include "shapes/torus_shape.h" // for TorusShape
#include "shapes/wave_grid_shape.h" // for WaveGridShape
// getExecutableDirectory() ya está definido en defines.h como inline
@@ -48,8 +48,8 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
}
int num_displays = 0;
SDL_DisplayID *displays = SDL_GetDisplays(&num_displays);
const auto *dm = (displays && num_displays > 0) ? SDL_GetCurrentDisplayMode(displays[0]) : nullptr;
SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
const auto* dm = (displays && num_displays > 0) ? SDL_GetCurrentDisplayMode(displays[0]) : nullptr;
int screen_w = dm ? dm->w : 1920; // Fallback si falla
int screen_h = dm ? dm->h - WINDOW_DECORATION_HEIGHT : 1080;
@@ -132,9 +132,9 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
std::string balls_dir = resources_dir + "/data/balls";
struct TextureInfo {
std::string name;
std::shared_ptr<Texture> texture;
int width;
std::string name;
std::shared_ptr<Texture> texture;
int width;
};
std::vector<TextureInfo> texture_files;
@@ -162,7 +162,7 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
// Filtrar solo los recursos en balls/ con extensión .png
for (const auto& resource : pack_resources) {
if (resource.substr(0, 6) == "balls/" && resource.substr(resource.size() - 4) == ".png") {
std::string tex_name = resource.substr(6); // Quitar "balls/"
std::string tex_name = resource.substr(6); // Quitar "balls/"
std::string name = tex_name.substr(0, tex_name.find('.')); // Quitar extensión
auto texture = std::make_shared<Texture>(renderer_, resource);
@@ -175,10 +175,9 @@ bool Engine::initialize(int width, int height, int zoom, bool fullscreen) {
}
// Ordenar por tamaño (grande → pequeño): big(16) → normal(10) → small(6) → tiny(4)
std::sort(texture_files.begin(), texture_files.end(),
[](const TextureInfo& a, const TextureInfo& b) {
return a.width > b.width; // Descendente por tamaño
});
std::sort(texture_files.begin(), texture_files.end(), [](const TextureInfo& a, const TextureInfo& b) {
return a.width > b.width; // Descendente por tamaño
});
// Guardar texturas ya cargadas en orden (0=big, 1=normal, 2=small, 3=tiny)
for (const auto& info : texture_files) {
@@ -405,7 +404,7 @@ void Engine::handleEvents() {
break;
case SDLK_W:
activateShape(ShapeType::WAVE_GRID);
activateShape(ShapeType::LISSAJOUS);
break;
case SDLK_E:
@@ -1461,41 +1460,54 @@ void Engine::initializeDynamicThemes() {
dynamic_themes_[0] = {
"SUNRISE",
"AMANECER",
255, 200, 100, // Color texto: amarillo cálido
255,
200,
100, // Color texto: amarillo cálido
{
// Keyframe 0: Noche oscura (estado inicial)
{
20.0f / 255.0f, 25.0f / 255.0f, 60.0f / 255.0f, // Fondo superior: azul medianoche
10.0f / 255.0f, 10.0f / 255.0f, 30.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{100, 100, 150}, {120, 120, 170}, {90, 90, 140}, {110, 110, 160},
{95, 95, 145}, {105, 105, 155}, {100, 100, 150}, {115, 115, 165}}, // Pelotas azules tenues
0.0f // Sin transición (estado inicial)
20.0f / 255.0f,
25.0f / 255.0f,
60.0f / 255.0f, // Fondo superior: azul medianoche
10.0f / 255.0f,
10.0f / 255.0f,
30.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{100, 100, 150}, {120, 120, 170}, {90, 90, 140}, {110, 110, 160}, {95, 95, 145}, {105, 105, 155}, {100, 100, 150}, {115, 115, 165}}, // Pelotas azules tenues
0.0f // Sin transición (estado inicial)
},
// Keyframe 1: Alba naranja-rosa
{
180.0f / 255.0f, 100.0f / 255.0f, 120.0f / 255.0f, // Fondo superior: naranja-rosa
255.0f / 255.0f, 140.0f / 255.0f, 100.0f / 255.0f, // Fondo inferior: naranja cálido
{{255, 180, 100}, {255, 160, 80}, {255, 200, 120}, {255, 150, 90},
{255, 190, 110}, {255, 170, 95}, {255, 185, 105}, {255, 165, 88}}, // Pelotas naranjas
4.0f // 4 segundos para llegar aquí
180.0f / 255.0f,
100.0f / 255.0f,
120.0f / 255.0f, // Fondo superior: naranja-rosa
255.0f / 255.0f,
140.0f / 255.0f,
100.0f / 255.0f, // Fondo inferior: naranja cálido
{{255, 180, 100}, {255, 160, 80}, {255, 200, 120}, {255, 150, 90}, {255, 190, 110}, {255, 170, 95}, {255, 185, 105}, {255, 165, 88}}, // Pelotas naranjas
4.0f // 4 segundos para llegar aquí
},
// Keyframe 2: Día brillante amarillo
{
255.0f / 255.0f, 240.0f / 255.0f, 180.0f / 255.0f, // Fondo superior: amarillo claro
255.0f / 255.0f, 255.0f / 255.0f, 220.0f / 255.0f, // Fondo inferior: amarillo muy claro
{{255, 255, 200}, {255, 255, 180}, {255, 255, 220}, {255, 255, 190},
{255, 255, 210}, {255, 255, 185}, {255, 255, 205}, {255, 255, 195}}, // Pelotas amarillas brillantes
3.0f // 3 segundos para llegar aquí
255.0f / 255.0f,
240.0f / 255.0f,
180.0f / 255.0f, // Fondo superior: amarillo claro
255.0f / 255.0f,
255.0f / 255.0f,
220.0f / 255.0f, // Fondo inferior: amarillo muy claro
{{255, 255, 200}, {255, 255, 180}, {255, 255, 220}, {255, 255, 190}, {255, 255, 210}, {255, 255, 185}, {255, 255, 205}, {255, 255, 195}}, // Pelotas amarillas brillantes
3.0f // 3 segundos para llegar aquí
},
// Keyframe 3: Vuelta a noche (para loop)
{
20.0f / 255.0f, 25.0f / 255.0f, 60.0f / 255.0f, // Fondo superior: azul medianoche
10.0f / 255.0f, 10.0f / 255.0f, 30.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{100, 100, 150}, {120, 120, 170}, {90, 90, 140}, {110, 110, 160},
{95, 95, 145}, {105, 105, 155}, {100, 100, 150}, {115, 115, 165}}, // Pelotas azules tenues
5.0f // 5 segundos para volver a noche
}
},
20.0f / 255.0f,
25.0f / 255.0f,
60.0f / 255.0f, // Fondo superior: azul medianoche
10.0f / 255.0f,
10.0f / 255.0f,
30.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{100, 100, 150}, {120, 120, 170}, {90, 90, 140}, {110, 110, 160}, {95, 95, 145}, {105, 105, 155}, {100, 100, 150}, {115, 115, 165}}, // Pelotas azules tenues
5.0f // 5 segundos para volver a noche
}},
true // Loop = true
};
@@ -1505,33 +1517,43 @@ void Engine::initializeDynamicThemes() {
dynamic_themes_[1] = {
"OCEAN WAVES",
"OLAS OCEANICAS",
100, 220, 255, // Color texto: cian claro
100,
220,
255, // Color texto: cian claro
{
// Keyframe 0: Profundidad oceánica (azul oscuro)
{
20.0f / 255.0f, 50.0f / 255.0f, 100.0f / 255.0f, // Fondo superior: azul marino
10.0f / 255.0f, 30.0f / 255.0f, 60.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{60, 100, 180}, {50, 90, 170}, {70, 110, 190}, {55, 95, 175},
{65, 105, 185}, {58, 98, 172}, {62, 102, 182}, {52, 92, 168}}, // Pelotas azul oscuro
0.0f // Estado inicial
20.0f / 255.0f,
50.0f / 255.0f,
100.0f / 255.0f, // Fondo superior: azul marino
10.0f / 255.0f,
30.0f / 255.0f,
60.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{60, 100, 180}, {50, 90, 170}, {70, 110, 190}, {55, 95, 175}, {65, 105, 185}, {58, 98, 172}, {62, 102, 182}, {52, 92, 168}}, // Pelotas azul oscuro
0.0f // Estado inicial
},
// Keyframe 1: Aguas poco profundas (turquesa brillante)
{
100.0f / 255.0f, 200.0f / 255.0f, 230.0f / 255.0f, // Fondo superior: turquesa claro
50.0f / 255.0f, 150.0f / 255.0f, 200.0f / 255.0f, // Fondo inferior: turquesa medio
{{100, 220, 255}, {90, 210, 245}, {110, 230, 255}, {95, 215, 250},
{105, 225, 255}, {98, 218, 248}, {102, 222, 252}, {92, 212, 242}}, // Pelotas turquesa brillante
4.0f // 4 segundos para llegar
100.0f / 255.0f,
200.0f / 255.0f,
230.0f / 255.0f, // Fondo superior: turquesa claro
50.0f / 255.0f,
150.0f / 255.0f,
200.0f / 255.0f, // Fondo inferior: turquesa medio
{{100, 220, 255}, {90, 210, 245}, {110, 230, 255}, {95, 215, 250}, {105, 225, 255}, {98, 218, 248}, {102, 222, 252}, {92, 212, 242}}, // Pelotas turquesa brillante
4.0f // 4 segundos para llegar
},
// Keyframe 2: Vuelta a profundidad (para loop)
{
20.0f / 255.0f, 50.0f / 255.0f, 100.0f / 255.0f, // Fondo superior: azul marino
10.0f / 255.0f, 30.0f / 255.0f, 60.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{60, 100, 180}, {50, 90, 170}, {70, 110, 190}, {55, 95, 175},
{65, 105, 185}, {58, 98, 172}, {62, 102, 182}, {52, 92, 168}}, // Pelotas azul oscuro
4.0f // 4 segundos para volver
}
},
20.0f / 255.0f,
50.0f / 255.0f,
100.0f / 255.0f, // Fondo superior: azul marino
10.0f / 255.0f,
30.0f / 255.0f,
60.0f / 255.0f, // Fondo inferior: azul muy oscuro
{{60, 100, 180}, {50, 90, 170}, {70, 110, 190}, {55, 95, 175}, {65, 105, 185}, {58, 98, 172}, {62, 102, 182}, {52, 92, 168}}, // Pelotas azul oscuro
4.0f // 4 segundos para volver
}},
true // Loop = true
};
@@ -1541,33 +1563,43 @@ void Engine::initializeDynamicThemes() {
dynamic_themes_[2] = {
"NEON PULSE",
"PULSO NEON",
255, 60, 255, // Color texto: magenta brillante
255,
60,
255, // Color texto: magenta brillante
{
// Keyframe 0: Apagado (negro)
{
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo superior: negro
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior: negro
{{40, 40, 40}, {50, 50, 50}, {45, 45, 45}, {48, 48, 48},
{42, 42, 42}, {47, 47, 47}, {44, 44, 44}, {46, 46, 46}}, // Pelotas grises muy oscuras
0.0f // Estado inicial
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo superior: negro
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo inferior: negro
{{40, 40, 40}, {50, 50, 50}, {45, 45, 45}, {48, 48, 48}, {42, 42, 42}, {47, 47, 47}, {44, 44, 44}, {46, 46, 46}}, // Pelotas grises muy oscuras
0.0f // Estado inicial
},
// Keyframe 1: Encendido (neón cian-magenta)
{
20.0f / 255.0f, 20.0f / 255.0f, 40.0f / 255.0f, // Fondo superior: azul oscuro
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior: negro
{{0, 255, 255}, {255, 0, 255}, {0, 255, 200}, {255, 50, 255},
{50, 255, 255}, {255, 0, 200}, {0, 255, 230}, {255, 80, 255}}, // Pelotas neón vibrante
1.5f // 1.5 segundos para encender (rápido)
20.0f / 255.0f,
20.0f / 255.0f,
40.0f / 255.0f, // Fondo superior: azul oscuro
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo inferior: negro
{{0, 255, 255}, {255, 0, 255}, {0, 255, 200}, {255, 50, 255}, {50, 255, 255}, {255, 0, 200}, {0, 255, 230}, {255, 80, 255}}, // Pelotas neón vibrante
1.5f // 1.5 segundos para encender (rápido)
},
// Keyframe 2: Vuelta a apagado (para loop ping-pong)
{
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo superior: negro
0.0f / 255.0f, 0.0f / 255.0f, 0.0f / 255.0f, // Fondo inferior: negro
{{40, 40, 40}, {50, 50, 50}, {45, 45, 45}, {48, 48, 48},
{42, 42, 42}, {47, 47, 47}, {44, 44, 44}, {46, 46, 46}}, // Pelotas grises muy oscuras
1.5f // 1.5 segundos para apagar
}
},
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo superior: negro
0.0f / 255.0f,
0.0f / 255.0f,
0.0f / 255.0f, // Fondo inferior: negro
{{40, 40, 40}, {50, 50, 50}, {45, 45, 45}, {48, 48, 48}, {42, 42, 42}, {47, 47, 47}, {44, 44, 44}, {46, 46, 46}}, // Pelotas grises muy oscuras
1.5f // 1.5 segundos para apagar
}},
true // Loop = true
};
}
@@ -1641,7 +1673,7 @@ void Engine::updateDynamicTheme() {
target_keyframe_index_ = 0; // Volver al inicio
} else {
target_keyframe_index_ = theme.keyframes.size() - 1; // Quedarse en el último
dynamic_theme_active_ = false; // Detener animación
dynamic_theme_active_ = false; // Detener animación
}
}
@@ -1925,7 +1957,7 @@ void Engine::performDemoAction(bool is_lite) {
// Activar figura 3D (25%) - PNG_SHAPE excluido (reservado para Logo Mode)
accumulated_weight += DEMO_LITE_WEIGHT_SHAPE;
if (random_value < accumulated_weight) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
int shape_index = rand() % 8;
activateShape(shapes[shape_index]);
return;
@@ -1968,7 +2000,7 @@ void Engine::performDemoAction(bool is_lite) {
// Activar figura 3D (20%) - PNG_SHAPE excluido (reservado para Logo Mode)
accumulated_weight += DEMO_WEIGHT_SHAPE;
if (random_value < accumulated_weight) {
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
int shape_index = rand() % 8;
activateShape(shapes[shape_index]);
return;
@@ -2063,7 +2095,7 @@ void Engine::randomizeOnDemoStart(bool is_lite) {
}
} else {
// Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial)
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
activateShape(shapes[rand() % 8]);
}
@@ -2099,7 +2131,7 @@ void Engine::randomizeOnDemoStart(bool is_lite) {
}
} else {
// Modo figura: elegir figura aleatoria (excluir PNG_SHAPE - es logo especial)
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::WAVE_GRID, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
ShapeType shapes[] = {ShapeType::SPHERE, ShapeType::LISSAJOUS, ShapeType::HELIX, ShapeType::TORUS, ShapeType::CUBE, ShapeType::CYLINDER, ShapeType::ICOSAHEDRON, ShapeType::ATOM};
activateShape(shapes[rand() % 8]);
// 5. Profundidad (solo si estamos en figura)
@@ -2403,8 +2435,8 @@ void Engine::activateShape(ShapeType type) {
case ShapeType::TORUS:
active_shape_ = std::make_unique<TorusShape>();
break;
case ShapeType::WAVE_GRID:
active_shape_ = std::make_unique<WaveGridShape>();
case ShapeType::LISSAJOUS:
active_shape_ = std::make_unique<LissajousShape>();
break;
case ShapeType::CYLINDER:
active_shape_ = std::make_unique<CylinderShape>();