use super::Animation;
use crate::render::Canvas;
use rand::RngExt;
const SENSOR_ANGLE: f64 = 0.5;
const ROTATION_ANGLE: f64 = 0.35;
const SENSOR_DIST: f64 = 6.0;
const STEP_SIZE: f64 = 1.0;
const DEPOSIT: f64 = 5.0;
const DECAY: f64 = 0.05;
const STEP_INTERVAL: f64 = 1.0 / 30.0;
const MAX_STEPS_PER_FRAME: usize = 4;
struct Agent {
x: f64,
y: f64,
angle: f64,
}
pub struct Physarum {
width: usize,
height: usize,
scale: f64,
grid: Vec<f64>,
grid_back: Vec<f64>,
agents: Vec<Agent>,
step_timer: f64,
}
impl Physarum {
pub fn new(width: usize, height: usize, scale: f64) -> Self {
let mut p = Physarum {
width: 1,
height: 1,
scale,
grid: Vec::new(),
grid_back: Vec::new(),
agents: Vec::new(),
step_timer: 0.0,
};
p.init(width.max(1), height.max(1));
p
}
fn init(&mut self, width: usize, height: usize) {
self.width = width;
self.height = height;
let n = width * height;
self.grid = vec![0.0; n];
self.grid_back = vec![0.0; n];
let count = ((n as f64 / 30.0) * self.scale.max(0.25)).clamp(400.0, 5000.0) as usize;
let mut rng = rand::rng();
self.agents.clear();
self.agents.reserve(count);
let cx = width as f64 * 0.5;
let cy = height as f64 * 0.5;
let radius = width.min(height) as f64 * 0.25;
for _ in 0..count {
let a = rng.random_range(0.0..std::f64::consts::TAU);
let r = rng.random_range(0.0..radius);
self.agents.push(Agent {
x: cx + a.cos() * r,
y: cy + a.sin() * r,
angle: rng.random_range(0.0..std::f64::consts::TAU),
});
}
}
fn sample(grid: &[f64], width: usize, height: usize, x: f64, y: f64) -> f64 {
let xi = x.round() as i64;
let yi = y.round() as i64;
let xi = xi.rem_euclid(width as i64) as usize;
let yi = yi.rem_euclid(height as i64) as usize;
grid[yi * width + xi]
}
fn step(&mut self) {
let width = self.width;
let height = self.height;
let wf = width as f64;
let hf = height as f64;
let mut rng = rand::rng();
for a in &mut self.agents {
let (ca, sa) = a.angle.sin_cos();
let (lca, lsa) = (a.angle - SENSOR_ANGLE).sin_cos();
let (rca, rsa) = (a.angle + SENSOR_ANGLE).sin_cos();
let val_l = Self::sample(
&self.grid,
width,
height,
a.x + lca * SENSOR_DIST,
a.y + lsa * SENSOR_DIST,
);
let val_c = Self::sample(
&self.grid,
width,
height,
a.x + ca * SENSOR_DIST,
a.y + sa * SENSOR_DIST,
);
let val_r = Self::sample(
&self.grid,
width,
height,
a.x + rca * SENSOR_DIST,
a.y + rsa * SENSOR_DIST,
);
if val_l > val_r {
a.angle -= ROTATION_ANGLE;
} else if val_r > val_l {
a.angle += ROTATION_ANGLE;
} else if val_c < val_l {
a.angle += if rng.random_range(0.0..1.0) < 0.5 {
ROTATION_ANGLE
} else {
-ROTATION_ANGLE
};
}
a.x = (a.x + a.angle.cos() * STEP_SIZE).rem_euclid(wf);
a.y = (a.y + a.angle.sin() * STEP_SIZE).rem_euclid(hf);
}
for a in &self.agents {
let xi = a.x as usize;
let yi = a.y as usize;
self.grid[yi * width + xi] += DEPOSIT;
}
for y in 0..height {
for x in 0..width {
let mut sum = 0.0;
for dy in -1i64..=1 {
for dx in -1i64..=1 {
let nx = (x as i64 + dx).rem_euclid(width as i64) as usize;
let ny = (y as i64 + dy).rem_euclid(height as i64) as usize;
sum += self.grid[ny * width + nx];
}
}
self.grid_back[y * width + x] = (sum / 9.0) * (1.0 - DECAY);
}
}
std::mem::swap(&mut self.grid, &mut self.grid_back);
}
}
impl Animation for Physarum {
fn name(&self) -> &str {
"physarum"
}
fn update(&mut self, canvas: &mut Canvas, dt: f64, _time: f64) {
if canvas.width != self.width || canvas.height != self.height {
self.init(canvas.width, canvas.height);
}
self.step_timer += dt;
let mut steps = 0;
while self.step_timer >= STEP_INTERVAL && steps < MAX_STEPS_PER_FRAME {
self.step();
self.step_timer -= STEP_INTERVAL;
steps += 1;
}
if self.step_timer > STEP_INTERVAL * MAX_STEPS_PER_FRAME as f64 {
self.step_timer = 0.0;
}
canvas.clear();
for y in 0..self.height {
for x in 0..self.width {
let v = self.grid[y * self.width + x];
if v > 0.03 {
let intensity = (v / 4.0).clamp(0.0, 1.0);
let hue = 0.66 * (1.0 - intensity);
let (r, g, b) = hsv_to_rgb(hue, 0.9, 0.4 + 0.6 * intensity);
canvas.set_colored(x, y, intensity, r, g, b);
}
}
}
}
}
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,
)
}