dingusim 0.1.0

A pretty simple particle sim, that can create emergent behaviour. Basic concept taken from https://youtu.be/0Kx4Y9TVMGg
#![allow(dead_code)]

use rand::random;
use serde::{Serialize, Deserialize};
use winit::{event_loop::EventLoop, window::{WindowBuilder, Window}, event::{WindowEvent, Event}, dpi::LogicalSize};
use std::{ops::*, collections::HashMap, time::{Duration, Instant}, env::{Args, args}, path::Path, fs::{read_to_string, write}, process::exit};
use common_macros::*;
use pixels::{Pixels, SurfaceTexture};


#[derive(Debug, Default, Copy, Clone, PartialEq)]
struct Vector2 {
    x: f64,
    y: f64
}
impl Add for Vector2 {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self{x: self.x + other.x, y: self.y + other.y}
    }
}
impl Sub for Vector2 {
    type Output = Self;

    fn sub(self, other: Self) -> Self {
        Self{x: self.x - other.x, y: self.y - other.y}
    }
}
impl Mul for Vector2 {
    type Output = Self;

    fn mul(self, other: Self) -> Self {
        Self{x: self.x * other.x, y: self.y * other.y}
    }
}
impl Div for Vector2 {
    type Output = Self;

    fn div(self, other: Self) -> Self {
        Self{x: self.x / other.x, y: self.y / other.y}
    }
}
impl Vector2 {
    fn len(&self) -> f64 {
        (self.x.powf(2.0) + self.y.powf(2.0)).powf(0.5)
    }
}
#[derive(Debug, Hash, Serialize, Deserialize, Eq, PartialEq,Clone,Copy)]
enum Color {
    Red,
    Orange,
    Yellow,
    Green,
    Teal,
    Blue,
    Purple,
    Pink,
    White
}
impl Color {
    fn get_quad(self) -> [u8;4] {
        match self {
            Color::Red => [252,73,60,255],
            Color::Orange => [237,151,80,255],
            Color::Yellow => [242,233,111,255],
            Color::Green => [111,242,113,255],
            Color::Teal => [21,234,202,255],
            Color::Blue => [58,131,234,255],
            Color::Purple => [161,38,255,255],
            Color::Pink => [255,38,186,255],
            Color::White => [255,255,255,255]
        }
    }
}

#[derive(Debug, Copy, Clone, PartialEq)]
struct Ball {
    color: Color,
    pos: Vector2,
    vel: Vector2
}
impl Ball {
    fn new(color: Color, minx:f64, maxx:f64, miny:f64, maxy:f64) -> Self {
        let rangex = maxx - minx;
        let rangey = maxy - miny;
        let valx = random::<f64>() * rangex + minx;
        let valy = random::<f64>() * rangey + miny;
        Ball {color, vel: Vector2::default(), pos:Vector2{x:valx, y: valy}}
    }
}
#[derive(Debug, Serialize, Deserialize)]
struct Config {
    #[serde(default)]
    counts: HashMap<Color, u32>,
    #[serde(default)]
    forces: HashMap<(Color, Color), f64>,
    #[serde(default)]
    speed: f64,
    #[serde(default)]
    damp: f64,
    #[serde(default)]
    radius: f64,
}

