๐Ÿ”ฎ :: Drifting Geometry

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

Nannou <Generative Art>

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

๐Ÿ“ Rust Code

use nannou::prelude::*;

struct Shape {
    pos_x: f32,
    pos_y: f32,
    vel_x: f32,
    vel_y: f32,
    size: f32,
    color: Hsla,
    kind: ShapeKind,
}

enum ShapeKind {
    Circle,
    Rectangle,
}

struct Model {
    shapes: Vec<Shape>,
}

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

fn model(app: &App) -> Model {
    app.new_window().size(1080, 1080).title("Moving Shapes with Trail").view(view).build().unwrap();

    let mut shapes = Vec::new();

    for _ in 0..50 {
        let shape = Shape {
            pos_x: rand::random_range(-540.0..=540.0),
            pos_y: rand::random_range(-540.0..=540.0),
            vel_x: rand::random_range(-0.5..0.5),
            vel_y: rand::random_range(-0.5..0.5),
            size: rand::random_range(5.0..100.0),
            color: hsla(rand::random_range(0.0..1.0), 0.0, 0.7, 0.5),
            kind: if rand::random() {
                ShapeKind::Circle
            } else {
                ShapeKind::Rectangle
            },
        };
        shapes.push(shape);
    }

    Model { shapes }
}

fn update(app: &App, model: &mut Model, _update: Update) {
    let win_rect = app.window_rect();

    for shape in &mut model.shapes {
        shape.pos_x += shape.vel_x;
        shape.pos_y += shape.vel_y;

        if shape.pos_x > win_rect.right() + 50.0 {
            shape.pos_x = win_rect.left() - 50.0;
        } else if shape.pos_x < win_rect.left() - 50.0 {
            shape.pos_x = win_rect.right() + 50.0;
        }

        if shape.pos_y > win_rect.top() + 50.0 {
            shape.pos_y = win_rect.bottom() - 50.0;
        } else if shape.pos_y < win_rect.bottom() - 50.0 {
            shape.pos_y = win_rect.top() + 50.0;
        }
    }
}

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

    let frame_no = frame.nth();
    if frame_no == 0 {
        draw.background().color(hsla(0.0, 0.0, 0.0, 1.0));
    } else {
        draw.rect().wh(app.window_rect().wh()).color(hsla(0.0, 0.0, 0.0, 0.02));
    }

    for shape in &model.shapes {
        match shape.kind {
            ShapeKind::Circle => {
                draw.ellipse()
                    .x_y(shape.pos_x, shape.pos_y)
                    .radius(shape.size / 2.0)
                    .color(shape.color);
            }
            ShapeKind::Rectangle => {
                draw.rect()
                    .x_y(shape.pos_x, shape.pos_y)
                    .w_h(shape.size, shape.size)
                    .color(shape.color);
            }
        }
    }

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

๐Ÿ“ Rust Code + Comment

// Nannou์˜ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ๋ชจ๋“ˆ ์ž„ํฌํŠธ
// ์ด ์•ˆ์—๋Š” ์ž์ฃผ ์“ฐ์ด๋Š” ํƒ€์ž…(Point2, Vec2), ์ƒ‰์ƒ(Hsla, rgba ๋“ฑ),
// ๋žœ๋ค ํ•จ์ˆ˜(random, random_range), ์ˆ˜ํ•™ ์ƒ์ˆ˜(TAU) ๋“ฑ์ด ํฌํ•จ๋จ
use nannou::prelude::*;

// === Shape ๊ตฌ์กฐ์ฒด: ์›€์ง์ด๋Š” ๋„ํ˜•์˜ ์ƒํƒœ๋ฅผ ์ €์žฅ ===
struct Shape {
    pos_x: f32,   // x ์ขŒํ‘œ (ํ™”๋ฉด ์ค‘์•™ ๊ธฐ์ค€)
    pos_y: f32,   // y ์ขŒํ‘œ
    vel_x: f32,   // x ๋ฐฉํ–ฅ ์†๋„ (ํ”ฝ์…€/ํ”„๋ ˆ์ž„)
    vel_y: f32,   // y ๋ฐฉํ–ฅ ์†๋„
    size: f32,    // ํฌ๊ธฐ โ€” ์›์ด๋ฉด ์ง€๋ฆ„, ์‚ฌ๊ฐํ˜•์ด๋ฉด ํ•œ ๋ณ€์˜ ๊ธธ์ด
    color: Hsla,  // ์ƒ‰์ƒ (HSLA ๋ชจ๋ธ: ์ƒ‰์กฐ, ์ฑ„๋„, ๋ฐ๊ธฐ, ํˆฌ๋ช…๋„)
    kind: ShapeKind, // ๋„ํ˜• ์ข…๋ฅ˜ (์› ๋˜๋Š” ์‚ฌ๊ฐํ˜•) โ€” enum ์‚ฌ์šฉ
}

