๐Ÿ”ฎ :: Blackhole Explosion

BamgasiJMยท2025๋…„ 12์›” 17์ผ

Nannou <Generative Art>

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

๐Ÿ“ Rust Code

project/
 โ”œโ”€ Cargo.toml
 โ”œโ”€ src/main.rs
 โ””โ”€ assets/image.jpg   โ† ์—ฌ๊ธฐ์— ์ด๋ฏธ์ง€
use nannou::prelude::*;
use nannou::image as nimage;

// =======================================================
// ์ „์—ญ ์ƒ์ˆ˜
// =======================================================

const WIN_W: u32 = 1920;
const WIN_H: u32 = 1080;
const IMAGE_TARGET_WIDTH: f32 = 1000.0;
const PIXEL_STEP: u32 = 10;
const RETURN_FORCE: f32 = 0.03;
const DAMPING: f32 = 0.88;
const MOUSE_RADIUS: f32 = 150.0;
const ATTRACTION_FORCE: f32 = 4.0;
const EXPLOSION_FORCE: f32 = 100.0;
const GAUSSIAN_SIGMA: f32 = 0.55;

// ์ตœ์ ํ™”: ๋ฐ˜๊ฒฝ ์ œ๊ณฑ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ (sqrt ์—ฐ์‚ฐ ํšŒํ”ผ)
const MOUSE_RADIUS_SQ: f32 = MOUSE_RADIUS * MOUSE_RADIUS;

// =======================================================
// ํŒŒํ‹ฐํด ๊ตฌ์กฐ์ฒด
// =======================================================

struct Particle {
    origin: Vec2,
    pos: Vec2,
    vel: Vec2,
    color: Rgba,
}

// =======================================================
// ๋ชจ๋ธ
// =======================================================

struct Model {
    particles: Vec<Particle>,
    mouse_pos: Vec2,
    mouse_down: bool,
}

// =======================================================
// ๋ฉ”์ธ
// =======================================================

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

// =======================================================
// ๋ชจ๋ธ ์ƒ์„ฑ
// =======================================================

fn model(app: &App) -> Model {
    app.new_window()
        .size(WIN_W, WIN_H)
        .title("Blackhole Explosion")
        .build()
        .unwrap();

    // ์ˆ˜์ •: ์ตœ์‹  nannou API
    let assets = app.assets_path().expect("assets ๊ฒฝ๋กœ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค");
    let img_path = assets.join("galaxy.jpg");
    
    let img = nimage::open(&img_path)
        .expect("์ด๋ฏธ์ง€๋ฅผ ๋กœ๋“œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค")
        .to_rgba8();

    let (iw, ih) = img.dimensions();
    let scale = IMAGE_TARGET_WIDTH / iw as f32;
    let target_h = ih as f32 * scale;

    let offset_x = -IMAGE_TARGET_WIDTH / 2.0;
    let offset_y = -target_h / 2.0;

    // ์ˆ˜์ •: ์šฉ๋Ÿ‰ ๋ฏธ๋ฆฌ ํ• ๋‹น (์žฌํ• ๋‹น ๋ฐฉ์ง€)
    let estimated_size = ((iw / PIXEL_STEP) * (ih / PIXEL_STEP)) as usize;
    let mut particles = Vec::with_capacity(estimated_size);

    for y in (0..ih).step_by(PIXEL_STEP as usize) {
        for x in (0..iw).step_by(PIXEL_STEP as usize) {
            let p = img.get_pixel(x, y);
            let alpha = p[3] as f32 / 255.0;

            if alpha < 0.1 {
                continue;
            }

            let px = x as f32 * scale + offset_x;
            let py = (ih - y) as f32 * scale + offset_y;

            particles.push(Particle {
                origin: vec2(px, py),
                pos: vec2(px, py),
                vel: Vec2::ZERO,
                color: rgba(
                    p[0] as f32 / 255.0,
                    p[1] as f32 / 255.0,
                    p[2] as f32 / 255.0,
                    alpha,
                ),
            });
        }
    }

    // ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™”: ์—ฌ์œ  ๊ณต๊ฐ„ ์ œ๊ฑฐ
    particles.shrink_to_fit();

    Model {
        particles,
        mouse_pos: Vec2::ZERO,
        mouse_down: false,
    }
}

// =======================================================
// ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
// =======================================================

fn event(_app: &App, model: &mut Model, event: Event) {
    match event {
        Event::WindowEvent { simple: Some(ev), .. } => match ev {
            MouseMoved(pos) => {
                model.mouse_pos = pos;
            }
            MousePressed(MouseButton::Left) => {
                model.mouse_down = true;
                
                // ์ฆ‰์‹œ ํญ๋ฐœ ์ ์šฉ (์ง€์—ฐ ์ œ๊ฑฐ)
                for p in model.particles.iter_mut() {
                    let to_mouse = model.mouse_pos - p.pos;
                    let dist_sq = to_mouse.length_squared();
                    
                    if dist_sq < MOUSE_RADIUS_SQ && dist_sq > 0.01 {
                        let dist = dist_sq.sqrt();
                        let g = gaussian(dist, MOUSE_RADIUS);
                        let dir = to_mouse / dist;
                        p.vel -= dir * g * EXPLOSION_FORCE;
                    }
                }
            }
            MouseReleased(MouseButton::Left) => {
                model.mouse_down = false;
            }
            _ => {}
        },
        _ => {}
    }
}

// =======================================================
// ๊ฐ€์šฐ์‹œ์•ˆ ํ•จ์ˆ˜
// =======================================================

#[inline]
fn gaussian(dist: f32, radius: f32) -> f32 {
    let x = dist / radius;
    (-x * x / GAUSSIAN_SIGMA).exp()
}

// =======================================================
// ์—…๋ฐ์ดํŠธ (ํก์ธ๋ ฅ ๊ธฐ๋ฐ˜)
// =======================================================

fn update(_app: &App, model: &mut Model, _update: Update) {
    for p in model.particles.iter_mut() {
        // 1. ๋ณต๊ท€๋ ฅ
        let to_origin = p.origin - p.pos;
        p.vel += to_origin * RETURN_FORCE;

        // 2. ๋งˆ์šฐ์Šค ํก์ธ๋ ฅ (ํด๋ฆญ ์ค‘์ด ์•„๋‹ ๋•Œ๋งŒ)
        if !model.mouse_down {
            let to_mouse = model.mouse_pos - p.pos;
            let dist_sq = to_mouse.length_squared();

            if dist_sq < MOUSE_RADIUS_SQ && dist_sq > 0.01 {
                let dist = dist_sq.sqrt();
                let g = gaussian(dist, MOUSE_RADIUS);
                let dir = to_mouse / dist;
                p.vel += dir * g * ATTRACTION_FORCE;
            }
        }

        // 3. ๋Œํ•‘
        p.vel *= DAMPING;

        // 4. ์œ„์น˜ ์—…๋ฐ์ดํŠธ
        p.pos += p.vel;
    }
}

// =======================================================
// ๋ทฐ
// =======================================================

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();
    draw.background().color(BLACK);

    for p in model.particles.iter() {
        draw.ellipse()
            .xy(p.pos)
            .radius(3.0)
            .color(p.color);
    }

    draw.to_frame(app, &frame).unwrap();
}
profile
Coding Art with Blender / oF / Processing / p5.js / nannou

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