๐Ÿ”ฎ :: Chasing Particles

BamgasiJMยท2025๋…„ 10์›” 16์ผ

Nannou <Generative Art>

๋ชฉ๋ก ๋ณด๊ธฐ
44/55
post-thumbnail

๐Ÿ“ 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();
        }
    }
}

// ========== Nannou ๊ธฐ๋ณธ ํ•จ์ˆ˜๋“ค ==========
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();
                }
                // Dํ‚ค: ๋””๋ฒ„๊ทธ ํ† ๊ธ€
                KeyPressed(Key::D) => {
                    model.toggle_debug();
                }
                // Cํ‚ค: ํŒŒํ‹ฐํด ์ „์ฒด ์‚ญ์ œ
                KeyPressed(Key::C) => {
                    model.clear_particles();
                }
                // Fํ‚ค: ์ „์ฒดํ™”๋ฉด ํ† ๊ธ€
                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();
}

profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0๊ฐœ์˜ ๋Œ“๊ธ€