Files
fn_registry/cpp/functions/core/timeline.md
T
egutierrez 448765fa15 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.
2026-04-25 21:50:35 +02:00

4.9 KiB

name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, tested, tests, test_file_path, file_path, framework, params, output
name kind lang domain version purity signature description tags uses_functions uses_types returns returns_optional error_type imports tested tests test_file_path file_path framework params output
timeline component cpp core 1.0.0 pure 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) 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.
imgui
timeline
animation
keyframes
daw
tween
tokens_cpp_core
tween_curves_cpp_core
false
imgui
false
cpp/functions/core/timeline.cpp imgui
name desc
id ID ImGui unico
name desc
state TimelineState con tracks (vector<Track>), current_time, duration, playing, loop. Modificada por el widget.
name desc
size Tamaño total. size.x = -1 toma el ancho disponible. size.y >= 100 recomendado.
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

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

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

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.