rosu_renderer/layout/
mania.rs

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            }, // Rectangle par défaut
26            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), // Pointe
82                    pos2(center_x + note_width / 2.0, y_pos + note_height / 2.0), // Droite
83                    pos2(center_x - note_width / 2.0, y_pos + note_height / 2.0), // Gauche
84                ];
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        // Hold body
122        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        // Hold end cap
132        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            // Draw columns
192            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                // Draw hold notes first
212                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                // Then draw regular notes
239                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}