

ð Rust Code
use nannou::prelude::*;
use nannou::noise::{NoiseFn, Perlin, Seedable};
const NUM_PARTICLES: usize = 10_000;
const WIN_W: u32 = 1200;
const WIN_H: u32 = 900;
fn main() {
nannou::app(model).update(update).run();
}
struct Particle {
pos: Vec2,
vel: Vec2,
acc: Vec2,
hue: f32,
mass: f32,
trail: Vec<Vec2>,
}
impl Particle {
fn new(rect: &Rect) -> Self {
Self {
pos: vec2(
random_range(rect.left(), rect.right()),
random_range(rect.bottom(), rect.top()),
),
vel: Vec2::ZERO,
acc: Vec2::ZERO,
hue: random_f32(),
mass: random_range(0.8, 2.0),
trail: Vec::new(),
}
}
fn apply_force(&mut self, f: Vec2) {
self.acc += f / self.mass;
}
fn update(&mut self, rect: &Rect, dt: f32) {
self.vel += self.acc * dt;
self.vel *= 0.96;
let max_spd = 300.0;
if self.vel.length() > max_spd {
self.vel = self.vel.normalize() * max_spd;
}
self.pos += self.vel * dt;
self.acc = Vec2::ZERO;
self.trail.push(self.pos);
if self.trail.len() > 20 {
self.trail.remove(0);
}
if self.pos.x < rect.left() { self.pos.x = rect.right(); self.trail.clear(); }
if self.pos.x > rect.right() { self.pos.x = rect.left(); self.trail.clear(); }
if self.pos.y < rect.bottom() { self.pos.y = rect.top(); self.trail.clear(); }
if self.pos.y > rect.top() { self.pos.y = rect.bottom(); self.trail.clear(); }
}
}
struct Model {
_window: window::Id,
particles: Vec<Particle>,
noise: Perlin,
noise_z: f64,
mouse: Vec2,
mouse_down: bool,
attractor_pos: Vec2,
}
fn model(app: &App) -> Model {
let _window = app
.new_window()
.size(WIN_W, WIN_H)
.title("Noise Field â Swarm")
.view(view)
.mouse_pressed(mouse_pressed)
.mouse_released(mouse_released)
.build()
.unwrap();
let rect = Rect::from_w_h(WIN_W as f32, WIN_H as f32);
let particles = (0..NUM_PARTICLES).map(|_| Particle::new(&rect)).collect();
let noise = Perlin::new().set_seed(42);
Model {
_window,
particles,
noise,
noise_z: 0.0,
mouse: Vec2::ZERO,
mouse_down: false,
attractor_pos: vec2(200.0, 150.0),
}
}
fn mouse_pressed(_app: &App, model: &mut Model, _button: MouseButton) {
model.mouse_down = true;
}
fn mouse_released(_app: &App, model: &mut Model, _button: MouseButton) {
model.mouse_down = false;
}
fn gaussian(dist: f32, sigma: f32) -> f32 {
(-0.5 * (dist / sigma).powi(2)).exp()
}
fn update(app: &App, model: &mut Model, _update: Update) {
let dt = 1.0 / 60.0_f32;
let rect = app.window_rect();
model.mouse = app.mouse.position();
model.noise_z += 0.003;
let t = app.time;
model.attractor_pos = vec2(t.sin() * 250.0, (t * 0.7).cos() * 180.0);
let noise = &model.noise;
let nz = model.noise_z;
let mouse = model.mouse;
let mouse_down = model.mouse_down;
let attractor = model.attractor_pos;
let com: Vec2 = model.particles.iter().map(|p| p.pos).fold(Vec2::ZERO, |a, b| a + b)
/ NUM_PARTICLES as f32;
let positions: Vec<Vec2> = model.particles.iter().map(|p| p.pos).collect();
for (i, p) in model.particles.iter_mut().enumerate() {
let scale = 0.003;
let nx = noise.get([p.pos.x as f64 * scale, p.pos.y as f64 * scale, nz]) as f32;
let ny = noise.get([
p.pos.x as f64 * scale + 100.0,
p.pos.y as f64 * scale + 100.0,
nz,
]) as f32;
let noise_force = vec2(nx, ny) * 120.0;
p.apply_force(noise_force);
let to_mouse = mouse - p.pos;
let dist = to_mouse.length().max(1.0);
let sigma = 200.0;
let g = gaussian(dist, sigma);
if mouse_down {
let attract = to_mouse.normalize() * g * 400.0;
p.apply_force(attract);
} else {
let repel = -to_mouse.normalize() * g * 250.0;
p.apply_force(repel);
}
let target_hue = if g > 0.05 {
map_range(g, 0.05, 1.0, p.hue, 0.0)
} else {
map_range(nx, -1.0, 1.0, 0.45, 0.75)
};
p.hue += (target_hue - p.hue) * 0.05;
let to_att = attractor - p.pos;
let att_dist = to_att.length().max(1.0);
let att_strength = (80.0 / att_dist).min(1.0) * 60.0;
p.apply_force(to_att.normalize() * att_strength);
let to_com = com - p.pos;
p.apply_force(to_com * 0.02);
let repel_radius = 25.0;
let mut sep = Vec2::ZERO;
for j in ((i + 1)..NUM_PARTICLES).step_by(8) {
let diff = p.pos - positions[j];
let d = diff.length();
if d > 0.0 && d < repel_radius {
sep += diff.normalize() / d;
}
}
p.apply_force(sep * 50.0);
p.update(&rect, dt);
}
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
draw.rect()
.wh(app.window_rect().wh())
.color(srgba(0.02, 0.02, 0.05, 0.12));
let mouse = model.mouse;
for p in &model.particles {
let dist_to_mouse = (p.pos - mouse).length();
let g = gaussian(dist_to_mouse, 200.0);
let size = map_range(g, 0.0, 1.0, 1.5, 5.0) * (1.0 / p.mass);
let lum = map_range(g, 0.0, 1.0, 0.4, 0.9);
let alpha = map_range(g, 0.0, 1.0, 0.5, 1.0);
if p.trail.len() > 2 {
let pts: Vec<(Vec2, Hsla)> = p.trail.iter().enumerate().map(|(ti, &tp)| {
let t_ratio = ti as f32 / p.trail.len() as f32;
(tp, hsla(p.hue, 0.6, lum * 0.4, t_ratio * alpha * 0.3))
}).collect();
draw.polyline().weight(1.0).points_colored(pts);
}
draw.ellipse()
.xy(p.pos)
.w_h(size, size)
.color(hsla(p.hue, 0.7, lum, alpha));
}
for r in (1..=5).rev() {
let radius = r as f32 * 30.0;
let a = 0.015 / r as f32;
draw.ellipse()
.xy(mouse)
.w_h(radius, radius)
.color(hsla(0.6, 0.5, 0.7, a));
}
draw.to_frame(app, &frame).unwrap();
}