๐Ÿ”ฎ :: Wavy Portrait

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

Nannou <Generative Art>

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

๐Ÿ“ Rust Code

use nannou::prelude::*;
use nannou::noise::{ Perlin, NoiseFn };
use nannou::image::{DynamicImage, GenericImageView};

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,
    image_data: DynamicImage,
    image_dimensions: Vec2,
}

fn model(app: &App) -> Model {
    let window_id = app
        .new_window()
        .size(800, 800)
        .title("Layered Line Depth - Image Guided")
        .view(view)
        .build()
        .unwrap();

    let perlin = Perlin::default();

    // ์ด๋ฏธ์ง€ ๋กœ๋“œ - ํ”„๋กœ์ ํŠธ ๋ฃจํŠธ ๊ธฐ์ค€ ์ƒ๋Œ€ ๊ฒฝ๋กœ
    let img_path = std::path::Path::new("assets/test_image_07.jpg");

    // nannou::image ํฌ๋ ˆ์ดํŠธ๋กœ ์ด๋ฏธ์ง€ ๋กœ๋“œ
    let image_data = nannou::image::open(&img_path).expect("Failed to load image");
    
    // ์ด๋ฏธ์ง€ ํฌ๊ธฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    let (width, height) = image_data.dimensions();
    let image_dimensions = vec2(width as f32, height as f32);
    
    Model {
        _window: window_id,
        perlin,
        layers: 130,
        samples: 800,
        spacing: 6.0,
        amplitude: 100.0,
        freq: 0.005,
        time_speed: 0.2,
        start_time: std::time::Instant::now(),
        image_data,
        image_dimensions,
    }
}

fn update(_app: &App, _model: &mut Model, _update: Update) {
    // ์—…๋ฐ์ดํŠธ ๋กœ์ง
}

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();
    // ์–ด๋‘์šด ํšŒ์ƒ‰ ๋ฐฐ๊ฒฝ
    draw.background().hsla(0.0, 0.0, 0.01, 1.0);

    let win = app.window_rect();
    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);

        // ์•„๋ž˜์ชฝ์ด ๋‘๊ป๊ฒŒ: depth๊ฐ€ -1.0์ผ ๋•Œ ๋‘๊ป๊ฒŒ, 1.0์ผ ๋•Œ ๊ฐ€๋Š˜๊ฒŒ
        let stroke_w = map_range(depth, -1.0, 1.0, 2.5, 0.5);
        let alpha = map_range(depth, -1.0, 1.0, 0.3, 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 v = i as f32 / (model.layers - 1) as f32;

            // ์ด๋ฏธ์ง€ ์ƒํ•˜ ๋ฐ˜์ „: v๋ฅผ 1.0 - v๋กœ ๋ณ€ํ™˜
            let v_flipped = 1.0 - v;

            // ํ”ฝ์…€ ์ขŒํ‘œ ๊ณ„์‚ฐ (๋ฒ”์œ„ ์ฒดํฌ ํฌํ•จ)
            let pixel_x = ((u * model.image_dimensions.x) as u32)
                .min(model.image_dimensions.x as u32 - 1);
            let pixel_y = ((v_flipped * model.image_dimensions.y) as u32)
                .min(model.image_dimensions.y as u32 - 1);

            // ํ”ฝ์…€ ์ƒ‰์ƒ ๊ฐ€์ ธ์˜ค๊ธฐ
            let pixel_color = model.image_data.get_pixel(pixel_x, pixel_y);
            
            // ๋ฐ๊ธฐ ๊ณ„์‚ฐ (์ •ํ™•ํ•œ ๋ฐฉ๋ฒ•: sRGB ๊ฐ€์ค‘์น˜ ์ ์šฉ)
            let brightness = (0.299 * pixel_color[0] as f32 
                            + 0.587 * pixel_color[1] as f32 
                            + 0.114 * pixel_color[2] as f32) / 255.0;

            // ๋ฐ๊ธฐ์— ๋”ฐ๋ฅธ ์ง„ํญ ์กฐ์ ˆ
            let current_amplitude = map_range(brightness, 0.0, 1.0, 0.0, model.amplitude * 2.0);

            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 * current_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(1.0, 1.0, 1.0, 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)
}

