๐Ÿ”ฎ :: Exoplanets

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

Nannou <Generative Art>

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

SPACEBAR : ํ–‰์„ฑ ํšŒ์ „ toggle
H : ํ–‰์„ฑ์˜ hue ๋ณ€๊ฒฝ
R : ํ•ญ์„ฑ์˜ ์ด๋ฆ„๊ณผ ๋ฐ˜์ง€๋ฆ„, ํ–‰์„ฑ์˜ ๊ถค๋„, ๋ฐฐ๊ฒฝ ๋ณ„ ๋ณ€๊ฒฝ
S : ์Šคํฌ๋ฆฐ์ƒท ์ €์žฅ

๐Ÿ“ Rust Code

use nannou::prelude::*;
use rand::seq::SliceRandom;
use rand::Rng;
use std::fs;

// --- Global Constants ---
const WIDTH: u32 = 540;
const HEIGHT: u32 = 960;
const ORBIT_COUNT: usize = 10; // Number of orbits
const STARFIELD_COUNT: usize = 100; // ๋ฐฐ๊ฒฝ ๋ณ„ ๊ฐœ์ˆ˜

// --- App Code ---
fn main() {
    nannou::app(model).update(update).run();
}

// App state
struct Model {
    star_position: Vec2,               // Star position
    star_radius: f32,                  // Star radius (randomized)
    orbits: Vec<(f32, f32)>,           // (orbit radius, stroke weight)
    planets: Vec<(usize, f32, f32)>,   // (orbit index, planet radius, angle in degrees)
    planet_speeds: Vec<f32>,           // ๊ฐ ํ–‰์„ฑ์˜ ํšŒ์ „ ์†๋„
    is_rotating: bool,                 // Tracks if planets are rotating
    font: nannou::text::Font,          // ํฐํŠธ
    label: String,                     // ๋žœ๋ค ๋ผ๋ฒจ
    planet_hue: f32,                   // ํ–‰์„ฑ ์ƒ‰์ƒ์˜ hue (์ „์ฒด ํ–‰์„ฑ ๊ณตํ†ต)
    planet_colors: Vec<(f32, f32)>,    // ๊ฐ ํ–‰์„ฑ์˜ (s, l)
    screenshot_index: usize,           // ์ €์žฅ๋˜๋Š” ์ด๋ฏธ์ง€ ์ˆœ๋ฒˆ
    starfield: Vec<(Vec2, f32)>,       // (๋ณ„ ์œ„์น˜, ๋ฐ˜์ง€๋ฆ„)
}

// --- Starfield generator ---
fn generate_starfield() -> Vec<(Vec2, f32)> {
    let mut rng = rand::thread_rng();
    let mut stars = Vec::new();
    for _ in 0..STARFIELD_COUNT {
        let x = rng.gen_range(-(WIDTH as f32) / 2.0..(WIDTH as f32) / 2.0);
        let y = rng.gen_range(-(HEIGHT as f32) / 2.0..(HEIGHT as f32) / 2.0);
        let radius = rng.gen_range(0.3..1.0);
        stars.push((vec2(x, y), radius));
    }
    stars
}

