#include "core/timeline.h" #include "core/tokens.h" #include #include #include #include #include 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