1use egui::{
2 Align2, Color32, Frame, PointerButton, Rect, Rgba, ScrollArea, Shape, Stroke, TextStyle,
3 emath::GuiRounding, pos2, remap_clamp,
4};
5
6use crate::core::{TimelineInfo, color::palettes::manim};
7
8use super::TimelineInfoState;
9
10pub struct TimelineState {
11 pub total_sec: f64,
12 pub current_sec: f64,
13 pub width_sec: f64,
14 pub offset_points: f32,
15 pub timeline_infos: Vec<TimelineInfo>,
16}
17
18#[allow(unused)]
19impl TimelineState {
20 pub fn new(total_sec: f64, timeline_infos: Vec<TimelineInfo>) -> Self {
21 Self {
22 total_sec,
23 current_sec: 0.0,
24 width_sec: total_sec,
25 offset_points: 0.0,
26 timeline_infos,
27 }
28 }
29 pub fn ui_preview_timeline(&mut self, ui: &mut egui::Ui) {
30 const PREVIEW_HEIGHT: f32 = 30.0;
31
32 Frame::canvas(ui.style()).show(ui, |ui| {
33 let mut rect = ui.available_rect_before_wrap();
34 rect.set_height(PREVIEW_HEIGHT);
36
37 let font_id = TextStyle::Body.resolve(ui.style());
38 let painter = ui.painter_at(rect);
39 let shape_id = painter.add(Shape::Noop);
40 painter.set(
41 shape_id,
42 Shape::Vec(paint_time_grid(
43 rect,
44 &painter,
45 font_id,
46 (self.total_sec * 1000.0) as i64,
47 (self.current_sec * 1000.0) as i64,
48 )),
49 );
50
51 ui.allocate_rect(rect, egui::Sense::hover());
52 });
53 }
54
55 pub fn interact_preview_timeline(&mut self, info: &TimelineInfoState) {
56 let response = &info.response;
57
58 if response.clicked()
59 && let Some(mouse_pos) = response.hover_pos()
60 {}
61
62 }
91
92 pub fn ui_main_timeline(&mut self, ui: &mut egui::Ui) {
93 Frame::canvas(ui.style()).show(ui, |ui| {
94 let available_height = ui.max_rect().bottom() - ui.min_rect().bottom();
95 ScrollArea::vertical().show(ui, |ui| {
96 let mut canvas = ui.available_rect_before_wrap();
97 canvas.max.y = f32::INFINITY;
98
99 let response = ui.interact(
100 canvas,
101 ui.id().with("canvas"),
102 egui::Sense::click_and_drag(),
103 );
104 let info = TimelineInfoState {
105 ctx: ui.ctx().clone(),
106 canvas,
107 response,
108 painter: ui.painter_at(canvas),
109 text_height: 15.0,
110 font_id: TextStyle::Body.resolve(ui.style()),
111 };
112
113 self.interact_main_timeline(&info);
114
115 let timeline_shape_id = info.painter.add(Shape::Noop);
116
117 let max_y = ui_canvas(self, &info);
118 let mut used_rect = canvas;
119 used_rect.max.y = max_y.max(used_rect.min.y + available_height);
120
121 info.painter.set(
122 timeline_shape_id,
123 Shape::Vec(paint_timeline(
124 &info,
125 used_rect,
126 self,
127 (self.current_sec * 1000.0) as i64,
128 )),
129 );
130
131 ui.allocate_rect(used_rect, egui::Sense::hover());
132 });
133 });
134 }
135
136 pub fn interact_main_timeline(&mut self, info: &TimelineInfoState) {
137 let response = &info.response;
138
139 if response.drag_delta().x != 0.0 {
140 self.offset_points += response.drag_delta().x;
141 }
142
143 if response.hovered() {
144 let mut zoom_factor = info.ctx.input(|i| i.zoom_delta_2d().x);
145
146 if response.dragged_by(PointerButton::Secondary) {
147 let delta = (response.drag_delta().y * 0.01).exp();
148 zoom_factor *= delta;
150 }
151
152 if zoom_factor != 1.0 {
154 let old_width_sec = self.width_sec;
155 self.width_sec /= zoom_factor as f64;
156 self.width_sec = self.width_sec.clamp(100.0 / 1000.0, self.total_sec);
157 zoom_factor = (old_width_sec / self.width_sec) as f32;
158 if let Some(mouse_pos) = response.hover_pos() {
159 let zoom_center = mouse_pos.x - info.canvas.min.x;
160 self.offset_points =
161 (self.offset_points - zoom_center) * zoom_factor + zoom_center;
162 }
163 }
164 }
165
166 if response.double_clicked() {
168 }
170 }
171}
172
173pub fn ui_canvas(state: &mut TimelineState, info: &TimelineInfoState) -> f32 {
174 let line_height = 16.0;
175 let gap = 4.0;
176
177 let mut start_y = info.canvas.top();
178 start_y += info.text_height; let end_y = start_y + state.timeline_infos.len() as f32 * (line_height + gap);
180
181 for (idx, timeline_info) in state.timeline_infos.iter().enumerate() {
182 let local_y = idx as f32 * (line_height + gap);
183
184 let top_y = start_y + local_y;
185 let bottom_y = top_y + line_height;
186
187 for animation_info in &timeline_info.animation_infos {
188 let start_x = info.point_from_ms(state, (animation_info.range.start * 1000.0) as i64);
192 let end_x = info.point_from_ms(state, (animation_info.range.end * 1000.0) as i64);
193
194 if info.canvas.max.x < start_x || end_x < info.canvas.min.x {
195 continue;
196 }
197
198 let rect = Rect::from_min_max(pos2(start_x, top_y), pos2(end_x, bottom_y));
199 let rect_color = if animation_info
200 .anim_name
201 .starts_with("ranim_core::animation::Static")
202 {
203 manim::YELLOW_C.to_rgba8()
204 } else {
205 manim::BLUE_C.to_rgba8()
206 };
207
208 info.painter.rect_filled(
209 rect,
210 4.0,
211 egui::Rgba::from_srgba_unmultiplied(
212 rect_color.r,
213 rect_color.g,
214 rect_color.b,
215 (0.9 * 255.0) as u8,
216 ),
217 );
218
219 let wide_enough_for_text = end_x - start_x > 32.0;
220 if wide_enough_for_text {
221 let text = format!(
222 "{} {:6.3} s",
223 animation_info.anim_name,
224 animation_info.range.end - animation_info.range.start
225 );
226
227 let painter = info.painter.with_clip_rect(rect.intersect(info.canvas));
228
229 let pos = pos2(start_x + 4.0, top_y + 0.5 * (16.0 - info.text_height));
230 let pos = pos.round_to_pixels(painter.pixels_per_point());
231 const TEXT_COLOR: Color32 = Color32::BLACK;
232 painter.text(
233 pos,
234 Align2::LEFT_TOP,
235 text,
236 info.font_id.clone(),
237 TEXT_COLOR,
238 );
239 }
240 }
241 }
242
243 end_y
244}
245
246pub fn paint_time_grid(
247 rect: egui::Rect,
248 painter: &egui::Painter,
249 font_id: egui::FontId,
250 width_ms: i64,
251 current_ms: i64,
252) -> Vec<egui::Shape> {
253 if width_ms <= 0 {
254 return vec![];
255 }
256
257 let mut shapes = vec![];
258
259 let alpha_multiplier = 0.3;
260
261 let max_lines = (rect.width() / 4.0).floor() as i64;
263 let mut grid_spacing_ms = 1;
265 while width_ms / grid_spacing_ms > max_lines {
267 grid_spacing_ms *= 10;
268 }
269
270 let num_tiny_lines = width_ms / grid_spacing_ms;
271 let zoom_factor = remap_clamp(
272 num_tiny_lines as f32,
273 (0.1 * max_lines as f32)..=max_lines as f32,
274 1.0..=0.0,
275 );
276 let zoom_factor = zoom_factor * zoom_factor;
277 let big_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.5..=1.0);
278 let medium_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.1..=0.5);
279 let tiny_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.0..=0.1);
280
281 let ppms = rect.width() / width_ms as f32;
282 (0..num_tiny_lines).for_each(|i| {
283 let ms = grid_spacing_ms * i;
284 let line_x = rect.min.x + ms as f32 * ppms;
285
286 let big_line = ms % (grid_spacing_ms * 100) == 0;
287 let medium_line = ms % (grid_spacing_ms * 10) == 0;
288
289 let line_alpha = if big_line {
290 big_alpha
291 } else if medium_line {
292 medium_alpha
293 } else {
294 tiny_alpha
295 };
296
297 shapes.push(egui::Shape::line_segment(
298 [pos2(line_x, rect.min.y), pos2(line_x, rect.max.y)],
299 Stroke::new(1.0, Rgba::from_white_alpha(line_alpha * alpha_multiplier)),
300 ));
301
302 let text_alpha = if big_line {
303 medium_alpha
304 } else if medium_line {
305 tiny_alpha
306 } else {
307 0.0
308 };
309
310 if text_alpha > 0.0 {
311 let text = grid_text(ms);
312 let text_x = line_x + 4.0;
313 let text_color = Rgba::from_white_alpha((text_alpha * 2.0).min(1.0)).into();
314 painter.fonts_mut(|f| {
316 shapes.push(egui::Shape::text(
317 f,
318 pos2(text_x, rect.min.y),
319 Align2::LEFT_TOP,
320 &text,
321 font_id.clone(),
322 text_color,
323 ));
324 });
325 painter.fonts_mut(|f| {
327 shapes.push(egui::Shape::text(
328 f,
329 pos2(text_x, rect.max.y - 12.0),
330 Align2::LEFT_TOP,
331 &text,
332 font_id.clone(),
333 text_color,
334 ));
335 });
336 }
337 });
338
339 let current_line_x = current_ms as f32 * ppms;
340 shapes.push(egui::Shape::line_segment(
341 [
342 pos2(current_line_x, rect.min.y),
343 pos2(current_line_x, rect.max.y),
344 ],
345 Stroke::new(1.0, Rgba::from_white_alpha(alpha_multiplier)),
346 ));
347 shapes
348}
349
350pub fn paint_timeline(
351 info: &TimelineInfoState,
352 rect: egui::Rect,
353 state: &TimelineState,
354 current_ms: i64,
355) -> Vec<egui::Shape> {
356 let mut shapes = vec![];
357
358 if state.width_sec <= 0.0 {
359 return shapes;
360 }
361
362 let alpha_multiplier = 0.3;
363
364 let start_ms = 0;
365 let max_lines = rect.width() / 4.0;
367 let mut grid_spacing_ms = 1;
369 while state.width_sec as f32 * 1000.0 / grid_spacing_ms as f32 > max_lines {
371 grid_spacing_ms *= 10;
372 }
373 let num_tiny_lines = state.width_sec as f32 * 1000.0 / grid_spacing_ms as f32;
378 let zoom_factor = remap_clamp(num_tiny_lines, (0.1 * max_lines)..=max_lines, 1.0..=0.0);
379 let zoom_factor = zoom_factor * zoom_factor;
380 let big_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.5..=1.0);
381 let medium_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.1..=0.5);
382 let tiny_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.0..=0.1);
383
384 let mut grid_ms = 0;
385
386 let current_line_x = info.point_from_ms(state, current_ms);
387 shapes.push(egui::Shape::line_segment(
388 [
389 pos2(current_line_x, rect.min.y),
390 pos2(current_line_x, rect.max.y),
391 ],
392 Stroke::new(1.0, Rgba::from_white_alpha(alpha_multiplier)),
393 ));
394 loop {
395 let line_x = info.point_from_ms(state, start_ms + grid_ms);
396 if line_x > rect.max.x {
397 break;
398 }
399 if rect.min.x <= line_x {
400 let big_line = grid_ms % (grid_spacing_ms * 100) == 0;
401 let medium_line = grid_ms % (grid_spacing_ms * 10) == 0;
402
403 let line_alpha = if big_line {
404 big_alpha
405 } else if medium_line {
406 medium_alpha
407 } else {
408 tiny_alpha
409 };
410
411 shapes.push(egui::Shape::line_segment(
412 [pos2(line_x, rect.min.y), pos2(line_x, rect.max.y)],
413 Stroke::new(1.0, Rgba::from_white_alpha(line_alpha * alpha_multiplier)),
414 ));
415
416 let text_alpha = if big_line {
417 medium_alpha
418 } else if medium_line {
419 tiny_alpha
420 } else {
421 0.0
422 };
423
424 if text_alpha > 0.0 {
425 let text = grid_text(grid_ms);
426 let text_x = line_x + 4.0;
427 let text_color = Rgba::from_white_alpha((text_alpha * 2.0).min(1.0)).into();
428 info.painter.fonts_mut(|f| {
430 shapes.push(egui::Shape::text(
431 f,
432 pos2(text_x, rect.min.y),
433 Align2::LEFT_TOP,
434 &text,
435 info.font_id.clone(),
436 text_color,
437 ));
438 });
439 info.painter.fonts_mut(|f| {
441 shapes.push(egui::Shape::text(
442 f,
443 pos2(text_x, rect.max.y - info.text_height),
444 Align2::LEFT_TOP,
445 &text,
446 info.font_id.clone(),
447 text_color,
448 ));
449 });
450 }
451 }
452
453 grid_ms += grid_spacing_ms;
454 }
455
456 shapes
459}
460
461fn grid_text(grid_ms: i64) -> String {
462 let sec = grid_ms as f64 / 1000.0;
463 if grid_ms % 1_000 == 0 {
464 format!("{sec:.0} s")
465 } else if grid_ms % 100 == 0 {
466 format!("{sec:.1} s")
467 } else if grid_ms % 10 == 0 {
468 format!("{sec:.2} s")
469 } else {
470 format!("{sec:.3} s")
471 }
472}