๐Ÿ“ Rust Code + Comment

use nannou::prelude::*;                                      // Nannou์˜ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ ์ž„ํฌํŠธ
use nannou::noise::{Perlin, NoiseFn};                        // Perlin ๋…ธ์ด์ฆˆ ๊ด€๋ จ ๊ธฐ๋Šฅ ์ž„ํฌํŠธ
use nannou::image::{DynamicImage, GenericImageView};         // ์ด๋ฏธ์ง€ ์ฒ˜๋ฆฌ ๊ด€๋ จ ๊ธฐ๋Šฅ ์ž„ํฌํŠธ

// ์ „์—ญ ์ƒ์ˆ˜
const WINDOW_WIDTH: u32 = 1000;                               // ์œˆ๋„์šฐ ๋„ˆ๋น„ (ํ”ฝ์…€ ๋‹จ์œ„)
const WINDOW_HEIGHT: u32 = 1000;                              // ์œˆ๋„์šฐ ๋†’์ด (ํ”ฝ์…€ ๋‹จ์œ„)
const WINDOW_TITLE: &str = "Layered Line Depth";             // ์œˆ๋„์šฐ ์ œ๋ชฉ
const DEFAULT_LAYERS: usize = 130;                           // ๊ธฐ๋ณธ ๋ ˆ์ด์–ด ์ˆ˜
const DEFAULT_SAMPLES: usize = 800;                          // ๊ฐ ๋ ˆ์ด์–ด๋‹น ์ƒ˜ํ”Œ ํฌ์ธํŠธ ์ˆ˜
const DEFAULT_SPACING: f32 = 6.0;                            // ๋ ˆ์ด์–ด ๊ฐ„ ๊ฐ„๊ฒฉ
const DEFAULT_AMPLITUDE: f32 = 30.0;                        // ๊ธฐ๋ณธ ํŒŒ๋™ ์ง„ํญ
const DEFAULT_FREQ: f64 = 0.005;                             // ๋…ธ์ด์ฆˆ ์ฃผํŒŒ์ˆ˜ (๊ฐ’์ด ์ž‘์„์ˆ˜๋ก ๋” ๋„“์€ ํŒŒ๋™)
const DEFAULT_TIME_SPEED: f32 = 0.2;                         // ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์†๋„
const IMAGE_PATH: &str = "assets/test4.jpg";                 // ๋กœ๋“œํ•  ์ด๋ฏธ์ง€ ํŒŒ์ผ ๊ฒฝ๋กœ 
const BACKGROUND_GRAY: f32 = 0.01;                           // ๋ฐฐ๊ฒฝ์ƒ‰์˜ ํšŒ์ƒ‰์กฐ ๊ฐ’ (0=๊ฒ€์ •, 1=ํ•˜์–‘)
const MIN_STROKE_WEIGHT: f32 = 0.5;                          // ๋ผ์ธ์˜ ์ตœ์†Œ ๋‘๊ป˜
const MAX_STROKE_WEIGHT: f32 = 2.5;                          // ๋ผ์ธ์˜ ์ตœ๋Œ€ ๋‘๊ป˜
const MIN_ALPHA: f32 = 0.3;                                  // ๋ผ์ธ์˜ ์ตœ์†Œ ํˆฌ๋ช…๋„
const MAX_ALPHA: f32 = 1.0;                                  // ๋ผ์ธ์˜ ์ตœ๋Œ€ ํˆฌ๋ช…๋„ (๋ถˆํˆฌ๋ช…)
const EDGE_FALLOFF_FACTOR: f32 = 0.7;                        // ๊ฐ€์žฅ์ž๋ฆฌ ๊ฐ์‡  ๊ณ„์ˆ˜


