
๐ Rust Code
use nannou::prelude::*;
use nannou::noise::{NoiseFn, Perlin};
struct Particle {
pos: Vec2,
vel: Vec2,
acc: Vec2,
size: f32,
color: Hsla,
lifetime: f32,
max_lifetime: f32,
}
impl Particle {
fn new(pos: Vec2) -> Self {
let angle = random_range(0.0, TAU);
let speed = random_range(0.2, 1.0);
Self {
pos,
vel: vec2(angle.cos() * speed, angle.sin() * speed),
acc: Vec2::ZERO,
size: random_range(2.0, 5.0),
color: hsla(random_range(0.5, 0.7), 0.8, 0.6, 1.0),
lifetime: 0.0,
max_lifetime: random_range(3.0, 6.0),
}
}
fn update(&mut self, dt: f32) {
self.vel += self.acc;
self.pos += self.vel * dt * 60.0;
self.acc = Vec2::ZERO;
self.lifetime += dt;
let life_ratio = 1.0 - (self.lifetime / self.max_lifetime).clamp(0.0, 1.0);
self.color.alpha = life_ratio;
}
fn apply_force(&mut self, force: Vec2) {
self.acc += force;
}
fn is_alive(&self) -> bool {
self.lifetime < self.max_lifetime
}
}
struct Model {
particles: Vec<Particle>,
noise: Perlin,
time: f32,
window_size: Vec2,
mouse_pos: Vec2,
is_paused: bool,
show_debug: bool,
}
impl Model {
fn new(window_size: Vec2) -> Self {
Self {
particles: Vec::new(),
noise: Perlin::new(),
time: 0.0,
window_size,
mouse_pos: Vec2::ZERO,
is_paused: false,
show_debug: false,
}
}
fn add_particle(&mut self, pos: Vec2) {
self.particles.push(Particle::new(pos));
}
fn add_particles(&mut self, pos: Vec2, count: usize) {
for _ in 0..count {
self.add_particle(pos);
}
}
fn update_particles(&mut self, dt: f32) {
for p in &mut self.particles {
let noise_val = self.noise.get([
p.pos.x as f64 * 0.005,
p.pos.y as f64 * 0.005,
self.time as f64,
]) as f32;
let angle = noise_val * TAU;
let force = vec2(angle.cos(), angle.sin()) * 0.05;
p.apply_force(force);
let to_mouse = self.mouse_pos - p.pos;
let dist = to_mouse.length();
if dist < 100.0 && dist > 0.0 {
let repulsion = -to_mouse.normalize() * (1.0 - dist / 100.0) * 0.6;
p.apply_force(repulsion);
}
p.update(dt);
}
self.particles.retain(|p| p.is_alive());
}
fn wrap_particles(&mut self) {
let half_w = self.window_size.x * 0.5;
let half_h = self.window_size.y * 0.5;
for p in &mut self.particles {
if p.pos.x > half_w { p.pos.x = -half_w; }
if p.pos.x < -half_w { p.pos.x = half_w; }
if p.pos.y > half_h { p.pos.y = -half_h; }
if p.pos.y < -half_h { p.pos.y = half_h; }
}
}
fn clear_particles(&mut self) {
self.particles.clear();
}
fn update_time(&mut self, dt: f32) {
self.time += dt;
}
fn update_window_size(&mut self, size: Vec2) {
self.window_size = size;
}
fn update_mouse(&mut self, pos: Vec2) {
self.mouse_pos = pos;
}
fn toggle_pause(&mut self) {
self.is_paused = !self.is_paused;
}
fn toggle_debug(&mut self) {
self.show_debug = !self.show_debug;
}
fn draw_particles(&self, draw: &Draw) {
for p in &self.particles {
draw.ellipse()
.xy(p.pos)
.radius(p.size)
.color(p.color);
}
}
fn draw_connections(&self, draw: &Draw, max_dist: f32) {
for i in 0..self.particles.len() {
for j in (i + 1)..self.particles.len() {
let p1 = &self.particles[i];
let p2 = &self.particles[j];
let dist = p1.pos.distance(p2.pos);
if dist < max_dist {
let alpha = (1.0 - dist / max_dist) * 0.3;
draw.line()
.start(p1.pos)
.end(p2.pos)
.weight(0.5)
.color(hsla(0.6, 0.8, 0.7, alpha));
}
}
}
}
fn draw_debug(&self, draw: &Draw) {
if self.show_debug {
let text = format!(
"Particles: {}\nFPS: ~60\nPaused: {}\nTime: {:.2}",
self.particles.len(),
self.is_paused,
self.time
);
draw.text(&text)
.xy(vec2(-self.window_size.x * 0.45, self.window_size.y * 0.45))
.color(BLUE)
.font_size(10)
.left_justify();
}
}
}
fn main() {
nannou::app(model)
.update(update)
.event(event)
.run();
}
fn model(app: &App) -> Model {
let window_id = app.new_window()
.size(800, 800)
.view(view)
.build()
.unwrap();
let window = app.window(window_id).unwrap();
let win_rect = window.rect();
let mut model = Model::new(vec2(win_rect.w(), win_rect.h()));
for _ in 0..50 {
let x = random_range(-300.0, 300.0);
let y = random_range(-300.0, 300.0);
model.add_particle(vec2(x, y));
}
model
}
fn update(app: &App, model: &mut Model, update: Update) {
if model.is_paused {
return;
}
let dt = update.since_last.as_secs_f32();
model.update_time(dt);
model.update_mouse(app.mouse.position());
model.update_particles(dt);
model.wrap_particles();
if model.particles.len() < 200 {
model.add_particle(vec2(
random_range(-300.0, 300.0),
random_range(-300.0, 300.0)
));
}
}
fn event(app: &App, model: &mut Model, event: Event) {
match event {
Event::WindowEvent { simple: Some(event), .. } => {
match event {
KeyPressed(Key::Space) => {
model.toggle_pause();
}
KeyPressed(Key::D) => {
model.toggle_debug();
}
KeyPressed(Key::C) => {
model.clear_particles();
}
KeyPressed(Key::F) => {
let window = app.main_window();
let is_fullscreen = window.fullscreen().is_some();
window.set_fullscreen(!is_fullscreen);
}
MousePressed(_button) => {
model.add_particles(model.mouse_pos, 10);
}
Resized(size) => {
model.update_window_size(vec2(size.x as f32, size.y as f32));
}
_ => {}
}
}
_ => {}
}
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
if frame.nth() == 0 {
draw.background().color(BLACK);
}
draw.rect()
.wh(model.window_size)
.color(srgba(0.0, 0.0, 0.0, 0.17));
model.draw_connections(&draw, 80.0);
model.draw_particles(&draw);
draw.ellipse()
.xy(model.mouse_pos)
.radius(5.0)
.color(hsla(0.0, 0.0, 1.0, 0.3));
model.draw_debug(&draw);
draw.to_frame(app, &frame).unwrap();
}