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 let background_rect = egui::Rect::from_min_size(position, egui::Vec2::new(total_width, total_height));
183 ui.painter().rect_filled(background_rect, 0.0, egui::Color32::from_gray(20));
184
185 for i in 0..keycount {
187 let column_rect = egui::Rect::from_min_size(
188 egui::pos2(position.x + i as f32 * self.column_width, position.y),
189 egui::Vec2::new(self.column_width, total_height),
190 );
191 ui.painter()
192 .rect_filled(column_rect, 0.0, egui::Color32::from_gray(30));
193 }
194
195 let judgment_line_y = position.y + total_height - 100.0;
196 ui.painter().line_segment(
197 [
198 egui::pos2(position.x, judgment_line_y),
199 egui::pos2(position.x + total_width, judgment_line_y),
200 ],
201 egui::Stroke::new(2.0, egui::Color32::WHITE),
202 );
203
204 if !hit_objects.is_empty() {
206 let visible_start_time = current_time - scroll_time_ms as f64 * 2.0;
208 let visible_end_time = current_time + scroll_time_ms as f64 * 0.5;
209
210 for hit_object in hit_objects
212 .iter()
213 .filter(|h| {
214 let obj_time = h.start_time / speed;
215 let obj_end_time = if let HitObjectKind::Hold(hold) = &h.kind {
217 (h.start_time + hold.duration) / speed
218 } else {
219 obj_time
220 };
221 (obj_time >= visible_start_time && obj_time <= visible_end_time) ||
223 (obj_end_time >= visible_start_time && obj_end_time <= visible_end_time) ||
224 (obj_time <= visible_start_time && obj_end_time >= visible_end_time) })
226 .filter(|h| matches!(h.kind, HitObjectKind::Hold(_)))
227 {
228 if let HitObjectKind::Hold(h) = &hit_object.kind {
229 let column = (h.pos_x / 512.0 * keycount as f32) as usize % keycount;
230 let x_pos = position.x + column as f32 * self.column_width;
231
232 let note_time = hit_object.start_time / speed + scroll_time_ms as f64;
233 let end_time =
234 (hit_object.start_time + h.duration) / speed + scroll_time_ms as f64;
235
236 let time_diff = note_time - current_time;
237 let end_time_diff = end_time - current_time;
238
239 let y_pos =
240 judgment_line_y - (time_diff as f32 / scroll_time_ms) * total_height;
241 let end_y_pos = judgment_line_y
242 - (end_time_diff as f32 / scroll_time_ms) * total_height;
243
244 if end_y_pos <= judgment_line_y {
245 self.render_hold(ui, x_pos, y_pos, end_y_pos, judgment_line_y);
246 }
247 }
248 }
249
250 for hit_object in hit_objects
252 .iter()
253 .filter(|h| {
254 let obj_time = h.start_time / speed;
255 obj_time >= visible_start_time && obj_time <= visible_end_time
256 })
257 {
258 let note_time = hit_object.start_time / speed + scroll_time_ms as f64;
259 let time_diff = note_time - current_time;
260 let y_pos =
261 judgment_line_y - (time_diff as f32 / scroll_time_ms) * total_height;
262
263 if y_pos <= judgment_line_y {
264 let x_pos = match &hit_object.kind {
265 HitObjectKind::Circle(h) => {
266 let column =
267 (h.pos.x / 512.0 * keycount as f32) as usize % keycount;
268 position.x + column as f32 * self.column_width
269 }
270 HitObjectKind::Hold(h) => {
271 let column =
272 (h.pos_x / 512.0 * keycount as f32) as usize % keycount;
273 position.x + column as f32 * self.column_width
274 }
275 _ => continue,
276 };
277
278 let note_height = self.note_size * 0.25; if y_pos >= -note_height {
281 self.draw_note(ui, x_pos, y_pos);
282 }
283 }
284 }
285 }
286 }
287}