// === ShapeKind ์—ด๊ฑฐํ˜•(enum): ๋„ํ˜•์˜ ์ข…๋ฅ˜๋ฅผ ํ‘œํ˜„ ===
// enum์€ "ํ•˜๋‚˜์˜ ๊ฐ’์ด ์—ฌ๋Ÿฌ ๊ฐ€๋Šฅํ•œ ๋ณ€ํ˜•(variant) ์ค‘ ํ•˜๋‚˜์ž„"์„ ํ‘œํ˜„ํ•  ๋•Œ ์‚ฌ์šฉ
// โ†’ ์ด ๊ฒฝ์šฐ, ๋„ํ˜•์€ 'Circle'์ด๊ฑฐ๋‚˜ 'Rectangle' ์ค‘ ํ•˜๋‚˜์—ฌ์•ผ ํ•จ
enum ShapeKind {
    Circle,      // ์›ํ˜• ๋„ํ˜•
    Rectangle,   // ์‚ฌ๊ฐํ˜• ๋„ํ˜•
}

// === Model ๊ตฌ์กฐ์ฒด: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด ์ƒํƒœ ===
struct Model {
    shapes: Vec<Shape>, // ์—ฌ๋Ÿฌ ๋„ํ˜•์„ ์ €์žฅํ•˜๋Š” ๋ฒกํ„ฐ
}

// ํ”„๋กœ๊ทธ๋žจ ์ง„์ž…์ 
fn main() {
    nannou::app(model)    // ์ดˆ๊ธฐ ์ƒํƒœ ์ƒ์„ฑ
        .update(update)   // ๋งค ํ”„๋ ˆ์ž„ ์—…๋ฐ์ดํŠธ
        .run();           // ์‹คํ–‰
}

// ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜: ์ฐฝ ์ƒ์„ฑ ๋ฐ ๋„ํ˜• ์ดˆ๊ธฐ ๋ฐฐ์น˜
fn model(app: &App) -> Model {
    // 1080x1080 ํ”ฝ์…€ ์ฐฝ ์ƒ์„ฑ, ์ œ๋ชฉ ์„ค์ •, ๋ทฐ ํ•จ์ˆ˜ ์—ฐ๊ฒฐ
    app.new_window()
        .size(1080, 1080)
        .title("Moving Shapes with Trail")
        .view(view)
        .build()
        .unwrap();

    let mut shapes = Vec::new();

    // 50๊ฐœ์˜ ๋žœ๋ค ๋„ํ˜• ์ƒ์„ฑ
    for _ in 0..50 {
        let shape = Shape {
            // ์œ„์น˜: ์ฐฝ ์ „์ฒด ๋ฒ”์œ„ (-540 ~ +540, -540 ~ +540)
            pos_x: rand::random_range(-540.0..=540.0),
            pos_y: rand::random_range(-540.0..=540.0),
            // ์†๋„: ๋งค์šฐ ๋А๋ฆฌ๊ฒŒ (-0.5 ~ +0.5 ํ”ฝ์…€/ํ”„๋ ˆ์ž„)
            vel_x: rand::random_range(-0.5..0.5),
            vel_y: rand::random_range(-0.5..0.5),
            // ํฌ๊ธฐ: 5 ~ 100 ํ”ฝ์…€ ์‚ฌ์ด
            size: rand::random_range(5.0..100.0),
            // ์ƒ‰์ƒ: ๋ฌด์ž‘์œ„ ์ƒ‰์กฐ(Hue), ๋ฌด์ฑ„์ƒ‰(์ฑ„๋„=0), ๋ฐ๊ธฐ 70%, ํˆฌ๋ช…๋„ 50%
            color: hsla(rand::random_range(0.0..1.0), 0.0, 0.7, 0.5),
            // ๋„ํ˜• ์ข…๋ฅ˜: 50% ํ™•๋ฅ ๋กœ ์› ๋˜๋Š” ์‚ฌ๊ฐํ˜•
            kind: if rand::random() {
                ShapeKind::Circle
            } else {
                ShapeKind::Rectangle
            },
        };
        shapes.push(shape);
    }

    Model { shapes }
}

