

use nannou::prelude::*;
use nannou::noise::{ Perlin, NoiseFn };
fn main() {
nannou::app(model).update(update).run();
}
struct Model {
_window: window::Id,
perlin: Perlin,
layers: usize,
samples: usize,
spacing: f32,
amplitude: f32,
freq: f64,
time_speed: f32,
start_time: std::time::Instant,
}
fn model(app: &App) -> Model {
let window_id = app
.new_window()
.size(1200, 800)
.title("Layered Line Depth")
.view(view)
.build()
.unwrap();
let perlin = Perlin::default(); // 시드 값을 명시적으로 전달하거나 Default 사용 (최신 API)
Model {
_window: window_id,
perlin,
layers: 80,
samples: 400,
spacing: 8.0,
amplitude: 120.0,
freq: 0.003,
time_speed: 0.2,
start_time: std::time::Instant::now(),
}
}
fn update(_app: &App, _model: &mut Model, _update: Update) {
// 업데이트 로직이 필요하면 여기에 추가
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(hsla(0.0, 0.0, 0.02, 1.0));
let win = app.window_rect();
// app.time 대신 경과 시간 계산
let t = model.start_time.elapsed().as_secs_f32();
let x_min = win.left();
let x_max = win.right();
let width = x_max - x_min;
let center_y = 0.0;
for i in 0..model.layers {
let depth = ((i as f32) / ((model.layers - 1) as f32)) * 2.0 - 1.0;
let base_y = center_y + depth * ((model.layers as f32) * 0.5 * model.spacing);
let stroke_w = map_range(depth, -1.0, 1.0, 0.6, 2.6);
let alpha = map_range(depth, -1.0, 1.0, 0.45, 1.0);
let mut pts: Vec<Point2> = Vec::with_capacity(model.samples);
for si in 0..model.samples {
let u = (si as f32) / ((model.samples - 1) as f32);
let x = x_min + u * width;
let nx = (x as f64) * model.freq;
let ny = (i as f64) * 0.05 + (t as f64) * (model.time_speed as f64);
let noise_val = model.perlin.get([nx, ny]);
let dy = (noise_val as f32) * model.amplitude * (1.0 - depth.abs());
let edge_falloff = cubic_ease(map_range(u, 0.0, 1.0, -1.0, 1.0).abs());
let final_y = base_y + dy * (1.0 - 0.7 * edge_falloff);
pts.push(pt2(x, final_y));
}
draw.polyline().weight(stroke_w).points(pts).rgba(0.9, 0.9, 0.9, alpha);
}
draw.to_frame(app, &frame).unwrap();
}
fn cubic_ease(x: f32) -> f32 {
let x = x.clamp(0.0, 1.0);
x * x * (3.0 - 2.0 * x)
}
use nannou::prelude::*;
// nannou에 포함된 노이즈 라이브러리에서 Perlin 노이즈와 NoiseFn 트레이트를 가져옵니다.
// Perlin은 절차적 노이즈를 생성하는 데 사용되고, NoiseFn은 .get() 메서드를 제공합니다.
use nannou::noise::{Perlin, NoiseFn};
// 프로그램의 진입점(entry point)입니다.
fn main() {
// nannou 애플리케이션을 설정하고 실행합니다.
// model 함수로 초기 상태를 만들고, update 함수로 매 프레임 상태를 갱신하며, view 함수로 화면에 그립니다.
// .run()이 호출되면 이벤트 루프가 시작됩니다.
nannou::app(model).update(update).run();
}
// 애플리케이션의 전체 상태(state)를 저장하는 구조체입니다.
// 이 구조체의 인스턴스가 model, update, view 함수에 전달됩니다.
struct Model {
// 생성된 창의 ID를 저장합니다. 직접 사용하지 않더라도 창을 유지하기 위해 소유권(ownership)을 가져야 합니다.
// 변수명 앞에 '_'를 붙여 사용하지 않는 변수임을 명시합니다.
_window: window::Id,
// Perlin 노이즈 생성기 인스턴스입니다. 파형을 만드는 데 사용됩니다.
perlin: Perlin,
// 화면에 그릴 선의 총 개수 (레이어 수)
layers: usize,
// 각 선을 구성하는 점의 개수 (샘플 수)
samples: usize,
// 각 레이어(선) 사이의 y축 간격
spacing: f32,
// 노이즈가 선의 형태에 미치는 영향력의 크기 (진폭)
amplitude: f32,
// 노이즈 공간의 스케일을 조절하는 값 (주파수). 값이 작을수록 부드러운 노이즈가 생성됩니다.
freq: f64,
// 애니메이션의 시간 흐름 속도를 조절합니다.
time_speed: f32,
// 애니메이션 시작 시점을 기록하여 경과 시간을 측정하는 데 사용됩니다.
start_time: std::time::Instant,
}
// 애플리케이션이 처음 시작될 때 한 번만 호출되는 함수입니다.
// Model 구조체를 초기화하고 반환합니다.
fn model(app: &App) -> Model {
// 새로운 창을 생성하고 설정합니다.
let window_id = app
.new_window() // 새 창 빌더 시작
.size(1200, 800) // 창 크기 설정
.title("Layered Line Depth - Nannou") // 창 제목 설정
.view(view) // 이 창을 그릴 때 사용할 함수를 'view'로 지정
.build() // 설정을 바탕으로 창 생성
.unwrap(); // 창 생성이 실패하면 패닉 발생 (보통 실패하지 않음)
// Perlin 노이즈 생성기를 초기화합니다.
// 최신 noise-rs API에서는 .default()를 사용하여 기본 시드(seed) 값으로 생성하는 것을 권장합니다.
// 매번 실행 시 동일한 패턴을 보장하려면 Perlin::new(seed_value) 처럼 시드를 명시할 수 있습니다.
let perlin = Perlin::default();
// Model 구조체의 각 필드에 초기값을 설정하여 인스턴스를 생성하고 반환합니다.
Model {
_window: window_id,
perlin,
layers: 80,
samples: 400,
spacing: 8.0,
amplitude: 120.0,
freq: 0.003,
time_speed: 0.2,
// 프로그램 시작 시점의 시간을 기록합니다. `app.time`보다 정밀하고 프레임에 독립적인 시간 제어가 가능합니다.
start_time: std::time::Instant::now(),
}
}
// 매 프레임마다 호출되어 Model의 상태를 업데이트하는 함수입니다.
// 이 예제에서는 애니메이션 로직이 view 함수 내의 시간 계산으로 처리되므로, update 함수는 비워둡니다.
fn update(_app: &App, _model: &mut Model, _update: Update) {
// 상태 업데이트 로직이 필요하면 여기에 추가합니다.
}
// 매 프레임마다 화면을 그리는 함수입니다.
// App과 Model의 현재 상태를 읽어서 프레임에 그림을 그립니다.
fn view(app: &App, model: &Model, frame: Frame) {
// 그리기를 위한 Draw 객체를 생성합니다.
let draw = app.draw();
// 배경을 hsla 모드로 칠합니다. (짙은 회색)
draw.background().(hsla(0.0, 0.0, 0.02, 1.0));
// 창의 사각형 영역 정보를 가져옵니다. (left, right, bottom, top 좌표)
let win = app.window_rect();
// model에 저장된 시작 시간으로부터 현재까지 경과된 시간을 초 단위(f32)로 계산합니다.
// 이 't' 값은 애니메이션을 만드는 데 사용됩니다.
let t = model.start_time.elapsed().as_secs_f32();
// 창의 왼쪽, 오른쪽 x좌표와 전체 너비를 계산합니다.
let x_min = win.left();
let x_max = win.right();
let width = x_max - x_min;
// 모든 선의 y축 기준점이 될 중앙 y좌표입니다.
let center_y = 0.0;
// 설정된 레이어 수만큼 반복하여 각 선을 그립니다.
for i in 0..model.layers {
// 현재 레이어의 깊이(depth)를 계산합니다. -1.0 (뒤) ~ 1.0 (앞) 범위의 값입니다.
let depth = (i as f32 / (model.layers - 1) as f32) * 2.0 - 1.0;
// 깊이에 따라 선의 기본 y 위치를 계산합니다. 뒤에 있을수록 위쪽에, 앞에 있을수록 아래쪽에 배치됩니다.
let base_y = center_y + depth * (model.layers as f32 * 0.5 * model.spacing);
// 깊이에 따라 선의 굵기(stroke_w)와 투명도(alpha)를 조절합니다.
// 뒤에 있는 선일수록 가늘고 투명하게, 앞에 있는 선일수록 굵고 진하게 보입니다.
let stroke_w = map_range(depth, -1.0, 1.0, 0.6, 2.6);
let alpha = map_range(depth, -1.0, 1.0, 0.45, 1.0);
// 현재 선을 구성할 점(Point2)들을 담을 벡터를 생성합니다.
// with_capacity를 사용해 미리 필요한 만큼 메모리를 할당하여 성능을 최적화합니다.
let mut pts: Vec<Point2> = Vec::with_capacity(model.samples);
// 설정된 샘플 수만큼 반복하여 선 위의 각 점의 위치를 계산합니다.
for si in 0..model.samples {
// 현재 샘플의 위치를 0.0 ~ 1.0 범위로 정규화합니다.
let u = si as f32 / (model.samples - 1) as f32;
// 정규화된 위치 'u'를 사용해 실제 x 좌표를 계산합니다.
let x = x_min + u * width;
// Perlin 노이즈 함수에 입력할 2차원 좌표를 계산합니다. f64 타입으로 변환해야 합니다.
// nx: 공간적인 차원. x 좌표에 주파수를 곱해 노이즈 스케일을 조절합니다.
let nx = (x as f64) * model.freq;
// ny: 시간과 깊이를 결합한 차원. 레이어 인덱스(i)와 시간(t)을 조합하여 각 선이 다르게, 그리고 시간에 따라 움직이게 합니다.
let ny = (i as f64) * 0.05 + (t as f64) * (model.time_speed as f64);
// 계산된 [nx, ny] 좌표로 Perlin 노이즈 값을 가져옵니다. 이 값은 -1.0 ~ 1.0 범위입니다.
let noise_val = model.perlin.get([nx, ny]);
// 노이즈 값에 진폭(amplitude)과 깊이 보정치를 곱하여 최종 y축 변위(dy)를 계산합니다.
// (1.0 - depth.abs())는 중앙에 가까운 선일수록 변위가 커지게 하는 효과를 줍니다.
let dy = noise_val as f32 * model.amplitude * (1.0 - depth.abs());
// 화면 가장자리로 갈수록 선의 변위를 줄여주는 효과(falloff)를 계산합니다.
// 1. u(0~1)를 -1~1 범위로 매핑하고 절대값을 취하면, 중앙(0)에서 가장자리(1)로 가는 값이 됩니다.
// 2. 이 값을 cubic_ease 함수에 넣어 부드러운 곡선 값을 얻습니다.
let edge_falloff = cubic_ease(map_range(u, 0.0, 1.0, -1.0, 1.0).abs());
// 기본 y 위치에 노이즈 변위를 더하여 최종 y 좌표를 계산합니다.
// 가장자리(edge_falloff가 1에 가까움)일수록 노이즈 변위(dy)가 줄어들도록 합니다.
let final_y = base_y + dy * (1.0 - 0.7 * edge_falloff);
// 계산된 (x, y) 좌표를 점 벡터에 추가합니다.
pts.push(pt2(x, final_y));
}
// 점 벡터(pts)를 사용하여 하나의 연결된 선(polyline)을 그립니다.
draw.polyline()
.weight(stroke_w) // 선의 굵기 설정
.points(pts) // 선을 구성할 점들 전달
.rgba(0.0, 0.0, 0.0, alpha); // 선의 색상(검정)과 투명도 설정
}
// 지금까지 'draw' 객체에 쌓인 모든 그리기 명령을 실제 프레임에 렌더링합니다.
draw.to_frame(app, &frame).unwrap();
}
// 0.0 ~ 1.0 사이의 입력값 x를 받아 부드러운 S자 곡선(Smoothstep)으로 변환하는 함수입니다.
// 이 함수는 가장자리 효과를 자연스럽게 만드는 데 사용됩니다.
fn cubic_ease(x: f32) -> f32 {
// 입력값이 0.0 ~ 1.0 범위를 벗어나지 않도록 강제합니다.
let x = x.clamp(0.0, 1.0);
// Smoothstep 공식: 3x^2 - 2x^3
x * x * (3.0 - 2.0 * x)
}