Preparando la interfaz

This commit is contained in:
Clinv committed 2026-09-29 23:35:03 -03:00
commit 66bedbb5f1
17 files changed
+876

No files matched your search

+25
View File
@@ -0,0 +1,25 @@
#pragma once
// Constantes globales de Tahgibu. Sin dependencias (ni raylib).
// CMake define TAHGIBU_VERSION a partir de project(VERSION ...).
#ifndef TAHGIBU_VERSION
#define TAHGIBU_VERSION "2.0.0"
#endif
namespace tahgibu {
inline constexpr const char* kAppName = "Tahgibu";
inline constexpr const char* kAppVersion = TAHGIBU_VERSION;
// Límite duro: ni el proyecto ni ningún frame individual pueden superarlo.
inline constexpr float kMaxFps = 60.0f;
// Mínimo permitido. 0.1 FPS = un frame visible hasta 10 s, para poder hacer
// pausas largas sin duplicar frames.
inline constexpr float kMinFps = 0.1f;
// FPS base de un proyecto nuevo.
inline constexpr float kDefaultFps = 24.0f;
} // namespace tahgibu
+28
View File
@@ -0,0 +1,28 @@
#include "core/Frame.hpp"
#include <algorithm>
#include <cmath>
namespace tahgibu {
float Frame::clampFps(float fps) noexcept {
if (!std::isfinite(fps)) {
return kDefaultFps;
}
return std::clamp(fps, kMinFps, kMaxFps);
}
void Frame::setFpsOverride(float fps) noexcept {
fpsOverride_ = clampFps(fps);
}
float Frame::effectiveFps(float baseFps) const noexcept {
return fpsOverride_ ? *fpsOverride_ : clampFps(baseFps);
}
double Frame::durationSeconds(float baseFps) const noexcept {
// effectiveFps() >= kMinFps, así que nunca se divide por cero.
return 1.0 / static_cast<double>(effectiveFps(baseFps));
}
} // namespace tahgibu
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <optional>
#include "core/Constants.hpp"
namespace tahgibu {
// Un frame de la animación. Solo guarda cuánto dura: usa los FPS base del
// proyecto, salvo que tenga FPS propios (override).
class Frame {
public:
// Fuerza el valor al rango [kMinFps, kMaxFps]. Valores no finitos (NaN, inf)
// devuelven kDefaultFps.
static float clampFps(float fps) noexcept;
bool hasFpsOverride() const noexcept { return fpsOverride_.has_value(); }
void setFpsOverride(float fps) noexcept; // aplica clampFps
void clearFpsOverride() noexcept { fpsOverride_.reset(); }
float effectiveFps(float baseFps) const noexcept;
double durationSeconds(float baseFps) const noexcept;
private:
std::optional<float> fpsOverride_;
};
} // namespace tahgibu
+30
View File
@@ -0,0 +1,30 @@
#include "core/Project.hpp"
namespace tahgibu {
Project::Project() : name_("Sin título") {
frames_.emplace_back();
}
void Project::setBaseFps(float fps) noexcept {
baseFps_ = Frame::clampFps(fps);
}
std::size_t Project::addFrame() {
frames_.emplace_back();
return frames_.size() - 1;
}
double Project::frameDurationSeconds(std::size_t index) const {
return frames_.at(index).durationSeconds(baseFps_);
}
double Project::totalDurationSeconds() const {
double total = 0.0;
for (const Frame& frame : frames_) {
total += frame.durationSeconds(baseFps_);
}
return total;
}
} // namespace tahgibu
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <cstddef>
#include <string>
#include <vector>
#include "core/Frame.hpp"
namespace tahgibu {
// Modelo de datos del proyecto. No conoce raylib ni la interfaz.
class Project {
public:
Project(); // 1280x720, FPS base kDefaultFps, un frame inicial
const std::string& name() const noexcept { return name_; }
int width() const noexcept { return width_; }
int height() const noexcept { return height_; }
float baseFps() const noexcept { return baseFps_; }
void setBaseFps(float fps) noexcept; // aplica Frame::clampFps
std::size_t frameCount() const noexcept { return frames_.size(); }
const Frame& frame(std::size_t index) const { return frames_.at(index); }
Frame& frame(std::size_t index) { return frames_.at(index); }
std::size_t addFrame(); // devuelve el índice del frame nuevo
double frameDurationSeconds(std::size_t index) const;
double totalDurationSeconds() const;
private:
std::string name_;
int width_ = 1280;
int height_ = 720;
float baseFps_ = kDefaultFps;
std::vector<Frame> frames_;
};
} // namespace tahgibu
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <cstddef>
#include "editor/Tool.hpp"
namespace tahgibu {
// Estado del editor que NO forma parte del proyecto guardado.
struct EditorState {
Tool activeTool = Tool::Select;
std::size_t currentFrame = 0;
};
} // namespace tahgibu
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <cstddef>
namespace tahgibu {
enum class Tool {
Select,
Move,
Scale,
Rotate,
Transform,
Vertices,
Draw,
Eraser,
Sprite,
ColorPicker,
Zoom,
Pan,
};
struct ToolInfo {
Tool id;
const char* label;
};
// Registro de herramientas. La barra superior se genera a partir de esta
// tabla: agregar una herramienta nueva = agregar un valor al enum y una línea aquí.
inline constexpr ToolInfo kToolInfos[] = {
{Tool::Select, "Seleccionar"},
{Tool::Move, "Mover"},
{Tool::Scale, "Escalar"},
{Tool::Rotate, "Rotar"},
{Tool::Transform, "Transformar"},
{Tool::Vertices, "Vértices"},
{Tool::Draw, "Dibujar"},
{Tool::Eraser, "Borrador"},
{Tool::Sprite, "Sprite"},
{Tool::ColorPicker, "Color"},
{Tool::Zoom, "Zoom"},
{Tool::Pan, "Mano"},
};
inline constexpr std::size_t kToolCount = sizeof(kToolInfos) / sizeof(kToolInfos[0]);
inline const char* toolLabel(Tool tool) noexcept {
for (const ToolInfo& info : kToolInfos) {
if (info.id == tool) {
return info.label;
}
}
return "?";
}
} // namespace tahgibu
+14
View File
@@ -0,0 +1,14 @@
#include <cstdio>
#include <exception>
#include "ui/Application.hpp"
int main() {
try {
tahgibu::Application app;
return app.run();
} catch (const std::exception& error) {
std::fprintf(stderr, "Error fatal: %s\n", error.what());
return 1;
}
}
+242
View File
@@ -0,0 +1,242 @@
#include "ui/Application.hpp"
#include <raylib.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdio>
#include <string>
#include "core/Constants.hpp"
#include "editor/Tool.hpp"
#include "ui/Theme.hpp"
namespace tahgibu {
namespace {
constexpr int kInitialWidth = 1280;
constexpr int kInitialHeight = 800;
constexpr int kMinWidth = 1000;
constexpr int kMinHeight = 640;
constexpr int kUiTargetFps = 60; // FPS de la interfaz; no tiene relación con los FPS de la animación
constexpr int kFontSize = 16;
constexpr int kSmallFontSize = 14;
constexpr int kButtonPadding = 10;
constexpr float kButtonGap = 4.0f;
constexpr float kPanelHeaderHeight = 24.0f;
constexpr std::array<const char*, 7> kMenus = {
"Archivo", "Editar", "Animación", "Sprite", "Audio", "Ver", "Lua"};
int px(float value) {
return static_cast<int>(value);
}
void drawTextVCentered(const char* text, float x, const Rectangle& area, int size, Color color) {
DrawText(text, px(x), px(area.y + (area.height - static_cast<float>(size)) / 2.0f), size, color);
}
void drawPanelHeader(const Rectangle& panel, const char* title) {
DrawRectangle(px(panel.x), px(panel.y), px(panel.width), px(kPanelHeaderHeight), theme::kPanelHeader);
DrawText(title, px(panel.x + 12.0f), px(panel.y + 4.0f), kFontSize, theme::kText);
}
std::array<Rectangle, kToolCount> computeToolButtons(const Rectangle& bar) {
std::array<Rectangle, kToolCount> rects{};
float x = bar.x + 8.0f;
for (std::size_t i = 0; i < kToolCount; ++i) {
const float w = static_cast<float>(MeasureText(kToolInfos[i].label, kFontSize) + 2 * kButtonPadding);
rects[i] = {x, bar.y + 6.0f, w, bar.height - 12.0f};
x += w + kButtonGap;
}
return rects;
}
std::array<Rectangle, kMenus.size()> computeMenuRects(const Rectangle& bar) {
std::array<Rectangle, kMenus.size()> rects{};
float x = bar.x + 6.0f;
for (std::size_t i = 0; i < kMenus.size(); ++i) {
const float w = static_cast<float>(MeasureText(kMenus[i], kFontSize) + 2 * kButtonPadding);
rects[i] = {x, bar.y + 2.0f, w, bar.height - 4.0f};
x += w + 2.0f;
}
return rects;
}
} // namespace
Application::Application() {
SetConfigFlags(FLAG_WINDOW_RESIZABLE);
const std::string title = std::string(kAppName) + " " + kAppVersion;
InitWindow(kInitialWidth, kInitialHeight, title.c_str());
if (!IsWindowReady()) {
return;
}
SetWindowMinSize(kMinWidth, kMinHeight);
SetExitKey(KEY_NULL); // Esc no debe cerrar el editor
SetTargetFPS(kUiTargetFps);
layout_ = Layout::compute(GetScreenWidth(), GetScreenHeight());
}
Application::~Application() {
if (IsWindowReady()) {
CloseWindow();
}
}
int Application::run() {
if (!IsWindowReady()) {
std::fprintf(stderr, "No se pudo crear la ventana (¿hay entorno gráfico y OpenGL disponible?).\n");
return 1;
}
while (!WindowShouldClose()) {
update();
draw();
}
return 0;
}
void Application::update() {
layout_ = Layout::compute(GetScreenWidth(), GetScreenHeight());
if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) {
const Vector2 mouse = GetMousePosition();
const auto rects = computeToolButtons(layout_.toolBar);
for (std::size_t i = 0; i < kToolCount; ++i) {
if (CheckCollisionPointRec(mouse, rects[i])) {
state_.activeTool = kToolInfos[i].id;
break;
}
}
}
}
void Application::draw() const {
BeginDrawing();
ClearBackground(theme::kWindowBg);
drawCanvas();
drawProperties();
drawTimeline();
drawToolBar();
drawMenuBar();
EndDrawing();
}
void Application::drawMenuBar() const {
const Rectangle& bar = layout_.menuBar;
DrawRectangleRec(bar, theme::kPanelHeader);
const Vector2 mouse = GetMousePosition();
const auto rects = computeMenuRects(bar);
for (std::size_t i = 0; i < kMenus.size(); ++i) {
if (CheckCollisionPointRec(mouse, rects[i])) {
DrawRectangleRec(rects[i], theme::kHover);
}
drawTextVCentered(kMenus[i], rects[i].x + static_cast<float>(kButtonPadding), rects[i], kFontSize,
theme::kText);
}
}
void Application::drawToolBar() const {
const Rectangle& bar = layout_.toolBar;
DrawRectangleRec(bar, theme::kPanelBg);
DrawRectangle(px(bar.x), px(bar.y + bar.height - 1.0f), px(bar.width), 1, theme::kBorder);
const Vector2 mouse = GetMousePosition();
const auto rects = computeToolButtons(bar);
for (std::size_t i = 0; i < kToolCount; ++i) {
const bool active = state_.activeTool == kToolInfos[i].id;
const bool hover = CheckCollisionPointRec(mouse, rects[i]);
const Color background = active ? theme::kAccent : (hover ? theme::kHover : theme::kPanelHeader);
DrawRectangleRec(rects[i], background);
drawTextVCentered(kToolInfos[i].label, rects[i].x + static_cast<float>(kButtonPadding), rects[i],
kFontSize, active ? theme::kWindowBg : theme::kText);
}
}
void Application::drawCanvas() const {
const Rectangle& area = layout_.canvas;
DrawRectangleRec(area, theme::kCanvasBg);
// Escenario del tamaño del proyecto, ajustado al área disponible.
constexpr float kMargin = 32.0f;
const float projectW = static_cast<float>(project_.width());
const float projectH = static_cast<float>(project_.height());
const float availableW = std::max(1.0f, area.width - 2.0f * kMargin);
const float availableH = std::max(1.0f, area.height - 2.0f * kMargin);
const float scale = std::min(availableW / projectW, availableH / projectH);
const float w = projectW * scale;
const float h = projectH * scale;
const Rectangle stage{area.x + (area.width - w) / 2.0f, area.y + (area.height - h) / 2.0f, w, h};
DrawRectangleRec(stage, theme::kStage);
DrawRectangleLinesEx(stage, 1.0f, theme::kBorder);
char caption[96];
std::snprintf(caption, sizeof caption, "Escenario %d x %d", project_.width(), project_.height());
DrawText(caption, px(area.x + 10.0f), px(area.y + 8.0f), kSmallFontSize, theme::kTextDim);
}
void Application::drawProperties() const {
const Rectangle& panel = layout_.properties;
DrawRectangleRec(panel, theme::kPanelBg);
drawPanelHeader(panel, "Propiedades");
DrawRectangle(px(panel.x), px(panel.y), 1, px(panel.height), theme::kBorder);
const std::size_t index = std::min(state_.currentFrame, project_.frameCount() - 1);
const Frame& frame = project_.frame(index);
float y = panel.y + kPanelHeaderHeight + 12.0f;
const float x = panel.x + 12.0f;
char line[128];
auto put = [&](Color color) {
DrawText(line, px(x), px(y), kFontSize, color);
y += 24.0f;
};
std::snprintf(line, sizeof line, "Proyecto: %s", project_.name().c_str());
put(theme::kText);
std::snprintf(line, sizeof line, "Resolución: %d x %d", project_.width(), project_.height());
put(theme::kText);
std::snprintf(line, sizeof line, "FPS base: %.1f (máx. %.0f)", static_cast<double>(project_.baseFps()),
static_cast<double>(kMaxFps));
put(theme::kText);
std::snprintf(line, sizeof line, "Frame: %zu / %zu", index + 1, project_.frameCount());
put(theme::kText);
std::snprintf(line, sizeof line, "FPS del frame: %.1f %s",
static_cast<double>(frame.effectiveFps(project_.baseFps())),
frame.hasFpsOverride() ? "(propio)" : "(base)");
put(theme::kText);
std::snprintf(line, sizeof line, "Herramienta: %s", toolLabel(state_.activeTool));
put(theme::kText);
y += 8.0f;
std::snprintf(line, sizeof line, "Sprites y vértices: etapas 4 y 8");
put(theme::kTextDim);
}
void Application::drawTimeline() const {
const Rectangle& panel = layout_.timeline;
DrawRectangleRec(panel, theme::kPanelBg);
drawPanelHeader(panel, "Timeline");
DrawRectangle(px(panel.x), px(panel.y), px(panel.width), 1, theme::kBorder);
constexpr std::array<const char*, 3> kTracks = {"Fondo", "Sprite", "Audio"};
constexpr float kLaneHeight = 32.0f;
constexpr float kLabelWidth = 90.0f;
float y = panel.y + kPanelHeaderHeight + 12.0f;
for (const char* name : kTracks) {
DrawText(name, px(panel.x + 12.0f), px(y + (kLaneHeight - static_cast<float>(kFontSize)) / 2.0f),
kFontSize, theme::kText);
const Rectangle lane{panel.x + kLabelWidth, y, panel.width - kLabelWidth - 12.0f, kLaneHeight - 4.0f};
DrawRectangleRec(lane, theme::kPanelHeader);
y += kLaneHeight;
}
DrawText("La timeline se implementa en la etapa 3", px(panel.x + 12.0f), px(y + 8.0f), kSmallFontSize,
theme::kTextDim);
}
} // namespace tahgibu
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include "core/Project.hpp"
#include "editor/EditorState.hpp"
#include "ui/Layout.hpp"
namespace tahgibu {
// Ventana principal: crea la ventana de raylib, ejecuta el bucle y dibuja las zonas.
// La lógica del programa vive en core/ y editor/, no aquí.
class Application {
public:
Application();
~Application();
Application(const Application&) = delete;
Application& operator=(const Application&) = delete;
// Devuelve el código de salida del proceso.
int run();
private:
void update();
void draw() const;
void drawMenuBar() const;
void drawToolBar() const;
void drawCanvas() const;
void drawProperties() const;
void drawTimeline() const;
Project project_;
EditorState state_;
Layout layout_;
};
} // namespace tahgibu
+35
View File
@@ -0,0 +1,35 @@
#include "ui/Layout.hpp"
#include <algorithm>
namespace tahgibu {
namespace {
constexpr float kMenuBarHeight = 26.0f;
constexpr float kToolBarHeight = 40.0f;
constexpr float kPropertiesWidth = 280.0f;
constexpr float kTimelineHeight = 200.0f;
constexpr float kMinCanvasWidth = 400.0f;
constexpr float kMinCanvasHeight = 200.0f;
} // namespace
Layout Layout::compute(int windowWidth, int windowHeight) {
const float w = static_cast<float>(windowWidth);
const float h = static_cast<float>(windowHeight);
const float top = kMenuBarHeight + kToolBarHeight;
const float timelineH = std::min(kTimelineHeight, std::max(0.0f, h - top - kMinCanvasHeight));
const float propertiesW = std::min(kPropertiesWidth, std::max(0.0f, w - kMinCanvasWidth));
const float middleH = std::max(0.0f, h - top - timelineH);
Layout layout;
layout.menuBar = {0.0f, 0.0f, w, kMenuBarHeight};
layout.toolBar = {0.0f, kMenuBarHeight, w, kToolBarHeight};
layout.canvas = {0.0f, top, w - propertiesW, middleH};
layout.properties = {w - propertiesW, top, propertiesW, middleH};
layout.timeline = {0.0f, top + middleH, w, timelineH};
return layout;
}
} // namespace tahgibu
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <raylib.h>
namespace tahgibu {
// Reparto de la ventana en zonas. Función pura del tamaño de la ventana.
struct Layout {
Rectangle menuBar{};
Rectangle toolBar{};
Rectangle canvas{};
Rectangle properties{};
Rectangle timeline{};
static Layout compute(int windowWidth, int windowHeight);
};
} // namespace tahgibu
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <raylib.h>
namespace tahgibu::theme {
inline constexpr Color kWindowBg{24, 24, 28, 255};
inline constexpr Color kPanelBg{34, 34, 40, 255};
inline constexpr Color kPanelHeader{44, 44, 52, 255};
inline constexpr Color kBorder{14, 14, 18, 255};
inline constexpr Color kHover{62, 62, 74, 255};
inline constexpr Color kText{225, 225, 230, 255};
inline constexpr Color kTextDim{140, 140, 152, 255};
inline constexpr Color kAccent{255, 140, 60, 255};
inline constexpr Color kCanvasBg{18, 18, 22, 255};
inline constexpr Color kStage{245, 245, 245, 255};
} // namespace tahgibu::theme