// Called once to set up the model
fn model(app: &App) -> Model {
    // Create window with key press handler
    app.new_window()
        .size(WIDTH, HEIGHT)
        .view(view)
        .key_pressed(key_pressed)
        .build()
        .unwrap();

    let star_position = vec2(0.0, -480.0);

    // Random star radius
    let mut rng = rand::thread_rng();
    let star_radius: f32 = rng.gen_range(150.0..=400.0);
    println!("Star radius chosen: {}", star_radius);

    // Dynamic min orbit radius (star_radius + 50~80)
    let min_orbit_radius = star_radius + rng.gen_range(50..=80) as f32;

    // Generate random orbits
    let mut orbits = Vec::new();
    for _ in 0..ORBIT_COUNT {
        let radius = random_range(min_orbit_radius, HEIGHT as f32);
        let stroke_weight = random_range(0.3, 0.5);
        orbits.push((radius, stroke_weight));
    }
    orbits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());

    // Generate planets
    let mut planets = Vec::new();
    for i in 0..ORBIT_COUNT {
        let orbit_index = i;
        let planet_radius = random_range(3.0, 10.0);
        let angle_degrees = 90.0;
        planets.push((orbit_index, planet_radius, angle_degrees));
    }

    // ์†๋„ ์ดˆ๊ธฐํ™” (์ฒ˜์Œ์—” 0)
    let planet_speeds = vec![0.0; ORBIT_COUNT];

    // ๊ฐ ํ–‰์„ฑ์˜ (s, l) ์ƒ‰์ƒ ๋žœ๋ค ์ƒ์„ฑ (๊ณ ์ •)
    let mut planet_colors = Vec::new();
    for _ in 0..ORBIT_COUNT {
        let s: f32 = rng.gen_range(0.5..1.0);
        let l: f32 = rng.gen_range(0.4..0.8);
        planet_colors.push((s, l));
    }

    // --- Font & Random Label ---
    let assets = app.assets_path().expect("no assets directory found");
    let font_path = assets.join("fonts").join("font.ttf");
    let font_data = fs::read(font_path).expect("failed to read font file");
    let font = nannou::text::Font::from_bytes(font_data).expect("failed to load font");

    let names = [
        "Kepler", "Galileo", "Newton", "Herschel", "Hubble",
        "Tycho", "Halley", "Cassini", "Copernicus", "Sagan",
        "Messier", "Laplace", "Chandrasekhar", "Hawking", "Eddington",
    ];

    let name = names.choose(&mut rng).unwrap_or(&"Kepler");
    let num: u16 = rng.gen_range(100..1000);
    let label = format!("{}-{}", name, num);

    // ํ–‰์„ฑ ์ƒ‰์ƒ hue๋Š” ๊ธฐ๋ณธ๊ฐ’ 0.0
    let planet_hue = 0.0;

    // --- Starfield ---
    let starfield = generate_starfield();

    Model {
        star_position,
        star_radius,
        orbits,
        planets,
        planet_speeds,
        is_rotating: false,
        font,
        label,
        planet_hue,
        planet_colors,
        screenshot_index: 0,
        starfield,
    }
}

// Handle key press events
fn key_pressed(app: &App, model: &mut Model, key: Key) {
    match key {
        Key::Space => {
            if model.is_rotating {
                model.is_rotating = false;
            } else {
                let mut rng = rand::thread_rng();
                for speed in model.planet_speeds.iter_mut() {
                    *speed = rng.gen_range(0.1..1.0);
                }
                model.is_rotating = true;
            }
        }
        Key::S => {
            model.screenshot_index += 1;
            let filename = format!("exoplanet_{:03}.png", model.screenshot_index);
            app.main_window().capture_frame(&filename);
            println!("๐Ÿ“ธ Screenshot saved: {}", filename);
        }
        Key::H => {
            let mut rng = rand::thread_rng();
            model.planet_hue = rng.gen_range(0.0..360.0);
            println!("๐ŸŽจ Hue changed to: {:.2}", model.planet_hue);
        }
        Key::R => {
            let mut rng = rand::thread_rng();

            // --- ์ƒˆ๋กœ์šด ํ•ญ์„ฑ ๋ฐ˜์ง€๋ฆ„ ---
            model.star_radius = rng.gen_range(150.0..=400.0);

            // --- ์ƒˆ๋กœ์šด ์ตœ์†Œ ๊ถค๋„ ๋ฐ˜์ง€๋ฆ„ (ํ•ญ์„ฑ ๋ฐ˜์ง€๋ฆ„ + 50~80) ---
            let min_orbit_radius = model.star_radius + rng.gen_range(50.0..=80.0);

            // --- ๊ถค๋„ ์ƒˆ๋กœ ์ƒ์„ฑ ---
            let mut new_orbits = Vec::new();
            for _ in 0..ORBIT_COUNT {
                let radius = random_range(min_orbit_radius, HEIGHT as f32);
                let stroke_weight = random_range(0.3, 0.5);
                new_orbits.push((radius, stroke_weight));
            }
            new_orbits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
            model.orbits = new_orbits;

            // --- ์ƒˆ๋กœ์šด ๋ผ๋ฒจ ---
            let names = [
                "Kepler", "Galileo", "Newton", "Herschel", "Hubble",
                "Tycho", "Halley", "Cassini", "Copernicus", "Sagan",
            ];
            let name = names.choose(&mut rng).unwrap_or(&"Kepler");
            let num: u16 = rng.gen_range(100..1000);
            model.label = format!("{}-{}", name, num);

            // --- ์ƒˆ๋กœ์šด ์Šคํƒ€ํ•„๋“œ ---
            model.starfield = generate_starfield();

            // --- ๋กœ๊ทธ ์ถœ๋ ฅ ---
            println!("โœจ Star radius changed to: {:.2}", model.star_radius);
            println!("๐ŸŒ€ Orbits regenerated ({} total)", model.orbits.len());
            println!("๐ŸŒŒ Starfield regenerated ({} stars)", model.starfield.len());
            println!("๐Ÿช Label changed to: {}", model.label);
        }
        _ => {}
    }
}

