circle_rasterizer2/
circlerasterizer2.rs

1use std::io;
2use crossterm::event::{Event, MouseEvent, MouseEventKind};
3use teng::components::Component;
4use teng::rendering::pixel::Pixel;
5use teng::rendering::render::Render;
6use teng::rendering::renderer::Renderer;
7use teng::{install_panic_handler, terminal_cleanup, terminal_setup, BreakingAction, Game, SetupInfo, SharedState, UpdateInfo};
8use teng::rendering::display::Display;
9use teng::util::fixedupdate::FixedUpdateRunner;
10use crate::ball::Ball;
11
12mod ball {
13    use teng::rendering::display::Display;
14    use teng::rendering::pixel::Pixel;
15    use teng::rendering::renderer::Renderer;
16
17    #[derive(Clone)]
18    pub struct Ball {
19        // in local space
20        x: f64,
21        // in loca lspace
22        y: f64,
23        // in local space
24        pub x_vel: f64,
25        // in local space
26        pub y_vel: f64,
27        pub radius: f64,
28        pub mass: f64,
29        old_x: f64,
30        old_y: f64,
31    }
32
33    impl Ball {
34        pub fn new(x: f64, y: f64, radius: f64) -> Self {
35            let (local_x, local_y) = Self::world_to_local(x, y);
36            Self {
37                x: local_x,
38                y: local_y,
39                x_vel: 0.0,
40                y_vel: 0.0,
41                radius,
42                mass: radius * radius,
43                old_x: local_x,
44                old_y: local_y,
45            }
46        }
47
48        pub fn set_world_x(&mut self, x: f64) {
49            self.x = x;
50        }
51
52        pub fn set_world_y(&mut self, y: f64) {
53            self.y = y * 2.0;
54        }
55
56        pub fn world_x(&self) -> f64 {
57            self.x
58        }
59
60        pub fn world_y(&self) -> f64 {
61            self.y / 2.0
62        }
63
64        pub fn local_x(&self) -> f64 {
65            self.x
66        }
67
68        pub fn local_y(&self) -> f64 {
69            self.y
70        }
71
72        pub fn world_to_local(x: f64, y: f64) -> (f64, f64) {
73            (x, y * 2.0)
74        }
75        pub fn local_to_world(x: f64, y: f64) -> (f64, f64) {
76            (x, y / 2.0)
77        }
78
79        fn update_local(&mut self, dt: f64, bottom_height_local: f64) -> (f64, f64) {
80            let (old_x, old_y) = (self.x, self.y);
81            self.old_x = old_x;
82            self.old_y = old_y;
83
84            self.x += self.x_vel * dt;
85            self.y += self.y_vel * dt;
86
87            if self.y + self.radius >= bottom_height_local {
88                self.y = (bottom_height_local - self.radius).floor();
89                self.y_vel = -self.y_vel * 0.8;
90            }
91
92            (old_x, old_y)
93        }
94
95        // calls f with local space
96        fn for_each_coord_in_outline(&self, mut f: impl FnMut(f64, f64) -> bool) {
97            let mut x = self.radius as i64 + 1;
98            let mut y = 0;
99            let mut err = 0;
100
101            let center_x = self.x as i64;
102            let center_y = self.y as i64;
103
104            while x >= y {
105                if f((center_x + x) as f64, (center_y + y) as f64) {
106                    return;
107                }
108                if f((center_x + y) as f64, (center_y + x) as f64) {
109                    return;
110                }
111                if f((center_x - y) as f64, (center_y + x) as f64) {
112                    return;
113                }
114                if f((center_x - x) as f64, (center_y + y) as f64) {
115                    return;
116                }
117                if f((center_x - x) as f64, (center_y - y) as f64) {
118                    return;
119                }
120                if f((center_x - y) as f64, (center_y - x) as f64) {
121                    return;
122                }
123                if f((center_x + y) as f64, (center_y - x) as f64) {
124                    return;
125                }
126                if f((center_x + x) as f64, (center_y - y) as f64) {
127                    return;
128                }
129
130                y += 1;
131                if err <= 0 {
132                    err += 2 * y + 1;
133                }
134                if err > 0 {
135                    x -= 1;
136                    err -= 2 * x + 1;
137                }
138            }
139        }
140
141        // calls f with local space
142        fn for_each_coord_in_filled(&self, mut f: impl FnMut(f64, f64) -> bool, radius_adjustment: f64) {
143            let center_x = self.x as i64;
144            let center_y = self.y as i64;
145
146            let mut x = (self.radius + radius_adjustment) as i64;
147            let mut y = 0;
148            let mut err = 0.0;
149
150            while x >= y {
151                for i in center_x - x..=center_x + x {
152                    if f(i as f64, (center_y + y) as f64) {
153                        return;
154                    }
155                    if f(i as f64, (center_y - y) as f64) {
156                        return;
157                    }
158                }
159                for i in center_x - y..=center_x + y {
160                    if f(i as f64, (center_y + x) as f64) {
161                        return;
162                    }
163                    if f(i as f64, (center_y - x) as f64) {
164                        return;
165                    }
166                }
167
168                y += 1;
169                if err <= 0.0 {
170                    err += 2.0 * y as f64 + 1.0;
171                }
172                if err > 0.0 {
173                    x -= 1;
174                    err -= 2.0 * x as f64 + 1.0;
175                }
176            }
177        }
178
179        pub fn render(&self, renderer: &mut dyn Renderer, render_outline: bool, depth: i32) {
180            // rasterize the circle, filling it in
181            // account for the fact that a pixel has 2:1 aspect ratio, so half the y radius
182            // TODO: Fairly sure I'm redrawing some pixels
183
184            let pixel = Pixel::new('X');
185
186            self.for_each_coord_in_filled(|x, y| {
187                let (x, y) = Self::local_to_world(x, y);
188                if y < 0.0 || x < 0.0 {
189                    return false;
190                }
191                renderer.render_pixel(x as usize, y as usize, pixel, depth);
192                false
193            }, 0.0);
194
195
196            if !render_outline {
197                return;
198            }
199
200            let depth_radius = depth + 1;
201
202
203            // todo: think about inlining for_each_coord here?
204            let pixel = Pixel::new('X').with_color([255, 0, 0]);
205            self.for_each_coord_in_outline(|x, y| {
206                let (x, y) = Self::local_to_world(x, y);
207                if y < 0.0 || x < 0.0 {
208                    return false;
209                }
210                renderer.render_pixel(x as usize, y as usize, pixel, depth_radius);
211                false
212            });
213        }
214    }
215    
216    pub fn update_balls(dt: f64, balls: &mut [Ball], bottom_wall_height_world: f64, is_solid_world: impl Fn(f64, f64) -> bool) {
217        let bottom_wall_height_local = bottom_wall_height_world * 2.0;
218        let is_solid_local = |x: f64, y: f64| is_solid_world(x, y / 2.0);
219        
220        for ball in balls.iter_mut() {
221            // first handle floor collisions
222            ball.update_local(dt, bottom_wall_height_local);
223
224        }
225
226
227        // then handle ball-ball collisions
228        for i in 0..balls.len() {
229            for j in i+1..balls.len() {
230                let (balls1, balls2) = balls.split_at_mut(j);
231                let ball1 = &mut balls1[i];
232                let ball2 = &mut balls2[0];
233                let dx = ball1.x - ball2.x;
234                let dy = ball1.y - ball2.y;
235                let distance = (dx*dx + dy*dy).sqrt();
236                let overlap = ball1.radius + ball2.radius - distance;
237                if overlap > 0.0 {
238                    let overlap = overlap / 2.0;
239                    let dx = dx / distance * overlap;
240                    let dy = dy / distance * overlap;
241                    ball1.x += dx;
242                    ball1.y += dy;
243                    ball2.x -= dx;
244                    ball2.y -= dy;
245                    // also update velocities, but take into account the mass of each ball
246                    let ball1_mass = ball1.mass;
247                    let ball2_mass = ball2.mass;
248                    let normal_x = dx / overlap;
249                    let normal_y = dy / overlap;
250                    let relative_velocity_x = ball1.x_vel - ball2.x_vel;
251                    let relative_velocity_y = ball1.y_vel - ball2.y_vel;
252                    let dot_product = relative_velocity_x * normal_x + relative_velocity_y * normal_y;
253                    if dot_product < 0.0 {
254                        let impulse = 2.0 * dot_product / (ball1_mass + ball2_mass);
255                        ball1.x_vel -= impulse * normal_x * ball2_mass;
256                        ball1.y_vel -= impulse * normal_y * ball2_mass;
257                        ball2.x_vel += impulse * normal_x * ball1_mass;
258                        ball2.y_vel += impulse * normal_y * ball1_mass;
259                    }
260
261                }
262            }
263        }
264
265        // then handle ball-solid collisions
266        for ball in balls.iter_mut() {
267
268            // then handle collisions with solid objects
269            let mut closest_hit = None;
270            let mut closest_distance_2 = f64::INFINITY;
271
272            ball.for_each_coord_in_filled(|x, y| {
273                if is_solid_local(x, y) {
274                    let dx = x - ball.x;
275                    let dy = y - ball.y;
276                    let distance = dx*dx + dy*dy;
277                    if distance < closest_distance_2 {
278                        closest_distance_2 = distance;
279                        closest_hit = Some((x, y));
280                    }
281
282                }
283
284                false
285            }, 1.0);
286
287            if let Some((x, y)) = closest_hit {
288                // find the closest point on the outline
289                let dx = x - ball.x;
290                let dy = y - ball.y;
291                let distance = (dx*dx + dy*dy).sqrt();
292
293                // undo the move that we did
294                // NOTE: important to do this after ball-ball, because another ball could've moved us into the solid. and we want to undo that.
295                ball.x = ball.old_x;
296                ball.y = ball.old_y;
297
298                // points from solid surface to ball
299                let normal_x = -dx / distance;
300                let normal_y = -dy / distance;
301
302                // first, just move the ball out of the collision by translating it by the overlap along the normal
303                // let overlap = ball.radius - distance;
304                // ball.x += normal_x * overlap;
305                // ball.y += normal_y * overlap;
306
307                // bounce off against normal, reduce velocities to 80%
308
309                let x_vel = ball.x_vel;
310                let y_vel = ball.y_vel;
311                let dot = x_vel * normal_x + y_vel * normal_y;
312                // only if velocities are going towards collision
313                if dot > 0.0 {
314                    // this is to avoid immediate collision again and vanishing velocities due to stacking reductions.
315                    // however, when a ball is 'stuck' on a solid object and due to gravity it thinks it's moving away,
316                    // we're not actually moving away, but still skipping the velocity reduction, so our y velocity builds up infinitely due to gravity.
317                    // to fix this, we need to check if we're actually moving away from the object.
318                    continue;
319                }
320
321                // reflect velocities against normal
322                let r_x = x_vel - 2.0 * dot * normal_x;
323                let r_y = y_vel - 2.0 * dot * normal_y;
324
325                ball.x_vel = r_x * 0.8;
326                ball.y_vel = r_y * 0.8;
327
328            }
329        }
330    }
331}
332
333
334struct CircleRasterizerComponent {
335    free_balls: Vec<Ball>,
336    current_ball: Option<Ball>,
337    center_samples: Vec<(f64, f64)>,
338    fixed_update_runner: FixedUpdateRunner,
339    // TODO: add mouse_released struct to shared state
340    did_hold_last: bool,
341    default_radius: f64,
342    static_collision: Display<bool>,
343}
344
345impl Default for CircleRasterizerComponent {
346    fn default() -> Self {
347        Self {
348            free_balls: vec![],
349            current_ball: None,
350            center_samples: vec![],
351            did_hold_last: false,
352            fixed_update_runner: FixedUpdateRunner::new(1.0 / 60.0),
353            default_radius: 10.0,
354            static_collision: Display::new(0, 0, false),
355        }
356    }
357}
358
359const MAX_SAMPLES: usize = 5;
360
361impl Component for CircleRasterizerComponent {
362    fn setup(&mut self, setup_info: &SetupInfo, shared_state: &mut SharedState<()>) {
363        self.on_resize(setup_info.display_info.width(), setup_info.display_info.height(), shared_state);
364    }
365
366    fn on_resize(&mut self, width: usize, height: usize, shared_state: &mut SharedState<()>) {
367        self.static_collision.resize_keep(width, height);
368    }
369
370    fn on_event(&mut self, event: Event, shared_state: &mut SharedState<()>) -> Option<BreakingAction> {
371            if let Event::Mouse(MouseEvent { kind: kind @ (MouseEventKind::ScrollDown | MouseEventKind::ScrollUp), .. }) = event {
372                let delta = match kind {
373                    MouseEventKind::ScrollDown => -1.0,
374                    MouseEventKind::ScrollUp => 1.0,
375                    _ => 0.0,
376                };
377                if let Some(current_ball) = &mut self.current_ball {
378                    current_ball.radius += delta;
379                    current_ball.mass = current_ball.radius * current_ball.radius;
380                }
381                self.default_radius += delta;
382            }
383
384        None
385    }
386
387    fn update(&mut self, update_info: UpdateInfo, shared_state: &mut SharedState<()>) {
388        if shared_state.pressed_keys.did_press_char_ignore_case('c') {
389            self.free_balls.clear();
390        }
391
392        if shared_state.pressed_keys.did_press_char_ignore_case('r') {
393            self.static_collision.fill(false);
394        }
395
396        if shared_state.mouse_info.left_mouse_down {
397            let world_x = shared_state.mouse_info.last_mouse_pos.0 as f64;
398            let world_y = shared_state.mouse_info.last_mouse_pos.1 as f64;
399            let current_ball = self.current_ball.get_or_insert_with(|| {
400                Ball::new(world_x, world_y, self.default_radius)
401            });
402
403            current_ball.set_world_x(world_x);
404            current_ball.set_world_y(world_y);
405            current_ball.x_vel = 0.0;
406            if !self.did_hold_last {
407                // first time we're holding again, so we clear the samples
408                self.center_samples.clear();
409            }
410            self.did_hold_last = true;
411        } else if self.did_hold_last {
412            // must have current ball
413            let current_ball = self.current_ball.as_mut().unwrap();
414            // just released
415            self.did_hold_last = false;
416            // compute a force based on average velocity over the samples
417            let mut sum_x_delta = 0.0;
418            let mut sum_y_delta = 0.0;
419            for i in 1..self.center_samples.len() {
420                let (x1, y1) = self.center_samples[i - 1];
421                let (x2, y2) = self.center_samples[i];
422                sum_x_delta += x2 - x1;
423                sum_y_delta += y2 - y1;
424            }
425            let delta_length = self.center_samples.len() as f64 / 60.0;
426            let avg_x_vel = sum_x_delta / delta_length;
427            let avg_y_vel = sum_y_delta / delta_length;
428            let strength = 1.0;
429            current_ball.x_vel = avg_x_vel * strength;
430            current_ball.y_vel = avg_y_vel * strength;
431            // release ball
432            self.free_balls.push(current_ball.clone());
433            self.current_ball = None;
434        }
435
436        if let Some(current_ball) = &mut self.current_ball {
437            shared_state.debug_info.custom.insert("Circle Radius".to_string(), format!("{:.2}", current_ball.radius));
438            shared_state.debug_info.custom.insert("Circle Center (local)".to_string(), format!("({}, {})", current_ball.local_x(), current_ball.local_y()));
439            shared_state.debug_info.custom.insert("Circle Center (world)".to_string(), format!("({}, {})", current_ball.world_x(), current_ball.world_y()));
440        }
441
442        if let Some(first_ball) = self.free_balls.first() {
443            shared_state.debug_info.custom.insert("First Ball Center (local)".to_string(), format!("({:.2}, {:.2})", first_ball.local_x(), first_ball.local_y()));
444            shared_state.debug_info.custom.insert("First Ball Center (world)".to_string(), format!("({:.2}, {:.2})", first_ball.world_x(), first_ball.world_y()));
445            shared_state.debug_info.custom.insert("First Ball velocity".to_string(), format!("({:.2}, {:.2})", first_ball.x_vel, first_ball.y_vel));
446
447        }
448
449        update_balls(update_info.dt, &mut self.free_balls, shared_state.display_info.height() as f64, &self.static_collision);
450
451        self.fixed_update_runner.fuel(update_info.dt);
452        while self.fixed_update_runner.has_gas() {
453            self.fixed_update_runner.consume();
454            if let Some(current_ball) = &mut self.current_ball {
455                self.center_samples.push((current_ball.local_x(), current_ball.local_y()));
456                if self.center_samples.len() > MAX_SAMPLES {
457                    self.center_samples.remove(0);
458                }
459            }
460        }
461
462        // update static collision board
463        shared_state.mouse_events.for_each_linerp_only_fresh(|mi| {
464            if mi.right_mouse_down {
465                self.static_collision.set(mi.last_mouse_pos.0, mi.last_mouse_pos.1, true);
466            }
467        })
468    }
469
470    fn render(&self, renderer: &mut dyn Renderer, shared_state: &SharedState, depth_base: i32) {
471        for ball in &self.free_balls {
472            ball.render(renderer, false, depth_base);
473        }
474        if let Some(current_ball) = &self.current_ball {
475            current_ball.render(renderer, true, depth_base+10);
476        }
477
478        // render static collision board
479        for x in 0..self.static_collision.width() {
480            for y in 0..self.static_collision.height() {
481                if self.static_collision[(x, y)] {
482                    renderer.render_pixel(x, y, Pixel::new('O').with_color([0, 255, 0]), depth_base);
483                }
484            }
485        }
486    }
487}
488
489fn update_balls(dt: f64, balls: &mut [Ball], bottom_wall_height: f64, static_collision: &Display<bool>) {
490    for i in 0..balls.len() {
491        // update velocities (TODO: move to ball module)
492        let ball = &mut balls[i];
493        ball.y_vel = ball.y_vel + 80.0 * dt;
494        // x drag
495        ball.x_vel = ball.x_vel  + ball.x_vel.signum() * -10.0 * dt;
496
497        // ball.update(dt, bottom_wall_height);
498    }
499
500    let is_solid_world = |x: f64, y: f64| {
501        if x < 0.0 || y < 0.0 {
502            return false;
503        }
504        *static_collision.get(x as usize, y as usize).unwrap_or(&false)
505    };
506
507    ball::update_balls(dt, balls, bottom_wall_height, is_solid_world);
508
509    //
510    // // check if it hits static collision
511    // // TODO: this does not work well yet. balls slowly drift through the wall
512    // for ball in balls.iter_mut() {
513    //     let mut closest_hit = None;
514    //     let mut closest_distance_2 = f64::INFINITY;
515    //
516    //     ball.for_each_coord_in_filled(|x, y| {
517    //         let x_u = x as usize;
518    //         let y_u = y as usize;
519    //
520    //         if let Some(true) = static_collision.get(x_u, y_u) {
521    //             let dx = x - ball.x;
522    //             let dy = y - ball.y;
523    //             let distance = dx*dx + dy*dy;
524    //             if distance < closest_distance_2 {
525    //                 closest_distance_2 = distance;
526    //                 closest_hit = Some((x, y));
527    //             }
528    //
529    //         }
530    //
531    //         false
532    //     }, 1.0);
533    //
534    //     if let Some((x, y)) = closest_hit {
535    //         // find the closest point on the outline
536    //         let dx = x - ball.x;
537    //         let dy = y - ball.y;
538    //         let distance = (dx*dx + dy*dy).sqrt();
539    //
540    //
541    //
542    //         // points from solid surface to ball
543    //         let normal_x = -dx / distance;
544    //         let normal_y = -dy / distance;
545    //
546    //         // first, just move the ball out of the collision by translating it by the overlap along the normal
547    //         // TODO: cannot just use radius here, since that is not the same for x and y
548    //         // let overlap = ball.radius - distance;
549    //         // let overlap_x = ball.radius - distance;
550    //         // let overlap_y = ball.radius/2.0 - distance;
551    //         // // assert!(overlap >= 0.0);
552    //         // let move_by_x = (normal_x * overlap_x).round();
553    //         // let move_by_y = (normal_y * overlap_y).round();
554    //         // if move_by_x.abs() > 0.5 {
555    //         //     ball.x += move_by_x;
556    //         // }
557    //         // if move_by_y.abs() > 0.5 {
558    //         //     ball.y += move_by_y;
559    //         // }
560    //
561    //         // continue;
562    //
563    //         // bounce off against normal, reduce velocities to 80%
564    //
565    //         // TODO: nonlinearity of radius y
566    //
567    //         let x_vel = ball.x_vel;
568    //         let y_vel = ball.y_vel;
569    //         let dot = x_vel * normal_x + y_vel * normal_y;
570    //         // only if velocities are going towards collision
571    //         if dot > 0.0 {
572    //             continue;
573    //         }
574    //
575    //         // reflect velocities against normal
576    //         let r_x = x_vel - 2.0 * dot * normal_x;
577    //         let r_y = y_vel - 2.0 * dot * normal_y;
578    //
579    //         ball.x_vel = r_x * 0.8;
580    //         ball.y_vel = r_y * 0.8;
581    //
582    //         // // reflect velocities:
583    //         // let dot_product = ball.x_vel * normal_x + ball.y_vel * normal_y;
584    //         //
585    //         //
586    //         // // bounce off against the normal
587    //         // let dot_product = ball.x_vel * normal_x + ball.y_vel * normal_y;
588    //         // if dot_product < 0.0 {
589    //         //     let impulse = 2.0 * dot_product / (1.0 + 1.0);
590    //         //     ball.x_vel -= impulse * normal_x;
591    //         //     ball.y_vel -= impulse * normal_y;
592    //         // }
593    //     }
594    // }
595    //
596    // // then check each ball against each other
597    // for i in 0..balls.len() {
598    //     for j in i+1..balls.len() {
599    //         let (balls1, balls2) = balls.split_at_mut(j);
600    //         let ball1 = &mut balls1[i];
601    //         let ball2 = &mut balls2[0];
602    //         let dx = ball1.x - ball2.x;
603    //         let dy = ball1.y - ball2.y;
604    //         // account for skewed y-scale
605    //         let dy = dy * 2.0;
606    //         let distance = (dx*dx + dy*dy).sqrt();
607    //         let overlap = ball1.radius + ball2.radius - distance;
608    //         if overlap > 0.0 {
609    //             let overlap = overlap / 2.0;
610    //             let dx = dx / distance * overlap;
611    //             let dy = dy / distance * overlap;
612    //             ball1.x += dx;
613    //             ball1.y += dy;
614    //             ball2.x -= dx;
615    //             ball2.y -= dy;
616    //             // also update velocities, but take into account the mass of each ball
617    //             let ball1_mass = ball1.mass;
618    //             let ball2_mass = ball2.mass;
619    //             let normal_x = dx / overlap;
620    //             let normal_y = dy / overlap;
621    //             let relative_velocity_x = ball1.x_vel - ball2.x_vel;
622    //             let relative_velocity_y = ball1.y_vel - ball2.y_vel;
623    //             let dot_product = relative_velocity_x * normal_x + relative_velocity_y * normal_y;
624    //             if dot_product < 0.0 {
625    //                 // let impulse = 2.0 * dot_product / (1.0 + 1.0);
626    //                 let impulse = 2.0 * dot_product / (ball1_mass + ball2_mass);
627    //                 ball1.x_vel -= impulse * normal_x * ball2_mass;
628    //                 ball1.y_vel -= impulse * normal_y * ball2_mass;
629    //                 ball2.x_vel += impulse * normal_x * ball1_mass;
630    //                 ball2.y_vel += impulse * normal_y * ball1_mass;
631    //                 // ball1.x_vel -= impulse * normal_x;
632    //                 // ball1.y_vel -= impulse * normal_y;
633    //                 // ball2.x_vel += impulse * normal_x;
634    //                 // ball2.y_vel += impulse * normal_y;
635    //             }
636    //
637    //         }
638    //     }
639    // }
640}
641
642fn main() -> io::Result<()> {
643    terminal_setup()?;
644    install_panic_handler();
645
646    let mut game = Game::new_with_custom_buf_writer();
647    game.install_recommended_components();
648    game.add_component(Box::new(CircleRasterizerComponent::default()));
649    game.run()?;
650
651    terminal_cleanup()?;
652
653    Ok(())
654}