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(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            // Timestamp on top
315            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            // Timestamp on bottom
326            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    // The maximum number of lines, 4 pixels gap
366    let max_lines = rect.width() / 4.0;
367    // The minimum grid spacing, 1 ms
368    let mut grid_spacing_ms = 1;
369    // Increase the grid spacing until it's less than the maximum number of lines
370    while state.width_sec as f32 * 1000.0 / grid_spacing_ms as f32 > max_lines {
371        grid_spacing_ms *= 10;
372    }
373    // dbg!(state.sideways_pan_in_points);
374    // dbg!(state.canvas_width_ms);
375    // dbg!(grid_spacing_ms);
376
377    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                // Timestamp on top
429                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                // Timestamp on bottom
440                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    // println!("paint_timeline: {:?}", shapes.len());
457
458    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}