diff --git a/cpp/apps/primitives_gallery/CMakeLists.txt b/cpp/apps/primitives_gallery/CMakeLists.txt index 896054da..45fc7d1b 100644 --- a/cpp/apps/primitives_gallery/CMakeLists.txt +++ b/cpp/apps/primitives_gallery/CMakeLists.txt @@ -10,6 +10,11 @@ add_imgui_app(primitives_gallery demos_gl_texture.cpp demos_extras.cpp demos_mesh.cpp + # animation primitives (issue 0031) + demos_animation.cpp + ${CMAKE_SOURCE_DIR}/functions/core/tween_curves.cpp + ${CMAKE_SOURCE_DIR}/functions/core/bezier_editor.cpp + ${CMAKE_SOURCE_DIR}/functions/core/timeline.cpp # text_editor + file_watcher (issue 0025) ${CMAKE_SOURCE_DIR}/functions/core/text_editor.cpp ${CMAKE_SOURCE_DIR}/functions/core/file_watcher.cpp diff --git a/cpp/apps/primitives_gallery/demos.h b/cpp/apps/primitives_gallery/demos.h index 3720e5cb..19df5664 100644 --- a/cpp/apps/primitives_gallery/demos.h +++ b/cpp/apps/primitives_gallery/demos.h @@ -21,6 +21,9 @@ void demo_dashboard_panel(); void demo_text_editor(); // wave 1, issue 0025 void demo_file_watcher(); // wave 1, issue 0025 void demo_process_runner(); +void demo_tween(); // issue 0031 +void demo_bezier_editor(); // issue 0031 +void demo_timeline(); // issue 0031 // --- Viz --- void demo_bar_chart(); diff --git a/cpp/apps/primitives_gallery/demos_animation.cpp b/cpp/apps/primitives_gallery/demos_animation.cpp new file mode 100644 index 00000000..877324a0 --- /dev/null +++ b/cpp/apps/primitives_gallery/demos_animation.cpp @@ -0,0 +1,249 @@ +// Demos para los primitivos de animacion (issue 0031): +// - tween_curves +// - bezier_editor +// - timeline + +#include "demos.h" +#include "demo.h" + +#include "core/tween_curves.h" +#include "core/bezier_editor.h" +#include "core/timeline.h" +#include "core/tokens.h" + +#include + +#include +#include + +namespace gallery { + +// --------------------------------------------------------------------------- +// demo_tween — dropdown + plot animado +// --------------------------------------------------------------------------- + +void demo_tween() { + using namespace fn_tokens; + using fn::tween::Ease; + + demo_header("tween_curves", "v1.0.0", + "Funciones de easing (Penner): linear, quad, cubic, expo, elastic, " + "bounce con variantes in/out/inOut. Header-mostly: el compilador " + "inlinea cada curva en el sitio de llamada."); + + section("Selector + plot"); + + static int ease_idx = (int)Ease::OutCubic; + static float anim_t = 0.0f; + anim_t += ImGui::GetIO().DeltaTime * 0.5f; + if (anim_t > 1.5f) anim_t = -0.25f; // hold un poco antes de reiniciar + + // Build labels + const char* labels[fn::tween::ease_count]; + for (int i = 0; i < fn::tween::ease_count; i++) { + labels[i] = fn::tween::name((Ease)i); + } + ImGui::SetNextItemWidth(220.0f); + ImGui::Combo("##tween_ease", &ease_idx, labels, fn::tween::ease_count); + + Ease ease = (Ease)ease_idx; + float t_clamped = anim_t; + if (t_clamped < 0.0f) t_clamped = 0.0f; + if (t_clamped > 1.0f) t_clamped = 1.0f; + float v = fn::tween::apply(ease, t_clamped); + + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); + ImGui::Text(" t=%.2f f(t)=%.3f", t_clamped, v); + ImGui::PopStyleColor(); + + // Canvas plot + ImVec2 canvas_min = ImGui::GetCursorScreenPos(); + ImVec2 canvas_size(360.0f, 220.0f); + ImVec2 canvas_max = ImVec2(canvas_min.x + canvas_size.x, canvas_min.y + canvas_size.y); + + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->AddRectFilled(canvas_min, canvas_max, ImGui::GetColorU32(colors::bg), radius::sm); + dl->AddRect(canvas_min, canvas_max, ImGui::GetColorU32(colors::border), radius::sm); + + auto to_px = [&](float tx, float ty) { + // ty puede salir de [0,1] (elastic/bounce); damos algo de margen vertical. + return ImVec2(canvas_min.x + tx * canvas_size.x, + canvas_min.y + (1.0f - ty) * canvas_size.y); + }; + + // Grid 4x4 + ImU32 grid = 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); + dl->AddLine(ImVec2(canvas_min.x, fy), ImVec2(canvas_max.x, fy), grid); + } + + // Diagonal linear + dl->AddLine(to_px(0.0f, 0.0f), to_px(1.0f, 1.0f), + ImGui::GetColorU32(colors::text_dim), 1.0f); + + // Curva + constexpr int N = 96; + ImVec2 prev = to_px(0.0f, fn::tween::apply(ease, 0.0f)); + ImU32 col = ImGui::GetColorU32(colors::primary); + for (int i = 1; i <= N; i++) { + float x = (float)i / (float)N; + float y = fn::tween::apply(ease, x); + ImVec2 cur = to_px(x, y); + dl->AddLine(prev, cur, col, 2.0f); + prev = cur; + } + + // Marker animado + ImVec2 m = to_px(t_clamped, v); + dl->AddCircleFilled(m, 5.0f, ImGui::GetColorU32(colors::primary_light)); + dl->AddCircle(m, 6.0f, ImGui::GetColorU32(colors::text), 0, 1.5f); + + // Avanzar cursor + ImGui::Dummy(canvas_size); + + code_block( + "#include \"core/tween_curves.h\"\n\n" + "float k = fn::tween::apply(fn::tween::Ease::OutCubic, t);\n" + "// o named:\n" + "float k2 = fn::tween::out_cubic(t);" + ); +} + +// --------------------------------------------------------------------------- +// demo_bezier_editor — editor + plot evaluado +// --------------------------------------------------------------------------- + +void demo_bezier_editor() { + using namespace fn_tokens; + + demo_header("bezier_editor", "v1.0.0", + "Editor visual de curva Bezier cubica (4 puntos). Para diseñar " + "easing curves custom. p1/p2 son draggable; p0/p3 fijos en (0,0)/(1,1)."); + + section("Editor"); + + static fn::BezierCurve curve; // identidad por defecto: ease lineal con handles desplazados + + if (ImGui::Button("Reset##bz_reset")) { + curve = fn::BezierCurve{}; + } + ImGui::SameLine(); + if (ImGui::Button("Ease-out preset##bz_eo")) { + curve = {{0,0}, {0.0f, 0.0f}, {0.58f, 1.0f}, {1,1}}; + } + ImGui::SameLine(); + if (ImGui::Button("Ease-in-out preset##bz_eio")) { + curve = {{0,0}, {0.42f, 0.0f}, {0.58f, 1.0f}, {1,1}}; + } + + fn::bezier_editor("##bz_editor", curve, ImVec2(220, 220)); + + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); + ImGui::Text("p0=(%.2f,%.2f) p1=(%.2f,%.2f) p2=(%.2f,%.2f) p3=(%.2f,%.2f)", + curve.p0.x, curve.p0.y, curve.p1.x, curve.p1.y, + curve.p2.x, curve.p2.y, curve.p3.x, curve.p3.y); + ImGui::PopStyleColor(); + + // Plot evaluation + section("bezier_eval(curve, t)"); + static float t = 0.0f; + ImGui::SetNextItemWidth(360.0f); + ImGui::SliderFloat("t##bz_t", &t, 0.0f, 1.0f, "%.3f"); + float y = fn::bezier_eval(curve, t); + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); + ImGui::Text("y(t=%.3f) = %.3f", t, y); + ImGui::PopStyleColor(); + + code_block( + "#include \"core/bezier_editor.h\"\n\n" + "static fn::BezierCurve curve;\n" + "if (fn::bezier_editor(\"##my\", curve, ImVec2(220, 220))) {\n" + " // user dragged a control point\n" + "}\n" + "float k = fn::bezier_eval(curve, t);" + ); +} + +// --------------------------------------------------------------------------- +// demo_timeline — 2 tracks + display +// --------------------------------------------------------------------------- + +void demo_timeline() { + using namespace fn_tokens; + using fn::tween::Ease; + + demo_header("timeline", "v1.0.0", + "Timeline tipo DAW: tracks horizontales con keyframes draggable, " + "scrub con el ruler, play/pause/loop. track_value_at(t) interpola " + "aplicando la Ease de cada keyframe destino."); + + static fn::TimelineState tl; + static bool inited = false; + if (!inited) { + tl.duration = 4.0f; + tl.playing = true; + tl.tracks.push_back({"hue", { + {0.0f, 0.0f, Ease::Linear}, + {2.0f, 1.0f, Ease::OutCubic}, + {4.0f, 0.0f, Ease::InOutCubic}, + }}); + tl.tracks.push_back({"amp", { + {0.0f, 0.2f, Ease::Linear}, + {3.0f, 1.0f, Ease::OutElastic}, + }}); + inited = true; + } + + // Update + fn::timeline_update(tl, ImGui::GetIO().DeltaTime); + + // Display values + section("Live values"); + float hue = fn::track_value_at(tl.tracks[0], tl.current_time); + float amp = fn::track_value_at(tl.tracks[1], tl.current_time); + + ImGui::PushStyleColor(ImGuiCol_Text, colors::text); + ImGui::Text("t = %.3fs", tl.current_time); + ImGui::PopStyleColor(); + + auto draw_bar = [&](const char* name, float value, float vmin, float vmax) { + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); + ImGui::Text("%-4s", name); + ImGui::PopStyleColor(); + ImGui::SameLine(); + ImVec2 cmin = ImGui::GetCursorScreenPos(); + ImVec2 csize = ImVec2(280.0f, 14.0f); + ImDrawList* dl = ImGui::GetWindowDrawList(); + dl->AddRectFilled(cmin, ImVec2(cmin.x + csize.x, cmin.y + csize.y), + ImGui::GetColorU32(colors::surface_active), radius::sm); + float k = (value - vmin) / (vmax - vmin); + if (k < 0.0f) k = 0.0f; + if (k > 1.0f) k = 1.0f; + dl->AddRectFilled(cmin, ImVec2(cmin.x + csize.x * k, cmin.y + csize.y), + ImGui::GetColorU32(colors::primary), radius::sm); + ImGui::Dummy(csize); + ImGui::SameLine(); + ImGui::Text("%.3f", value); + }; + draw_bar("hue", hue, 0.0f, 1.0f); + draw_bar("amp", amp, 0.0f, 1.0f); + + section("Widget"); + fn::timeline_widget("##gallery_tl", tl, ImVec2(-1, 220)); + + code_block( + "#include \"core/timeline.h\"\n\n" + "static fn::TimelineState tl;\n" + "tl.tracks.push_back({\"hue\", {{0,0}, {2,1, fn::tween::Ease::OutCubic}, {4,0}}});\n" + "tl.duration = 4.0f; tl.playing = true;\n\n" + "fn::timeline_update(tl, ImGui::GetIO().DeltaTime);\n" + "float h = fn::track_value_at(tl.tracks[0], tl.current_time);\n" + "fn::timeline_widget(\"##tl\", tl);" + ); +} + +} // namespace gallery diff --git a/cpp/apps/primitives_gallery/main.cpp b/cpp/apps/primitives_gallery/main.cpp index 0ae7fb64..b968be79 100644 --- a/cpp/apps/primitives_gallery/main.cpp +++ b/cpp/apps/primitives_gallery/main.cpp @@ -49,6 +49,9 @@ static const DemoEntry k_demos[] = { {"text_editor", "text_editor", "Core", &gallery::demo_text_editor}, // wave 1 {"file_watcher", "file_watcher", "Core", &gallery::demo_file_watcher}, // wave 1 {"process_runner", "process_runner", "Core", &gallery::demo_process_runner}, + {"tween", "tween_curves", "Core", &gallery::demo_tween}, + {"bezier_editor", "bezier_editor", "Core", &gallery::demo_bezier_editor}, + {"timeline", "timeline", "Core", &gallery::demo_timeline}, // Viz {"bar_chart", "bar_chart", "Viz", &gallery::demo_bar_chart}, {"pie_chart", "pie_chart", "Viz", &gallery::demo_pie_chart}, 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). diff --git a/cpp/functions/core/timeline.cpp b/cpp/functions/core/timeline.cpp new file mode 100644 index 00000000..38e057dc --- /dev/null +++ b/cpp/functions/core/timeline.cpp @@ -0,0 +1,264 @@ +#include "core/timeline.h" +#include "core/tokens.h" + +#include +#include + +#include +#include +#include + +namespace fn { + +// --------------------------------------------------------------------------- +// Pure interp +// --------------------------------------------------------------------------- + +float track_value_at(const Track& track, float t) { + if (track.keys.empty()) return 0.0f; + if (track.keys.size() == 1) return track.keys[0].value; + + if (t <= track.keys.front().time) return track.keys.front().value; + if (t >= track.keys.back().time) return track.keys.back().value; + + // Encontrar el segmento [k_i, k_{i+1}] con k_i.time <= t < k_{i+1}.time + for (size_t i = 0; i + 1 < track.keys.size(); i++) { + const Keyframe& a = track.keys[i]; + const Keyframe& b = track.keys[i + 1]; + if (t >= a.time && t <= b.time) { + float dt = b.time - a.time; + float u = (dt > 1e-6f) ? (t - a.time) / dt : 0.0f; + float k = tween::apply(b.ease, u); // ease de la key destino + return a.value + (b.value - a.value) * k; + } + } + return track.keys.back().value; +} + +// --------------------------------------------------------------------------- +// Update +// --------------------------------------------------------------------------- + +void timeline_update(TimelineState& s, float dt) { + if (!s.playing) return; + s.current_time += dt; + if (s.current_time >= s.duration) { + if (s.loop) { + // Wrap por modulo (puede saltar varios loops si dt es enorme). + if (s.duration > 1e-6f) + s.current_time = std::fmod(s.current_time, s.duration); + else + s.current_time = 0.0f; + } else { + s.current_time = s.duration; + s.playing = false; + } + } + if (s.current_time < 0.0f) s.current_time = 0.0f; +} + +// --------------------------------------------------------------------------- +// Render helpers +// --------------------------------------------------------------------------- + +namespace { + +constexpr float k_label_w = 80.0f; // ancho de la columna de nombre de track +constexpr float k_track_h = 36.0f; // alto de cada track +constexpr float k_ruler_h = 22.0f; // alto del ruler superior +constexpr float k_header_h = 28.0f; // alto de la cabecera (play/pause + tiempo) +constexpr float k_diamond_r = 6.0f; // radio del diamante de keyframe + +float time_to_x(float time, float duration, float track_x0, float track_w) { + if (duration <= 1e-6f) return track_x0; + return track_x0 + (time / duration) * track_w; +} + +float x_to_time(float x, float duration, float track_x0, float track_w) { + if (track_w <= 1e-6f) return 0.0f; + float t = (x - track_x0) / track_w * duration; + if (t < 0.0f) t = 0.0f; + if (t > duration) t = duration; + return t; +} + +void draw_diamond(ImDrawList* dl, ImVec2 c, float r, ImU32 col) { + ImVec2 pts[4] = { + ImVec2(c.x, c.y - r), + ImVec2(c.x + r, c.y), + ImVec2(c.x, c.y + r), + ImVec2(c.x - r, c.y), + }; + dl->AddConvexPolyFilled(pts, 4, col); +} + +} // namespace + +bool timeline_widget(const char* id, TimelineState& s, ImVec2 size) { + using namespace fn_tokens; + + ImGui::PushID(id); + bool changed = false; + + // ---- size resolution + ImVec2 region = ImGui::GetContentRegionAvail(); + if (size.x <= 0.0f) size.x = region.x; + if (size.y <= 0.0f) size.y = 200.0f; + + ImVec2 origin = ImGui::GetCursorScreenPos(); + ImDrawList* dl = ImGui::GetWindowDrawList(); + + // ---- Background + ImVec2 bg_max = ImVec2(origin.x + size.x, origin.y + size.y); + dl->AddRectFilled(origin, bg_max, ImGui::GetColorU32(colors::surface), radius::sm); + dl->AddRect(origin, bg_max, ImGui::GetColorU32(colors::border), radius::sm); + + // ---- Header (play/pause + tiempo) + ImGui::SetCursorScreenPos(ImVec2(origin.x + spacing::sm, origin.y + spacing::xs)); + const char* play_label = s.playing ? "Pause##tl_play" : "Play##tl_play"; + if (ImGui::Button(play_label, ImVec2(60.0f, k_header_h - spacing::xs))) { + s.playing = !s.playing; + changed = true; + } + ImGui::SameLine(); + if (ImGui::Button("Reset##tl_reset", ImVec2(60.0f, k_header_h - spacing::xs))) { + s.current_time = 0.0f; + changed = true; + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(60.0f); + if (ImGui::DragFloat("##tl_dur", &s.duration, 0.1f, 0.1f, 600.0f, "dur %.2fs")) { + if (s.duration < 0.1f) s.duration = 0.1f; + changed = true; + } + ImGui::SameLine(); + ImGui::PushStyleColor(ImGuiCol_Text, colors::text_muted); + ImGui::Text("t=%.3fs", s.current_time); + ImGui::PopStyleColor(); + ImGui::SameLine(); + ImGui::Checkbox("loop##tl_loop", &s.loop); + + // ---- Layout + float track_x0 = origin.x + k_label_w; + float track_w = size.x - k_label_w - spacing::sm; + float ruler_y0 = origin.y + k_header_h; + float ruler_y1 = ruler_y0 + k_ruler_h; + + // ---- Ruler + dl->AddRectFilled(ImVec2(origin.x, ruler_y0), ImVec2(bg_max.x, ruler_y1), + ImGui::GetColorU32(colors::bg)); + dl->AddLine(ImVec2(origin.x, ruler_y1), ImVec2(bg_max.x, ruler_y1), + ImGui::GetColorU32(colors::border)); + + // Tick marks cada 0.5s + ImU32 tick_col = ImGui::GetColorU32(colors::text_dim); + int n_ticks = (int)std::floor(s.duration / 0.5f); + for (int i = 0; i <= n_ticks; i++) { + float ts = (float)i * 0.5f; + if (ts > s.duration) break; + float x = time_to_x(ts, s.duration, track_x0, track_w); + bool whole = (i % 2 == 0); + float h = whole ? 8.0f : 4.0f; + dl->AddLine(ImVec2(x, ruler_y1 - h), ImVec2(x, ruler_y1), tick_col, 1.0f); + if (whole) { + char buf[16]; + std::snprintf(buf, sizeof(buf), "%.0fs", ts); + dl->AddText(ImVec2(x + 2.0f, ruler_y0 + 2.0f), tick_col, buf); + } + } + + // Scrub interaction en el ruler + { + ImGui::SetCursorScreenPos(ImVec2(track_x0, ruler_y0)); + ImGui::InvisibleButton("##tl_ruler_scrub", ImVec2(track_w, k_ruler_h)); + if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left, 0.0f)) { + float mx = ImGui::GetIO().MousePos.x; + s.current_time = x_to_time(mx, s.duration, track_x0, track_w); + changed = true; + } else if (ImGui::IsItemClicked()) { + float mx = ImGui::GetIO().MousePos.x; + s.current_time = x_to_time(mx, s.duration, track_x0, track_w); + changed = true; + } + } + + // ---- Tracks + ImU32 col_text = ImGui::GetColorU32(colors::text); + ImU32 col_track_bg = ImGui::GetColorU32(colors::bg); + ImU32 col_key = ImGui::GetColorU32(colors::primary); + ImU32 col_key_hover = ImGui::GetColorU32(colors::primary_hover); + ImU32 col_separator = ImGui::GetColorU32(colors::border); + + float track_y = ruler_y1; + float available_h = (origin.y + size.y) - ruler_y1; + int n_tracks = (int)s.tracks.size(); + float row_h = (n_tracks > 0) ? std::min(k_track_h, available_h / (float)n_tracks) + : k_track_h; + if (row_h < 18.0f) row_h = 18.0f; + + for (int ti = 0; ti < n_tracks; ti++) { + Track& tr = s.tracks[ti]; + float y0 = track_y; + float y1 = y0 + row_h; + + // bg + dl->AddRectFilled(ImVec2(track_x0, y0), ImVec2(bg_max.x, y1), col_track_bg); + dl->AddLine(ImVec2(origin.x, y1), ImVec2(bg_max.x, y1), col_separator); + + // label + dl->AddText(ImVec2(origin.x + spacing::sm, y0 + (row_h - ImGui::GetTextLineHeight()) * 0.5f), + col_text, tr.name.c_str()); + + // keyframes + for (size_t ki = 0; ki < tr.keys.size(); ki++) { + Keyframe& k = tr.keys[ki]; + float kx = time_to_x(k.time, s.duration, track_x0, track_w); + ImVec2 c = ImVec2(kx, y0 + row_h * 0.5f); + + char btn_id[64]; + std::snprintf(btn_id, sizeof(btn_id), "##tl_k_%d_%zu", ti, ki); + ImGui::SetCursorScreenPos(ImVec2(c.x - k_diamond_r, c.y - k_diamond_r)); + ImGui::InvisibleButton(btn_id, ImVec2(k_diamond_r * 2.0f, k_diamond_r * 2.0f)); + + bool hovered = ImGui::IsItemHovered(); + draw_diamond(dl, c, k_diamond_r, hovered ? col_key_hover : col_key); + + if (ImGui::IsItemActive() && ImGui::IsMouseDragging(ImGuiMouseButton_Left, 0.0f)) { + float mx = ImGui::GetIO().MousePos.x; + float new_t = x_to_time(mx, s.duration, track_x0, track_w); + k.time = new_t; + changed = true; + } + } + + // Reordenar keys por tiempo si el usuario solto el drag (consistencia). + // Lo hacemos en cada frame que hubo cambio: cheap, O(n log n) per track. + if (changed) { + std::sort(tr.keys.begin(), tr.keys.end(), + [](const Keyframe& a, const Keyframe& b) { return a.time < b.time; }); + } + + track_y = y1; + } + + // ---- Playhead vertical + { + float px = time_to_x(s.current_time, s.duration, track_x0, track_w); + ImU32 ph_col = ImGui::GetColorU32(colors::primary_light); + dl->AddLine(ImVec2(px, ruler_y0), ImVec2(px, bg_max.y), ph_col, 1.5f); + // pequeno triangulo en el ruler + ImVec2 t0(px - 4.0f, ruler_y0); + ImVec2 t1(px + 4.0f, ruler_y0); + ImVec2 t2(px, ruler_y0 + 6.0f); + dl->AddTriangleFilled(t0, t1, t2, ph_col); + } + + // Reservar espacio en el layout vertical + ImGui::SetCursorScreenPos(ImVec2(origin.x, origin.y + size.y + spacing::xs)); + ImGui::Dummy(ImVec2(size.x, 1.0f)); + + ImGui::PopID(); + return changed; +} + +} // namespace fn diff --git a/cpp/functions/core/timeline.h b/cpp/functions/core/timeline.h new file mode 100644 index 00000000..ab06f3ff --- /dev/null +++ b/cpp/functions/core/timeline.h @@ -0,0 +1,67 @@ +#pragma once + +// timeline — widget tipo DAW: tracks horizontales con keyframes interpolados, +// scrub y play/pause. Sirve para animar valores escalares (uniforms shader, +// parametros UI, etc) a lo largo del tiempo. +// +// Estado puro (TimelineState) + funciones puras de interpolacion +// (track_value_at) + render impuro (timeline_widget). +// +// Uso: +// static fn::TimelineState tl; +// tl.tracks.push_back({"hue", {{0,0}, {2,1}, {4,0}}}); +// tl.duration = 4.0f; +// +// fn::timeline_update(tl, dt); +// float h = fn::track_value_at(tl.tracks[0], tl.current_time); +// fn::timeline_widget("##tl", tl); + +#include "core/tween_curves.h" + +#include +#include +#include + +namespace fn { + +struct Keyframe { + float time; + float value; + tween::Ease ease = tween::Ease::Linear; +}; + +struct Track { + std::string name; + std::vector keys; +}; + +struct TimelineState { + std::vector tracks; + float current_time = 0.0f; + float duration = 5.0f; + bool playing = false; + bool loop = true; +}; + +// --- Pure ------------------------------------------------------------------- + +// Interpola el valor de `track` en el tiempo `t`. Asume keys ordenadas por +// time. Si `t` cae antes del primer keyframe devuelve el value del primero; +// si cae despues del ultimo, devuelve el value del ultimo. Entre keyframes +// usa el ease de la SEGUNDA key (la "curva entrante" hasta esa key). +float track_value_at(const Track& track, float t); + +// --- Update ----------------------------------------------------------------- + +// Avanza current_time si playing. Si loop=true hace wrap; si no, satura en +// duration y pone playing=false al llegar. +void timeline_update(TimelineState& s, float dt); + +// --- Render ----------------------------------------------------------------- + +// Widget completo: cabecera con play/pause + tiempo, ruler con scrub, y un +// panel por track con keyframes draggable. Devuelve true si el usuario hizo +// algun cambio (drag de keyframe, scrub, play/pause). +bool timeline_widget(const char* id, TimelineState& s, ImVec2 size = ImVec2(-1, 200)); + +} // namespace fn diff --git a/cpp/functions/core/timeline.md b/cpp/functions/core/timeline.md new file mode 100644 index 00000000..5e760020 --- /dev/null +++ b/cpp/functions/core/timeline.md @@ -0,0 +1,137 @@ +--- +name: timeline +kind: component +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "bool fn::timeline_widget(const char* id, fn::TimelineState&, ImVec2 size = {-1,200}) + float fn::track_value_at(const Track&, float t) + void fn::timeline_update(TimelineState&, float dt)" +description: "Widget tipo DAW: tracks horizontales con keyframes draggable, scrub, play/pause/loop, evaluacion track_value_at(time) interpolando entre keyframes con la Ease de cada keyframe destino. Estado puro (TimelineState) + render con tokens." +tags: [imgui, timeline, animation, keyframes, daw, tween] +uses_functions: + - tokens_cpp_core + - tween_curves_cpp_core +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [imgui] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/timeline.cpp" +framework: imgui +params: + - name: id + desc: "ID ImGui unico" + - name: state + desc: "TimelineState con tracks (vector), current_time, duration, playing, loop. Modificada por el widget." + - name: size + desc: "Tamaño total. size.x = -1 toma el ancho disponible. size.y >= 100 recomendado." +output: "true en el frame en que el usuario interactua (drag de keyframe, scrub, play/pause/reset, cambio de duration o loop)" +--- + +# timeline + +Widget de timeline tipo DAW para animar valores escalares en el tiempo. Cada **Track** es un canal con keyframes (`time`, `value`, `ease`); `track_value_at(time)` interpola entre keyframes consecutivos aplicando la `Ease` de la keyframe destino. + +## Tipos + +```cpp +struct fn::Keyframe { + float time; + float value; + fn::tween::Ease ease = fn::tween::Ease::Linear; +}; + +struct fn::Track { + std::string name; + std::vector keys; // ordenadas por time +}; + +struct fn::TimelineState { + std::vector tracks; + float current_time = 0.0f; + float duration = 5.0f; + bool playing = false; + bool loop = true; +}; +``` + +## API + +```cpp +// Pure: interp del valor del track en t +float fn::track_value_at(const Track&, float t); + +// Avanza current_time si playing; loop o satura segun flag +void fn::timeline_update(TimelineState&, float dt); + +// Render del widget. Devuelve true si hubo interaccion del usuario. +bool fn::timeline_widget(const char* id, TimelineState&, ImVec2 size = {-1, 200}); +``` + +## Ejemplo + +```cpp +static fn::TimelineState tl; +if (tl.tracks.empty()) { + tl.tracks.push_back({"hue", {{0, 0.0f}, {2.0f, 1.0f, fn::tween::Ease::OutCubic}, {4.0f, 0.0f}}}); + tl.tracks.push_back({"amp", {{0, 0.2f}, {3.0f, 1.0f, fn::tween::Ease::InOutQuad}}}); + tl.duration = 4.0f; +} + +float dt = ImGui::GetIO().DeltaTime; +fn::timeline_update(tl, dt); + +float hue = fn::track_value_at(tl.tracks[0], tl.current_time); +float amp = fn::track_value_at(tl.tracks[1], tl.current_time); +ImGui::Text("hue=%.3f amp=%.3f", hue, amp); + +fn::timeline_widget("##my_tl", tl); +``` + +## Comportamiento de track_value_at + +- 0 keys → devuelve 0.0 +- 1 key → devuelve siempre `keys[0].value` +- t antes de la primera key → `keys.front().value` (clamp izq) +- t despues de la ultima key → `keys.back().value` (clamp der) +- Entre dos keys `a` y `b`: + - `u = (t - a.time) / (b.time - a.time)` + - `k = tween::apply(b.ease, u)` (la ease es la "curva entrante" hasta b) + - resultado = `a.value + (b.value - a.value) * k` + +### Smoke tests (linear) + +Track con 2 keys `{(0,0), (1,1)}` con ease=Linear: +- `track_value_at(t, 0.0) == 0.0` +- `track_value_at(t, 0.5) == 0.5` +- `track_value_at(t, 1.0) == 1.0` + +## Render + +- **Header**: Play/Pause + Reset + DragFloat de duration + indicador `t=...s` + checkbox loop. +- **Ruler**: ticks cada 0.5s, etiqueta cada segundo, scrub con click+drag. +- **Tracks**: filas horizontales con nombre a la izquierda (k_label_w=80px) y keyframes como diamantes draggable. +- **Playhead**: linea vertical `primary_light` con triangulo en la cabeza del ruler. +- Colores via `fn_tokens` (surface bg, border, primary keys, text labels). + +## Interaccion + +- **Drag horizontal de un diamante**: cambia `key.time`. Al soltar (o cualquier frame con `changed`), las keys se reordenan por tiempo. +- **Scrub**: click + drag en el ruler mueve `current_time`. +- **Play/Pause/Reset**: cambian `playing` y `current_time`. +- **Duration drag**: clampada >= 0.1s. + +## Decisiones + +- **Ease en la keyframe destino**: convencion comun en animation toolings (Maya, AfterEffects). La curva define como _llegamos_ a esa key. +- **Sin edicion vertical (value drag)**: para mantener el widget simple. Si hace falta editar `value` con la UI, el caller puede mostrar un campo numerico al lado o dentro de un popup. +- **Sort en cada cambio**: O(n log n) por track no es problema para timelines tipicas (<100 keys); evita estado intermedio "drag in progress". + +## Limitaciones / TODO + +- No hay editor de `ease` por keyframe en el MVP (queda en `Linear` salvo que el caller lo configure). +- No hay seleccion multiple ni copy/paste de keyframes. +- No hay zoom horizontal: la timeline siempre encaja `duration` completa al ancho del widget. diff --git a/cpp/functions/core/tween_curves.cpp b/cpp/functions/core/tween_curves.cpp new file mode 100644 index 00000000..d4cb26a9 --- /dev/null +++ b/cpp/functions/core/tween_curves.cpp @@ -0,0 +1,49 @@ +#include "core/tween_curves.h" + +namespace fn::tween { + +float apply(Ease e, float t) { + switch (e) { + case Ease::Linear: return linear(t); + case Ease::InQuad: return in_quad(t); + case Ease::OutQuad: return out_quad(t); + case Ease::InOutQuad: return in_out_quad(t); + case Ease::InCubic: return in_cubic(t); + case Ease::OutCubic: return out_cubic(t); + case Ease::InOutCubic: return in_out_cubic(t); + case Ease::InExpo: return in_expo(t); + case Ease::OutExpo: return out_expo(t); + case Ease::InOutExpo: return in_out_expo(t); + case Ease::InElastic: return in_elastic(t); + case Ease::OutElastic: return out_elastic(t); + case Ease::InOutElastic: return in_out_elastic(t); + case Ease::InBounce: return in_bounce(t); + case Ease::OutBounce: return out_bounce(t); + case Ease::InOutBounce: return in_out_bounce(t); + } + return t; +} + +const char* name(Ease e) { + switch (e) { + case Ease::Linear: return "Linear"; + case Ease::InQuad: return "InQuad"; + case Ease::OutQuad: return "OutQuad"; + case Ease::InOutQuad: return "InOutQuad"; + case Ease::InCubic: return "InCubic"; + case Ease::OutCubic: return "OutCubic"; + case Ease::InOutCubic: return "InOutCubic"; + case Ease::InExpo: return "InExpo"; + case Ease::OutExpo: return "OutExpo"; + case Ease::InOutExpo: return "InOutExpo"; + case Ease::InElastic: return "InElastic"; + case Ease::OutElastic: return "OutElastic"; + case Ease::InOutElastic: return "InOutElastic"; + case Ease::InBounce: return "InBounce"; + case Ease::OutBounce: return "OutBounce"; + case Ease::InOutBounce: return "InOutBounce"; + } + return "?"; +} + +} // namespace fn::tween diff --git a/cpp/functions/core/tween_curves.h b/cpp/functions/core/tween_curves.h new file mode 100644 index 00000000..d7403e4a --- /dev/null +++ b/cpp/functions/core/tween_curves.h @@ -0,0 +1,128 @@ +#pragma once + +// tween_curves — easing functions (Penner) for animations and interpolations. +// Header-mostly so the compiler inlines on hot paths. Pure: no I/O, no state. +// +// Reference: easings.net (Penner formulas). +// Conventions: +// - All functions take t in [0,1] and return f(t). +// - linear, *_quad, *_cubic, *_expo: f(0)=0, f(1)=1 exactly. +// - elastic / bounce: f(0)=0, f(1)=1 but f overshoots/undershoots in between. +// +// Usage: +// #include "core/tween_curves.h" +// float y = fn::tween::out_cubic(t); +// float y2 = fn::tween::apply(fn::tween::Ease::OutElastic, t); + +#include + +namespace fn::tween { + +enum class Ease { + Linear, + InQuad, OutQuad, InOutQuad, + InCubic, OutCubic, InOutCubic, + InExpo, OutExpo, InOutExpo, + InElastic, OutElastic, InOutElastic, + InBounce, OutBounce, InOutBounce, +}; + +// --- Linear ----------------------------------------------------------------- + +inline float linear(float t) { return t; } + +// --- Quadratic (t^2) -------------------------------------------------------- + +inline float in_quad(float t) { return t * t; } +inline float out_quad(float t) { return 1.0f - (1.0f - t) * (1.0f - t); } +inline float in_out_quad(float t) { + return (t < 0.5f) ? (2.0f * t * t) + : (1.0f - 0.5f * (2.0f - 2.0f * t) * (2.0f - 2.0f * t)); +} + +// --- Cubic (t^3) ------------------------------------------------------------ + +inline float in_cubic(float t) { return t * t * t; } +inline float out_cubic(float t) { float u = 1.0f - t; return 1.0f - u * u * u; } +inline float in_out_cubic(float t) { + if (t < 0.5f) return 4.0f * t * t * t; + float u = -2.0f * t + 2.0f; + return 1.0f - 0.5f * u * u * u; +} + +// --- Exponential ------------------------------------------------------------ + +inline float in_expo(float t) { + return (t <= 0.0f) ? 0.0f : std::pow(2.0f, 10.0f * t - 10.0f); +} +inline float out_expo(float t) { + return (t >= 1.0f) ? 1.0f : 1.0f - std::pow(2.0f, -10.0f * t); +} +inline float in_out_expo(float t) { + if (t <= 0.0f) return 0.0f; + if (t >= 1.0f) return 1.0f; + return (t < 0.5f) ? 0.5f * std::pow(2.0f, 20.0f * t - 10.0f) + : 1.0f - 0.5f * std::pow(2.0f, -20.0f * t + 10.0f); +} + +// --- Elastic (oscillation) -------------------------------------------------- + +inline float in_elastic(float t) { + if (t <= 0.0f) return 0.0f; + if (t >= 1.0f) return 1.0f; + constexpr float c4 = 6.283185307179586f / 3.0f; // 2*pi/3 + return -std::pow(2.0f, 10.0f * t - 10.0f) * std::sin((t * 10.0f - 10.75f) * c4); +} +inline float out_elastic(float t) { + if (t <= 0.0f) return 0.0f; + if (t >= 1.0f) return 1.0f; + constexpr float c4 = 6.283185307179586f / 3.0f; + return std::pow(2.0f, -10.0f * t) * std::sin((t * 10.0f - 0.75f) * c4) + 1.0f; +} +inline float in_out_elastic(float t) { + if (t <= 0.0f) return 0.0f; + if (t >= 1.0f) return 1.0f; + constexpr float c5 = 6.283185307179586f / 4.5f; // 2*pi/4.5 + if (t < 0.5f) { + return -0.5f * std::pow(2.0f, 20.0f * t - 10.0f) * std::sin((20.0f * t - 11.125f) * c5); + } + return 0.5f * std::pow(2.0f, -20.0f * t + 10.0f) * std::sin((20.0f * t - 11.125f) * c5) + 1.0f; +} + +// --- Bounce ----------------------------------------------------------------- + +inline float out_bounce(float t) { + constexpr float n1 = 7.5625f; + constexpr float d1 = 2.75f; + if (t < 1.0f / d1) { + return n1 * t * t; + } else if (t < 2.0f / d1) { + float u = t - 1.5f / d1; + return n1 * u * u + 0.75f; + } else if (t < 2.5f / d1) { + float u = t - 2.25f / d1; + return n1 * u * u + 0.9375f; + } else { + float u = t - 2.625f / d1; + return n1 * u * u + 0.984375f; + } +} +inline float in_bounce(float t) { return 1.0f - out_bounce(1.0f - t); } +inline float in_out_bounce(float t) { + return (t < 0.5f) ? 0.5f * (1.0f - out_bounce(1.0f - 2.0f * t)) + : 0.5f * (1.0f + out_bounce(2.0f * t - 1.0f)); +} + +// --- Dispatch --------------------------------------------------------------- + +// Apply easing by enum. Useful when the curve is data-driven (Keyframe.ease, +// dropdown selection in UI). +float apply(Ease e, float t); + +// Human-readable name for UI dropdowns. +const char* name(Ease e); + +// Total number of easing modes (for iterating dropdowns). +constexpr int ease_count = 16; + +} // namespace fn::tween diff --git a/cpp/functions/core/tween_curves.md b/cpp/functions/core/tween_curves.md new file mode 100644 index 00000000..7425261a --- /dev/null +++ b/cpp/functions/core/tween_curves.md @@ -0,0 +1,115 @@ +--- +name: tween_curves +kind: function +lang: cpp +domain: core +version: "1.0.0" +purity: pure +signature: "float fn::tween::apply(fn::tween::Ease e, float t) + ~16 named easing functions (linear, in_quad, out_quad, ...)" +description: "Set de funciones de easing puras (Penner) para animaciones e interpolaciones: linear, quad, cubic, expo, elastic, bounce, en variantes in/out/inOut. Header-mostly: el compilador inlinea cada curva en el sitio de llamada. Sin estado, sin I/O." +tags: [animation, easing, tween, penner, math, header-only] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +tested: false +tests: [] +test_file_path: "" +file_path: "cpp/functions/core/tween_curves.cpp" +framework: "" +params: + - name: e + desc: "Curva a aplicar (enum Ease): Linear, InQuad, OutQuad, InOutQuad, InCubic, OutCubic, InOutCubic, InExpo, OutExpo, InOutExpo, InElastic, OutElastic, InOutElastic, InBounce, OutBounce, InOutBounce" + - name: t + desc: "Progreso normalizado en [0,1]. Para t<0 o t>1 algunas curvas extrapolan razonablemente, otras saturan." +output: "f(t) — valor de la curva en t. Para curvas no oscilantes f(0)=0 y f(1)=1. Para elastic/bounce f(0)=0 y f(1)=1 pero f puede salir del rango [0,1] en valores intermedios." +--- + +# tween_curves + +Funciones de easing al estilo Penner. Permiten convertir un progreso lineal `t in [0,1]` en una curva temporal con feel concreto (acelerar, frenar, rebotar, oscilar). Son el ladrillo basico de toda animacion: timeline interpolation, transiciones UI, smooth lerp de uniforms, ramps de color. + +## API + +```cpp +namespace fn::tween { + +enum class Ease { + Linear, + InQuad, OutQuad, InOutQuad, + InCubic, OutCubic, InOutCubic, + InExpo, OutExpo, InOutExpo, + InElastic, OutElastic, InOutElastic, + InBounce, OutBounce, InOutBounce, +}; + +// Funciones inline named (todas (float)->float, t en [0,1]): +inline float linear(float t); +inline float in_quad(float t); inline float out_quad(float t); inline float in_out_quad(float t); +inline float in_cubic(float t); inline float out_cubic(float t); inline float in_out_cubic(float t); +inline float in_expo(float t); inline float out_expo(float t); inline float in_out_expo(float t); +inline float in_elastic(float t); inline float out_elastic(float t); inline float in_out_elastic(float t); +inline float in_bounce(float t); inline float out_bounce(float t); inline float in_out_bounce(float t); + +// Dispatch dinamico por enum (util para data-driven: Keyframe.ease, dropdown UI) +float apply(Ease e, float t); +const char* name(Ease e); // "Linear", "InQuad", ... + +constexpr int ease_count = 16; + +} // namespace fn::tween +``` + +## Ejemplo + +```cpp +#include "core/tween_curves.h" + +// Lerp con easing entre dos colores +float t = elapsed_ms / duration_ms; +float k = fn::tween::out_cubic(t); +ImVec4 c = lerp(c0, c1, k); + +// Data-driven (timeline keyframe) +fn::tween::Ease ease = key.ease; +float k = fn::tween::apply(ease, t_normalized); + +// Dropdown ImGui con todas las curvas +static int idx = 0; +const char* labels[fn::tween::ease_count]; +for (int i = 0; i < fn::tween::ease_count; i++) + labels[i] = fn::tween::name((fn::tween::Ease)i); +ImGui::Combo("Ease", &idx, labels, fn::tween::ease_count); +``` + +## Valores conocidos (smoke tests) + +| Curva | t=0 | t=0.5 | t=1 | +|-------------|-------|--------|-------| +| Linear | 0.000 | 0.500 | 1.000 | +| InQuad | 0.000 | 0.250 | 1.000 | +| OutQuad | 0.000 | 0.750 | 1.000 | +| InOutQuad | 0.000 | 0.500 | 1.000 | +| InCubic | 0.000 | 0.125 | 1.000 | +| OutCubic | 0.000 | 0.875 | 1.000 | +| InOutCubic | 0.000 | 0.500 | 1.000 | +| InExpo | 0.000 | ~0.031 | 1.000 | +| OutExpo | 0.000 | ~0.969 | 1.000 | +| InOutExpo | 0.000 | 0.500 | 1.000 | +| OutBounce | 0.000 | ~0.766 | 1.000 | + +Para todas las curvas: `f(0) == 0` y `f(1) == 1`. Las curvas elastic/bounce pueden salir de [0,1] en valores intermedios — no es un bug, es el overshoot deseado. + +## Decisiones + +- **Header-mostly**: las 16 funciones son `inline` en `.h` para que el compilador inline llamadas en hot paths (animaciones por frame, miles de muestras por curva en plots). +- **`apply(enum, t)` en .cpp**: para no forzar `` enorme en headers; util para casos data-driven. +- **Sin clamp interior**: el caller decide si saturar t. Esto permite efectos de overshoot deliberado. +- **Sin templates**: los easings se usan masivamente con `float`; un `T` genérico complicaría el header sin beneficio. + +## Referencias + +- easings.net (visualizaciones interactivas y formulas) +- Robert Penner — *Motion, Tweening, and Easing* (libro original) diff --git a/dev/issues/0031-cpp-animation-curves.md b/dev/issues/completed/0031-cpp-animation-curves.md similarity index 100% rename from dev/issues/0031-cpp-animation-curves.md rename to dev/issues/completed/0031-cpp-animation-curves.md