:: BASIC_06_white_circle_rotation

BamgasiJM·2025년 8월 31일

Nannou <BASIC>

목록 보기
19/41
post-thumbnail

📝 Rust Code

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

// --- 구조체 분리 ---
// 1. 애니메이션 관련 상태를 관리하는 구조체
struct Animation {
    rotation: f32,
}

// 2. 파일 저장 관련 상태를 관리하는 구조체
struct Saving {
    counter: u32,
}

// 3. 메인 모델 구조체는 위 두 구조체를 필드로 가짐
struct Model {
    animation: Animation,
    saving: Saving,
}

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

fn model(app: &App) -> Model {
    app.new_window().size(800, 800).build().unwrap();
    Model {
        animation: Animation { rotation: 0.0 }, // Animation 구조체 초기화
        saving: Saving { counter: 1 },          // Saving 구조체 초기화
    }
}

fn update(_app: &App, model: &mut Model, _update: Update) {
    // 애니메이션 상태에 접근하여 rotation 값을 업데이트
    model.animation.rotation += 0.08;
}

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, &mut model.saving, "png"), // saving 상태만 넘겨줌
            Key::J => save_frame(app, &mut model.saving, "jpg"), // saving 상태만 넘겨줌
            _ => {}
        }
    }
}

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();
    draw.rect().wh(app.window_rect().wh()).color(rgba(0.0, 0.0, 0.0, 0.05));

    let radius = 200.0;
    // 애니메이션 상태에 접근하여 rotation 값을 사용
    let angle = model.animation.rotation;
    let x = radius * angle.cos();
    let y = radius * angle.sin();

    draw.ellipse().x_y(x, y).radius(80.0).color(WHITE);
    draw.to_frame(app, &frame).unwrap();
}

// save_frame 함수는 Saving 상태만 필요로 함
fn save_frame(app: &App, saving: &mut Saving, format: &str) {
    let filename = format!("render_{:03}.{}", saving.counter, format);
    let path = Path::new(&filename);
    app.main_window().capture_frame(path);

    println!("Frame saved as {}", filename);

    saving.counter += 1;
}

📝 구조체 분리

1. Model 구조체 분리 🧱

  • Animation 구조체: 원의 회전과 관련된 rotation 필드만 가집니다. 순수하게 애니메이션의 상태를 표현합니다.

  • Saving 구조체: 이미지 저장 횟수를 세는 counter 필드만 가집니다. 파일 저장과 관련된 상태를 담당합니다.

  • Model 구조체: 이제 AnimationSaving 구조체를 필드로 가집니다. 이를 통해 전체 애플리케이션의 상태를 조합하여 관리합니다.

2. 더 좋은 상태 접근 방식 ➡️

  • Model 구조체 하나에 rotationcounter 필드를 몰아 넣으면 model.rotation, model.counter 와 같이 Model에서 직접 필드에 접근. 명확하지 않고 코드가 복잡해지면 수정이 힘들어 집니다.

  • 이 코드에서는model.animation.rotation, model.saving.counter 와 같이 어떤 역할의 상태에 접근하는지 명확하게 알 수 있습니다.

3. save_frame 함수 시그니처 변경 ✍️

  • 기존 save_frame 함수는 &mut Model 전체를 인자로 받았습니다. 하지만 실제로는 counter 필드만 필요했죠.

  • 수정된 함수는 &mut Saving을 인자로 받습니다. 이렇게 하면 이 함수가 오직 저장 상태만 수정한다는 것을 명확히 알 수 있고, 불필요한 데이터 접근을 막을 수 있습니다.


📝 주요 문법

1. 이벤트 처리: event 함수와 Event 열거형

update 함수가 매 프레임마다 지속적으로 실행되어 상태를 바꾸는 역할이라면, event 함수는 키보드 입력이나 마우스 클릭처럼 불연속적으로, 특정 시점에만 발생하는 사건을 처리하는 역할을 합니다.

Event 열거형(Enum): Nannou에서 발생할 수 있는 모든 종류의 이벤트를 정의해놓은 타입입니다. KeyPressed, MouseMoved, WindowResized 등 다양한 종류가 있습니다.

