
ð Rust Code
use nannou::prelude::*;
use nannou::image::{DynamicImage, Luma};
use rand::seq::SliceRandom;
use rand::Rng;
const WINDOW_W: u32 = 1080;
const WINDOW_H: u32 = 1080;
const PARTICLE_RADIUS: f32 = 5.0;
const MAX_PARTICLES: usize = 4000;
const SAMPLE_STEP: usize = 1;
const THRESHOLD: u8 = 170;
const TARGET_IMAGE: &str = "demon_bird.jpg";
const GRID_CELL_SIZE: f32 = 20.0;
const K_SPRING: f32 = 80.0;
const DAMPING: f32 = 0.9;
const MAX_ACC: f32 = 2000.0;
const COLLISION_DAMPING: f32 = 0.6;
const BOUND_DAMPING: f32 = -0.4;
fn main() {
nannou::app(model).update(update).run();
}
#[derive(Clone, Copy)]
struct Particle {
pos: Vec2,
vel: Vec2,
acc: Vec2,
radius: f32,
target: Vec2,
color: Srgb<u8>,
}
struct SpatialGrid {
cell_size: f32,
grid: std::collections::HashMap<(i32, i32), Vec<usize>>,
}
impl SpatialGrid {
fn new(cell_size: f32) -> Self {
SpatialGrid {
cell_size,
grid: std::collections::HashMap::new(),
}
}
fn get_cell(&self, pos: Vec2) -> (i32, i32) {
(
(pos.x / self.cell_size).floor() as i32,
(pos.y / self.cell_size).floor() as i32,
)
}
fn build(&mut self, particles: &[Particle]) {
self.grid.clear();
for (i, p) in particles.iter().enumerate() {
let cell = self.get_cell(p.pos);
self.grid.entry(cell).or_insert_with(Vec::new).push(i);
}
}
fn get_neighbors(&self, pos: Vec2) -> Vec<usize> {
let (cx, cy) = self.get_cell(pos);
let mut neighbors = Vec::new();
for dx in -1..=1 {
for dy in -1..=1 {
if let Some(indices) = self.grid.get(&(cx + dx, cy + dy)) {
neighbors.extend(indices);
}
}
}
neighbors
}
}
struct Model {
particles: Vec<Particle>,
spatial_grid: SpatialGrid,
_window: window::Id,
bg_color: Srgb<u8>,
}
fn model(app: &App) -> Model {
let _window = app
.new_window()
.size(WINDOW_W, WINDOW_H)
.view(view)
.build()
.unwrap();
let assets = app.assets_path().expect("failed to find assets folder");
let img_path = assets.join(TARGET_IMAGE);
let dyn_img = nannou::image::open(&img_path).expect("failed to load image from assets");
let mut target_points = mask_points_from_image(&dyn_img, SAMPLE_STEP, THRESHOLD, WINDOW_W, WINDOW_H);
let mut rng = rand::thread_rng();
target_points.shuffle(&mut rng);
if target_points.len() > MAX_PARTICLES {
target_points.truncate(MAX_PARTICLES);
}
let particles = target_points
.iter()
.map(|&t| {
let pos = vec2(
rng.gen_range(-(WINDOW_W as f32) / 2.0..(WINDOW_W as f32) / 2.0),
rng.gen_range(-(WINDOW_H as f32) / 2.0..(WINDOW_H as f32) / 2.0),
);
Particle {
pos,
vel: vec2(0.0, 0.0),
acc: vec2(0.0, 0.0),
radius: PARTICLE_RADIUS,
target: t,
color: random_color_like(&mut rng),
}
})
.collect();
Model {
particles,
spatial_grid: SpatialGrid::new(GRID_CELL_SIZE),
_window,
bg_color: srgb8(245, 223, 210),
}
}
fn mask_points_from_image(
img: &DynamicImage,
step: usize,
threshold: u8,
window_w: u32,
window_h: u32,
) -> Vec<Vec2> {
let gray = img.to_luma8();
let (w, h) = gray.dimensions();
let sx = window_w as f32 / w as f32;
let sy = window_h as f32 / h as f32;
let scale = sx.min(sy);
let img_width_on_screen = (w as f32) * scale;
let img_height_on_screen = (h as f32) * scale;
let left = -img_width_on_screen / 2.0;
let bottom = -img_height_on_screen / 2.0;
let mut points = Vec::new();
for y in (0..h).step_by(step) {
for x in (0..w).step_by(step) {
let Luma([v]) = gray.get_pixel(x, y);
if *v < threshold {
let fx = left + (x as f32 + 0.5) * scale;
let fy = bottom + ((h - y) as f32 + 0.5) * scale;
points.push(vec2(fx, fy));
}
}
}
points
}
fn random_color_like<R: Rng>(rng: &mut R) -> Srgb<u8> {
const PALETTE: [(u8, u8, u8); 6] = [
(233, 94, 105),
(234, 148, 129),
(193, 64, 90),
(166, 102, 70),
(224, 130, 149),
(183, 45, 82),
];
let (r, g, b) = PALETTE[rng.gen_range(0..PALETTE.len())];
srgb8(r, g, b)
}
#[inline]
fn apply_force(particles: &mut [Particle]) {
for p in particles.iter_mut() {
let to_target = p.target - p.pos;
let dist_sq = to_target.length_squared();
if dist_sq > 0.0001 {
let dist = dist_sq.sqrt();
p.acc = (to_target / dist) * (K_SPRING * dist);
} else {
p.acc = Vec2::ZERO;
}
}
}
#[inline]
fn clamp_acceleration(particles: &mut [Particle]) {
let max_acc_sq = MAX_ACC * MAX_ACC;
for p in particles.iter_mut() {
let acc_sq = p.acc.length_squared();
if acc_sq > max_acc_sq {
p.acc = p.acc.normalize() * MAX_ACC;
}
}
}
#[inline]
fn integrate(particles: &mut [Particle], dt: f32) {
for p in particles.iter_mut() {
p.vel += p.acc * dt;
p.vel *= DAMPING;
p.pos += p.vel * dt;
}
}
fn resolve_collisions(particles: &mut [Particle], spatial_grid: &mut SpatialGrid) {
spatial_grid.build(particles);
let mut collision_pairs = Vec::new();
for i in 0..particles.len() {
let pos = particles[i].pos;
let neighbors = spatial_grid.get_neighbors(pos);
for &j in &neighbors {
if i >= j {
continue;
}
let delta = particles[j].pos - particles[i].pos;
let dist_sq = delta.length_squared();
let min_dist = particles[i].radius + particles[j].radius;
let min_dist_sq = min_dist * min_dist;
if dist_sq > 0.0001 && dist_sq < min_dist_sq {
collision_pairs.push((i, j, delta, dist_sq.sqrt()));
}
}
}
for (i, j, delta, dist) in collision_pairs {
let overlap = 0.5 * (particles[i].radius + particles[j].radius - dist + 0.0001);
let ndelta = delta / dist;
particles[i].pos -= ndelta * overlap;
particles[j].pos += ndelta * overlap;
let rel_vel = particles[j].vel - particles[i].vel;
let sep_vel = rel_vel.dot(ndelta);
if sep_vel < 0.0 {
let impulse = -sep_vel * COLLISION_DAMPING;
let imp_vec = ndelta * impulse * 0.5;
particles[i].vel -= imp_vec;
particles[j].vel += imp_vec;
}
}
}
#[inline]
fn resolve_bounds(particles: &mut [Particle]) {
let half_w = WINDOW_W as f32 / 2.0;
let half_h = WINDOW_H as f32 / 2.0;
for p in particles.iter_mut() {
if p.pos.x < -half_w + p.radius {
p.pos.x = -half_w + p.radius;
p.vel.x *= BOUND_DAMPING;
} else if p.pos.x > half_w - p.radius {
p.pos.x = half_w - p.radius;
p.vel.x *= BOUND_DAMPING;
}
if p.pos.y < -half_h + p.radius {
p.pos.y = -half_h + p.radius;
p.vel.y *= BOUND_DAMPING;
} else if p.pos.y > half_h - p.radius {
p.pos.y = half_h - p.radius;
p.vel.y *= BOUND_DAMPING;
}
}
}
fn update(app: &App, model: &mut Model, _update: Update) {
let dt = app
.duration
.since_prev_update
.as_secs_f32()
.min(0.033);
apply_force(&mut model.particles);
clamp_acceleration(&mut model.particles);
integrate(&mut model.particles, dt);
resolve_collisions(&mut model.particles, &mut model.spatial_grid);
resolve_bounds(&mut model.particles);
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(model.bg_color);
for p in model.particles.iter() {
draw.ellipse()
.xy(p.pos)
.radius(p.radius)
.color(p.color);
}
draw.to_frame(app, &frame).unwrap();
}