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.speed = speed;
164
165 let total_width = self.required_width(keycount);
166 let total_height = self.required_height();
167
168 egui::Frame::dark_canvas(ui.style()).show(ui, |ui| {
169 let rect = Rect::from_min_size(pos2(0.0, 0.0), Vec2::new(total_width, total_height));
170
171 ui.set_min_size(Vec2::new(total_width, total_height));
172 ui.set_max_size(Vec2::new(total_width, total_height));
173
174 let play_area = rect;
175 let clip_rect = ui.clip_rect().intersect(play_area);
176 ui.set_clip_rect(clip_rect);
177
178 for i in 0..keycount {
180 let column_rect = Rect::from_min_size(
181 pos2(i as f32 * self.column_width, 0.0),
182 Vec2::new(self.column_width, total_height),
183 );
184 ui.painter()
185 .rect_filled(column_rect, 0.0, egui::Color32::from_gray(20));
186 }
187
188 let judgment_line_y = total_height - 100.0;
189 ui.painter().line_segment(
190 [
191 pos2(0.0, judgment_line_y),
192 pos2(total_width, judgment_line_y),
193 ],
194 egui::Stroke::new(2.0, egui::Color32::WHITE),
195 );
196
197 if hit_objects.last().is_some() {
198 for hit_object in hit_objects
200 .iter()
201 .filter(|h| matches!(h.kind, HitObjectKind::Hold(_)))
202 {
203 if let HitObjectKind::Hold(h) = &hit_object.kind {
204 let column = (h.pos_x / 512.0 * keycount as f32) as usize % keycount;
205 let x_pos = column as f32 * self.column_width;
206
207 let note_time = hit_object.start_time / speed + scroll_time_ms as f64;
208 let end_time =
209 (hit_object.start_time + h.duration) / speed + scroll_time_ms as f64;
210
211 let time_diff = note_time - current_time;
212 let end_time_diff = end_time - current_time;
213
214 let y_pos =
215 judgment_line_y - (time_diff as f32 / scroll_time_ms) * total_height;
216 let end_y_pos = judgment_line_y
217 - (end_time_diff as f32 / scroll_time_ms) * total_height;
218
219 if end_y_pos <= judgment_line_y {
220 self.render_hold(ui, x_pos, y_pos, end_y_pos, judgment_line_y);
221 }
222 }
223 }
224
225 for hit_object in hit_objects {
227 let note_time = hit_object.start_time / speed + scroll_time_ms as f64;
228 let time_diff = note_time - current_time;
229 let y_pos =
230 judgment_line_y - (time_diff as f32 / scroll_time_ms) * total_height;
231
232 if y_pos <= judgment_line_y {
233 let x_pos = match &hit_object.kind {
234 HitObjectKind::Circle(h) => {
235 let column =
236 (h.pos.x / 512.0 * keycount as f32) as usize % keycount;
237 column as f32 * self.column_width
238 }
239 HitObjectKind::Hold(h) => {
240 let column =
241 (h.pos_x / 512.0 * keycount as f32) as usize % keycount;
242 column as f32 * self.column_width
243 }
244 _ => continue,
245 };
246
247 if y_pos >= 0.0 {
248 self.draw_note(ui, x_pos, y_pos);
249 }
250 }
251 }
252 }
253 });
254 }
255}