๐Ÿ”ฎ :: Make You Own Constellation

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

Nannou <Generative Art>

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

๐Ÿ“ Rust Code

use nannou::prelude::*;
use rand::Rng;
use std::time::{SystemTime, UNIX_EPOCH};
use std::collections::HashMap;

const CONNECT_DISTANCE: f32 = 150.0;
const PARTICLE_MIN_SIZE: f32 = 1.0;  
const PARTICLE_MAX_SIZE: f32 = 5.0;  
const PARTICLE_MIN_SPEED: f32 = 10.0;
const PARTICLE_MAX_SPEED: f32 = 80.0;
const PARTICLE_MIN_LIFETIME: f32 = 7.0;
const PARTICLE_MAX_LIFETIME: f32 = 10.0;
const VELOCITY_DAMPING: f32 = 0.995;
const MIN_LINE_ALPHA: f32 = 0.01;

struct Particle {
    pos: Vec2,
    vel: Vec2,
    born: f32,
    lifetime: f32,
    size: f32,  
    color: Rgba,
}

impl Particle {
    fn new(origin: Vec2, now: f32) -> Self {
        let mut rng = rand::thread_rng();
        let angle = rng.gen_range(0.0..TAU);
        let speed = rng.gen_range(PARTICLE_MIN_SPEED..PARTICLE_MAX_SPEED);
        let vel = vec2(angle.cos() * speed, angle.sin() * speed);
        let lifetime = rng.gen_range(PARTICLE_MIN_LIFETIME..PARTICLE_MAX_LIFETIME);
        let size = rng.gen_range(PARTICLE_MIN_SIZE..PARTICLE_MAX_SIZE);  
        let hue = rng.gen_range(0.5..0.72);
        let color = hsla(hue, 0.7, 0.5, 1.0).into();

        Self {
            pos: origin,
            vel,
            born: now,
            lifetime,
            size, 
            color,
        }
    }

    fn age(&self, now: f32) -> f32 {
        now - self.born
    }

    fn life_ratio(&self, now: f32) -> f32 {
        1.0 - (self.age(now) / self.lifetime).clamp(0.0, 1.0)
    }

    fn alive(&self, now: f32) -> bool {
        self.age(now) < self.lifetime
    }

    fn update(&mut self, dt: f32) {
        self.pos += self.vel * dt;
        self.vel *= VELOCITY_DAMPING;
    }
}

struct Model {
    particles: Vec<Particle>,
    show_title: bool,
    is_fullscreen: bool,
    grid: HashMap<(i32, i32), Vec<usize>>,
    cell_size: f32,
}

impl Model {
    fn new() -> Self {
        Self {
            particles: Vec::new(),
            show_title: true,
            is_fullscreen: false,
            grid: HashMap::new(),
            cell_size: CONNECT_DISTANCE,
        }
    }

    fn update_grid(&mut self) {
        self.grid.clear();
        for (i, particle) in self.particles.iter().enumerate() {
            let cell_x = (particle.pos.x / self.cell_size).floor() as i32;
            let cell_y = (particle.pos.y / self.cell_size).floor() as i32;
            self.grid.entry((cell_x, cell_y)).or_insert_with(Vec::new).push(i);
        }
    }

    fn clear_particles(&mut self) {
        self.particles.clear();
        self.grid.clear();
        self.show_title = true;
    }
}

fn main() {
    nannou::app(model).update(update).run();
}

fn model(app: &App) -> Model {
    app.new_window()
        .size(1080, 1080)
        .key_pressed(key_pressed)
        .mouse_pressed(mouse_pressed)
        .view(view)
        .title("๋ณ„์ž๋ฆฌ๋งŒ๋“ค๊ธฐ")
        .build()
        .unwrap();

    Model::new()
}

fn key_pressed(app: &App, model: &mut Model, key: Key) {
    match key {
        Key::F => {
            model.is_fullscreen = !model.is_fullscreen;
            app.main_window().set_fullscreen(model.is_fullscreen);
        }
        Key::S => {
            let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
            let filename = format!("constellation_{}.png", ts);
            app.main_window().capture_frame(filename);
            println!("๐Ÿ“ธ Screenshot saved!");
        }
        Key::N => {
            model.clear_particles();
        }
        Key::C => {
            let now = app.time as f32;
            model.particles.retain(|p| p.alive(now));
            model.update_grid();
        }
        _ => (),
    }
}

fn mouse_pressed(app: &App, model: &mut Model, _button: MouseButton) {
    if model.show_title {
        model.show_title = false;
    }

    let mut rng = rand::thread_rng();
    let count = rng.gen_range(15..=30);
    let now = app.time as f32;
    let origin = app.mouse.position();

    for _ in 0..count {
        model.particles.push(Particle::new(origin, now)); 
    }
    
    model.update_grid();
}