// Model ๊ตฌ์กฐ์ฒด ์ •์˜ - ํ”„๋กœ๊ทธ๋žจ์˜ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ
struct Model {
    perlin: Perlin,                                        // Perlin ๋…ธ์ด์ฆˆ ์ƒ์„ฑ๊ธฐ (๋žœ๋คํ•œ ์ž์—ฐ์Šค๋Ÿฌ์šด ํŒจํ„ด ์ƒ์„ฑ)
    layers: usize,                                         // ๋ ˆ์ด์–ด ์ˆ˜ (์ˆ˜์ง์œผ๋กœ ๊ทธ๋ ค์งˆ ๋ผ์ธ์˜ ์ˆ˜)
    samples: usize,                                        // ๊ฐ ๋ ˆ์ด์–ด์˜ ์ƒ˜ํ”Œ ์ˆ˜ (๊ฐ ๋ ˆ์ด์–ด๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ์ ์˜ ์ˆ˜)
    spacing: f32,                                          // ๋ ˆ์ด์–ด ๊ฐ„ ๊ฐ„๊ฒฉ (์ˆ˜์ง ๊ฐ„๊ฒฉ)
    amplitude: f32,                                        // ๊ธฐ๋ณธ ์ง„ํญ (ํŒŒ๋™์˜ ๋†’์ด)
    freq: f64,                                             // ๋…ธ์ด์ฆˆ ์ฃผํŒŒ์ˆ˜ (ํŒŒ๋™์˜ ๋ฐ€๋„)
    time_speed: f32,                                       // ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ๋ณ€ํ™” ์†๋„
    start_time: std::time::Instant,                        // ํ”„๋กœ๊ทธ๋žจ ์‹œ์ž‘ ์‹œ๊ฐ„ (์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ๊ฐ„ ๊ณ„์‚ฐ์šฉ)
    image_data: DynamicImage,                              // ๋กœ๋“œ๋œ ์ด๋ฏธ์ง€ ๋ฐ์ดํ„ฐ
    image_dimensions: Vec2,                                // ์ด๋ฏธ์ง€ ์ฐจ์› (๋„ˆ๋น„, ๋†’์ด)
}

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

// Model์„ ์ƒ์„ฑํ•˜๊ณ  ์ดˆ๊ธฐํ™”ํ•˜๋Š” ์„ค์ • ํ•จ์ˆ˜
fn model(app: &App) -> Model {
    // ์ด๋ฏธ์ง€ ๋กœ๋”ฉ ๋””๋ฒ„๊น… ์ฝ”๋“œ
    match std::env::current_dir() {
        Ok(path) => println!("ํ˜„์žฌ ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ: {}", path.display()),
        Err(e) => println!("ํ˜„์žฌ ์ž‘์—… ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†์Œ: {}", e),
    }

    // ์œˆ๋„์šฐ ์ƒ์„ฑ
    app.new_window()                                       // ์ƒˆ ์œˆ๋„์šฐ ์ƒ์„ฑ ์‹œ์ž‘
        .size(WINDOW_WIDTH, WINDOW_HEIGHT)                 // ์œˆ๋„์šฐ ํฌ๊ธฐ ์„ค์ •
        .title(WINDOW_TITLE)                               // ์œˆ๋„์šฐ ์ œ๋ชฉ ์„ค์ •
        .view(view)                                        // ๋ทฐ ํ•จ์ˆ˜ ์„ค์ • (๊ทธ๋ฆฌ๊ธฐ ์ฝœ๋ฐฑ)
        .build()                                           // ์œˆ๋„์šฐ ์ƒ์„ฑ ์™„๋ฃŒ
        .expect("์œˆ๋„์šฐ ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.");                // ์œˆ๋„์šฐ ์ƒ์„ฑ ์‹คํŒจ ์‹œ ํ”„๋กœ๊ทธ๋žจ ์ข…๋ฃŒ

    // Perlin ๋…ธ์ด์ฆˆ ์ƒ์„ฑ๊ธฐ ์ดˆ๊ธฐํ™” - ์ž์—ฐ์Šค๋Ÿฌ์šด ๋žœ๋ค ํŒจํ„ด ์ƒ์„ฑ์— ์‚ฌ์šฉ
    let perlin = Perlin::default();

    // ์ด๋ฏธ์ง€ ๋กœ๋“œ (๊ฒฝ๋กœ๋ฅผ ํฌํ•จํ•œ ์ƒ์ˆ˜๋ฅผ ์ง์ ‘ ์‚ฌ์šฉ)
    let image_data = match nannou::image::open(IMAGE_PATH) { 
        Ok(img) => img,
        Err(e) => {
            eprintln!("์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ (๊ฒฝ๋กœ: {:?}): {}", IMAGE_PATH, e);
            std::process::exit(1);
        }
    };
    
    // ์ด๋ฏธ์ง€ ํฌ๊ธฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    let (width, height) = image_data.dimensions();              // ์ด๋ฏธ์ง€์˜ ๋„ˆ๋น„์™€ ๋†’์ด ๊ฐ€์ ธ์˜ค๊ธฐ
    let image_dimensions = vec2(width as f32, height as f32);   // Vec2 ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜
    
    // Model ์ธ์Šคํ„ด์Šค ๋ฐ˜ํ™˜ - ๋ชจ๋“  ํ•„๋“œ ์ดˆ๊ธฐํ™”
    Model {
        perlin,
        layers: DEFAULT_LAYERS,
        samples: DEFAULT_SAMPLES,
        spacing: DEFAULT_SPACING,
        amplitude: DEFAULT_AMPLITUDE,
        freq: DEFAULT_FREQ,
        time_speed: DEFAULT_TIME_SPEED,
        start_time: std::time::Instant::now(),
        image_data,
        image_dimensions,
    }
}

// ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ - ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค Model์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝ (ํ˜„์žฌ๋Š” ๋น„์–ด์žˆ์Œ)
fn update(_app: &App, _model: &mut Model, _update: Update) {
    // ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋กœ์ง์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์—ฌ๊ธฐ์— ์ถ”๊ฐ€
}

// ๋ทฐ ํ•จ์ˆ˜ ๊ตฌํ˜„ - ํ”„๋ ˆ์ž„๋งˆ๋‹ค ํ˜ธ์ถœ๋˜์–ด ํ™”๋ฉด์— ๊ทธ๋ฆฌ๊ธฐ ์ˆ˜ํ–‰
fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();                                   // ๋“œ๋กœ์ž‰ ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ

    draw.background().hsla(0.0, 0.0, BACKGROUND_GRAY, 1.0);  // HSLA ์ƒ‰์ƒ ๋ชจ๋ธ๋กœ ๋ฐฐ๊ฒฝ์ƒ‰ ์„ค์ •

    let win = app.window_rect();                             // ํ˜„์žฌ ์œˆ๋„์šฐ์˜ ํฌ๊ธฐ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
    let t = model.start_time.elapsed().as_secs_f32();        // ์‹œ์ž‘ ์‹œ๊ฐ„๋ถ€ํ„ฐ ๊ฒฝ๊ณผํ•œ ์‹œ๊ฐ„(์ดˆ) ๊ณ„์‚ฐ

    // 0์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ๋ถ„๋ชจ๊ฐ€ 0์ด ๋˜์ง€ ์•Š๋„๋ก ๋ณด์žฅ
    let layers_denom = (model.layers.saturating_sub(1)).max(1) as f32;
    let samples_denom = (model.samples.saturating_sub(1)).max(1) as f32;

    // ๊ฐ ๋ ˆ์ด์–ด์— ๋Œ€ํ•œ ๋ผ์ธ ๊ทธ๋ฆฌ๊ธฐ
    for i in 0..model.layers {                                                  // 0๋ถ€ํ„ฐ ๋ ˆ์ด์–ด ์ˆ˜๋งŒํผ ๋ฐ˜๋ณต
        let depth = (i as f32 / layers_denom) * 2.0 - 1.0;                      // -1.0(์•„๋ž˜)์—์„œ 1.0(์œ„) ๊นŒ์ง€์˜ ๊นŠ์ด ๊ฐ’
        let base_y = 0.0 + depth * (model.layers as f32 * 0.5 * model.spacing); // ๋ ˆ์ด์–ด์˜ ๊ธฐ๋ณธ y ์œ„์น˜ ๊ณ„์‚ฐ (center_y=0.0)

        // ์•„๋ž˜์ชฝ์ด ๋‘๊ป๊ฒŒ: depth๊ฐ€ -1.0์ผ ๋•Œ ๋‘๊ป๊ฒŒ, 1.0์ผ ๋•Œ ๊ฐ€๋Š˜๊ฒŒ
        let stroke_w = map_range(depth, -1.0, 1.0, MAX_STROKE_WEIGHT, MIN_STROKE_WEIGHT); // ๊นŠ์ด์— ๋”ฐ๋ผ ๋ผ์ธ ๋‘๊ป˜ ๋งคํ•‘
        let alpha = map_range(depth, -1.0, 1.0, MIN_ALPHA, MAX_ALPHA);                    // ๊นŠ์ด์— ๋”ฐ๋ผ ํˆฌ๋ช…๋„ ๋งคํ•‘

        let mut pts: Vec<Point2> = Vec::with_capacity(model.samples);  // ์ƒ˜ํ”Œ ํฌ์ธํŠธ๋ฅผ ์ €์žฅํ•  ๋ฒกํ„ฐ (์šฉ๋Ÿ‰ ๋ฏธ๋ฆฌ ํ• ๋‹น)

        // ๊ฐ ๋ ˆ์ด์–ด์˜ ์ƒ˜ํ”Œ ํฌ์ธํŠธ ๊ณ„์‚ฐ
        for si in 0..model.samples {                         // 0๋ถ€ํ„ฐ ์ƒ˜ํ”Œ ์ˆ˜๋งŒํผ ๋ฐ˜๋ณต
            let u = si as f32 / samples_denom;               // 0.0์—์„œ 1.0๊นŒ์ง€์˜ ์ •๊ทœํ™”๋œ x ์œ„์น˜
            let x = win.left() + u * win.w();                // ์‹ค์ œ x ์ขŒํ‘œ ๊ณ„์‚ฐ
            
            let v = i as f32 / layers_denom;                 // 0.0์—์„œ 1.0๊นŒ์ง€์˜ ์ •๊ทœํ™”๋œ y ์œ„์น˜ (๋ ˆ์ด์–ด ์ธ๋ฑ์Šค ๊ธฐ๋ฐ˜)
            let v_flipped = 1.0 - v;                         // ์ด๋ฏธ์ง€๋ฅผ ์ƒํ•˜ ๋ฐ˜์ „์‹œํ‚ค๊ธฐ ์œ„ํ•ด y ์ขŒํ‘œ ๋ณ€ํ™˜

            // ํ”ฝ์…€ ์ขŒํ‘œ ๊ณ„์‚ฐ (๋ฒ”์œ„ ์ฒดํฌ ํฌํ•จ)
            let pixel_x = ((u * model.image_dimensions.x) as u32)
                .clamp(0, model.image_dimensions.x as u32 - 1);             // ์ด๋ฏธ์ง€ ๊ฒฝ๊ณ„๋ฅผ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ํด๋žจํ•‘
            let pixel_y = ((v_flipped * model.image_dimensions.y) as u32)
                .clamp(0, model.image_dimensions.y as u32 - 1);             // ์ด๋ฏธ์ง€ ๊ฒฝ๊ณ„๋ฅผ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๋„๋ก ํด๋žจํ•‘

            // ํ”ฝ์…€ ์ƒ‰์ƒ ๊ฐ€์ ธ์˜ค๊ธฐ
            let pixel_color = model.image_data.get_pixel(pixel_x, pixel_y); // ์ง€์ •๋œ ํ”ฝ์…€์˜ ์ƒ‰์ƒ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ
            
            // ๋ฐ๊ธฐ ๊ณ„์‚ฐ - sRGB ๊ฐ€์ค‘์น˜ ์ ์šฉ (R: 0.299, G: 0.587, B: 0.114)
            let brightness = (0.299 * pixel_color[0] as f32 
                            + 0.587 * pixel_color[1] as f32 
                            + 0.114 * pixel_color[2] as f32) / 255.0;       // 0.0์—์„œ 1.0๊นŒ์ง€์˜ ๊ฐ’์œผ๋กœ ์ •๊ทœํ™”

            // ๋ฐ๊ธฐ์— ๋”ฐ๋ฅธ ์ง„ํญ ๋งตํ•‘ - ๋ฐ์€ ์˜์—ญ์—์„œ ๋” ํฐ ์ง„ํญ์„ ๊ฐ€์ง
            let current_amplitude = map_range(brightness, 0.0, 1.0, 0.0, model.amplitude * 2.0);

            let nx = (x as f64) * model.freq;                                    // ๋…ธ์ด์ฆˆ x ์ขŒํ‘œ (์‹ค์ œ x ์ขŒํ‘œ์— ์ฃผํŒŒ์ˆ˜ ๊ณฑํ•˜๊ธฐ)
            let ny = (i as f64) * 0.05 + (t as f64) * (model.time_speed as f64); // ๋…ธ์ด์ฆˆ y ์ขŒํ‘œ (๋ ˆ์ด์–ด ์ธ๋ฑ์Šค์™€ ์‹œ๊ฐ„์— ๋”ฐ๋ผ ๋ณ€ํ™”)

            let noise_val = model.perlin.get([nx, ny]);                          // Perlin ๋…ธ์ด์ฆˆ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ (-1.0์—์„œ 1.0 ์‚ฌ์ด)
            let dy = noise_val as f32 * current_amplitude * (1.0 - depth.abs()); // y ๋ฐฉํ–ฅ ๋ณ€์œ„ ๊ณ„์‚ฐ
            let edge_falloff = cubic_ease((u * 2.0 - 1.0).abs());       // ๊ฐ€์žฅ์ž๋ฆฌ์—์„œ ๋ถ€๋“œ๋Ÿฝ๊ฒŒ ๊ฐ์†Œ (u๋ฅผ -1~1 ๋ฒ”์œ„๋กœ ๋ณ€ํ™˜ ํ›„ ์ ˆ๋Œ€๊ฐ’)
            
            // ์ตœ์ข… y ์ขŒํ‘œ ๊ณ„์‚ฐ
            let final_y = base_y + dy * (1.0 - EDGE_FALLOFF_FACTOR * edge_falloff); 

            pts.push(pt2(x, final_y));                  // ๊ณ„์‚ฐ๋œ ์ ์„ ํฌ์ธํŠธ ๋ฒกํ„ฐ์— ์ถ”๊ฐ€
        }

        // ๋‹ค๊ฐํ˜• ๋ผ์ธ ๊ทธ๋ฆฌ๊ธฐ
        draw.polyline()                                 // ๋‹ค๊ฐํ˜• ๋ผ์ธ ๊ทธ๋ฆฌ๊ธฐ ์‹œ์ž‘
            .weight(stroke_w)                           // ๋ผ์ธ ๋‘๊ป˜ ์„ค์ •
            .points(pts)                                // ๊ทธ๋ฆผ ํฌ์ธํŠธ๋“ค ์„ค์ •
            .rgba(1.0, 1.0, 1.0, alpha);                // RGB ์ƒ‰์ƒ๊ณผ ์•ŒํŒŒ(ํˆฌ๋ช…๋„) ์„ค์ •
    }

    // ํ”„๋ ˆ์ž„์— ๊ทธ๋ฆฌ๊ธฐ - ํ™”๋ฉด์— ์ตœ์ข… ๊ฒฐ๊ณผ ํ‘œ์‹œ
    draw.to_frame(app, &frame).unwrap();                // ๊ทธ๋ฆฐ ๋‚ด์šฉ์„ ํ”„๋ ˆ์ž„์— ์ ์šฉ
}

// ์ž…๋ฐฉ ๋ณด๊ฐ„ ํ•จ์ˆ˜ (cubic easing) - ๋ถ€๋“œ๋Ÿฌ์šด ์ „ํ™˜์„ ์œ„ํ•œ ๋ณด๊ฐ„ ํ•จ์ˆ˜
fn cubic_ease(x: f32) -> f32 {
    let x = x.clamp(0.0, 1.0);                          // ์ž…๋ ฅ ๊ฐ’์„ 0.0๊ณผ 1.0 ์‚ฌ์ด๋กœ ์ œํ•œ
    x * x * (3.0 - 2.0 * x)                             // ์ž…๋ฐฉ ๋ณด๊ฐ„ ๊ณต์‹ ์ ์šฉ
}

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

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