ranim/cmd/preview/
timeline.rs

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_bottom(ui.min_rect().bottom());
35            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        // if response.drag_delta().x != 0.0 {
63        //     self.offset_points += response.drag_delta().x;
64        // }
65
66        // if response.hovered() {
67        //     let mut zoom_factor = info.ctx.input(|i| i.zoom_delta_2d().x);
68
69        //     if response.dragged_by(PointerButton::Secondary) {
70        //         let delta = (response.drag_delta().y * 0.01).exp();
71        //         // dbg!(delta);
72        //         zoom_factor *= delta;
73        //     }
74
75        //     // dbg!(state.canvas_width_ms);
76        //     if zoom_factor != 1.0 {
77        //         self.width_sec /= zoom_factor as f64;
78        //         if let Some(mouse_pos) = response.hover_pos() {
79        //             let zoom_center = mouse_pos.x - info.canvas.min.x;
80        //             self.offset_points =
81        //                 (self.offset_points - zoom_center) * zoom_factor + zoom_center;
82        //         }
83        //     }
84        // }
85
86        // // Reset view
87        // if response.double_clicked() {
88        //     // TODO
89        // }
90    }
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                // dbg!(delta);
149                zoom_factor *= delta;
150            }
151
152            // dbg!(state.canvas_width_ms);
153            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        // Reset view
167        if response.double_clicked() {
168            // TODO
169        }
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; // Time labels
179    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            // if animation_info.anim_name.as_str() == "Static" {
189            //     continue;
190            // }
191            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    // The maximum number of lines, 4 pixels gap
262    let max_lines = (rect.width() / 4.0).floor() as i64;
263    // The minimum grid spacing, 1 ms
264    let mut grid_spacing_ms = 1;
265    // Increase the grid spacing until it's less than the maximum number of lines
266    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(
300                1.0f32,
301                Rgba::from_white_alpha(line_alpha * alpha_multiplier),
302            ),
303        ));
304
305        let text_alpha = if big_line {
306            medium_alpha
307        } else if medium_line {
308            tiny_alpha
309        } else {
310            0.0
311        };
312
313        if text_alpha > 0.0 {
314            let text = grid_text(ms);
315            let text_x = line_x + 4.0;
316            let text_color = Rgba::from_white_alpha((text_alpha * 2.0).min(1.0)).into();
317            // Timestamp on top
318            painter.fonts_mut(|f| {
319                shapes.push(egui::Shape::text(
320                    f,
321                    pos2(text_x, rect.min.y),
322                    Align2::LEFT_TOP,
323                    &text,
324                    font_id.clone(),
325                    text_color,
326                ));
327            });
328            // Timestamp on bottom
329            painter.fonts_mut(|f| {
330                shapes.push(egui::Shape::text(
331                    f,
332                    pos2(text_x, rect.max.y - 12.0),
333                    Align2::LEFT_TOP,
334                    &text,
335                    font_id.clone(),
336                    text_color,
337                ));
338            });
339        }
340    });
341
342    let current_line_x = current_ms as f32 * ppms;
343    shapes.push(egui::Shape::line_segment(
344        [
345            pos2(current_line_x, rect.min.y),
346            pos2(current_line_x, rect.max.y),
347        ],
348        Stroke::new(1.0f32, Rgba::from_white_alpha(alpha_multiplier)),
349    ));
350    shapes
351}
352
353pub fn paint_timeline(
354    info: &TimelineInfoState,
355    rect: egui::Rect,
356    state: &TimelineState,
357    current_ms: i64,
358) -> Vec<egui::Shape> {
359    let mut shapes = vec![];
360
361    if state.width_sec <= 0.0 {
362        return shapes;
363    }
364
365    let alpha_multiplier = 0.3;
366
367    let start_ms = 0;
368    // The maximum number of lines, 4 pixels gap
369    let max_lines = rect.width() / 4.0;
370    // The minimum grid spacing, 1 ms
371    let mut grid_spacing_ms = 1;
372    // Increase the grid spacing until it's less than the maximum number of lines
373    while state.width_sec as f32 * 1000.0 / grid_spacing_ms as f32 > max_lines {
374        grid_spacing_ms *= 10;
375    }
376    // dbg!(state.sideways_pan_in_points);
377    // dbg!(state.canvas_width_ms);
378    // dbg!(grid_spacing_ms);
379
380    let num_tiny_lines = state.width_sec as f32 * 1000.0 / grid_spacing_ms as f32;
381    let zoom_factor = remap_clamp(num_tiny_lines, (0.1 * max_lines)..=max_lines, 1.0..=0.0);
382    let zoom_factor = zoom_factor * zoom_factor;
383    let big_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.5..=1.0);
384    let medium_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.1..=0.5);
385    let tiny_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.0..=0.1);
386
387    let mut grid_ms = 0;
388
389    let current_line_x = info.point_from_ms(state, current_ms);
390    shapes.push(egui::Shape::line_segment(
391        [
392            pos2(current_line_x, rect.min.y),
393            pos2(current_line_x, rect.max.y),
394        ],
395        Stroke::new(1.0f32, Rgba::from_white_alpha(alpha_multiplier)),
396    ));
397    loop {
398        let line_x = info.point_from_ms(state, start_ms + grid_ms);
399        if line_x > rect.max.x {
400            break;
401        }
402        if rect.min.x <= line_x {
403            let big_line = grid_ms % (grid_spacing_ms * 100) == 0;
404            let medium_line = grid_ms % (grid_spacing_ms * 10) == 0;
405
406            let line_alpha = if big_line {
407                big_alpha
408            } else if medium_line {
409                medium_alpha
410            } else {
411                tiny_alpha
412            };
413
414            shapes.push(egui::Shape::line_segment(
415                [pos2(line_x, rect.min.y), pos2(line_x, rect.max.y)],
416                Stroke::new(
417                    1.0f32,
418                    Rgba::from_white_alpha(line_alpha * alpha_multiplier),
419                ),
420            ));
421
422            let text_alpha = if big_line {
423                medium_alpha
424            } else if medium_line {
425                tiny_alpha
426            } else {
427                0.0
428            };
429
430            if text_alpha > 0.0 {
431                let text = grid_text(grid_ms);
432                let text_x = line_x + 4.0;
433                let text_color = Rgba::from_white_alpha((text_alpha * 2.0).min(1.0)).into();
434                // Timestamp on top
435                info.painter.fonts_mut(|f| {
436                    shapes.push(egui::Shape::text(
437                        f,
438                        pos2(text_x, rect.min.y),
439                        Align2::LEFT_TOP,
440                        &text,
441                        info.font_id.clone(),
442                        text_color,
443                    ));
444                });
445                // Timestamp on bottom
446                info.painter.fonts_mut(|f| {
447                    shapes.push(egui::Shape::text(
448                        f,
449                        pos2(text_x, rect.max.y - info.text_height),
450                        Align2::LEFT_TOP,
451                        &text,
452                        info.font_id.clone(),
453                        text_color,
454                    ));
455                });
456            }
457        }
458
459        grid_ms += grid_spacing_ms;
460    }
461
462    // println!("paint_timeline: {:?}", shapes.len());
463
464    shapes
465}
466
467fn grid_text(grid_ms: i64) -> String {
468    let sec = grid_ms as f64 / 1000.0;
469    if grid_ms % 1_000 == 0 {
470        format!("{sec:.0} s")
471    } else if grid_ms % 100 == 0 {
472        format!("{sec:.1} s")
473    } else if grid_ms % 10 == 0 {
474        format!("{sec:.2} s")
475    } else {
476        format!("{sec:.3} s")
477    }
478}