auto(0129): agents_dashboard — secret_store_cpp_infra + CMakeLists register #4
@@ -0,0 +1,264 @@
|
||||
#include "core/timeline.h"
|
||||
#include "core/tokens.h"
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
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
|
||||
@@ -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 <imgui.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fn {
|
||||
|
||||
struct Keyframe {
|
||||
float time;
|
||||
float value;
|
||||
tween::Ease ease = tween::Ease::Linear;
|
||||
};
|
||||
|
||||
struct Track {
|
||||
std::string name;
|
||||
std::vector<Keyframe> keys;
|
||||
};
|
||||
|
||||
struct TimelineState {
|
||||
std::vector<Track> 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
|
||||
@@ -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<Track>), 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<Keyframe> keys; // ordenadas por time
|
||||
};
|
||||
|
||||
struct fn::TimelineState {
|
||||
std::vector<Track> 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.
|
||||
Reference in New Issue
Block a user