use eframe::{run_native, App, CreationContext};
use egui_graphs::{
default_edge_transform, default_node_transform, to_graph_custom, DefaultEdgeShape, Graph,
GraphView, SettingsInteraction, SettingsNavigation,
};
use node::NodeShapeAnimated;
use petgraph::{
stable_graph::{DefaultIx, StableGraph},
Directed,
};
const GLYPH_CLOCKWISE: &str = "↻";
const GLYPH_ANTICLOCKWISE: &str = "↺";
pub struct AnimatedNodesApp {
g: Graph<node::NodeData, (), Directed, DefaultIx, NodeShapeAnimated>,
}
impl AnimatedNodesApp {
fn new(_: &CreationContext<'_>) -> Self {
let g = generate_graph();
Self {
g: to_graph_custom(
&g,
|n| {
if n.payload().clockwise {
default_node_transform(n);
n.set_label(GLYPH_CLOCKWISE.to_string());
} else {
default_node_transform(n);
n.set_label(GLYPH_ANTICLOCKWISE.to_string());
}
},
default_edge_transform,
),
}
}
}
impl App for AnimatedNodesApp {
fn ui(&mut self, ui: &mut egui::Ui, _: &mut eframe::Frame) {
egui::CentralPanel::default().show_inside(ui, |ui| {
ui.add(
&mut GraphView::<_, _, _, _, NodeShapeAnimated, DefaultEdgeShape>::new(&mut self.g)
.with_navigations(
&SettingsNavigation::default()
.with_fit_to_screen_enabled(false)
.with_zoom_and_pan_enabled(true),
)
.with_interactions(
&SettingsInteraction::default()
.with_dragging_enabled(true)
.with_node_selection_enabled(true)
.with_edge_selection_enabled(true),
),
);
});
}
}
fn generate_graph() -> StableGraph<node::NodeData, ()> {
let mut g = StableGraph::new();
let a = g.add_node(node::NodeData { clockwise: true });
let b = g.add_node(node::NodeData { clockwise: false });
let c = g.add_node(node::NodeData { clockwise: false });
g.add_edge(a, b, ());
g.add_edge(b, c, ());
g.add_edge(c, a, ());
g
}
fn main() {
let native_options = eframe::NativeOptions::default();
run_native(
"animated",
native_options,
Box::new(|cc| Ok(Box::new(AnimatedNodesApp::new(cc)))),
)
.unwrap();
}
mod node {
use std::time::Instant;
use egui::{
emath::Rot2, epaint::TextShape, Color32, FontFamily, FontId, Pos2, Rect, Shape, Stroke,
Vec2,
};
use egui_graphs::{DisplayNode, NodeProps};
use petgraph::{stable_graph::IndexType, EdgeType};
pub trait IsClockwise {
fn get_is_clockwise(&self) -> bool;
}
#[derive(Clone, Debug)]
pub struct NodeData {
pub clockwise: bool,
}
impl IsClockwise for NodeData {
fn get_is_clockwise(&self) -> bool {
self.clockwise
}
}
#[derive(Clone)]
pub struct NodeShapeAnimated {
label: String,
loc: Pos2,
dragged: bool,
clockwise: bool,
angle_rad: f32,
speed_per_second: f32,
last_time_update: Option<Instant>,
size: f32,
}
impl NodeShapeAnimated {
pub fn get_rotation_increment(&mut self) -> f32 {
let now = Instant::now();
let mult = if self.clockwise { 1. } else { -1. };
if let Some(last_time) = self.last_time_update {
self.last_time_update = Some(now);
let seconds_passed = now.duration_since(last_time);
seconds_passed.as_secs_f32() * self.speed_per_second * mult
} else {
self.last_time_update = Some(now);
0.
}
}
}
impl<N: Clone + IsClockwise> From<NodeProps<N>> for NodeShapeAnimated {
fn from(node_props: NodeProps<N>) -> Self {
Self {
label: node_props.label.clone(),
loc: node_props.location(),
dragged: node_props.dragged,
clockwise: node_props.payload.get_is_clockwise(),
angle_rad: Default::default(),
last_time_update: Option::default(),
speed_per_second: 1.,
size: 30.,
}
}
}
impl<N: Clone + IsClockwise, E: Clone, Ty: EdgeType, Ix: IndexType> DisplayNode<N, E, Ty, Ix>
for NodeShapeAnimated
{
fn is_inside(&self, pos: Pos2) -> bool {
let rotated_pos = rotate_point_around(self.loc, pos, -self.angle_rad);
let rect = Rect::from_center_size(self.loc, Vec2::new(self.size, self.size));
rect.contains(rotated_pos)
}
fn closest_boundary_point(&self, dir: Vec2) -> Pos2 {
let rotated_dir = rotate_vector(dir, -self.angle_rad);
let intersection_point = find_intersection(self.loc, self.size, rotated_dir);
rotate_point_around(self.loc, intersection_point, self.angle_rad)
}
fn shapes(&mut self, ctx: &egui_graphs::DrawContext) -> Vec<egui::Shape> {
let center = ctx.meta.canvas_to_screen_pos(self.loc);
let size = ctx.meta.canvas_to_screen_size(self.size);
let rect_default = Rect::from_center_size(center, Vec2::new(size, size));
let color = ctx.ctx.global_style().visuals.weak_text_color();
let diff = if self.dragged {
self.get_rotation_increment()
} else {
if self.last_time_update.is_some() {
self.last_time_update = None;
}
0.
};
if diff.abs() > 0. {
let curr_angle = self.angle_rad + diff;
let rot = Rot2::from_angle(curr_angle).normalized();
self.angle_rad = rot.angle();
};
let points = rect_to_points(rect_default)
.into_iter()
.map(|p| rotate_point_around(center, p, self.angle_rad))
.collect::<Vec<_>>();
let shape_rect =
Shape::convex_polygon(points, Color32::default(), Stroke::new(1., color));
let color = ctx.ctx.global_style().visuals.text_color();
let galley = ctx.ctx.fonts_mut(|f| {
f.layout_no_wrap(
self.label.clone(),
FontId::new(ctx.meta.canvas_to_screen_size(10.), FontFamily::Monospace),
color,
)
});
let offset = Vec2::new(-galley.size().x / 2., -galley.size().y / 2.);
let shape_label = TextShape::new(center + offset, galley, color);
vec![shape_rect, shape_label.into()]
}
fn update(&mut self, state: &NodeProps<N>) {
self.label.clone_from(&state.label);
self.loc = state.location();
self.dragged = state.dragged;
self.clockwise = state.payload.get_is_clockwise();
}
}
fn find_intersection(center: Pos2, size: f32, direction: Vec2) -> Pos2 {
if direction.x.abs() > direction.y.abs() {
let x = if direction.x > 0.0 {
center.x + size / 2.0
} else {
center.x - size / 2.0
};
let y = center.y + direction.y / direction.x * (x - center.x);
Pos2::new(x, y)
} else {
let y = if direction.y > 0.0 {
center.y + size / 2.0
} else {
center.y - size / 2.0
};
let x = center.x + direction.x / direction.y * (y - center.y);
Pos2::new(x, y)
}
}
fn rotate_point_around(center: Pos2, point: Pos2, angle: f32) -> Pos2 {
let sin_angle = angle.sin();
let cos_angle = angle.cos();
let translated_point = point - center;
let rotated_x = translated_point.x * cos_angle - translated_point.y * sin_angle;
let rotated_y = translated_point.x * sin_angle + translated_point.y * cos_angle;
Pos2::new(rotated_x, rotated_y) + center.to_vec2()
}
fn rect_to_points(rect: Rect) -> Vec<Pos2> {
let top_left = rect.min;
let bottom_right = rect.max;
let top_right = Pos2::new(bottom_right.x, top_left.y);
let bottom_left = Pos2::new(top_left.x, bottom_right.y);
vec![top_left, top_right, bottom_right, bottom_left]
}
fn rotate_vector(vec: Vec2, angle: f32) -> Vec2 {
let cos = angle.cos();
let sin = angle.sin();
Vec2::new(cos * vec.x - sin * vec.y, sin * vec.x + cos * vec.y)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_intersection_right_side() {
let center = Pos2::new(0.0, 0.0);
let size = 10.;
let direction = Vec2::new(1.0, 0.0);
let expected = Pos2::new(5.0, 0.0);
assert_eq!(find_intersection(center, size, direction), expected);
}
#[test]
fn test_intersection_top_side() {
let center = Pos2::new(0.0, 0.0);
let size = 10.;
let direction = Vec2::new(0.0, 1.0);
let expected = Pos2::new(0.0, 5.0);
assert_eq!(find_intersection(center, size, direction), expected);
}
}
}