// Called every frame to update the model
fn update(_app: &App, model: &mut Model, _update: Update) {
    if model.is_rotating {
        for ((_, _, angle_degrees), speed) in model.planets.iter_mut().zip(model.planet_speeds.iter()) {
            *angle_degrees += *speed;
            if *angle_degrees >= 360.0 {
                *angle_degrees -= 360.0;
            }
        }
    }
}

// Called every frame to draw the scene
fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();

    // 1. Black background
    draw.background().color(hsla(0.0, 0.0, 0.007, 1.0));

    // 2. Draw starfield
    for (pos, radius) in model.starfield.iter() {
        draw.ellipse()
            .xy(*pos)
            .radius(*radius)
            .color(hsla(0.0, 0.0, 1.0, 1.0));
    }

    // 3. Draw star
    draw.ellipse()
        .xy(model.star_position)
        .radius(model.star_radius)
        .color(hsla(200.0, 0.2, 0.8, 1.0));

    // 4. Draw orbits (gray)
    for (radius, stroke_weight) in model.orbits.iter() {
        draw.ellipse()
            .xy(model.star_position)
            .radius(*radius)
            .no_fill()
            .stroke(hsla(0.0, 0.0, 0.3, 1.0))
            .stroke_weight(*stroke_weight);
    }

    // 5. Draw planets
    for ((orbit_index, planet_radius, angle_degrees), (s, l)) in
        model.planets.iter().zip(model.planet_colors.iter())
    {
        let orbit_radius = model.orbits[*orbit_index].0;
        let angle_rad = deg_to_rad(*angle_degrees);
        let planet_pos =
            model.star_position + vec2(angle_rad.cos() * orbit_radius, angle_rad.sin() * orbit_radius);

        draw.ellipse()
            .xy(planet_pos)
            .radius(*planet_radius)
            .color(hsla(model.planet_hue, *s, *l, 1.0));
    }

    // 6. Draw random label
    draw.text(&model.label)
        .font(model.font.clone())
        .xy(vec2(0.0, -400.0))
        .font_size(24)
        .color(hsla(0.0, 0.0, 0.0, 1.0));

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

๐ŸŽ›๏ธ ๋ฌธ๋ฒ•์— ๋Œ€ํ•œ ์ถ”๊ฐ€ ์„ค๋ช…

for ((_, _, angle_degrees), speed) in model.planets.iter_mut().zip(model.planet_speeds.iter()) {
    *angle_degrees += *speed;
    if *angle_degrees >= 360.0 {
        *angle_degrees -= 360.0;
    }
}

