use egui::{
Align2, Color32, Frame, PointerButton, Rect, Rgba, ScrollArea, Shape, Stroke, TextStyle,
emath::GuiRounding, pos2, remap_clamp,
};
use crate::core::{TimelineInfo, color::palettes::manim};
use super::TimelineInfoState;
pub struct TimelineState {
pub total_sec: f64,
pub current_sec: f64,
pub width_sec: f64,
pub offset_points: f32,
pub timeline_infos: Vec<TimelineInfo>,
}
#[allow(unused)]
impl TimelineState {
pub fn new(total_sec: f64, timeline_infos: Vec<TimelineInfo>) -> Self {
Self {
total_sec,
current_sec: 0.0,
width_sec: total_sec,
offset_points: 0.0,
timeline_infos,
}
}
pub fn ui_preview_timeline(&mut self, ui: &mut egui::Ui) {
const PREVIEW_HEIGHT: f32 = 30.0;
Frame::canvas(ui.style()).show(ui, |ui| {
let mut rect = ui.available_rect_before_wrap();
rect.set_height(PREVIEW_HEIGHT);
let font_id = TextStyle::Body.resolve(ui.style());
let painter = ui.painter_at(rect);
let shape_id = painter.add(Shape::Noop);
painter.set(
shape_id,
Shape::Vec(paint_time_grid(
rect,
&painter,
font_id,
(self.total_sec * 1000.0) as i64,
(self.current_sec * 1000.0) as i64,
)),
);
ui.allocate_rect(rect, egui::Sense::hover());
});
}
pub fn interact_preview_timeline(&mut self, info: &TimelineInfoState) {
let response = &info.response;
if response.clicked()
&& let Some(mouse_pos) = response.hover_pos()
{}
}
pub fn ui_main_timeline(&mut self, ui: &mut egui::Ui) {
Frame::canvas(ui.style()).show(ui, |ui| {
let available_height = ui.max_rect().bottom() - ui.min_rect().bottom();
ScrollArea::vertical().show(ui, |ui| {
let mut canvas = ui.available_rect_before_wrap();
canvas.max.y = f32::INFINITY;
let response = ui.interact(
canvas,
ui.id().with("canvas"),
egui::Sense::click_and_drag(),
);
let info = TimelineInfoState {
ctx: ui.ctx().clone(),
canvas,
response,
painter: ui.painter_at(canvas),
text_height: 15.0,
font_id: TextStyle::Body.resolve(ui.style()),
};
self.interact_main_timeline(&info);
let timeline_shape_id = info.painter.add(Shape::Noop);
let max_y = ui_canvas(self, &info);
let mut used_rect = canvas;
used_rect.max.y = max_y.max(used_rect.min.y + available_height);
info.painter.set(
timeline_shape_id,
Shape::Vec(paint_timeline(
&info,
used_rect,
self,
(self.current_sec * 1000.0) as i64,
)),
);
ui.allocate_rect(used_rect, egui::Sense::hover());
});
});
}
pub fn interact_main_timeline(&mut self, info: &TimelineInfoState) {
let response = &info.response;
if response.drag_delta().x != 0.0 {
self.offset_points += response.drag_delta().x;
}
if response.hovered() {
let mut zoom_factor = info.ctx.input(|i| i.zoom_delta_2d().x);
if response.dragged_by(PointerButton::Secondary) {
let delta = (response.drag_delta().y * 0.01).exp();
zoom_factor *= delta;
}
if zoom_factor != 1.0 {
let old_width_sec = self.width_sec;
self.width_sec /= zoom_factor as f64;
self.width_sec = self.width_sec.clamp(100.0 / 1000.0, self.total_sec);
zoom_factor = (old_width_sec / self.width_sec) as f32;
if let Some(mouse_pos) = response.hover_pos() {
let zoom_center = mouse_pos.x - info.canvas.min.x;
self.offset_points =
(self.offset_points - zoom_center) * zoom_factor + zoom_center;
}
}
}
if response.double_clicked() {
}
}
}
pub fn ui_canvas(state: &mut TimelineState, info: &TimelineInfoState) -> f32 {
let line_height = 16.0;
let gap = 4.0;
let mut start_y = info.canvas.top();
start_y += info.text_height; let end_y = start_y + state.timeline_infos.len() as f32 * (line_height + gap);
for (idx, timeline_info) in state.timeline_infos.iter().enumerate() {
let local_y = idx as f32 * (line_height + gap);
let top_y = start_y + local_y;
let bottom_y = top_y + line_height;
for animation_info in &timeline_info.animation_infos {
let start_x = info.point_from_ms(state, (animation_info.range.start * 1000.0) as i64);
let end_x = info.point_from_ms(state, (animation_info.range.end * 1000.0) as i64);
if info.canvas.max.x < start_x || end_x < info.canvas.min.x {
continue;
}
let rect = Rect::from_min_max(pos2(start_x, top_y), pos2(end_x, bottom_y));
let rect_color = if animation_info
.anim_name
.starts_with("ranim_core::animation::Static")
{
manim::YELLOW_C.to_rgba8()
} else {
manim::BLUE_C.to_rgba8()
};
info.painter.rect_filled(
rect,
4.0,
egui::Rgba::from_srgba_unmultiplied(
rect_color.r,
rect_color.g,
rect_color.b,
(0.9 * 255.0) as u8,
),
);
let wide_enough_for_text = end_x - start_x > 32.0;
if wide_enough_for_text {
let text = format!(
"{} {:6.3} s",
animation_info.anim_name,
animation_info.range.end - animation_info.range.start
);
let painter = info.painter.with_clip_rect(rect.intersect(info.canvas));
let pos = pos2(start_x + 4.0, top_y + 0.5 * (16.0 - info.text_height));
let pos = pos.round_to_pixels(painter.pixels_per_point());
const TEXT_COLOR: Color32 = Color32::BLACK;
painter.text(
pos,
Align2::LEFT_TOP,
text,
info.font_id.clone(),
TEXT_COLOR,
);
}
}
}
end_y
}
pub fn paint_time_grid(
rect: egui::Rect,
painter: &egui::Painter,
font_id: egui::FontId,
width_ms: i64,
current_ms: i64,
) -> Vec<egui::Shape> {
if width_ms <= 0 {
return vec![];
}
let mut shapes = vec![];
let alpha_multiplier = 0.3;
let max_lines = (rect.width() / 4.0).floor() as i64;
let mut grid_spacing_ms = 1;
while width_ms / grid_spacing_ms > max_lines {
grid_spacing_ms *= 10;
}
let num_tiny_lines = width_ms / grid_spacing_ms;
let zoom_factor = remap_clamp(
num_tiny_lines as f32,
(0.1 * max_lines as f32)..=max_lines as f32,
1.0..=0.0,
);
let zoom_factor = zoom_factor * zoom_factor;
let big_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.5..=1.0);
let medium_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.1..=0.5);
let tiny_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.0..=0.1);
let ppms = rect.width() / width_ms as f32;
(0..num_tiny_lines).for_each(|i| {
let ms = grid_spacing_ms * i;
let line_x = rect.min.x + ms as f32 * ppms;
let big_line = ms % (grid_spacing_ms * 100) == 0;
let medium_line = ms % (grid_spacing_ms * 10) == 0;
let line_alpha = if big_line {
big_alpha
} else if medium_line {
medium_alpha
} else {
tiny_alpha
};
shapes.push(egui::Shape::line_segment(
[pos2(line_x, rect.min.y), pos2(line_x, rect.max.y)],
Stroke::new(1.0, Rgba::from_white_alpha(line_alpha * alpha_multiplier)),
));
let text_alpha = if big_line {
medium_alpha
} else if medium_line {
tiny_alpha
} else {
0.0
};
if text_alpha > 0.0 {
let text = grid_text(ms);
let text_x = line_x + 4.0;
let text_color = Rgba::from_white_alpha((text_alpha * 2.0).min(1.0)).into();
painter.fonts_mut(|f| {
shapes.push(egui::Shape::text(
f,
pos2(text_x, rect.min.y),
Align2::LEFT_TOP,
&text,
font_id.clone(),
text_color,
));
});
painter.fonts_mut(|f| {
shapes.push(egui::Shape::text(
f,
pos2(text_x, rect.max.y - 12.0),
Align2::LEFT_TOP,
&text,
font_id.clone(),
text_color,
));
});
}
});
let current_line_x = current_ms as f32 * ppms;
shapes.push(egui::Shape::line_segment(
[
pos2(current_line_x, rect.min.y),
pos2(current_line_x, rect.max.y),
],
Stroke::new(1.0, Rgba::from_white_alpha(alpha_multiplier)),
));
shapes
}
pub fn paint_timeline(
info: &TimelineInfoState,
rect: egui::Rect,
state: &TimelineState,
current_ms: i64,
) -> Vec<egui::Shape> {
let mut shapes = vec![];
if state.width_sec <= 0.0 {
return shapes;
}
let alpha_multiplier = 0.3;
let start_ms = 0;
let max_lines = rect.width() / 4.0;
let mut grid_spacing_ms = 1;
while state.width_sec as f32 * 1000.0 / grid_spacing_ms as f32 > max_lines {
grid_spacing_ms *= 10;
}
let num_tiny_lines = state.width_sec as f32 * 1000.0 / grid_spacing_ms as f32;
let zoom_factor = remap_clamp(num_tiny_lines, (0.1 * max_lines)..=max_lines, 1.0..=0.0);
let zoom_factor = zoom_factor * zoom_factor;
let big_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.5..=1.0);
let medium_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.1..=0.5);
let tiny_alpha = remap_clamp(zoom_factor, 0.0..=1.0, 0.0..=0.1);
let mut grid_ms = 0;
let current_line_x = info.point_from_ms(state, current_ms);
shapes.push(egui::Shape::line_segment(
[
pos2(current_line_x, rect.min.y),
pos2(current_line_x, rect.max.y),
],
Stroke::new(1.0, Rgba::from_white_alpha(alpha_multiplier)),
));
loop {
let line_x = info.point_from_ms(state, start_ms + grid_ms);
if line_x > rect.max.x {
break;
}
if rect.min.x <= line_x {
let big_line = grid_ms % (grid_spacing_ms * 100) == 0;
let medium_line = grid_ms % (grid_spacing_ms * 10) == 0;
let line_alpha = if big_line {
big_alpha
} else if medium_line {
medium_alpha
} else {
tiny_alpha
};
shapes.push(egui::Shape::line_segment(
[pos2(line_x, rect.min.y), pos2(line_x, rect.max.y)],
Stroke::new(1.0, Rgba::from_white_alpha(line_alpha * alpha_multiplier)),
));
let text_alpha = if big_line {
medium_alpha
} else if medium_line {
tiny_alpha
} else {
0.0
};
if text_alpha > 0.0 {
let text = grid_text(grid_ms);
let text_x = line_x + 4.0;
let text_color = Rgba::from_white_alpha((text_alpha * 2.0).min(1.0)).into();
info.painter.fonts_mut(|f| {
shapes.push(egui::Shape::text(
f,
pos2(text_x, rect.min.y),
Align2::LEFT_TOP,
&text,
info.font_id.clone(),
text_color,
));
});
info.painter.fonts_mut(|f| {
shapes.push(egui::Shape::text(
f,
pos2(text_x, rect.max.y - info.text_height),
Align2::LEFT_TOP,
&text,
info.font_id.clone(),
text_color,
));
});
}
}
grid_ms += grid_spacing_ms;
}
shapes
}
fn grid_text(grid_ms: i64) -> String {
let sec = grid_ms as f64 / 1000.0;
if grid_ms % 1_000 == 0 {
format!("{sec:.0} s")
} else if grid_ms % 100 == 0 {
format!("{sec:.1} s")
} else if grid_ms % 10 == 0 {
format!("{sec:.2} s")
} else {
format!("{sec:.3} s")
}
}