feat(core): add bezier_editor — visual cubic Bezier curve editor
ImGui canvas with 4 draggable control points (p0/p3 locked at (0,0)/(1,1) by default for use as easing curves). Pure evaluation via De Casteljau (bezier_point) plus sampling-based y(x) lookup (bezier_eval). Render uses fn_tokens for visual coherence: bg, border, primary curve, text_dim diagonal reference, text_muted handle tangents. p1.x and p2.x clamped to [0,1] to keep the curve usable as easing; Y values are free to allow deliberate overshoot.
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
#include "core/bezier_editor.h"
|
||||
#include "core/tokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
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
|
||||
@@ -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 <imgui.h>
|
||||
|
||||
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
|
||||
@@ -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).
|
||||
Reference in New Issue
Block a user