ํ–‰์„ฑ์˜ ๊ถค๋„ ์ธ๋ฑ์Šค์™€ ๋ฐ˜์ง€๋ฆ„์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๋ฉด์„œ, ๊ฐ๋„์™€ ์†๋„๋งŒ์„ ํšจ์œจ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•˜๋Š” ์šฐ์•„ํ•œ ๋ฐฉ๋ฒ•

โœ… iter_mut() ์™€ iter()

  • iter_mut(): ์ปฌ๋ ‰์…˜์˜ ์š”์†Œ์— ๋Œ€ํ•œ ๊ฐ€๋ณ€ ์ฐธ์กฐ(&mut T) ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ฐ˜๋ณต์ž. ์š”์†Œ ๊ฐ’์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • iter(): ์ปฌ๋ ‰์…˜์˜ ์š”์†Œ์— ๋Œ€ํ•œ ๋ถˆ๋ณ€ ์ฐธ์กฐ(&T) ๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ฐ˜๋ณต์ž. ์š”์†Œ ๊ฐ’์„ ์ฝ๊ธฐ๋งŒ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

โœ… zip() ๋ฉ”์„œ๋“œ

  • ๋‘ ๊ฐœ์˜ ๋ฐ˜๋ณต์ž๋ฅผ "์ง์ง€์–ด" ํ•˜๋‚˜์˜ ๋ฐ˜๋ณต์ž๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
  • ์˜ˆ: [a, b, c].zip([1, 2, 3]) โ†’ [(a, 1), (b, 2), (c, 3)]
  • ๋‘ ์ปฌ๋ ‰์…˜์˜ ๊ธธ์ด๊ฐ€ ๋‹ค๋ฅด๋ฉด, ๋” ์งง์€ ์ชฝ์— ๋งž์ถฐ์ง‘๋‹ˆ๋‹ค.

โœ… ํŠœํ”Œ ๋ถ„ํ•ด(Destructuring):

  • ((_, _, angle_degrees), speed)์—์„œ:

    • ๋ฐ”๊นฅ ํŠœํ”Œ: zip()์ด ์ƒ์„ฑํ•œ (planet, speed) ์Œ
    • ์•ˆ์ชฝ ํŠœํ”Œ: planet์€ (usize, f32, f32) ํ˜•์‹
    • _: ํ•ด๋‹น ์œ„์น˜์˜ ๊ฐ’์€ ๋ฌด์‹œํ•˜๊ฒ ๋‹ค๋Š” ์˜๋ฏธ

โœ… ์—ญ์ฐธ์กฐ ์—ฐ์‚ฐ์ž *:

  • ์ฐธ์กฐ(&T ๋˜๋Š” &mut T)๊ฐ€ ๊ฐ€๋ฆฌํ‚ค๋Š” ์‹ค์ œ ๊ฐ’์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

  • *angle_degrees๋Š” &mut f32์—์„œ ์‹ค์ œ f32 ๊ฐ’์— ์ ‘๊ทผํ•˜์—ฌ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.


โœ… โš™๏ธ๐Ÿ› ๏ธ๐Ÿ“„๐Ÿ“š๐ŸŽจ๐Ÿ–ผ๏ธ๐Ÿงฉ๐Ÿง ๐Ÿงฎ๐Ÿ•น๏ธ๐Ÿงฟโณ๐ŸŽ›๏ธ๐Ÿ”ฎ๐ŸŒŒ๐Ÿชฉ๐Ÿ—‚๏ธ๐Ÿ“๐Ÿงฐ๐Ÿ”’

์•„๋ณด์นด๋„
Sample
Important
Highlighting
๋น„์ƒ‰
์ ํ™ฉ์ƒ‰
purple
hot pink
yellow
์‚ด๊ตฌ์ƒ‰
orange
green
blue

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

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