1use egui::{self, NumExt, Pos2, Rect, Vec2, widgets};
6
7mod images;
8use images::*;
9
10pub struct Neko {
13 bookkeeping: NekoBookkeeping,
14 animation: Box<dyn Animation>,
15}
16
17struct NekoBookkeeping {
20 pub pos: Pos2,
21 pub last_cursor_pos: Pos2,
22 pub speed: f32,
24 pub ticker: usize,
25}
26
27trait Animation {
28 fn display(
30 &mut self,
31 ui: &mut egui::Ui,
32 books: &mut NekoBookkeeping,
33 ) -> Option<Box<dyn Animation>>;
34}
35
36struct SleepingNeko;
37struct WakingNeko {
38 timer: usize,
39}
40struct RunningNeko {
41 direction: Direction,
42}
43
44impl Neko {
45 pub fn new() -> Self {
47 return Self {
48 bookkeeping: NekoBookkeeping {
49 pos: (0f32, 0f32).into(),
50 last_cursor_pos: (0f32, 0f32).into(),
51 speed: 12f32,
52 ticker: 0,
53 },
54 animation: Box::new(SleepingNeko),
55 };
56 }
57
58 pub fn draw(&mut self, ui: &mut egui::Ui) {
60 self.bookkeeping.ticker = self.bookkeeping.ticker.wrapping_add(1);
61
62 self.bookkeeping.pos = self
64 .bookkeeping
65 .pos
66 .at_most(ui.max_rect().max - Vec2::new(32f32, 32f32));
67
68 if let Some(cursor_pos) = ui.input(|state| state.pointer.latest_pos()) {
69 self.bookkeeping.last_cursor_pos = cursor_pos;
70 };
71
72 while let Some(animation) = self.animation.display(ui, &mut self.bookkeeping) {
73 self.animation = animation;
74 }
75 }
76}
77
78impl Animation for SleepingNeko {
79 fn display(
80 &mut self,
81 ui: &mut egui::Ui,
82 books: &mut NekoBookkeeping,
83 ) -> Option<Box<dyn Animation>> {
84 if books.last_cursor_pos.distance(books.pos) >= books.speed * 3. {
86 return Some(Box::new(WakingNeko::default()));
87 }
88
89 let animation_state = match books.ticker / 10 % 100 {
90 x if x <= 50 => 0,
91 x if x <= 60 => 1,
92 x if x <= 70 => 0,
93 x if x <= 90 => 2,
94 x if x <= 100 => 3,
95 _ => unreachable!(),
96 };
97
98 draw_frame(ui, SLEEPING_IMAGES[animation_state].clone(), books.pos);
99 None
100 }
101}
102
103impl Default for WakingNeko {
104 fn default() -> Self {
105 Self { timer: 0 }
106 }
107}
108
109impl Animation for WakingNeko {
110 fn display(
111 &mut self,
112 ui: &mut egui::Ui,
113 books: &mut NekoBookkeeping,
114 ) -> Option<Box<dyn Animation>> {
115 self.timer += 1;
116
117 if self.timer < 30 {
118 draw_frame(ui, WAKING_IMAGE.clone(), books.pos);
119 None
120 } else {
121 Some(Box::new(RunningNeko {
122 direction: Direction::RIGHT,
125 }))
126 }
127 }
128}
129
130impl RunningNeko {
131 fn step(&mut self, books: &mut NekoBookkeeping) {
133 let direction = (books.last_cursor_pos - books.pos).angle();
134 books.pos += Vec2::angled(direction) * books.speed;
135 self.direction = Direction::from_angle(direction)
136 }
137}
138
139impl Animation for RunningNeko {
140 fn display(
141 &mut self,
142 ui: &mut egui::Ui,
143 books: &mut NekoBookkeeping,
144 ) -> Option<Box<dyn Animation>> {
145 if books.last_cursor_pos.distance(books.pos) < books.speed * 3. {
147 return Some(Box::new(SleepingNeko));
148 }
149
150 if books.ticker % 10 == 0 {
152 self.step(books);
153 }
154
155 let image = match self.direction {
156 Direction::RIGHT => &RIGHT_IMAGES,
157 Direction::LEFT => &LEFT_IMAGES,
158 Direction::DOWN => &DOWN_IMAGES,
159 Direction::DOWNLEFT => &DOWNLEFT_IMAGES,
160 Direction::DOWNRIGHT => &DOWNRIGHT_IMAGES,
161 Direction::UP => &UP_IMAGES,
162 Direction::UPLEFT => &UPLEFT_IMAGES,
163 Direction::UPRIGHT => &UPRIGHT_IMAGES,
164 }[(books.ticker / 10) % 2]
165 .clone(); draw_frame(ui, image, books.pos);
168 None
169 }
170}
171
172#[derive(Debug, Copy, Clone)]
173enum Direction {
174 LEFT,
175 RIGHT,
176 DOWN,
177 DOWNLEFT,
178 DOWNRIGHT,
179 UP,
180 UPLEFT,
181 UPRIGHT,
182}
183
184impl Direction {
185 fn from_angle(angle: f32) -> Self {
186 use std::f32::consts::PI;
187
188 assert!(angle >= -PI);
190 assert!(angle <= PI);
191
192 if (0.0..=(PI / 8.)).contains(&angle) {
194 Self::RIGHT
195 } else if ((PI / 8.)..=(3. * PI / 8.)).contains(&angle) {
196 Self::DOWNRIGHT
197 } else if ((3. * PI / 8.)..=(5. * PI / 8.)).contains(&angle) {
198 Self::DOWN
199 } else if ((5. * PI / 8.)..=(7. * PI / 8.)).contains(&angle) {
200 Self::DOWNLEFT
201 } else if ((7. * PI / 8.)..=PI).contains(&angle) {
202 Self::LEFT
203 } else if (-PI..=(-7. * PI / 8.)).contains(&angle) {
204 Self::LEFT
205 } else if ((-7. * PI / 8.)..=(-5. * PI / 8.)).contains(&angle) {
206 Self::UPLEFT
207 } else if ((-5. * PI / 8.)..=(-3. * PI / 8.)).contains(&angle) {
208 Self::UP
209 } else if ((-3. * PI / 8.)..=(-1. * PI / 8.)).contains(&angle) {
210 Self::UPRIGHT
211 } else if ((-1. * PI / 8.)..=0.0).contains(&angle) {
212 Self::RIGHT
213 } else {
214 panic!("Checks are exhaustive. Didn't match {angle}")
215 }
216 }
217}
218
219fn draw_frame<'a>(ui: &mut egui::Ui, image: egui::ImageSource<'a>, pos: Pos2) {
220 let win_max = ui.max_rect().max;
221
222 let min = pos.at_most(win_max - Vec2::new(32f32, 32f32));
223 let max = min + Vec2::new(32f32, 32f32);
224 ui.put(Rect { min, max }, widgets::Image::new(image));
225}