๐Ÿ”ฎ :: Connecting Dots ver2

BamgasiJMยท2025๋…„ 8์›” 31์ผ

Nannou <Generative Art>

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

๐Ÿ“ Rust Code

use nannou::prelude::*;
use std::path::Path;

const POINT_COLOR: (u8, u8, u8) = (255, 255, 255); 
const LINE_COLOR: (u8, u8, u8)  = (255, 255, 255); 
const LINE_ALPHA: u8 = 16;                         
const POINT_SPEED: f32 = 0.3;                      

struct Point {
    position: Vec2,
    velocity: Vec2,
    color: Srgb<u8>,
}

struct Model {
    points: Vec<Point>,
    counter: u32,
}

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

fn model(app: &App) -> Model {
    app.new_window().size(1000, 1000).view(view).build().unwrap();

    let num_points = 3000;
    let mut points = Vec::new();
    for _ in 0..num_points {
        let position = pt2(
            random_range(-500.0, 500.0),
            random_range(-500.0, 500.0),
        );
        let velocity = pt2(
            random_range(-POINT_SPEED, POINT_SPEED),
            random_range(-POINT_SPEED, POINT_SPEED),
        );
        points.push(Point {
            position,
            velocity,
            color: Srgb::new(POINT_COLOR.0, POINT_COLOR.1, POINT_COLOR.2),
        });
    }

    Model { points, counter: 1 }
}

fn update(_app: &App, model: &mut Model, _update: Update) {
    for point in &mut model.points {
        point.position += point.velocity;

        if point.position.x > 500.0 || point.position.x < -500.0 {
            point.velocity.x *= -1.0;
        }
        if point.position.y > 500.0 || point.position.y < -500.0 {
            point.velocity.y *= -1.0;
        }
    }
}

fn event(app: &App, model: &mut Model, event: Event) {
    if let Event::WindowEvent { simple: Some(WindowEvent::KeyPressed(key)), .. } = event {
        match key {
            Key::P => save_frame(app, model, "png"),
            Key::J => save_frame(app, model, "jpg"),
            _ => {}
        }
    }
}

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

    for point in &model.points {
        draw.ellipse()
            .x_y(point.position.x, point.position.y)
            .radius(1.0)
            .color(point.color);
    }

    for i in 0..model.points.len() {
        for j in i + 1..model.points.len() {
            let p1 = &model.points[i];
            let p2 = &model.points[j];
            let distance = p1.position.distance(p2.position);

            if distance < 40.0 {
                draw.line()
                    .start(p1.position)
                    .end(p2.position)
                    .weight(0.4)
                    .color(Srgba::new(
                        LINE_COLOR.0,
                        LINE_COLOR.1,
                        LINE_COLOR.2,
                        LINE_ALPHA,
                    ));
            }
        }
    }

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

fn save_frame(app: &App, model: &mut Model, format: &str) {
    let filename = format!("output_{}.{}", model.counter, format);
    let path = Path::new(&filename);
    app.main_window().capture_frame(path);
    println!("Frame saved as {}", filename);
    model.counter += 1;
}

๐Ÿ“ Rust Code + Comment

// Nannou์˜ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ๊ณผ ํ‘œ์ค€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๊ฒฝ๋กœ ์ฒ˜๋ฆฌ ๊ธฐ๋Šฅ์„ ์ž„ํฌํŠธ
use nannou::prelude::*;
use std::path::Path;

// ===== ์ „์—ญ ์ƒ์ˆ˜ ์ •์˜ (์ฝ”๋“œ ์ „์ฒด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๊ณ ์ •๊ฐ’) =====
// RGB ์ƒ‰์ƒ์€ 0~255 ๋ฒ”์œ„์˜ u8 ํƒ€์ž…์œผ๋กœ ์ •์˜
const POINT_COLOR: (u8, u8, u8) = (255, 255, 255); // ์  ์ƒ‰์ƒ: ํฐ์ƒ‰ (R=255, G=255, B=255)
const LINE_COLOR: (u8, u8, u8)  = (255, 255, 255); // ์„  ์ƒ‰์ƒ: ํฐ์ƒ‰ (๋‚˜์ค‘์— ํˆฌ๋ช…๋„ ์กฐ์ ˆ)
const LINE_ALPHA: u8 = 16;                         // ์„ ์˜ ํˆฌ๋ช…๋„ (0=์™„์ „ ํˆฌ๋ช…, 255=์™„์ „ ๋ถˆํˆฌ๋ช…)
                                                   // 16์€ ์•ฝ 6% ํˆฌ๋ช…๋„ โ†’ ๋งค์šฐ ํ๋ฆฟํ•œ ์„ 
const POINT_SPEED: f32 = 0.3;                      // ์ ์˜ ์ตœ๋Œ€ ์ด๋™ ์†๋„ (ํ”ฝ์…€/ํ”„๋ ˆ์ž„ ๋‹จ์œ„)
// =================================

