๐Ÿ”ฎ :: Fireworks Generator

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

Nannou <Generative Art>

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

๐Ÿ“ Rust Code

use nannou::prelude::*;

// ์•ฑ์˜ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” Model ๊ตฌ์กฐ์ฒด
#[derive(Debug)]
struct Model {
    // ์œˆ๋„์šฐ ID
    window: WindowId,

    // ์‹œ๊ฐ„ ๊ด€๋ จ
    time: f32,
    frame_count: u64,

    // ๋งˆ์šฐ์Šค ์ƒํƒœ
    mouse_pos: Point2,
    mouse_pressed: bool,

    // ํ‚ค๋ณด๋“œ ์ƒํƒœ
    keys_pressed: std::collections::HashSet<Key>,

    // ์•ฑ ์ƒํƒœ
    is_paused: bool,
    background_color: Rgb<u8>,

    // ์‚ฌ์šฉ์ž ์ •์˜ ๋ฐ์ดํ„ฐ
    particles: Vec<Particle>,
}

// ์˜ˆ์‹œ์šฉ ํŒŒํ‹ฐํด ๊ตฌ์กฐ์ฒด
#[derive(Debug, Clone)]
struct Particle {
    position: Point2,
    velocity: Vec2,
    color: Rgb<u8>,
    size: f32,
    life: f32,
}

impl Particle {
    fn new(pos: Point2) -> Self {
        Self {
            position: pos,
            velocity: vec2(random_range(-2.0, 2.0), random_range(-2.0, 2.0)),
            color: rgb(
                random_range(0, 255),
                random_range(0, 255),
                random_range(0, 255),
            ),
            size: random_range(2.0, 8.0),
            life: 1.5, // ์ƒ์กด ์ฃผ๊ธฐ ์ฆ๊ฐ€
        }
    }

    fn update(&mut self) {
        self.position += self.velocity;
        self.life -= 0.005; // ๊ฐ์†Œ ์†๋„ ์ ˆ๋ฐ˜์œผ๋กœ ์ค„์ž„
        self.velocity *= 0.99; // ๋งˆ์ฐฐ๋ ฅ
    }

    fn is_alive(&self) -> bool {
        self.life > 0.0
    }
}

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

// ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
fn model(app: &App) -> Model {
    // ์œˆ๋„์šฐ ์ƒ์„ฑ ๋ฐ ์„ค์ •
    let window_id = app
        .new_window()
        .title("Fireworks Generator")
        .size(800, 800)
        .view(view)
        .build()
        .unwrap();

    Model {
        window: window_id,
        time: 0.0,
        frame_count: 0,
        mouse_pos: pt2(0.0, 0.0),
        mouse_pressed: false,
        keys_pressed: std::collections::HashSet::new(),
        is_paused: false,
        background_color: rgb(20, 20, 30),
        particles: Vec::new(),
    }
}

// ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ - ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ํ˜ธ์ถœ
fn update(app: &App, model: &mut Model, _update: Update) {
    if model.is_paused {
        return;
    }

    // ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
    model.time = app.time;
    model.frame_count += 1;

    // ๋งˆ์šฐ์Šค ์œ„์น˜ ์—…๋ฐ์ดํŠธ
    model.mouse_pos = app.mouse.position();

    // ํŒŒํ‹ฐํด ์—…๋ฐ์ดํŠธ
    for particle in &mut model.particles {
        particle.update();
    }

    // ์ฃฝ์€ ํŒŒํ‹ฐํด ์ œ๊ฑฐ
    model.particles.retain(|p| p.is_alive());

    // ๋งˆ์šฐ์Šค๋ฅผ ๋ˆ„๋ฅด๊ณ  ์žˆ์œผ๋ฉด ํŒŒํ‹ฐํด ์ƒ์„ฑ (๋” ๋งŽ์ด)
    if model.mouse_pressed && model.frame_count % 2 == 0 {
        model.particles.push(Particle::new(model.mouse_pos));
    }

    // ํŒŒํ‹ฐํด ์ˆ˜ ์ œํ•œ ์ฆ๊ฐ€
    if model.particles.len() > 500 {
        model.particles.drain(0..100);
    }
}

// ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
fn event(app: &App, model: &mut Model, event: Event) {
    match event {
        Event::WindowEvent {
            simple: Some(event),
            ..
        } => {
            match event {
                // ํ‚ค๋ณด๋“œ ์ด๋ฒคํŠธ
                KeyPressed(key) => {
                    model.keys_pressed.insert(key);
                    handle_key_pressed(app, model, key);
                }
                KeyReleased(key) => {
                    model.keys_pressed.remove(&key);
                }

                // ๋งˆ์šฐ์Šค ์ด๋ฒคํŠธ
                MousePressed(_button) => {
                    model.mouse_pressed = true;
                }
                MouseReleased(_button) => {
                    model.mouse_pressed = false;
                }
                MouseMoved(_pos) => {
                    // ๋งˆ์šฐ์Šค ์œ„์น˜๋Š” update์—์„œ ์ฒ˜๋ฆฌ
                }

                // ์œˆ๋„์šฐ ๋ฆฌ์‚ฌ์ด์ฆˆ
                Resized(_size) => {
                    // ํ•„์š”์‹œ ๋ฆฌ์‚ฌ์ด์ฆˆ ๋กœ์ง ์ถ”๊ฐ€
                }

                _ => {}
            }
        }
        _ => {}
    }
}

// ํ‚ค๋ณด๋“œ ์ž…๋ ฅ ์ฒ˜๋ฆฌ
fn handle_key_pressed(app: &App, model: &mut Model, key: Key) {
    match key {
        Key::Space => {
            model.is_paused = !model.is_paused;
        }
        Key::C => {
            model.particles.clear();
        }
        Key::R => {
            // Random background color
            model.background_color = rgb(
                random_range(0, 255),
                random_range(0, 255),
                random_range(0, 255),
            );
        }
        Key::S => {
            // Save screenshot
            let file_path = app
                .project_path()
                .expect("Cannot find project path")
                .join("screenshots")
                .join(format!("screenshot_{}.png", model.frame_count));

            if let Some(parent) = file_path.parent() {
                std::fs::create_dir_all(parent).ok();
            }

            app.window(model.window)
                .expect("Cannot find window")
                .capture_frame(file_path);
        }
        Key::Escape => {
            app.quit();
        }
        _ => {}
    }
}

// ๋ Œ๋”๋ง ํ•จ์ˆ˜
fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();

    // ๋ฐฐ๊ฒฝ ๊ทธ๋ฆฌ๊ธฐ
    draw.background().color(model.background_color);

    // ํŒŒํ‹ฐํด ๊ทธ๋ฆฌ๊ธฐ
    for particle in &model.particles {
        let alpha = particle.life;
        let color = rgba(
            particle.color.red as f32 / 255.0,
            particle.color.green as f32 / 255.0,
            particle.color.blue as f32 / 255.0,
            alpha,
        );

        draw.ellipse()
            .xy(particle.position)
            .radius(particle.size)
            .color(color);
    }

    // Debug info (top-left) - positioned within screen bounds
    let win = app.window_rect();

    // Background for debug text
    draw.rect()
        .xy(pt2(win.left() + 100.0, win.top() - 60.0))
        .w_h(180.0, 90.0)
        .color(rgba(0.0, 0.0, 0.0, 0.7));

    draw.text(&format!("Frame: {}", model.frame_count))
        .xy(pt2(win.left() + 20.0, win.top() - 30.0))
        .font_size(16)
        .color(WHITE);

    draw.text(&format!("Particles: {}", model.particles.len()))
        .xy(pt2(win.left() + 20.0, win.top() - 50.0))
        .font_size(16)
        .color(WHITE);

    draw.text(&format!("Time: {:.2}s", model.time))
        .xy(pt2(win.left() + 20.0, win.top() - 70.0))
        .font_size(16)
        .color(WHITE);

    if model.is_paused {
        draw.text("PAUSED")
            .xy(pt2(0.0, 0.0))
            .font_size(48)
            .color(RED);
    }

    // Control guide (bottom-right) - positioned within screen bounds
    let guide_text = [
        "SPACE: Play/Pause",
        "C: Clear particles",
        "R: Random background",
        "S: Save screenshot",
        "Mouse: Create particles",
        "ESC: Exit",
    ];

    // Background for guide text
    draw.rect()
        .xy(pt2(win.right() - 120.0, win.bottom() + 70.0))
        .w_h(220.0, 130.0)
        .color(rgba(0.0, 0.0, 0.0, 0.7));

    for (i, text) in guide_text.iter().enumerate() {
        draw.text(text)
            .xy(pt2(
                win.right() - 220.0,
                win.bottom() + 20.0 + (i as f32 * 20.0),
            ))
            .font_size(14)
            .color(WHITE);
    }

    // ํ™”๋ฉด์— ๊ทธ๋ฆฌ๊ธฐ
    draw.to_frame(app, &frame).unwrap();
}

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

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