diff --git a/cpp/functions/core/bezier_editor.cpp b/cpp/functions/core/bezier_editor.cpp new file mode 100644 index 00000000..4bf3b238 --- /dev/null +++ b/cpp/functions/core/bezier_editor.cpp @@ -0,0 +1,204 @@ +#include "core/bezier_editor.h" +#include "core/tokens.h" + +#include +#include + +#include +#include + +namespace fn { + +// --------------------------------------------------------------------------- +// Pure evaluation +// --------------------------------------------------------------------------- + +ImVec2 bezier_point(const BezierCurve& c, float u) { + // De Casteljau cubica: 3 niveles de lerp. + auto lerp2 = [](ImVec2 a, ImVec2 b, float t) { + return ImVec2(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); + }; + ImVec2 q0 = lerp2(c.p0, c.p1, u); + ImVec2 q1 = lerp2(c.p1, c.p2, u); + ImVec2 q2 = lerp2(c.p2, c.p3, u); + ImVec2 r0 = lerp2(q0, q1, u); + ImVec2 r1 = lerp2(q1, q2, u); + return lerp2(r0, r1, u); +} + +float bezier_eval(const BezierCurve& c, float t) { + // Approxima y(x=t) muestreando la curva en N puntos uniformes en u y + // buscando el segmento donde la X cae cerca de t. Suficiente para + // easing curves (monotonas en X por construccion). + if (t <= 0.0f) return c.p0.y; + if (t >= 1.0f) return c.p3.y; + + constexpr int N = 64; + ImVec2 prev = c.p0; + for (int i = 1; i <= N; i++) { + float u = (float)i / (float)N; + ImVec2 cur = bezier_point(c, u); + if (cur.x >= t) { + // Interp lineal en X dentro del segmento [prev, cur]. + float dx = cur.x - prev.x; + float k = (dx > 1e-6f) ? (t - prev.x) / dx : 0.0f; + return prev.y + (cur.y - prev.y) * k; + } + prev = cur; + } + return c.p3.y; +} + +// --------------------------------------------------------------------------- +// Editor widget +// --------------------------------------------------------------------------- + +namespace { + +// Convierte coords espacio-curva [0..1] a pixel-space del canvas. +ImVec2 to_canvas(const ImVec2& p, const ImVec2& canvas_min, const ImVec2& canvas_size) { + // Y se invierte porque ImGui crece hacia abajo. + return ImVec2(canvas_min.x + p.x * canvas_size.x, + canvas_min.y + (1.0f - p.y) * canvas_size.y); +} + +// Inverso: pixel-space -> espacio-curva. +ImVec2 to_curve(const ImVec2& p, const ImVec2& canvas_min, const ImVec2& canvas_size) { + float fx = (p.x - canvas_min.x) / canvas_size.x; + float fy = 1.0f - (p.y - canvas_min.y) / canvas_size.y; + return ImVec2(fx, fy); +} + +// Drag handle invisible centrado en `pos` con radio `r`. Devuelve true si se +// arrastro este frame; en ese caso `out` contiene la nueva pos en espacio +// curva. +bool drag_handle(const char* id, + const ImVec2& pos, + const ImVec2& canvas_min, + const ImVec2& canvas_size, + float r, + ImVec2& out) +{ + ImVec2 px = to_canvas(pos, canvas_min, canvas_size); + ImGui::SetCursorScreenPos(ImVec2(px.x - r, px.y - r)); + ImGui::InvisibleButton(id, ImVec2(r * 2.0f, r * 2.0f)); + bool changed = false; + if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left)) { + ImVec2 mouse = ImGui::GetIO().MousePos; + out = to_curve(mouse, canvas_min, canvas_size); + changed = true; + } + return changed; +} + +} // namespace + +bool bezier_editor(const char* id, BezierCurve& curve, ImVec2 size, bool lock_endpoints) { + using namespace fn_tokens; + + ImGui::PushID(id); + + // Reservar canvas + ImVec2 canvas_min = ImGui::GetCursorScreenPos(); + ImVec2 canvas_size = size; + if (canvas_size.x <= 0.0f) canvas_size.x = ImGui::GetContentRegionAvail().x; + if (canvas_size.y <= 0.0f) canvas_size.y = 200.0f; + ImVec2 canvas_max = ImVec2(canvas_min.x + canvas_size.x, + canvas_min.y + canvas_size.y); + + ImDrawList* dl = ImGui::GetWindowDrawList(); + + // Background + border + dl->AddRectFilled(canvas_min, canvas_max, ImGui::GetColorU32(colors::bg), radius::sm); + dl->AddRect(canvas_min, canvas_max, ImGui::GetColorU32(colors::border), radius::sm); + + // Grid sutil (4x4) + ImU32 grid_col = ImGui::GetColorU32(colors::border); + for (int i = 1; i < 4; i++) { + float fx = canvas_min.x + canvas_size.x * (float)i / 4.0f; + float fy = canvas_min.y + canvas_size.y * (float)i / 4.0f; + dl->AddLine(ImVec2(fx, canvas_min.y), ImVec2(fx, canvas_max.y), grid_col); + dl->AddLine(ImVec2(canvas_min.x, fy), ImVec2(canvas_max.x, fy), grid_col); + } + + // Diagonal de referencia (linear) + ImU32 diag_col = ImGui::GetColorU32(colors::text_dim); + dl->AddLine(to_canvas({0,0}, canvas_min, canvas_size), + to_canvas({1,1}, canvas_min, canvas_size), diag_col, 1.0f); + + // Lineas tangentes p0->p1 y p3->p2 + ImU32 tang_col = ImGui::GetColorU32(colors::text_muted); + dl->AddLine(to_canvas(curve.p0, canvas_min, canvas_size), + to_canvas(curve.p1, canvas_min, canvas_size), tang_col, 1.0f); + dl->AddLine(to_canvas(curve.p3, canvas_min, canvas_size), + to_canvas(curve.p2, canvas_min, canvas_size), tang_col, 1.0f); + + // La curva Bezier + ImU32 curve_col = ImGui::GetColorU32(colors::primary); + dl->AddBezierCubic( + to_canvas(curve.p0, canvas_min, canvas_size), + to_canvas(curve.p1, canvas_min, canvas_size), + to_canvas(curve.p2, canvas_min, canvas_size), + to_canvas(curve.p3, canvas_min, canvas_size), + curve_col, 2.0f, 0); + + // Handles + ImU32 handle_col = ImGui::GetColorU32(colors::primary); + ImU32 handle_locked = ImGui::GetColorU32(colors::text_dim); + constexpr float r = 6.0f; + + bool changed = false; + ImVec2 np; + + // p0 + if (lock_endpoints) { + ImVec2 px = to_canvas(curve.p0, canvas_min, canvas_size); + dl->AddCircleFilled(px, r, handle_locked); + } else { + if (drag_handle("##p0", curve.p0, canvas_min, canvas_size, r, np)) { + curve.p0 = np; changed = true; + } + ImVec2 px = to_canvas(curve.p0, canvas_min, canvas_size); + dl->AddCircleFilled(px, r, handle_col); + } + // p3 + if (lock_endpoints) { + ImVec2 px = to_canvas(curve.p3, canvas_min, canvas_size); + dl->AddCircleFilled(px, r, handle_locked); + } else { + if (drag_handle("##p3", curve.p3, canvas_min, canvas_size, r, np)) { + curve.p3 = np; changed = true; + } + ImVec2 px = to_canvas(curve.p3, canvas_min, canvas_size); + dl->AddCircleFilled(px, r, handle_col); + } + // p1 (draggable, sin clamp para permitir overshoot) + if (drag_handle("##p1", curve.p1, canvas_min, canvas_size, r, np)) { + // Clamp solo X a [0,1] para mantener monotonia razonable; Y libre. + np.x = (np.x < 0.0f) ? 0.0f : (np.x > 1.0f ? 1.0f : np.x); + curve.p1 = np; changed = true; + } + { + ImVec2 px = to_canvas(curve.p1, canvas_min, canvas_size); + dl->AddCircleFilled(px, r, handle_col); + } + // p2 + if (drag_handle("##p2", curve.p2, canvas_min, canvas_size, r, np)) { + np.x = (np.x < 0.0f) ? 0.0f : (np.x > 1.0f ? 1.0f : np.x); + curve.p2 = np; changed = true; + } + { + ImVec2 px = to_canvas(curve.p2, canvas_min, canvas_size); + dl->AddCircleFilled(px, r, handle_col); + } + + // Avanzar el cursor por debajo del canvas para que el siguiente widget no + // se solape (los InvisibleButton anteriores movieron el cursor). + ImGui::SetCursorScreenPos(ImVec2(canvas_min.x, canvas_max.y + spacing::xs)); + ImGui::Dummy(ImVec2(canvas_size.x, 1.0f)); + + ImGui::PopID(); + return changed; +} + +} // namespace fn diff --git a/cpp/functions/core/bezier_editor.h b/cpp/functions/core/bezier_editor.h new file mode 100644 index 00000000..af264ccf --- /dev/null +++ b/cpp/functions/core/bezier_editor.h @@ -0,0 +1,49 @@ +#pragma once + +// bezier_editor — editor visual de una curva Bezier cubica (4 puntos de +// control) usado tipicamente como diseñador de easing custom. +// +// Estado puro (struct BezierCurve), evaluacion algebraica (bezier_eval), +// editor en canvas ImGui con 4 puntos draggable (p0/p3 fijos por defecto). +// +// Uso: +// static fn::BezierCurve curve; // identidad: linear +// if (fn::bezier_editor("##my_ease", curve)) { /* curva cambio */ } +// float y = fn::bezier_eval(curve, t); + +#include + +namespace fn { + +// Curva Bezier cubica: 4 puntos de control en espacio [0,1]x[0,1]. +// Por convencion, para easing curves: p0 = (0,0) y p3 = (1,1) (fijos). +// p1, p2 son los handles que el usuario arrastra. +struct BezierCurve { + ImVec2 p0 {0.0f, 0.0f}; + ImVec2 p1 {0.25f, 0.0f}; + ImVec2 p2 {0.75f, 1.0f}; + ImVec2 p3 {1.0f, 1.0f}; +}; + +// Evaluacion puramente algebraica via De Casteljau de la coordenada Y de la +// curva en el parametro de curva u in [0,1]. +// +// NOTA: esto NO es y(x). Para una curva tipo easing donde queremos y dado un +// x temporal usamos bezier_eval (ver mas abajo) que aproxima y al x deseado +// asumiendo que la curva es monotona en X (lo es si p1.x, p2.x in [0,1] y la +// curva no cruza x=0 o x=1 fuera de los extremos). +ImVec2 bezier_point(const BezierCurve& c, float u); + +// y at x=t — aproximacion mediante sampling + interpolacion lineal entre +// muestras. Suficiente para easing curves (la curva es casi monotona en X +// por construccion). Pure. +float bezier_eval(const BezierCurve& c, float t); + +// Editor visual: canvas con 4 puntos draggable. p0 y p3 estan fijos por +// defecto (lock_endpoints=true) para que la curva sirva como easing +// (f(0)=0, f(1)=1). p1 y p2 son draggable libremente. +// +// Devuelve true en el frame en que el usuario arrastro algun punto. +bool bezier_editor(const char* id, BezierCurve& curve, ImVec2 size = ImVec2(200, 200), bool lock_endpoints = true); + +} // namespace fn diff --git a/cpp/functions/core/bezier_editor.md b/cpp/functions/core/bezier_editor.md new file mode 100644 index 00000000..2cd7d45a --- /dev/null +++ b/cpp/functions/core/bezier_editor.md @@ -0,0 +1,94 @@ +--- +name: bezier_editor +kind: component +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "bool fn::bezier_editor(const char* id, fn::BezierCurve& curve, ImVec2 size = {200,200}, bool lock_endpoints = true) + float fn::bezier_eval(const BezierCurve&, float t)" +description: "Editor visual de una curva Bezier cubica (4 puntos de control). Permite diseñar easing curves custom arrastrando p1 y p2 (p0 y p3 fijos en (0,0) y (1,1)). Evaluacion via De Casteljau + sampling. Render en canvas ImGui usando tokens (primary, surface, border)." +tags: [imgui, bezier, animation, easing, editor, canvas] +uses_functions: + - tokens_cpp_core +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/bezier_editor.cpp" +framework: imgui +params: + - name: id + desc: "ID ImGui unico (formato '##nombre' tipico para no mostrar texto)" + - name: curve + desc: "Estado de la curva (struct BezierCurve con p0..p3 en [0,1]x[0,1]). Modificada por el widget." + - name: size + desc: "Tamaño del canvas en pixels. Recomendado >= 180x180 para precision de drag." + - name: lock_endpoints + desc: "Si true (default) p0 y p3 quedan fijos en (0,0) y (1,1) — uso como easing curve. Si false, p0/p3 son draggable." +output: "true en el frame en que el usuario arrastro algun punto de control" +--- + +# bezier_editor + +Editor inline en canvas ImGui para diseñar curvas Bezier cubicas. La aplicacion tipica es disenar **easing curves custom** que no encajan con las preset de `tween_curves` (Penner). Tambien sirve para definir ramps de color, perfiles de velocidad, etc. + +## Estado y evaluacion (puro) + +```cpp +struct fn::BezierCurve { + ImVec2 p0 {0,0}; + ImVec2 p1 {0.25f, 0.0f}; + ImVec2 p2 {0.75f, 1.0f}; + ImVec2 p3 {1,1}; +}; + +ImVec2 fn::bezier_point(const BezierCurve&, float u); // De Casteljau, devuelve (x,y) en u +float fn::bezier_eval (const BezierCurve&, float t); // y at x=t (sampling 64 + interp lineal) +``` + +`bezier_eval` asume monotonia en X (caso tipico de easing). Para curvas con overshoot horizontal el resultado puede saltar — no es un bug, es una limitacion del modelo de easing-curve. + +## Render + +```cpp +static fn::BezierCurve curve; // identidad lineal por defecto +if (fn::bezier_editor("##my_ease", curve, ImVec2(220, 220))) { + // El usuario movio un punto este frame; recomputar lo que dependa. +} +float y = fn::bezier_eval(curve, t); +``` + +## Visuales + +- Fondo `bg`, borde `border`, grid 4x4 con `border` (subtle). +- Diagonal de referencia (linear) con `text_dim`. +- Lineas tangentes p0->p1 y p3->p2 con `text_muted`. +- Curva con `primary` y grosor 2. +- Handles: circulos `primary` para draggable, `text_dim` para los lockeados. + +## Interaccion + +- Drag de p1 / p2 con boton izquierdo del raton. +- Si `lock_endpoints=false`, p0 y p3 tambien arrastrables. +- p1.x y p2.x se clampan a [0,1]; las Y son libres (permiten overshoot deliberado). +- Tamaño minimo recomendado **180x180**: por debajo el drag se vuelve impreciso. + +## Tests conocidos + +- Curva identidad `{(0,0),(0.33,0.33),(0.66,0.66),(1,1)}` → `bezier_eval(c, 0.5) ~= 0.5` (tolerancia ~0.01 por sampling). +- `bezier_eval(c, 0.0) == 0.0` y `bezier_eval(c, 1.0) == 1.0` para cualquier curva con `lock_endpoints=true`. + +## Decisiones + +- **Sampling 64 + lerp** para `bezier_eval` en vez de inversion analitica (Cardano): mas simple, rapido, suficiente para easing. +- **Sin guardar curva en estado interno**: el caller posee `BezierCurve` (igual que el resto de primitivos del registry). +- **Endpoints lockeados por defecto**: el caso 99% de uso (easing) ya define p0=(0,0), p3=(1,1). + +## Notas + +- No hay snap a grid en el MVP. Si hace falta, exponer un parametro adicional o usar Shift+drag. +- El editor consume todo el ancho de la celda padre si `size.x = 0` (auto).