// ๊ฐœ๋ณ„ ์ ์˜ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๋Š” ๊ตฌ์กฐ์ฒด
struct Point {
    position: Vec2,        // ํ˜„์žฌ ์œ„์น˜ (x, y)
    velocity: Vec2,        // ์†๋„ ๋ฒกํ„ฐ (x, y ๋ฐฉํ–ฅ ์ด๋™๋Ÿ‰)
    color: Srgb<u8>,       // ์ƒ‰์ƒ โ€” Srgb<u8>๋Š” 0~255 ๋ฒ”์œ„์˜ RGB ์ƒ‰์ƒ ํƒ€์ž…
}

// ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๋Š” ๊ตฌ์กฐ์ฒด
struct Model {
    points: Vec<Point>,    // ์ˆ˜์ฒœ ๊ฐœ์˜ ์ ์„ ์ €์žฅํ•˜๋Š” ๋ฒกํ„ฐ
    counter: u32,          // ํ”„๋ ˆ์ž„ ์ €์žฅ ์‹œ ์‚ฌ์šฉํ•  ์ผ๋ จ๋ฒˆํ˜ธ (1, 2, 3, ...)
}

// ํ”„๋กœ๊ทธ๋žจ ์ง„์ž…์ : Nannou ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ค์ •
fn main() {
    nannou::app(model)
        .update(update)    // ๋งค ํ”„๋ ˆ์ž„ ์ƒํƒœ ๊ฐฑ์‹ 
        .event(event)      // ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ
        .view(view)        // ํ™”๋ฉด ๋ Œ๋”๋ง
        .run();            // ์‹คํ–‰ ์‹œ์ž‘
}

// ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
fn model(app: &App) -> Model {
    // 1000x1000 ํ”ฝ์…€์˜ ์ •์‚ฌ๊ฐํ˜• ์ฐฝ ์ƒ์„ฑ
    app.new_window().size(1000, 1000).view(view).build().unwrap();

    // 3000๊ฐœ์˜ ์ ์„ ๋žœ๋คํ•˜๊ฒŒ ์ƒ์„ฑ
    let num_points = 3000;
    let mut points = Vec::new();
    for _ in 0..num_points {
        // ์œ„์น˜: ํ™”๋ฉด ์ „์ฒด ๋ฒ”์œ„ (-500 ~ +500, -500 ~ +500)
        let position = pt2(
            random_range(-500.0, 500.0),
            random_range(-500.0, 500.0),
        );
        // ์†๋„: -0.3 ~ +0.3 ๋ฒ”์œ„์˜ ๋งค์šฐ ๋А๋ฆฐ ์†๋„
        let velocity = pt2(
            random_range(-POINT_SPEED, POINT_SPEED),
            random_range(-POINT_SPEED, POINT_SPEED),
        );
        // ์  ์ƒ์„ฑ ๋ฐ ๋ฒกํ„ฐ์— ์ถ”๊ฐ€
        points.push(Point {
            position,
            velocity,
            // ์ „์—ญ ์ƒ์ˆ˜ POINT_COLOR๋ฅผ Srgb<u8> ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜
            color: Srgb::new(POINT_COLOR.0, POINT_COLOR.1, POINT_COLOR.2),
        });
    }

    // ์ดˆ๊ธฐ Model ๋ฐ˜ํ™˜ โ€” counter๋Š” 1๋ถ€ํ„ฐ ์‹œ์ž‘ (ํŒŒ์ผ๋ช…์— ์‚ฌ์šฉ)
    Model { points, counter: 1 }
}

// ๋งค ํ”„๋ ˆ์ž„ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ํ•จ์ˆ˜
fn update(_app: &App, model: &mut Model, _update: Update) {
    // ๋ชจ๋“  ์ ์„ ์ˆœํšŒํ•˜๋ฉฐ ์œ„์น˜ ๊ฐฑ์‹ 
    for point in &mut model.points {
        // ์œ„์น˜ = ์œ„์น˜ + ์†๋„ (ํ”„๋ ˆ์ž„ ๋…๋ฆฝ์  ์ด๋™์€ ์•„๋‹˜ โ€” ๋งค์šฐ ๋А๋ ค์„œ ๋ฌด๊ด€)
        point.position += point.velocity;

        // ํ™”๋ฉด ๊ฒฝ๊ณ„(-500 ~ +500)์—์„œ ํŠ•๊ธฐ๋Š” ๋กœ์ง:
        if point.position.x > 500.0 || point.position.x < -500.0 {
            point.velocity.x *= -1.0; // x ๋ฐฉํ–ฅ ์†๋„ ๋ฐ˜์ „
        }
        if point.position.y > 500.0 || point.position.y < -500.0 {
            point.velocity.y *= -1.0; // y ๋ฐฉํ–ฅ ์†๋„ ๋ฐ˜์ „
        }
    }
}

// ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
fn event(app: &App, model: &mut Model, event: Event) {
    // ํ‚ค๋ณด๋“œ ํ‚ค๊ฐ€ ๋ˆŒ๋ ธ๋Š”์ง€ ํ™•์ธ
    if let Event::WindowEvent { simple: Some(WindowEvent::KeyPressed(key)), .. } = event {
        match key {
            Key::P => save_frame(app, model, "png"), // 'P' ํ‚ค: PNG๋กœ ์ €์žฅ
            Key::J => save_frame(app, model, "jpg"), // 'J' ํ‚ค: JPG๋กœ ์ €์žฅ
            _ => {} // ๋‹ค๋ฅธ ํ‚ค๋Š” ๋ฌด์‹œ
        }
    }
}

// ๋งค ํ”„๋ ˆ์ž„ ํ™”๋ฉด์„ ๊ทธ๋ฆฌ๋Š” ํ•จ์ˆ˜
fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();

    // ๋ฐฐ๊ฒฝ์„ ๊ฒ€์€์ƒ‰์œผ๋กœ ์„ค์ • (rgb(0.0, 0.0, 0.0) = ๊ฒ€์ •)
    draw.background().rgb(0.0, 0.0, 0.0);

    // ๋ชจ๋“  ์ ์„ ์ž‘์€ ์›(๋ฐ˜์ง€๋ฆ„ 1.0)์œผ๋กœ ๊ทธ๋ฆฌ๊ธฐ
    for point in &model.points {
        draw.ellipse()
            .x_y(point.position.x, point.position.y) // ์ค‘์‹ฌ ์ขŒํ‘œ
            .radius(1.0)                             // ๋งค์šฐ ์ž‘์€ ์ 
            .color(point.color);                     // ํฐ์ƒ‰
    }

    // ์ ๋“ค ์‚ฌ์ด์˜ ๊ฑฐ๋ฆฌ๊ฐ€ ๊ฐ€๊นŒ์šฐ๋ฉด ์„ ์œผ๋กœ ์—ฐ๊ฒฐ โ€” "์—ฐ๊ฒฐ๋œ ์ " ํšจ๊ณผ
    // โš ๏ธ ์ด์ค‘ ๋ฃจํ”„์ด๋ฏ€๋กœ O(nยฒ) ๋ณต์žก๋„ โ†’ 3000๊ฐœ ์ ์ด๋ฉด ์•ฝ 450๋งŒ ํšŒ ๋น„๊ต!
    //   ์„ฑ๋Šฅ์— ๋ฏผ๊ฐํ•œ ํ™˜๊ฒฝ์—์„œ๋Š” ์ตœ์ ํ™” ํ•„์š” (์˜ˆ: ๊ณต๊ฐ„ ๋ถ„ํ• )
    for i in 0..model.points.len() {
        for j in i + 1..model.points.len() { // ์ค‘๋ณต ์—ฐ๊ฒฐ ๋ฐฉ์ง€ (i-j์™€ j-i๋Š” ๋™์ผ)
            let p1 = &model.points[i];
            let p2 = &model.points[j];
            let distance = p1.position.distance(p2.position); // ๋‘ ์  ์‚ฌ์ด ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ

            // ๊ฑฐ๋ฆฌ๊ฐ€ 40.0 ํ”ฝ์…€ ๋ฏธ๋งŒ์ด๋ฉด ์„  ์—ฐ๊ฒฐ
            if distance < 40.0 {
                draw.line()
                    .start(p1.position) // ์„  ์‹œ์ž‘์ 
                    .end(p2.position)   // ์„  ๋์ 
                    .weight(0.4)        // ๋งค์šฐ ์–‡์€ ์„ 
                    .color(Srgba::new(  // Srgba๋Š” ์•ŒํŒŒ(ํˆฌ๋ช…๋„)๋ฅผ ํฌํ•จํ•œ ์ƒ‰์ƒ
                        LINE_COLOR.0,
                        LINE_COLOR.1,
                        LINE_COLOR.2,
                        LINE_ALPHA,     // 0~255 ๋ฒ”์œ„์˜ ํˆฌ๋ช…๋„
                    ));
            }
        }
    }

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

// ํ˜„์žฌ ํ”„๋ ˆ์ž„์„ ์ด๋ฏธ์ง€ ํŒŒ์ผ๋กœ ์ €์žฅํ•˜๋Š” ํ•จ์ˆ˜
fn save_frame(app: &App, model: &mut Model, format: &str) {
    // ํŒŒ์ผ๋ช…: output_1.png, output_2.jpg, ...
    let filename = format!("output_{}.{}", model.counter, format);
    let path = Path::new(&filename);

    // Nannou์˜ ์ฐฝ ์บก์ฒ˜ ๊ธฐ๋Šฅ์œผ๋กœ ํ˜„์žฌ ํ”„๋ ˆ์ž„์„ ์ด๋ฏธ์ง€๋กœ ์ €์žฅ
    app.main_window().capture_frame(path);

    // ์ฝ˜์†”์— ์ €์žฅ ์™„๋ฃŒ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ
    println!("Frame saved as {}", filename);

    // ๋‹ค์Œ ์ €์žฅ์„ ์œ„ํ•ด ์นด์šดํ„ฐ ์ฆ๊ฐ€
    model.counter += 1;
}
profile
Coding Art with Blender / oF / Processing / p5.js / nannou

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