feat(core): add timeline — DAW-style keyframe widget
Timeline widget with:
- Header: play/pause + reset + duration drag + loop checkbox
- Ruler: 0.5s ticks, scrub via click+drag
- Tracks: horizontal rows with diamond-shaped draggable keyframes
- Playhead: vertical primary_light line + ruler triangle marker
State and types:
- Keyframe { time, value, ease }
- Track { name, vector<Keyframe> }
- TimelineState { tracks, current_time, duration, playing, loop }
Pure functions:
- track_value_at(track, t): interp between keys, ease applied via the
destination keyframe (Maya/AfterEffects convention)
- timeline_update(state, dt): advance current_time, wrap or saturate
Render with fn_tokens for visual coherence with the rest of the design
system. Keys are sorted by time on every changed frame to keep order
consistent during drag.
This commit is contained in:
@@ -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