
๐ Rust Code
use nannou::prelude::*;
use std::collections::HashMap;
const GRID_SIZE: usize = 100;
const PARTICLE_SPACING: f32 = 8.0;
const VOID_RADIUS: f32 = 100.0;
const REPULSION_FORCE: f32 = 2.0;
const DAMPING: f32 = 0.95;
const GRID_CELL_SIZE: f32 = VOID_RADIUS * 0.8;
#[derive(Clone, Copy)]
struct Particle {
position: Vec2,
velocity: Vec2,
home_position: Vec2,
}
struct Model {
particles: Vec<Particle>,
window_rect: Rect,
}
fn main() {
nannou::app(model)
.update(update)
.event(event)
.run();
}
fn model(app: &App) -> Model {
let window = app.new_window().size(1000, 1000).view(view).build().unwrap();
let window_rect = app.window(window).unwrap().rect();
let mut particles = Vec::with_capacity(GRID_SIZE * GRID_SIZE);
let start_x = - ((GRID_SIZE as f32 - 1.0) / 2.0) * PARTICLE_SPACING;
let start_y = - ((GRID_SIZE as f32 - 1.0) / 2.0) * PARTICLE_SPACING;
for i in 0..GRID_SIZE {
for j in 0..GRID_SIZE {
let pos_x = start_x + i as f32 * PARTICLE_SPACING;
let pos_y = start_y + j as f32 * PARTICLE_SPACING;
let pos = vec2(pos_x, pos_y);
particles.push(Particle {
position: pos,
velocity: vec2(0.0, 0.0),
home_position: pos,
});
}
}
Model {
particles,
window_rect,
}
}
fn position_to_grid(pos: Vec2, window_rect: Rect) -> (i32, i32) {
let relative_x = pos.x - window_rect.left();
let relative_y = pos.y - window_rect.bottom();
let grid_x = (relative_x / GRID_CELL_SIZE).floor() as i32;
let grid_y = (relative_y / GRID_CELL_SIZE).floor() as i32;
(grid_x, grid_y)
}
fn get_nearby_grid_cells(mouse: Vec2, window_rect: Rect) -> Vec<(i32, i32)> {
let (center_x, center_y) = position_to_grid(mouse, window_rect);
let radius_in_cells = (VOID_RADIUS / GRID_CELL_SIZE).ceil() as i32;
let mut cells = Vec::new();
for dx in -radius_in_cells..=radius_in_cells {
for dy in -radius_in_cells..=radius_in_cells {
let cell_x = center_x + dx;
let cell_y = center_y + dy;
let cell_center = vec2(
window_rect.left() + (cell_x as f32 + 0.5) * GRID_CELL_SIZE,
window_rect.bottom() + (cell_y as f32 + 0.5) * GRID_CELL_SIZE,
);
if (cell_center - mouse).length() <= VOID_RADIUS + GRID_CELL_SIZE {
cells.push((cell_x, cell_y));
}
}
}
cells
}
fn update(app: &App, model: &mut Model, update: Update) {
let mouse = app.mouse.position();
let dt = update.since_last.as_secs_f32();
let mut spatial_grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
for (i, p) in model.particles.iter().enumerate() {
let (grid_x, grid_y) = position_to_grid(p.position, model.window_rect);
spatial_grid
.entry((grid_x, grid_y))
.or_insert_with(Vec::new)
.push(i);
}
let nearby_cells = get_nearby_grid_cells(mouse, model.window_rect);
let mut affected_particles: std::collections::HashSet<usize> = std::collections::HashSet::new();
for (cell_x, cell_y) in nearby_cells {
if let Some(indices) = spatial_grid.get(&(cell_x, cell_y)) {
for &index in indices {
affected_particles.insert(index);
}
}
}
for &index in &affected_particles {
let p = &mut model.particles[index];
let to_mouse = mouse - p.position;
let dist = to_mouse.length();
if dist < VOID_RADIUS && dist > 0.0 {
let repulsion_dir = -to_mouse.normalize();
let force = repulsion_dir * (REPULSION_FORCE * (1.0 - dist / VOID_RADIUS));
p.velocity += force;
}
}
for p in model.particles.iter_mut() {
let to_home = p.home_position - p.position;
p.velocity += to_home * 0.01;
p.position += p.velocity * dt * 60.0;
p.velocity *= DAMPING;
p.position = p.position.clamp(model.window_rect.bottom_left(), model.window_rect.top_right());
}
}
fn event(_app: &App, model: &mut Model, event: Event) {
if let Event::WindowEvent {
id: _,
simple: Some(WindowEvent::KeyPressed(Key::N)),
} = event {
for p in model.particles.iter_mut() {
p.position = p.home_position;
p.velocity = vec2(0.0, 0.0);
}
}
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(BLACK);
for p in &model.particles {
draw.ellipse()
.xy(p.position)
.radius(1.5)
.color(PINK);
}
let mouse = app.mouse.position();
draw.ellipse()
.xy(mouse)
.radius(VOID_RADIUS)
.no_fill()
.stroke(WHITE)
.stroke_weight(0.0);
draw.to_frame(app, &frame).unwrap();
}