egui_neko/
lib.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use egui::{self, NumExt, Pos2, Rect, Vec2, widgets};
6
7mod images;
8use images::*;
9
10/// This struct is used as a handle on a cat that may be drawn to follow a cursor. See examples for
11/// usage.
12pub struct Neko {
13    bookkeeping: NekoBookkeeping,
14    animation: Box<dyn Animation>,
15}
16
17/// The NekoBookkeeping struct stores the state of the cat such that it can follow the cursor
18/// appropriately.
19struct NekoBookkeeping {
20    pub pos: Pos2,
21    pub last_cursor_pos: Pos2,
22    /// Speed of the cat in pixels/update.
23    pub speed: f32,
24    pub ticker: usize,
25}
26
27trait Animation {
28    /// Draw the current animation frame or request that a different animation be used.
29    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    /// Create a new cat.
46    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    /// Draw the cat to the ui.
59    pub fn draw(&mut self, ui: &mut egui::Ui) {
60        self.bookkeeping.ticker = self.bookkeeping.ticker.wrapping_add(1);
61
62        // Push the cat inside the window if need be.
63        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 the cursor has gotten far enough away, wake up.
85        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                // Since the first thing that a running neko does is step forward, we can pick this
123                // direction arbitrarily, and it'll always be overriden with the correct one.
124                direction: Direction::RIGHT,
125            }))
126        }
127    }
128}
129
130impl RunningNeko {
131    /// Move one frame in the correct direction.
132    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 we're close enough to the cursor, go to sleep.
146        if books.last_cursor_pos.distance(books.pos) < books.speed * 3. {
147            return Some(Box::new(SleepingNeko));
148        }
149
150        // Don't move on each frame.
151        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(); // Cloning looks bad, but internally, an ImageSource will utilize Cows, so it should be okay.
166
167        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        // Arctan returns something in [-pi, pi].
189        assert!(angle >= -PI);
190        assert!(angle <= PI);
191
192        // Angles go clockwise. :(
193        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}