1use egui::{self, pos2, Color32, Rect, Vec2};
2use rosu_map::section::hit_objects::{HitObject, HitObjectKind};
3
4#[derive(Clone)]
5pub enum NoteShape {
6 Circle,
7 Rectangle { width: f32, height: f32 },
8 Arrow { width: f32, height: f32 },
9 Image(egui::Image<'static>),
10}
11
12pub struct NoteStyle {
13 pub shape: NoteShape,
14 pub color: Color32,
15 pub hold_body_color: Color32,
16 pub hold_cap_color: Color32,
17}
18
19impl Default for NoteStyle {
20 fn default() -> Self {
21 Self {
22 shape: NoteShape::Rectangle {
23 width: 0.8,
24 height: 0.25,
25 }, color: Color32::from_rgb(0, 174, 255),
27 hold_body_color: Color32::from_rgb(200, 200, 200),
28 hold_cap_color: Color32::from_rgb(0, 174, 255),
29 }
30 }
31}
32
33pub struct ManiaRenderer {
34 column_width: f32,
35 note_size: f32,
36 speed: f64,
37 height: f32,
38 note_style: NoteStyle,
39}
40
41impl ManiaRenderer {
42 pub fn with_sizes(column_width: f32, note_size: f32, height: f32) -> Self {
43 Self {
44 column_width,
45 note_size,
46 speed: 1.0,
47 height,
48 note_style: NoteStyle::default(),
49 }
50 }
51
52 pub fn set_note_style(&mut self, style: NoteStyle) {
53 self.note_style = style;
54 }
55
56 fn draw_note(&self, ui: &mut egui::Ui, x_pos: f32, y_pos: f32) {
57 let center_x = x_pos + self.column_width / 2.0;
58
59 match &self.note_style.shape {
60 NoteShape::Circle => {
61 let circle_radius = self.note_size / 2.0;
62 ui.painter().circle_filled(
63 pos2(center_x, y_pos),
64 circle_radius,
65 self.note_style.color,
66 );
67 }
68 NoteShape::Rectangle { width, height } => {
69 let note_width = self.note_size * width;
70 let note_height = self.note_size * height;
71 let rect = Rect::from_center_size(
72 pos2(center_x, y_pos),
73 Vec2::new(note_width, note_height),
74 );
75 ui.painter().rect_filled(rect, 0.0, self.note_style.color);
76 }
77 NoteShape::Arrow { width, height } => {
78 let note_width = self.note_size * width;
79 let note_height = self.note_size * height;
80 let points = vec![
81 pos2(center_x, y_pos - note_height / 2.0), pos2(center_x + note_width / 2.0, y_pos + note_height / 2.0), pos2(center_x - note_width / 2.0, y_pos + note_height / 2.0), ];
85 ui.painter().add(egui::Shape::convex_polygon(
86 points,
87 self.note_style.color,
88 egui::Stroke::NONE,
89 ));
90 }
91 NoteShape::Image(image) => {
92 image.paint_at(
93 ui,
94 Rect::from_min_size(
95 pos2(
96 center_x - self.note_size / 2.0,
97 y_pos - self.note_size / 2.0,
98 ),
99 Vec2::new(self.note_size, self.note_size),
100 ),
101 );
102 }
103 }
104 }
105
106 fn render_hold(
107 &self,
108 ui: &mut egui::Ui,
109 x_pos: f32,
110 start_y: f32,
111 end_y: f32,
112 judgment_line_y: f32,
113 ) {
114 let note_width = self.note_size * 0.8;
115 let x_center = x_pos + (self.column_width - note_width) / 2.0;
116
117 let y_start = start_y.min(end_y);
118 let y_end = (start_y.max(end_y)).min(judgment_line_y);
119 let visible_height = (y_end - y_start).abs();
120
121 ui.painter().rect_filled(
123 Rect::from_min_size(
124 pos2(x_center, y_start),
125 Vec2::new(note_width, visible_height),
126 ),
127 0.0,
128 self.note_style.hold_body_color,
129 );
130
131 let cap_height = note_width * 0.3;
133 if end_y <= judgment_line_y {
134 ui.painter().rect_filled(
135 Rect::from_min_size(pos2(x_center, end_y), Vec2::new(note_width, cap_height)),
136 0.0,
137 self.note_style.hold_cap_color,
138 );
139 }
140 }
141
142 pub fn set_height(&mut self, height: f32) {
143 self.height = height;
144 }
145
146 pub fn required_width(&self, keycount: usize) -> f32 {
147 self.column_width * keycount as f32
148 }
149
150 pub fn required_height(&self) -> f32 {
151 self.height
152 }
153
154 pub fn render(
155 &mut self,
156 ui: &mut egui::Ui,
157 hit_objects: &[HitObject],
158 current_time: f64,
159 scroll_time_ms: f32,
160 speed: f64,
161 keycount: usize,
162 ) {
163 self.render_at(ui, hit_objects, current_time, scroll_time_ms, speed, keycount, pos2(0.0, 0.0))
164 }
165
166 pub fn render_at(
167 &mut self,
168 ui: &mut egui::Ui,
169 hit_objects: &[HitObject],
170 current_time: f64,
171 scroll_time_ms: f32,
172 speed: f64,
173 keycount: usize,
174 position: egui::Pos2,
175 ) {
176 self.speed = speed;
177
178 let total_width = self.required_width(keycount);
179 let total_height = self.required_height();
180
181 egui::Frame::dark_canvas(ui.style()).show(ui, |ui| {
182 let rect = Rect::from_min_size(position, Vec2::new(total_width, total_height));
183
184 ui.set_min_size(Vec2::new(total_width, total_height));
185 ui.set_max_size(Vec2::new(total_width, total_height));
186
187 let play_area = rect;
188 let clip_rect = ui.clip_rect().intersect(play_area);
189 ui.set_clip_rect(clip_rect);
190
191 for i in 0..keycount {
193 let column_rect = Rect::from_min_size(
194 pos2(position.x + i as f32 * self.column_width, position.y),
195 Vec2::new(self.column_width, total_height),
196 );
197 ui.painter()
198 .rect_filled(column_rect, 0.0, egui::Color32::from_gray(20));
199 }
200
201 let judgment_line_y = position.y + total_height - 100.0;
202 ui.painter().line_segment(
203 [
204 pos2(position.x, judgment_line_y),
205 pos2(position.x + total_width, judgment_line_y),
206 ],
207 egui::Stroke::new(2.0, egui::Color32::WHITE),
208 );
209
210 if hit_objects.last().is_some() {
211 for hit_object in hit_objects
213 .iter()
214 .filter(|h| matches!(h.kind, HitObjectKind::Hold(_)))
215 {
216 if let HitObjectKind::Hold(h) = &hit_object.kind {
217 let column = (h.pos_x / 512.0 * keycount as f32) as usize % keycount;
218 let x_pos = position.x + column as f32 * self.column_width;
219
220 let note_time = hit_object.start_time / speed + scroll_time_ms as f64;
221 let end_time =
222 (hit_object.start_time + h.duration) / speed + scroll_time_ms as f64;
223
224 let time_diff = note_time - current_time;
225 let end_time_diff = end_time - current_time;
226
227 let y_pos =
228 judgment_line_y - (time_diff as f32 / scroll_time_ms) * total_height;
229 let end_y_pos = judgment_line_y
230 - (end_time_diff as f32 / scroll_time_ms) * total_height;
231
232 if end_y_pos <= judgment_line_y {
233 self.render_hold(ui, x_pos, y_pos, end_y_pos, judgment_line_y);
234 }
235 }
236 }
237
238 for hit_object in hit_objects {
240 let note_time = hit_object.start_time / speed + scroll_time_ms as f64;
241 let time_diff = note_time - current_time;
242 let y_pos =
243 judgment_line_y - (time_diff as f32 / scroll_time_ms) * total_height;
244
245 if y_pos <= judgment_line_y {
246 let x_pos = match &hit_object.kind {
247 HitObjectKind::Circle(h) => {
248 let column =
249 (h.pos.x / 512.0 * keycount as f32) as usize % keycount;
250 position.x + column as f32 * self.column_width
251 }
252 HitObjectKind::Hold(h) => {
253 let column =
254 (h.pos_x / 512.0 * keycount as f32) as usize % keycount;
255 position.x + column as f32 * self.column_width
256 }
257 _ => continue,
258 };
259
260 if y_pos >= 0.0 {
261 self.draw_note(ui, x_pos, y_pos);
262 }
263 }
264 }
265 }
266 });
267 }
268}