

use nannou::prelude::*;
const RADIUS: f32 = 300.0;
fn main() {
nannou::app(model).update(update).run();
}
struct Model {
dots: Vec<Dot>,
}
struct Dot {
position: Point2,
velocity: Vec2,
}
fn model(app: &App) -> Model {
app.new_window()
.size(800, 800)
.view(view)
.build()
.unwrap();
let mut dots = Vec::new();
dots.push(create_dot(app));
Model { dots }
}
fn create_dot(app: &App) -> Dot {
let win = app.window_rect();
Dot {
position: pt2(win.left() - RADIUS, win.bottom() - RADIUS), // 완전히 화면 밖에서 시작
velocity: vec2(2.0, 2.0), // 우상향 속도
}
}
fn update(app: &App, model: &mut Model, _update: Update) {
let win = app.window_rect();
// 모든 원 업데이트
for dot in &mut model.dots {
dot.position += dot.velocity;
}
// 현재 원이 우상단 경계에 도달했는지 확인
if let Some(current_dot) = model.dots.first() {
if current_dot.position.x >= win.right() - RADIUS &&
current_dot.position.y >= win.top() - RADIUS &&
model.dots.len() == 1 {
// 새로운 원 추가 (왼쪽 아래에서 시작)
model.dots.push(create_dot(app));
}
}
// 화면 밖으로 완전히 나간 원 제거
model.dots.retain(|dot| {
!(dot.position.x > win.right() + RADIUS && dot.position.y > win.top() + RADIUS)
});
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(hsl(0.0, 0.0, 0.01));
for dot in &model.dots {
draw.ellipse()
.xy(dot.position)
.radius(RADIUS)
.color(rgba(0.8, 0.4, 0.4, 1.0));
}
draw.to_frame(app, &frame).unwrap();
}
// nannou 라이브러리의 기본 기능들을 가져옵니다 (prelude는 자주 사용하는 모듈들을 한번에 가져오는 역할)
use nannou::prelude::*;
// 원의 반지름을 상수로 정의 (상수는 컴파일 타임에 값이 결정되며 변경 불가)
const RADIUS: f32 = 300.0;
// 프로그램의 진입점 (main 함수)
fn main() {
// nannou 앱을 설정하고 실행
// model 함수로 초기 모델 생성, update 함수로 매 프레임 업데이트, run으로 앱 실행
nannou::app(model).update(update).run();
}
// 앱의 상태를 나타내는 메인 모델 구조체
struct Model {
dots: Vec<Dot>, // 여러 개의 원을 관리하기 위한 벡터 (가변 길이 배열)
}
// 개별 원의 정보를 저장하는 구조체
struct Dot {
position: Point2, // 2D 좌표 (x, y) - 원의 중심 위치
velocity: Vec2, // 2D 벡터 (x, y) - 원의 이동 속도와 방향
}
// 앱 초기화 함수 - 윈도우 생성과 초기 모델 설정
fn model(app: &App) -> Model {
// 새로운 윈도우 생성
app.new_window()
.size(800, 800) // 윈도우 크기 800x800 픽셀
.view(view) // view 함수를 렌더링 콜백으로 지정
.build() // 윈도우 빌드
.unwrap(); // Result 타입을 unwrap으로 안전하게 처리 (에러 시 패닉)
// 빈 벡터 생성
let mut dots = Vec::new();
// create_dot 함수로 첫 번째 원 생성하여 벡터에 추가
dots.push(create_dot(app));
// Model 구조체 인스턴스 생성 후 반환
Model { dots }
}
// 새로운 원을 생성하는 도우미 함수
fn create_dot(app: &App) -> Dot {
// 현재 윈도우의 사각형 영역 정보 가져오기 (left, right, top, bottom 등)
let win = app.window_rect();
// Dot 구조체 인스턴스 생성
Dot {
// 위치: 화면 왼쪽 아래 모서리 밖에서 시작
// pt2()는 Point2 생성 함수 (x, y 좌표)
position: pt2(win.left() - RADIUS, win.bottom() - RADIUS),
// 속도: 우상향으로 이동 (x: 오른쪽, y: 위쪽)
// vec2()는 Vec2 생성 함수
velocity: vec2(2.0, 2.0),
}
}
// 매 프레임마다 호출되는 업데이트 함수
// app: 앱 참조, model: 가변 참조로 모델 수정, _update: 사용하지 않는 매개변수
fn update(app: &App, model: &mut Model, _update: Update) {
// 현재 윈도우의 사각형 영역 정보 가져오기
let win = app.window_rect();
// 모든 원들의 위치 업데이트
// &mut model.dots: 가변 참조로 원들에 접근하여 수정
for dot in &mut model.dots {
// 현재 위치에 속도를 더해서 새로운 위치 계산
// Vec2와 Point2의 덧셈 연산이 가능하도록 구현되어 있음
dot.position += dot.velocity;
}
// 현재 첫 번째 원이 존재하는지 확인 (if let 패턴 매칭)
if let Some(current_dot) = model.dots.first() {
// 첫 번째 원이 우상단 경계에 도달했는지 AND 원이 1개만 있는지 확인
// 조건 1: x 좌표가 오른쪽 경계 - 반지름에 도달 (원이 오른쪽 경계에 닿기 시작)
// 조건 2: y 좌표가 위쪽 경계 - 반지름에 도달 (원이 위쪽 경계에 닿기 시작)
// 조건 3: 현재 원이 1개만 있을 때 (중복 생성 방지)
if current_dot.position.x >= win.right() - RADIUS &&
current_dot.position.y >= win.top() - RADIUS &&
model.dots.len() == 1 {
// 새로운 원을 생성하여 벡터에 추가
// 이때 첫 번째 원은 아직 화면에 있음
model.dots.push(create_dot(app));
}
}
// 화면 밖으로 완전히 나간 원들 제거
// retain() 메서드: 클로저 조건이 true인 요소만 남기고 제거
model.dots.retain(|dot| {
// 조건: 원이 화면을 완전히 벗어나지 않았는지 확인
// !(NOT) 연산자: 조건이 false일 때만 retain (제거하지 않음)
// 조건: x 좌표가 오른쪽 경계 + 반지름보다 크고 AND y 좌표가 위쪽 경계 + 반지름보다 큼
!(dot.position.x > win.right() + RADIUS && dot.position.y > win.top() + RADIUS)
});
}
// 매 프레임마다 호출되는 렌더링 함수
// app: 앱 참조, model: 불변 참조로 모델 읽기, frame: 현재 프레임
fn view(app: &App, model: &Model, frame: Frame) {
// 그림을 그리기 위한 Draw 인스턴스 생성
let draw = app.draw();
// 배경색 설정 - hsl(색상, 채도, 명도) 색상 모델 사용
// 검은색에 가까운 어두운 배경 (명도 1%)
draw.background().color(hsl(0.0, 0.0, 0.01));
// 모든 원들에 대해 반복
for dot in &model.dots {
// 원(타원) 그리기
draw.ellipse()
.xy(dot.position) // 원의 중심 위치 설정
.radius(RADIUS) // 원의 반지름 설정
.color(rgba(0.8, 0.4, 0.4, 1.0)); // 원의 색상 설정
}
// 그려진 내용을 프레임 버퍼에 기록
draw.to_frame(app, &frame).unwrap();
}
Some은 Rust의 Option 열거형(enum)에서 값이 존재함을 나타내는 variant(배리언트)입니다.
Option 열거형 구조enum Option<T> {
Some(T), // 값이 있음 (T 타입의 값을 감싸고 있음)
None, // 값이 없음
}
if let Some(current_dot) = model.dots.first() 의 동작 원리1. model.dots.first()의 반환값
first() 메서드는 Option<&Dot> 타입을 반환합니다None을 반환Some(&첫번째_요소)를 반환 2. if let 패턴 매칭
if let Some(current_dot) = model.dots.first() {
// Some인 경우 실행 (값이 존재할 때)
// current_dot 변수에 &Dot 타입의 참조가 바인딩됨
}
3. 실제 동작 예시
let dots = vec![Dot { position: pt2(0, 0), velocity: vec2(1, 1) }];
// case 1: 벡터에 요소가 있을 때
if let Some(dot) = dots.first() {
// 이 블록이 실행됨
// dot 변수에 &Dot 참조가 바인딩됨
println!("원의 위치: {:?}", dot.position);
}
// case 2: 빈 벡터일 때
let empty_dots: Vec<Dot> = Vec::new();
if let Some(dot) = empty_dots.first() {
// 이 블록은 실행되지 않음 (None이므로)
}
Some이 필요한가?1. Null 안전성
null 개념이 없음Option을 사용하여 값의 존재/부재를 명시적으로 처리2. 명시적 에러 처리
// ❌ 이렇게 직접 접근하면 벡터가 비었을 때 패닉 발생
// let dot = model.dots.first().unwrap(); // 위험!
// ✅ 안전한 접근
if let Some(dot) = model.dots.first() {
// 값이 있을 때만 안전하게 사용
dot.position;
}
3. 대안 비교
// 방법 1: if let (권장 - 간결함)
if let Some(dot) = model.dots.first() {
// dot 사용
}
// 방법 2: match (더 장황함)
match model.dots.first() {
Some(dot) => {
// dot 사용
}
None => {
// 값이 없을 때 처리
}
}
// 방법 3: is_some() 체크 (덜 우아함)
if model.dots.first().is_some() {
let dot = model.dots.first().unwrap(); // 두 번 호출해야 함
}
우리 코드에서의 역할
if let Some(current_dot) = model.dots.first() {
// model.dots가 비어있지 않을 때만 이 블록 실행
// current_dot은 &Dot 타입으로, 첫 번째 원의 불변 참조
if current_dot.position.x >= win.right() - RADIUS {
// 조건 체크
}
}
// model.dots가 비어있으면 이 블록 건너뜀
즉, Some은 "값이 있어!"라고 알려주는 Rust의 안전한 방식이며, if let Some() 패턴은 그 값을 안전하게 추출하여 사용하는 관용적 방법입니다.
dots: Vec<Dot> 의 의미struct Model { dots: Vec<Dot>, // Dot 타입의 벡터를 저장하는 필드 선언 }
이 코드는 "Model구조체는 dots라는 필드를 가지며, 이 필드는 Dot 타입의 요소들을 저장하는 Vec(벡터)이다" 라고 선언하고 있습니다.
fn model(app: &App) -> Model {
let mut dots = Vec::new(); // 빈 벡터 생성 (아직 타입이 추론되지 않음)
dots.push(create_dot(app)); // Dot 타입의 값을 push → 벡터 타입이 Vec<Dot>으로 추론됨
Model { dots } // 구조체 인스턴스 생성
}
1. 빈 벡터 생성 시점
let mut dots = Vec::new();
dots의 타입은 아직 결정되지 않음2. 타입 추론 발생
dots.push(create_dot(app)); // create_dot()은 Dot 타입을 반환
create_dot() 함수가 Dot 타입을 반환함Dot 타입을 벡터에 push하면 컴파일러가 "아, 이 벡터는 Vec<Dot> 타입이구나!"라고 추론3. 구조체 인스턴스 생성
Model { dots } // 필드 초기화 shorthand (dots: dots와 동일)
Rust는 강타입 언어이지만 타입 추론을 지원합니다:
// 명시적 타입 선언 (가능하지만 불필요)
let mut dots: Vec<Dot> = Vec::new();
// 타입 추론 활용 (일반적인 방식)
let mut dots = Vec::new();
dots.push(Dot { ... }); // 여기서 타입이 Vec<Dot>으로 추론됨
Model {
dots: Vec<Dot> ─────→ [Heap 메모리]
│
↓
[Dot { position, velocity }, Dot { position, velocity }, ...]
}
Model 구조체는 스택에 저장Vec<Dot>은 Heap에 동적 배열로 저장Dot 인스턴스도 Heap에 저장// ❌ 이것은 컴파일 에러!
let mut dots = Vec::new();
dots.push("문자열"); // 에러: Model의 dots 필드는 Vec<Dot> 타입이어야 함
dots.push(123); // 에러: Dot 타입이 아님
// ✅ 이것만 가능
dots.push(Dot { ... }); // Dot 타입만 push 가능
Rust의 타입 시스템이 컴파일 타임에 타입 안정성을 보장해줍니다!
✅ ⚙️🛠️📄📚🎨🖼️🧩🧠🧮🕹️🧿⏳🎛️🔮🌌🪩🗂️📝🧰🔒
아보카도
Sample
Important
Highlighting
비색
적황색
purple
hot pink
yellow
살구색
orange
green
blue