--- 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.