use super::Animation;
use crate::render::Canvas;
use rand::RngExt;
const GRAVITY: f64 = 130.0;
const MAX_VY: f64 = 90.0;
const SPAWN_INTERVAL: f64 = 0.22;
const MAX_BALLS: usize = 140;
const RESET_EVERY: usize = 500;
struct Ball {
row: i32,
slot: usize,
x: f64,
y: f64,
vy: f64,
hue: f64,
}
pub struct Galton {
balls: Vec<Ball>,
bins: Vec<usize>,
n_rows: i32,
spawn_timer: f64,
total_collected: usize,
}
impl Galton {
#[allow(unused_variables)]
pub fn new(width: usize, height: usize, scale: f64) -> Self {
let _ = (width, height, scale);
Galton {
balls: Vec::new(),
bins: Vec::new(),
n_rows: 0,
spawn_timer: 0.0,
total_collected: 0,
}
}
fn layout(canvas: &Canvas) -> (f64, f64, f64, f64, i32) {
let h = canvas.height as f64;
let y0 = h * 0.16;
let y_bins = h * 0.80;
let row_spacing = 2.4;
let col_spacing = 3.2;
let field = (y_bins - y0).max(1.0);
let n_rows = ((field / row_spacing) as i32).clamp(6, 12);
(y0, y_bins, row_spacing, col_spacing, n_rows)
}
fn peg_x(center: f64, col_spacing: f64, row: i32, slot: usize) -> f64 {
center + (slot as f64 - row as f64 * 0.5) * col_spacing
}
}
impl Animation for Galton {
fn name(&self) -> &str {
"galton"
}
fn update(&mut self, canvas: &mut Canvas, dt: f64, _time: f64) {
let w = canvas.width as f64;
let h = canvas.height as f64;
let center = w * 0.5;
let (y0, y_bins, row_spacing, col_spacing, n_rows) = Self::layout(canvas);
let need = (n_rows + 1) as usize;
if self.bins.len() != need {
self.bins = vec![0usize; need];
self.balls.clear();
}
self.n_rows = n_rows;
canvas.clear();
let mut rng = rand::rng();
self.spawn_timer += dt;
while self.spawn_timer >= SPAWN_INTERVAL && self.balls.len() < MAX_BALLS {
self.spawn_timer -= SPAWN_INTERVAL;
self.balls.push(Ball {
row: -1,
slot: 0,
x: center,
y: y0 - row_spacing,
vy: 0.0,
hue: rng.random_range(0.0..1.0),
});
}
if self.spawn_timer > SPAWN_INTERVAL {
self.spawn_timer = SPAWN_INTERVAL;
}
let mut collected: Vec<usize> = Vec::new();
for b in &mut self.balls {
b.vy = (b.vy + GRAVITY * dt).min(MAX_VY);
b.y += b.vy * dt;
loop {
let next_row_y = y0 + (b.row as f64 + 1.0) * row_spacing;
if b.y < next_row_y {
break;
}
b.row += 1;
b.y = next_row_y;
if b.row >= 1 {
if rng.random_range(0.0..1.0) >= 0.5 {
b.slot += 1;
}
b.x = Self::peg_x(center, col_spacing, b.row, b.slot);
}
if b.row >= n_rows {
collected.push(b.slot.min(need - 1));
break;
}
}
}
for c in collected {
self.bins[c] += 1;
self.total_collected += 1;
}
self.balls.retain(|b| b.row < n_rows);
if self.total_collected >= RESET_EVERY {
for c in &mut self.bins {
*c = 0;
}
self.total_collected = 0;
}
for row in 0..n_rows {
let py = y0 + row as f64 * row_spacing;
for slot in 0..=row as usize {
let px = Self::peg_x(center, col_spacing, row, slot);
let xi = px as usize;
let yi = py as usize;
if xi < canvas.width && yi < canvas.height {
canvas.set_colored(xi, yi, 0.25, 90, 95, 115);
}
}
}
for b in &self.balls {
let (r, g, bl) = hsv_to_rgb(b.hue, 0.85, 1.0);
let xi = b.x as usize;
let yi = b.y as usize;
if xi < canvas.width && yi < canvas.height {
canvas.set_colored(xi, yi, 0.95, r, g, bl);
}
}
let max_count = (*self.bins.iter().max().unwrap_or(&0)).max(1) as f64;
let bin_zone_h = (h - y_bins).max(1.0);
for (i, &count) in self.bins.iter().enumerate() {
let bx = Self::peg_x(center, col_spacing, n_rows, i);
let frac = count as f64 / max_count;
let bar_h = frac * bin_zone_h;
let top = (h - bar_h) as i64;
let hue = i as f64 / need as f64;
let (r, g, bl) = hsv_to_rgb(hue, 0.8, 0.95);
let xxi = bx as i64;
for yy in top..=(h as i64) {
for dxx in -1i64..=1 {
let xx = (xxi + dxx) as usize;
let yyu = yy as usize;
if xx < canvas.width && yyu < canvas.height {
canvas.set_colored(xx, yyu, 0.6, r, g, bl);
}
}
}
}
}
}
fn hsv_to_rgb(h: f64, s: f64, v: f64) -> (u8, u8, u8) {
let h = h.rem_euclid(1.0);
let c = v * s;
let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
let m = v - c;
let (r, g, b) = match (h * 6.0) as u32 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
(
((r + m) * 255.0) as u8,
((g + m) * 255.0) as u8,
((b + m) * 255.0) as u8,
)
}