.event(event_fn) 등록: main 함수에서 .event(event) 를 추가함으로써, Nannou에게 "이제부터 이벤트가 발생하면 event 라는 이름의 함수를 호출해줘" 라고 알려주는 것입니다.


2. 패턴 매칭: if letmatch

Rust의 가장 강력하고 독특한 기능 중 하나는 패턴 매칭(Pattern Matching)입니다. 복잡한 데이터 구조에서 내가 원하는 모양의 데이터만 간결하게 뽑아내거나, 값의 종류에 따라 다른 코드를 실행할 때 사용됩니다.

if let - 한 가지 경우만 처리할 때

// 만약 'event'라는 변수가 'Event::WindowEvent'이고, 그 안의 'simple' 필드가
// 'Some(WindowEvent::KeyPressed(key))'라는 패턴과 일치한다면,
// 그 안의 'key' 값을 추출해서 중괄호 안의 코드를 실행해라.
if let Event::WindowEvent { simple: Some(WindowEvent::KeyPressed(key)), .. } = event {
    // ... key를 사용하는 코드 ...
}

이 코드는 여러 이벤트 중 오직 '키가 눌렸을 때'라는 단 한 가지 경우에만 관심이 있기 때문에 if let을 사용했습니다. if-else if-else를 길게 쓰는 것보다 훨씬 간결하고 명확합니다.

  • .. (나머지 패턴): WindowEvent 구조체 안에는 simple 외에 다른 필드들도 있지만, 우리는 관심이 없으므로 .. 로 "나머지는 무시하겠다"고 명시합니다.

match - 여러 경우를 처리할 때
if let을 통해 '눌린 키'(key)를 성공적으로 추출했다면, 이제 그 키가 'P'인지 'J'인지 아니면 다른 키인지 구별해야 합니다. 이럴 때 match가 완벽한 도구입니다.

match key {
    Key::P => { /* P 키 눌렸을 때 할 일 */ },
    Key::J => { /* J 키 눌렸을 때 할 일 */ },
    _ => { /* 그 외 모든 키에 대해 할 일 (여기서는 아무것도 안함) */ }
}

matchkey 변수의 값을 위에서부터 각 => 왼쪽에 있는 패턴과 비교합니다. 일치하는 첫 번째 패턴의 => 오른쪽 코드를 실행하고 match 블록을 빠져나옵니다.
_ 는 어떤 값과도 일치하는 와일드카드 패턴으로, switch-case 구문의 default와 같은 역할을 합니다.


3. 파일 경로 다루기: std::path::Path

프로그래밍에서 파일 경로를 문자열로만 다루면 운영체제(Windows는 \, macOS/Linux는 / 사용)에 따라 문제가 생길 수 있습니다.
Rust의 std::path::Path는 이런 차이점을 알아서 처리해주는 운영체제 독립적인 경로 표현 방식입니다.

window.capture_frame() 같은 파일 입출력 함수들은 보통 단순 문자열(&str)보다는 Path 타입을 인자로 받는 것을 선호합니다. 이는 더 안전하고 명확하기 때문입니다.


4. 문자열 포매팅: format! 매크로

println!이 문자열을 만들어 화면에 출력하는 매크로라면, format!문자열을 만들어 변수에 저장할 수 있게 해주는 매크로입니다.

// model.counter가 5이고, format이 "jpg" 라면
let filename = format!("output_{}.{}", model.counter, format);
// filename 변수에는 "output_5.jpg" 라는 문자열이 저장됩니다.

{} 안에 변수 이름을 넣어주면 그 위치에 해당 변수의 값이 문자열로 변환되어 들어갑니다. 동적으로 파일 이름을 생성하는 데 매우 유용합니다.


✅ ⚙️🛠️📄📚🎨🖼️🧩🧠🧮🕹️🧿⏳🎛️🔮🌌🪩🗂️📝🧰🔒

아보카도
Sample
Important
Highlighting
비색
적황색
purple
hot pink
yellow
살구색
orange
green
blue

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

0개의 댓글