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:
2026-04-25 21:50:35 +02:00
parent b9810a88d4
commit 66f5ca1a4f
3 changed files with 468 additions and 0 deletions
+67
View File
@@ -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