fn main() {
    let defaultconf = Config{
        //how many of each color ball to spawn.
        counts: hash_map!{
            Color::Green => 1500,
            Color::Red =>   200,
        },
        //forces are applied to the first tuple member, and scaled by distance to the second. negative values repel, positive attract.
        forces: hash_map!{
            (Color::Red,Color::Red) =>      0.5,
            (Color::Red,Color::Green) =>    0.1,
            
            (Color::Green,Color::Red) =>   -0.5,
            (Color::Green,Color::Green) => -0.1
        },
        //unused for now
        speed:   1.0,
        //resistance. higher values mean stuff moves slower.
        damp:    4.0,
        //the max. pixels something can be away and still affect a given ball.
        radius: 80.0
    };

    let conf:Config = if args().len() > 1 {
        let pathstr = args().nth(1).unwrap();
        let path = Path::new(&pathstr);
        if path.exists() {
            ron::from_str(&read_to_string(path).unwrap()).unwrap()
        }
        else {
            write(path, ron::to_string(&defaultconf).unwrap());
            defaultconf
        }
    }
    else {defaultconf};
    
    let events = EventLoop::new();
    let win = WindowBuilder::new()
        .with_maximized(false)
        .with_resizable(false)
        .with_inner_size::<LogicalSize<u32>>(LogicalSize::new(640, 480))
        .build(&events).unwrap();
    let mut pixels = {
        let ws = win.inner_size();
        let surface_texture = SurfaceTexture::new(ws.width, ws.height, &win);
        Pixels::new(ws.width, ws.height, surface_texture).unwrap()
    };
    
    let mut balls = {
        let dims = {
            let size = win.inner_size();
            Vector2{x: size.width as f64, y: size.height as f64}
        };
        conf.counts.iter()
            .map(|(color, count):(&Color,&u32)| vec![color; *count as usize])
            .flatten()
            .map(|color:&Color| Ball::new(*color, 0.0, dims.x, 0.0, dims.y))
            .collect::<Vec<Ball>>()
    };
    
    //println!("{:?}", balls);

    let mut last = Instant::now();

    events.run(move |e, _, cf| {
        //println!("{:?}", e);

        match e {
            Event::WindowEvent { window_id: _, event: WindowEvent::CloseRequested } => cf.set_exit(),

            Event::MainEventsCleared => {
                //main logic
                if Instant::now() - last < Duration::from_millis(20) {return};
                last = Instant::now();

                let dims = {
                    let size = win.inner_size();
                    Vector2{x: size.width as f64, y: size.height as f64}
                };
                update(&conf, &mut balls, dims);
                redraw(&balls, &mut pixels, win.inner_size().width, win.inner_size().height);
                pixels.render()
                .map_err(|e| panic!("pixels.render() failed: {}", e));
            },
            Event::LoopDestroyed => {
                exit(0);
            }
            _ => ()
        }
    });
}

fn apply(conf:&Config, a:&mut Ball,b:Ball) -> Vector2 {
    if !conf.forces.contains_key(&(a.color, b.color)) {return Vector2::default()}
    let atob = b.pos - a.pos;
    let dist = atob.len();
    if dist > 0.0 && dist < conf.radius {
        let factor = conf.forces[&(a.color, b.color)] / dist;
        atob * Vector2{x:factor, y:factor}
    } else {Vector2::default()}
}

fn update(conf: &Config, balls: &mut Vec<Ball>, dims: Vector2) {
    for a in 0..balls.len() {
        let mut newvel = balls[a].vel;
        for b in 0..balls.len() {
            if a == b {continue;}
            let ALRIGHT_RUST_I_MADE_IT_IMMUTABLE_FIRST_ARE_YOU_HAPPY = balls[b];
            newvel = newvel + apply(conf, &mut balls[a], ALRIGHT_RUST_I_MADE_IT_IMMUTABLE_FIRST_ARE_YOU_HAPPY);
        }
        balls[a].vel = newvel/Vector2{x:conf.damp,y:conf.damp};
    }
    for ball in balls.iter_mut() {
        if ball.pos.x <= 0.0 {ball.vel.x = -ball.vel.x * conf.damp}
        if ball.pos.y <= 0.0 {ball.vel.y = -ball.vel.y * conf.damp}
        if ball.pos.x >= dims.x {ball.vel.x = -ball.vel.x * conf.damp}
        if ball.pos.y >= dims.y {ball.vel.y = -ball.vel.y * conf.damp}
        ball.pos = ball.pos + ball.vel;
    }
}
fn redraw(balls: &Vec<Ball>, pix: &mut Pixels, width: u32, height: u32) {
    let frame = pix.get_frame();
    {
        let mut count: u8 = 0;
        frame.fill_with(move || {
            if count == 3 {count = 0; 1}
            else {count += 1; 0}
        });
    }
    for ball in balls {
        let px = ball.pos.x as i32;
        let py = ball.pos.y as i32;
        for (x,y) in [(px,py), (px+1,py), (px-1,py), (px,py+1), (px,py-1)] {
            if x <= 0 ||
                x >= width as i32 ||
                y <= 0 ||
                y >= height as i32
            {continue;}
            //println!("trying to draw ball {:?}, color {:?}", ball, ball.color.get_quad());

            let start = y * width as i32 + x;
            frame.chunks_exact_mut(4).nth(start as usize).unwrap().copy_from_slice(&ball.color.get_quad());
        }
    }
}