// ๋งค ํ”„๋ ˆ์ž„ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜: ๋„ํ˜• ์ด๋™ ๋ฐ ๊ฒฝ๊ณ„ ์ฒ˜๋ฆฌ
fn update(app: &App, model: &mut Model, _update: Update) {
    let win_rect = app.window_rect(); // ์ฐฝ ๊ฒฝ๊ณ„ ์ •๋ณด

    for shape in &mut model.shapes {
        // ์œ„์น˜ ๊ฐฑ์‹ : ํ˜„์žฌ ์†๋„๋งŒํผ ์ด๋™
        shape.pos_x += shape.vel_x;
        shape.pos_y += shape.vel_y;

        // X์ถ• ๊ฒฝ๊ณ„ ์ฒ˜๋ฆฌ: ํ™”๋ฉด ์˜ค๋ฅธ์ชฝ ๋์„ ๋„˜์œผ๋ฉด ์™ผ์ชฝ ๋์œผ๋กœ, ๋ฐ˜๋Œ€๋„ ๋งˆ์ฐฌ๊ฐ€์ง€
        // โ†’ 50ํ”ฝ์…€ ์—ฌ์œ ๋ฅผ ๋‘ฌ์„œ ๋„ํ˜•์ด ์™„์ „ํžˆ ์‚ฌ๋ผ์ง„ ํ›„ ๋“ฑ์žฅ (์‹œ๊ฐ์  ์ž์—ฐ์Šค๋Ÿฌ์›€)
        if shape.pos_x > win_rect.right() + 50.0 {
            shape.pos_x = win_rect.left() - 50.0;
        } else if shape.pos_x < win_rect.left() - 50.0 {
            shape.pos_x = win_rect.right() + 50.0;
        }

        // Y์ถ• ๊ฒฝ๊ณ„ ์ฒ˜๋ฆฌ: ์œ„/์•„๋ž˜๋„ ๋™์ผ
        if shape.pos_y > win_rect.top() + 50.0 {
            shape.pos_y = win_rect.bottom() - 50.0;
        } else if shape.pos_y < win_rect.bottom() - 50.0 {
            shape.pos_y = win_rect.top() + 50.0;
        }
    }
}

// ๋งค ํ”„๋ ˆ์ž„ ๋ Œ๋”๋ง ํ•จ์ˆ˜
fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();

    // ์ž”์ƒ(trail) ํšจ๊ณผ ๊ตฌํ˜„:
    // - ์ฒซ ํ”„๋ ˆ์ž„(frame_no == 0)๋งŒ ์™„์ „ ๊ฒ€์€ ๋ฐฐ๊ฒฝ
    // - ์ดํ›„ ํ”„๋ ˆ์ž„์€ ๋งค์šฐ ํˆฌ๋ช…ํ•œ ๊ฒ€์ • ์‚ฌ๊ฐํ˜• ๋ฎ์–ด์“ฐ๊ธฐ โ†’ ์ด์ „ ํ”„๋ ˆ์ž„์ด ์„œ์„œํžˆ ํ๋ ค์ง
    let frame_no = frame.nth();
    if frame_no == 0 {
        draw.background().color(hsla(0.0, 0.0, 0.0, 1.0));
    } else {
        draw.rect()
            .wh(app.window_rect().wh()) // ์ฐฝ ์ „์ฒด ํฌ๊ธฐ
            .color(hsla(0.0, 0.0, 0.0, 0.02)); // ์•ŒํŒŒ 0.02 โ†’ ๋งค์šฐ ํˆฌ๋ช…
    }

    // ๋ชจ๋“  ๋„ํ˜• ๊ทธ๋ฆฌ๊ธฐ
    for shape in &model.shapes {
        match shape.kind {
            // enum์˜ variant์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๊ทธ๋ฆฌ๊ธฐ ๋ฐฉ์‹ ์„ ํƒ
            ShapeKind::Circle => {
                draw.ellipse()
                    .x_y(shape.pos_x, shape.pos_y)     // ์ค‘์‹ฌ ์ขŒํ‘œ
                    .radius(shape.size / 2.0)          // ๋ฐ˜์ง€๋ฆ„ = ์ง€๋ฆ„ / 2
                    .color(shape.color);
            }
            ShapeKind::Rectangle => {
                draw.rect()
                    .x_y(shape.pos_x, shape.pos_y)     // ์ค‘์‹ฌ ์ขŒํ‘œ
                    .w_h(shape.size, shape.size)       // ์ •์‚ฌ๊ฐํ˜•
                    .color(shape.color);
            }
        }
    }

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

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