fn update(app: &App, model: &mut Model, _update: Update) {
    let dt = app.duration.since_prev_update.as_secs_f32();
    let now = app.time as f32;

    for particle in &mut model.particles {
        particle.update(dt);
    }

    model.particles.retain(|p| p.alive(now));
    
    model.update_grid();
}

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();
    draw.background().color(BLACK);
    let now = app.time as f32;
    let win_rect = app.window_rect();

    for p in &model.particles {
        let lr = p.life_ratio(now);
        let mut col = p.color;
        col.alpha *= lr;
        draw.ellipse().xy(p.pos).radius(p.size).color(col); 
    }

    let grid = &model.grid;
    let connect_distance_sq = CONNECT_DISTANCE * CONNECT_DISTANCE;

    for (cell_pos, indices) in grid {
        for &i in indices {
            let a = &model.particles[i];
            if !a.alive(now) { continue; }
            
            for dx in -1..=1 {
                for dy in -1..=1 {
                    let neighbor_cell = (cell_pos.0 + dx, cell_pos.1 + dy);
                    if let Some(neighbor_indices) = grid.get(&neighbor_cell) {
                        for &j in neighbor_indices {
                            if j <= i { continue; }
                            
                            let b = &model.particles[j];
                            if !b.alive(now) { continue; }
                            
                            let d_sq = a.pos.distance_squared(b.pos);
                            if d_sq <= connect_distance_sq {
                                let d = d_sq.sqrt();
                                let dist_alpha = 1.0 - d / CONNECT_DISTANCE;
                                let alpha = dist_alpha * a.life_ratio(now) * b.life_ratio(now);
                                
                                if alpha > MIN_LINE_ALPHA {
                                    let col = rgba(
                                        (a.color.red + b.color.red) * 0.5,
                                        (a.color.green + b.color.green) * 0.5,
                                        (a.color.blue + b.color.blue) * 0.5,
                                        alpha
                                    );
                                    draw.line().start(a.pos).end(b.pos).weight(0.8).color(col);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    if model.show_title {

        draw.text("CONSTELLATION")
            .color(WHITE)
            .font_size(120)
            .x_y(0.0, 150.0)
            .center_justify()
            .wh(win_rect.wh());
            
        draw.text("Create your own star constellations")
            .color(SILVER)
            .font_size(36)
            .x_y(0.0, 0.0)
            .center_justify()
            .wh(win_rect.wh());
            
        draw.text("CLICK TO CREATE STARS  โ€ข  F: FULLSCREEN  โ€ข  S: SCREENSHOT  โ€ข  N: NEW")
            .color(GRAY)
            .font_size(24)
            .x_y(0.0, -150.0)
            .center_justify()
            .wh(win_rect.wh());
    }

    if !model.show_title {
        draw.text(&format!("Particles: {}", model.particles.len()))
            .color(GRAY)
            .font_size(18)
            .x_y(-500.0, 500.0);
    }

    draw.to_frame(app, &frame).unwrap();
}

๐Ÿ“ Rust Code + Comment

use nannou::prelude::*;   				  // Nannou ๊ทธ๋ž˜ํ”ฝ ํ”„๋ ˆ์ž„์›Œํฌ์˜ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ๋“ค์„ ๊ฐ€์ ธ์˜ด
use rand::Rng;            				  // ๋žœ๋ค ์ˆซ์ž ์ƒ์„ฑ์„ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ
use std::time::{SystemTime, UNIX_EPOCH};  // ์‹œ์Šคํ…œ ์‹œ๊ฐ„ ๊ด€๋ จ ๊ธฐ๋Šฅ
use std::collections::HashMap;            // ํ•ด์‹œ๋งต ์ž๋ฃŒ๊ตฌ์กฐ

////////////////////////////////////////////////////////////////////////////////
// ์ƒ์ˆ˜ ์ •์˜ - ํ”„๋กœ๊ทธ๋žจ ์ „๋ฐ˜์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์„ค์ •๊ฐ’๋“ค
////////////////////////////////////////////////////////////////////////////////

const CONNECT_DISTANCE: f32 = 150.0;      // ๋ณ„๋“ค ์‚ฌ์ด๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ์ตœ๋Œ€ ๊ฑฐ๋ฆฌ
const PARTICLE_MIN_SIZE: f32 = 2.0;       // ๋ณ„์˜ ์ตœ์†Œ ํฌ๊ธฐ
const PARTICLE_MAX_SIZE: f32 = 8.0;       // ๋ณ„์˜ ์ตœ๋Œ€ ํฌ๊ธฐ
const PARTICLE_MIN_SPEED: f32 = 10.0;     // ๋ณ„์˜ ์ตœ์†Œ ์ด๋™ ์†๋„
const PARTICLE_MAX_SPEED: f32 = 80.0;     // ๋ณ„์˜ ์ตœ๋Œ€ ์ด๋™ ์†๋„
const PARTICLE_MIN_LIFETIME: f32 = 7.0;   // ๋ณ„์˜ ์ตœ์†Œ ์ˆ˜๋ช… (์ดˆ)
const PARTICLE_MAX_LIFETIME: f32 = 10.0;  // ๋ณ„์˜ ์ตœ๋Œ€ ์ˆ˜๋ช… (์ดˆ)
const VELOCITY_DAMPING: f32 = 0.995;      // ๋งค ํ”„๋ ˆ์ž„๋ณ„ ์†๋„ ๊ฐ์†Œ์œจ (๋งˆ์ฐฐ ํšจ๊ณผ)
const MIN_LINE_ALPHA: f32 = 0.01;         // ์—ฐ๊ฒฐ์„ ์„ ๊ทธ๋ฆด ์ตœ์†Œ ํˆฌ๋ช…๋„ ๊ฐ’

////////////////////////////////////////////////////////////////////////////////
// Particle ๊ตฌ์กฐ์ฒด - ๊ฐœ๋ณ„ ๋ณ„์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ
////////////////////////////////////////////////////////////////////////////////

struct Particle {
    pos: Vec2,      // ๋ณ„์˜ ํ˜„์žฌ ์œ„์น˜ (x, y ์ขŒํ‘œ)
    vel: Vec2,      // ๋ณ„์˜ ์ด๋™ ์†๋„์™€ ๋ฐฉํ–ฅ
    born: f32,      // ๋ณ„์ด ์ƒ์„ฑ๋œ ์‹œ๊ฐ„
    lifetime: f32,  // ๋ณ„์˜ ์ด ์ˆ˜๋ช…
    size: f32,      // ๋ณ„์˜ ํฌ๊ธฐ (๋ฐ˜์ง€๋ฆ„)
    color: Rgba,    // ๋ณ„์˜ ์ƒ‰์ƒ (RGBA ๊ฐ’)
}

impl Particle {
    // ์ƒˆ๋กœ์šด ๋ณ„ ์ƒ์„ฑ ํ•จ์ˆ˜
    fn new(origin: Vec2, now: f32) -> Self {
        let mut rng = rand::thread_rng();                    // ๋žœ๋ค ์ˆซ์ž ์ƒ์„ฑ๊ธฐ ์ดˆ๊ธฐํ™”
        let angle = rng.gen_range(0.0..TAU);                 // 0~2ฯ€ ์‚ฌ์ด์˜ ๋žœ๋ค ๊ฐ๋„
        let speed = rng.gen_range(PARTICLE_MIN_SPEED..PARTICLE_MAX_SPEED);  // ๋žœ๋ค ์†๋„
        let vel = vec2(angle.cos() * speed, angle.sin() * speed);  // ๊ฐ๋„์™€ ์†๋„๋ฅผ x,y ์†๋„๋กœ ๋ณ€ํ™˜
        let lifetime = rng.gen_range(PARTICLE_MIN_LIFETIME..PARTICLE_MAX_LIFETIME);  // ๋žœ๋ค ์ˆ˜๋ช…
        let size = rng.gen_range(PARTICLE_MIN_SIZE..PARTICLE_MAX_SIZE);     		 // ๋žœ๋ค ํฌ๊ธฐ
        let hue = rng.gen_range(0.5..0.72);                  // ํŒŒ๋ž€์ƒ‰ ๊ณ„์—ด์˜ ๋žœ๋ค ์ƒ‰์ƒ
        let color = hsla(hue, 0.7, 0.5, 1.0).into();         // HSL ์ƒ‰์ƒ์„ RGBA๋กœ ๋ณ€ํ™˜

        // ์ƒ์„ฑ๋œ ๊ฐ’๋“ค๋กœ ์ƒˆ๋กœ์šด Particle ์ธ์Šคํ„ด์Šค ๋ฐ˜ํ™˜
        Self {
            pos: origin,    // ์‹œ์ž‘ ์œ„์น˜๋Š” ๋งˆ์šฐ์Šค ํด๋ฆญ ์œ„์น˜
            vel,            // ๊ณ„์‚ฐ๋œ ์†๋„ ๋ฒกํ„ฐ
            born: now,      // ํ˜„์žฌ ์‹œ๊ฐ„์„ ์ƒ์„ฑ ์‹œ๊ฐ„์œผ๋กœ ์ €์žฅ
            lifetime,       // ๋žœ๋คํ•˜๊ฒŒ ๊ฒฐ์ •๋œ ์ˆ˜๋ช…
            size,           // ๋žœ๋คํ•˜๊ฒŒ ๊ฒฐ์ •๋œ ํฌ๊ธฐ
            color,          // ๊ฒฐ์ •๋œ ์ƒ‰์ƒ
        }
    }

    // ๋ณ„์˜ ํ˜„์žฌ ๋‚˜์ด ๊ณ„์‚ฐ (ํ˜„์žฌ ์‹œ๊ฐ„ - ์ƒ์„ฑ ์‹œ๊ฐ„)
    fn age(&self, now: f32) -> f32 {
        now - self.born
    }

    // ๋ณ„์˜ ์ˆ˜๋ช… ๋น„์œจ ๊ณ„์‚ฐ (1.0 = ์ƒˆ๋กœ ์ƒ์„ฑ, 0.0 = ์ˆ˜๋ช… ์ข…๋ฃŒ)
    fn life_ratio(&self, now: f32) -> f32 {
        1.0 - (self.age(now) / self.lifetime).clamp(0.0, 1.0)  // 0~1 ์‚ฌ์ด๋กœ ์ œํ•œ
    }

    // ๋ณ„์ด ์•„์ง ์‚ด์•„์žˆ๋Š”์ง€ ํ™•์ธ
    fn alive(&self, now: f32) -> bool {
        self.age(now) < self.lifetime  // ๋‚˜์ด๊ฐ€ ์ˆ˜๋ช…๋ณด๋‹ค ์ž‘์œผ๋ฉด ์‚ด์•„์žˆ์Œ
    }

    // ๋ณ„์˜ ์œ„์น˜์™€ ์†๋„ ์—…๋ฐ์ดํŠธ
    fn update(&mut self, dt: f32) {
        self.pos += self.vel * dt;     // ์†๋„์— ์‹œ๊ฐ„์„ ๊ณฑํ•ด์„œ ์œ„์น˜ ์ด๋™
        self.vel *= VELOCITY_DAMPING;  // ์†๋„์— ๊ฐ์‡ ์œจ์„ ๊ณฑํ•ด์„œ ์ ์  ๋А๋ ค์ง€๊ฒŒ ํ•จ
    }
}

////////////////////////////////////////////////////////////////////////////////
// Model ๊ตฌ์กฐ์ฒด - ํ”„๋กœ๊ทธ๋žจ์˜ ์ „์ฒด ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌ
////////////////////////////////////////////////////////////////////////////////

struct Model {
    particles: Vec<Particle>,           	// ๋ชจ๋“  ๋ณ„๋“ค์„ ์ €์žฅํ•˜๋Š” ๋ฒกํ„ฐ
    show_title: bool,                   	// ์ œ๋ชฉ ํ™”๋ฉด ํ‘œ์‹œ ์—ฌ๋ถ€
    is_fullscreen: bool,                	// ์ „์ฒด ํ™”๋ฉด ๋ชจ๋“œ ์—ฌ๋ถ€
    grid: HashMap<(i32, i32), Vec<usize>>,  // ๊ณต๊ฐ„ ๋ถ„ํ• ์„ ์œ„ํ•œ ๊ทธ๋ฆฌ๋“œ ์‹œ์Šคํ…œ
    cell_size: f32,                     	// ๊ทธ๋ฆฌ๋“œ์˜ ๊ฐ ์…€ ํฌ๊ธฐ
}

impl Model {
    // ์ƒˆ๋กœ์šด ๋ชจ๋ธ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
    fn new() -> Self {
        Self {
            particles: Vec::new(),      	// ๋นˆ ๋ณ„ ๋ชฉ๋ก์œผ๋กœ ์‹œ์ž‘
            show_title: true,           	// ์ฒ˜์Œ์—๋Š” ์ œ๋ชฉ ํ™”๋ฉด ํ‘œ์‹œ
            is_fullscreen: false,       	// ์ „์ฒด ํ™”๋ฉด ๋ชจ๋“œ๋Š” ๊บผ์ง„ ์ƒํƒœ๋กœ ์‹œ์ž‘
            grid: HashMap::new(),       	// ๋นˆ ๊ทธ๋ฆฌ๋“œ๋กœ ์‹œ์ž‘
            cell_size: CONNECT_DISTANCE,	// ์…€ ํฌ๊ธฐ๋ฅผ ์—ฐ๊ฒฐ ๊ฑฐ๋ฆฌ์™€ ๋™์ผํ•˜๊ฒŒ ์„ค์ •
        }
    }

    // ๊ณต๊ฐ„ ๊ทธ๋ฆฌ๋“œ ์—…๋ฐ์ดํŠธ - ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด
    fn update_grid(&mut self) {
        self.grid.clear();  // ๊ธฐ์กด ๊ทธ๋ฆฌ๋“œ ๋น„์šฐ๊ธฐ
        
        // ๋ชจ๋“  ๋ณ„๋“ค์„ ๊ทธ๋ฆฌ๋“œ์— ๋ฐฐ์น˜
        for (i, particle) in self.particles.iter().enumerate() {
            // ๋ณ„์˜ ์œ„์น˜๋ฅผ ์…€ ์ขŒํ‘œ๋กœ ๋ณ€ํ™˜
            let cell_x = (particle.pos.x / self.cell_size).floor() as i32;
            let cell_y = (particle.pos.y / self.cell_size).floor() as i32;
            
            // ํ•ด๋‹น ์…€์— ๋ณ„์˜ ์ธ๋ฑ์Šค ์ถ”๊ฐ€ (์—†์œผ๋ฉด ์ƒˆ ๋ฒกํ„ฐ ์ƒ์„ฑ)
            self.grid.entry((cell_x, cell_y)).or_insert_with(Vec::new).push(i);
        }
    }

    // ๋ชจ๋“  ๋ณ„๋“ค ์ œ๊ฑฐํ•˜๊ณ  ์ดˆ๊ธฐ ์ƒํƒœ๋กœ ๋ฆฌ์…‹
    fn clear_particles(&mut self) {
        self.particles.clear();   	// ๋ณ„ ๋ชฉ๋ก ๋น„์šฐ๊ธฐ
        self.grid.clear();        	// ๊ทธ๋ฆฌ๋“œ ๋น„์šฐ๊ธฐ
        self.show_title = true;   	// ์ œ๋ชฉ ํ™”๋ฉด ๋‹ค์‹œ ํ‘œ์‹œ
    }
}

////////////////////////////////////////////////////////////////////////////////
// ๋ฉ”์ธ ํ•จ์ˆ˜ ๋ฐ ํ”„๋กœ๊ทธ๋žจ ์ดˆ๊ธฐํ™”
////////////////////////////////////////////////////////////////////////////////

fn main() {
    // Nannou ์•ฑ ์‹œ์ž‘: ๋ชจ๋ธ ์ƒ์„ฑ โ†’ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ โ†’ ๋ทฐ ํ•จ์ˆ˜ ์ˆœ์œผ๋กœ ์‹คํ–‰
    nannou::app(model).update(update).run();
}

// ํ”„๋กœ๊ทธ๋žจ ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
fn model(app: &App) -> Model {
    // ์ƒˆ๋กœ์šด ์œˆ๋„์šฐ ์ƒ์„ฑ ๋ฐ ์„ค์ •
    app<.new_window()
        .size(1080, 1080)                // ์œˆ๋„์šฐ ํฌ๊ธฐ 1080x1080 ํ”ฝ์…€
        .key_pressed(key_pressed)        // ํ‚ค ์ž…๋ ฅ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜ ์—ฐ๊ฒฐ
        .mouse_pressed(mouse_pressed)    // ๋งˆ์šฐ์Šค ํด๋ฆญ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜ ์—ฐ๊ฒฐ
        .view(view)                      // ํ™”๋ฉด ๊ทธ๋ฆฌ๊ธฐ ํ•จ์ˆ˜ ์—ฐ๊ฒฐ
        .title("๋ณ„์ž๋ฆฌ๋งŒ๋“ค๊ธฐ")         	 // ์œˆ๋„์šฐ ์ œ๋ชฉ ์„ค์ •
        .build()                         // ์œˆ๋„์šฐ ์ƒ์„ฑ
        .unwrap();                       // ์ƒ์„ฑ ์‹คํŒจ ์‹œ ํ”„๋กœ๊ทธ๋žจ ์ข…๋ฃŒ

    Model::new()  						 // ์ƒˆ๋กœ์šด ๋ชจ๋ธ ์ธ์Šคํ„ด์Šค ๋ฐ˜ํ™˜
}

////////////////////////////////////////////////////////////////////////////////
// ํ‚ค๋ณด๋“œ ์ž…๋ ฅ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
////////////////////////////////////////////////////////////////////////////////

fn key_pressed(app: &App, model: &mut Model, key: Key) {
    match key {
        Key::F => {
            // F ํ‚ค: ์ „์ฒด ํ™”๋ฉด ํ† ๊ธ€
            model.is_fullscreen = !model.is_fullscreen;
            app.main_window().set_fullscreen(model.is_fullscreen);
        }
        Key::S => {
            // S ํ‚ค: ์Šคํฌ๋ฆฐ์ƒท ์ €์žฅ
            let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
            let filename = format!("constellation_{}.png", ts);  // ์‹œ๊ฐ„ ๊ธฐ๋ฐ˜์œผ๋กœ ํŒŒ์ผ๋ช… ์ƒ์„ฑ
            app.main_window().capture_frame(filename);           // ํ˜„์žฌ ํ”„๋ ˆ์ž„ ์บก์ฒ˜
            println!("๐Ÿ“ธ Screenshot saved!");                    // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ
        }
        Key::N => {
            // N ํ‚ค: ์ƒˆ๋กœ์šด ๋ณ„์ž๋ฆฌ ์‹œ์ž‘ (๋ชจ๋“  ๋ณ„ ์ œ๊ฑฐ)
            model.clear_particles();
        }
        Key::C => {
            // C ํ‚ค: ์ฃฝ์€ ๋ณ„๋“ค ์ •๋ฆฌ (๋””๋ฒ„๊น…์šฉ)
            let now = app.time as f32;
            model.particles.retain(|p| p.alive(now));  			// ์‚ด์•„์žˆ๋Š” ๋ณ„๋“ค๋งŒ ๋‚จ๊ธฐ๊ธฐ
            model.update_grid();                       			// ๊ทธ๋ฆฌ๋“œ ๋‹ค์‹œ ๊ณ„์‚ฐ
        }
        _ => (),  // ๋‹ค๋ฅธ ํ‚ค๋Š” ๋ฌด์‹œ
    }
}

////////////////////////////////////////////////////////////////////////////////
// ๋งˆ์šฐ์Šค ์ž…๋ ฅ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
////////////////////////////////////////////////////////////////////////////////

fn mouse_pressed(app: &App, model: &mut Model, _button: MouseButton) {
    // ๋งˆ์šฐ์Šค ํด๋ฆญ ์‹œ ์ œ๋ชฉ ํ™”๋ฉด ์ˆจ๊ธฐ๊ธฐ
    if model.show_title {
        model.show_title = false;
    }

    let mut rng = rand::thread_rng();                     // ๋žœ๋ค ์ˆซ์ž ์ƒ์„ฑ๊ธฐ
    let count = rng.gen_range(15..=30);                   // ์ƒ์„ฑํ•  ๋ณ„ ๊ฐœ์ˆ˜ (15~30๊ฐœ)
    let now = app.time as f32;                            // ํ˜„์žฌ ์‹œ๊ฐ„
    let origin = app.mouse.position();                    // ๋งˆ์šฐ์Šค ํด๋ฆญ ์œ„์น˜

    // ์ง€์ •๋œ ๊ฐœ์ˆ˜๋งŒํผ ๋ณ„ ์ƒ์„ฑ
    for _ in 0..count {
        model.particles.push(Particle::new(origin, now)); // ์ƒˆ ๋ณ„์„ ๋ชฉ๋ก์— ์ถ”๊ฐ€
    }
    
    model.update_grid();  							      // ๊ทธ๋ฆฌ๋“œ ์—…๋ฐ์ดํŠธ (์ƒˆ๋กœ์šด ๋ณ„๋“ค์„ ๋ฐ˜์˜)
}

////////////////////////////////////////////////////////////////////////////////
// ๊ฒŒ์ž„ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ (๋งค ํ”„๋ ˆ์ž„ ํ˜ธ์ถœ๋จ)
////////////////////////////////////////////////////////////////////////////////

fn update(app: &App, model: &mut Model, _update: Update) {
    let dt = app.duration.since_prev_update.as_secs_f32();  // ์ด์ „ ํ”„๋ ˆ์ž„๋ถ€ํ„ฐ์˜ ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ
    let now = app.time as f32;                              // ํ˜„์žฌ ์‹œ๊ฐ„

    // ๋ชจ๋“  ๋ณ„๋“ค ์—…๋ฐ์ดํŠธ
    for particle in &mut model.particles {
        particle.update(dt);  // ๊ฐ ๋ณ„์˜ ์œ„์น˜์™€ ์†๋„ ์—…๋ฐ์ดํŠธ
    }

    // ์ˆ˜๋ช…์ด ๋๋‚œ ๋ณ„๋“ค ์ œ๊ฑฐ
    model.particles.retain(|p| p.alive(now));
    
    model.update_grid();     // ๋ณ€๊ฒฝ๋œ ์œ„์น˜๋ฅผ ๋ฐ˜์˜ํ•˜์—ฌ ๊ทธ๋ฆฌ๋“œ ์—…๋ฐ์ดํŠธ
}

////////////////////////////////////////////////////////////////////////////////
// ํ™”๋ฉด ๊ทธ๋ฆฌ๊ธฐ ํ•จ์ˆ˜ (๋งค ํ”„๋ ˆ์ž„ ํ˜ธ์ถœ๋จ)
////////////////////////////////////////////////////////////////////////////////

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();              // ๊ทธ๋ฆผ ๊ทธ๋ฆฌ๊ธฐ ๊ฐ์ฒด ์ƒ์„ฑ
    draw.background().color(BLACK);     // ๋ฐฐ๊ฒฝ์„ ๊ฒ€์€์ƒ‰์œผ๋กœ ์ง€์šฐ๊ธฐ
    let now = app.time as f32;          // ํ˜„์žฌ ์‹œ๊ฐ„
    let win_rect = app.window_rect();   // ์œˆ๋„์šฐ ํฌ๊ธฐ ์ •๋ณด

    ////////////////////////////////////////////////////////////////////////////
    // 1. ๋ชจ๋“  ๋ณ„๋“ค ๊ทธ๋ฆฌ๊ธฐ
    ////////////////////////////////////////////////////////////////////////////
    
    for p in &model.particles {
        let lr = p.life_ratio(now);     // ๋ณ„์˜ ํ˜„์žฌ ์ˆ˜๋ช… ๋น„์œจ ๊ณ„์‚ฐ (1.0~0.0)
        let mut col = p.color;          // ๋ณ„์˜ ๊ธฐ๋ณธ ์ƒ‰์ƒ ๋ณต์‚ฌ
        col.alpha *= lr;                // ์ˆ˜๋ช…์— ๋”ฐ๋ผ ํˆฌ๋ช…๋„ ์กฐ์ ˆ (์ ์  ์‚ฌ๋ผ์ง)
        draw.ellipse()                  // ์›ํ˜•์œผ๋กœ ๋ณ„ ๊ทธ๋ฆฌ๊ธฐ
            .xy(p.pos)                  // ๋ณ„์˜ ์œ„์น˜์—
            .radius(p.size)             // ๋ณ„์˜ ํฌ๊ธฐ๋กœ
            .color(col);                // ์กฐ์ ˆ๋œ ์ƒ‰์ƒ์œผ๋กœ
    }

    ////////////////////////////////////////////////////////////////////////////
    // 2. ๋ณ„๋“ค ์‚ฌ์ด ์—ฐ๊ฒฐ์„  ๊ทธ๋ฆฌ๊ธฐ (์„ฑ๋Šฅ ์ตœ์ ํ™”๋œ ๋ฒ„์ „)
    ////////////////////////////////////////////////////////////////////////////
    
    let grid = &model.grid;                             // ๊ณต๊ฐ„ ๊ทธ๋ฆฌ๋“œ ์ฐธ์กฐ
    let connect_distance_sq = CONNECT_DISTANCE.powi(2); // ์ œ๊ณฑ ๊ฑฐ๋ฆฌ (์„ฑ๋Šฅ์„ ์œ„ํ•ด)

    // ๊ฐ ๊ทธ๋ฆฌ๋“œ ์…€์„ ์ˆœํšŒ
    for (cell_pos, indices) in grid {
        // ํ˜„์žฌ ์…€์˜ ๊ฐ ๋ณ„์— ๋Œ€ํ•ด
        for &i in indices {
            let a = &model.particles[i];      // ์ฒซ ๋ฒˆ์งธ ๋ณ„
            if !a.alive(now) { continue; }    // ์ฃฝ์€ ๋ณ„์€ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
            
            // ์ฃผ๋ณ€ 3x3 ์…€๋“ค ๊ฒ€์‚ฌ (์ธ์ ‘ํ•œ ์…€๋“ค๋งŒ)
            for dx in -1..=1 {
                for dy in -1..=1 {
                    let neighbor_cell = (cell_pos.0 + dx, cell_pos.1 + dy);  // ์ด์›ƒ ์…€ ์ขŒํ‘œ
                    
                    // ์ด์›ƒ ์…€์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
                    if let Some(neighbor_indices) = grid.get(&neighbor_cell) {
                        // ์ด์›ƒ ์…€์˜ ๊ฐ ๋ณ„์— ๋Œ€ํ•ด
                        for &j in neighbor_indices {
                            if j <= i { continue; }  // ์ค‘๋ณต ๊ฒ€์‚ฌ ๋ฐฉ์ง€ (i < j ์กฐ๊ฑด)
                            
                            let b = &model.particles[j];  // ๋‘ ๋ฒˆ์งธ ๋ณ„
                            if !b.alive(now) { continue; }  // ์ฃฝ์€ ๋ณ„์€ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
                            
                            // ๋‘ ๋ณ„ ์‚ฌ์ด์˜ ์ œ๊ณฑ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ (์„ฑ๋Šฅ์„ ์œ„ํ•ด sqrt ํšŒํ”ผ)
                            let d_sq = a.pos.distance_squared(b.pos);
                            
                            // ์—ฐ๊ฒฐ ๊ฐ€๋Šฅํ•œ ๊ฑฐ๋ฆฌ์ธ์ง€ ํ™•์ธ
                            if d_sq <= connect_distance_sq {
                                let d = d_sq.sqrt();  // ์‹ค์ œ ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ
                                let dist_alpha = 1.0 - d / CONNECT_DISTANCE;  // ๊ฑฐ๋ฆฌ์— ๋”ฐ๋ฅธ ํˆฌ๋ช…๋„
                                let alpha = dist_alpha * a.life_ratio(now) * b.life_ratio(now);  // ์ตœ์ข… ํˆฌ๋ช…๋„
                                
                                // ์ถฉ๋ถ„ํžˆ ๋ณด์ด๋Š” ์„ ๋งŒ ๊ทธ๋ฆฌ๊ธฐ
                                if alpha > MIN_LINE_ALPHA {
                                    // ๋‘ ๋ณ„ ์ƒ‰์ƒ์˜ ํ‰๊ท ๊ฐ’์œผ๋กœ ์„  ์ƒ‰์ƒ ๊ฒฐ์ •
                                    let col = rgba(
                                        (a.color.red + b.color.red) * 0.5,
                                        (a.color.green + b.color.green) * 0.5,
                                        (a.color.blue + b.color.blue) * 0.5,
                                        alpha  // ๊ณ„์‚ฐ๋œ ํˆฌ๋ช…๋„ ์ ์šฉ
                                    );
                                    // ์—ฐ๊ฒฐ์„  ๊ทธ๋ฆฌ๊ธฐ
                                    draw.line()
                                        .start(a.pos)   // ์ฒซ ๋ฒˆ์งธ ๋ณ„์—์„œ
                                        .end(b.pos)     // ๋‘ ๋ฒˆ์งธ ๋ณ„๊นŒ์ง€
                                        .weight(0.8)    // ์„  ๋‘๊ป˜
                                        .color(col);    // ๊ฒฐ์ •๋œ ์ƒ‰์ƒ์œผ๋กœ
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    // 3. ์ œ๋ชฉ ํ™”๋ฉด ๊ทธ๋ฆฌ๊ธฐ
    ////////////////////////////////////////////////////////////////////////////
    
    if model.show_title {
        // ๋ฉ”์ธ ์ œ๋ชฉ - ์ค‘์•™ ์ƒ๋‹จ
        draw.text("CONSTELLATION")
            .color(WHITE)                // ํฐ์ƒ‰ ํ…์ŠคํŠธ
            .font_size(120)              // ํฐ ๊ธ€์”จ ํฌ๊ธฐ
            .x_y(0.0, 150.0)             // ํ™”๋ฉด ์ค‘์•™ ์ƒ๋‹จ ์œ„์น˜
            .center_justify()            // ๊ฐ€์šด๋ฐ ์ •๋ ฌ
            .wh(win_rect.wh());          // ์ฐฝ ์ „์ฒด ๋„ˆ๋น„ ์‚ฌ์šฉ (์ค„๋ฐ”๊ฟˆ ๋ฐฉ์ง€)
            
        // ๋ถ€์ œ๋ชฉ - ํ™”๋ฉด ์ค‘์•™
        draw.text("Create your own star constellations")
            .color(SILVER)               // ์€์ƒ‰ ํ…์ŠคํŠธ
            .font_size(36)               // ์ค‘๊ฐ„ ๊ธ€์”จ ํฌ๊ธฐ
            .x_y(0.0, 0.0)               // ํ™”๋ฉด ์ •์ค‘์•™ ์œ„์น˜
            .center_justify()            // ๊ฐ€์šด๋ฐ ์ •๋ ฌ
            .wh(win_rect.wh());          // ์ฐฝ ์ „์ฒด ๋„ˆ๋น„ ์‚ฌ์šฉ
            
        // ์กฐ์ž‘๋ฒ• ์•ˆ๋‚ด - ์ค‘์•™ ํ•˜๋‹จ
        draw.text("CLICK TO CREATE STARS  โ€ข  F: FULLSCREEN  โ€ข  S: SCREENSHOT  โ€ข  N: NEW")
            .color(GRAY)                 // ํšŒ์ƒ‰ ํ…์ŠคํŠธ
            .font_size(24)               // ์ž‘์€ ๊ธ€์”จ ํฌ๊ธฐ
            .x_y(0.0, -150.0)            // ํ™”๋ฉด ์ค‘์•™ ํ•˜๋‹จ ์œ„์น˜
            .center_justify()            // ๊ฐ€์šด๋ฐ ์ •๋ ฌ
            .wh(win_rect.wh());          // ์ฐฝ ์ „์ฒด ๋„ˆ๋น„ ์‚ฌ์šฉ (ํ•œ ์ค„๋กœ ํ‘œ์‹œ)
    }

    ////////////////////////////////////////////////////////////////////////////
    // 4. ๋””๋ฒ„๊น… ์ •๋ณด ํ‘œ์‹œ
    ////////////////////////////////////////////////////////////////////////////
    
    if !model.show_title {
        // ํ˜„์žฌ ๋ณ„ ๊ฐœ์ˆ˜ ํ‘œ์‹œ (ํ™”๋ฉด ์ขŒ์ธก ์ƒ๋‹จ)
        draw.text(&format!("Particles: {}", model.particles.len()))
            .color(GRAY)                // ํšŒ์ƒ‰ ํ…์ŠคํŠธ
            .font_size(18)              // ๋งค์šฐ ์ž‘์€ ๊ธ€์”จ
            .x_y(-500.0, 500.0);        // ์ขŒ์ธก ์ƒ๋‹จ ์ฝ”๋„ˆ ์œ„์น˜
    }

    // ๊ทธ๋ฆฌ๊ธฐ ๋ช…๋ น๋“ค์„ ์‹ค์ œ ํ”„๋ ˆ์ž„์— ์ ์šฉ
    draw.to_frame(app, &frame).unwrap();
}

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

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