๐Ÿ”ฎ :: Particle Flow

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

Nannou <Generative Art>

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

๐Ÿ“ Rust Code

use nannou::noise::NoiseFn;
use nannou::prelude::*;

// ------------------ Particle ------------------

struct Particle {
    pos: Vec2,
    vel: Vec2,
}

impl Particle {
    fn new(x: f32, y: f32) -> Particle {
        Particle {
            pos: vec2(x, y),
            vel: vec2(0.0, 0.0),
        }
    }

    fn update(&mut self, dir: Vec2) {
        self.pos += self.vel;
        self.vel += dir;
        self.vel *= 0.8;
    }
}

// ------------------ Model ------------------

struct Model {
    particles: Vec<Particle>,
}

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

fn model(app: &App) -> Model {
    app.new_window().size(800, 800).view(view).build().unwrap();

    let rect = app.window_rect();
    let mut particles = Vec::new();

    for _ in 0..2000 {
        let x = random_range(rect.left(), rect.right());
        let y = random_range(rect.bottom(), rect.top());
        particles.push(Particle::new(x, y));
    }

    Model { particles }
}

// ------------------ Update ------------------

fn update(app: &App, model: &mut Model, _update: Update) {
    let noise = nannou::noise::Perlin::new();
    let t = app.elapsed_frames() as f64 / 100.0;

    for (i, p) in model.particles.iter_mut().enumerate() {
        let x = noise.get([
            p.pos.x as f64 / 128.0,
            p.pos.y as f64 / 137.0,
            t + i as f64 / 1000.0,
        ]);
        let y = noise.get([
            -p.pos.y as f64 / 128.0,
            p.pos.x as f64 / 137.0,
            t + i as f64 / 1000.0,
        ]);

        let force = vec2(x as f32, y as f32);
        p.update(force);
    }
}

// ------------------ View ------------------

fn view(app: &App, model: &Model, frame: Frame) {
    let draw = app.draw();

    // ์ž”์ƒ ์—†๋Š” ๊ฒ€์€ ๋ฐฐ๊ฒฝ์ด ํ•„์š”ํ•˜๋ฉด ์ฃผ์„ ํ•ด์ œ
    // draw.background().color(BLACK);

    for p in &model.particles {
        draw.ellipse()
            .xy(p.pos)               
            .w(1.0)                  
            .h(1.0)                  
            .color(hsla(0.5, 1.0, 0.6, 0.2)); 
    }

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

๐Ÿ“ Rust Code

// Nannou์—์„œ ์ œ๊ณตํ•˜๋Š” ํผ๋ฆฐ ๋…ธ์ด์ฆˆ(Perlin Noise) ํ•จ์ˆ˜ ์‚ฌ์šฉ์„ ์œ„ํ•œ ๋ชจ๋“ˆ ์ž„ํฌํŠธ
use nannou::noise::NoiseFn;
// Nannou์˜ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ(์ƒ‰์ƒ, ๋ฒกํ„ฐ, ์ฐฝ ๊ด€๋ฆฌ ๋“ฑ)์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ prelude ์ž„ํฌํŠธ
use nannou::prelude::*;

// ------------------ Particle ๊ตฌ์กฐ์ฒด ์ •์˜ ------------------
// Particle์€ ํ™”๋ฉด ์ƒ์—์„œ ์›€์ง์ด๋Š” ๊ฐœ๋ณ„ ์ (ํŒŒํ‹ฐํด)์„ ํ‘œํ˜„ํ•˜๋Š” ์‚ฌ์šฉ์ž ์ •์˜ ํƒ€์ž…์ž…๋‹ˆ๋‹ค.

/// `Particle` ๊ตฌ์กฐ์ฒด: ๊ฐ ํŒŒํ‹ฐํด์˜ ์ƒํƒœ๋ฅผ ์ €์žฅ
/// - `pos`: ํ˜„์žฌ ์œ„์น˜ (2D ๋ฒกํ„ฐ)
/// - `vel`: ํ˜„์žฌ ์†๋„ (2D ๋ฒกํ„ฐ)
struct Particle {
    pos: Vec2, // ์œ„์น˜ (x, y)
    vel: Vec2, // ์†๋„ (x, y ๋ฐฉํ–ฅ ์ด๋™๋Ÿ‰)
}

// ------------------ Particle ๊ตฌ์กฐ์ฒด์— ๋Œ€ํ•œ ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ ------------------
// `impl` ๋ธ”๋ก์€ `Particle` ํƒ€์ž…์— ๋ฉ”์„œ๋“œ(ํ•จ์ˆ˜)๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.
// ์ด๋ฅผ ํ†ตํ•ด `Particle::new()`๋‚˜ `particle.update()`์ฒ˜๋Ÿผ ๊ฐ์ฒด ์ง€ํ–ฅ ์Šคํƒ€์ผ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

impl Particle {
    /// ์ƒ์„ฑ์ž ๋ฉ”์„œ๋“œ: ์ฃผ์–ด์ง„ (x, y) ์ขŒํ‘œ์— ์œ„์น˜ํ•œ ์ƒˆ ํŒŒํ‹ฐํด์„ ์ƒ์„ฑ
    /// - ์ดˆ๊ธฐ ์†๋„๋Š” (0, 0)์œผ๋กœ ์„ค์ • โ†’ ์ •์ง€ ์ƒํƒœ์—์„œ ์‹œ์ž‘
    fn new(x: f32, y: f32) -> Particle {
        Particle {
            pos: vec2(x, y),     // ์œ„์น˜ ์ดˆ๊ธฐํ™”
            vel: vec2(0.0, 0.0), // ์†๋„ ์ดˆ๊ธฐํ™” (์ •์ง€)
        }
    }

    /// ํŒŒํ‹ฐํด ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฉ”์„œ๋“œ
    /// - `dir`: ์™ธ๋ถ€์—์„œ ๋ฐ›์€ ๊ฐ€์†๋„(ํž˜) ๋ฒกํ„ฐ
    /// - ๋ฌผ๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜: ์œ„์น˜ += ์†๋„, ์†๋„ += ๊ฐ€์†๋„, ์†๋„ *= ๊ฐ์† ๊ณ„์ˆ˜
    fn update(&mut self, dir: Vec2) {
        self.pos += self.vel;    // ํ˜„์žฌ ์†๋„๋งŒํผ ์œ„์น˜ ์ด๋™
        self.vel += dir;         // ์™ธ๋ถ€ ํž˜(dir)์— ์˜ํ•ด ์†๋„ ๋ณ€๊ฒฝ (๊ฐ€์†)
        self.vel *= 0.8;         // ๊ฐ์†: ๊ณต๊ธฐ ์ €ํ•ญ์ฒ˜๋Ÿผ ์†๋„๋ฅผ 80%๋กœ ๊ฐ์†Œ (์—๋„ˆ์ง€ ์†Œ์‹ค)
    }
}

// ------------------ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ตœ์ƒ์œ„ ์ƒํƒœ ๊ตฌ์กฐ์ฒด ------------------

/// `Model` ๊ตฌ์กฐ์ฒด: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „์ฒด ์ƒํƒœ๋ฅผ ์ €์žฅ
/// - `particles`: ํ™”๋ฉด์— ํ‘œ์‹œ๋  ๋ชจ๋“  ํŒŒํ‹ฐํด์˜ ๋ชฉ๋ก
struct Model {
    particles: Vec<Particle>, // ํŒŒํ‹ฐํด ๋ฒกํ„ฐ (๋™์  ๋ฐฐ์—ด)
}

// ------------------ ํ”„๋กœ๊ทธ๋žจ ์ง„์ž…์  ------------------

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

// ------------------ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ ------------------

/// `model` ํ•จ์ˆ˜: ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์‹œ์ž‘ ์‹œ ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœ
/// - ์ฐฝ ์ƒ์„ฑ ๋ฐ ์ดˆ๊ธฐ ํŒŒํ‹ฐํด ๋ฐฐ์น˜
fn model(app: &App) -> Model {
    // 800x800 ํ”ฝ์…€ ํฌ๊ธฐ์˜ ์ƒˆ ์ฐฝ ์ƒ์„ฑ, ๋ทฐ ํ•จ์ˆ˜ ์—ฐ๊ฒฐ
    app.new_window().size(800, 800).view(view).build().unwrap();

    // ํ˜„์žฌ ์ฐฝ์˜ ๊ฒฝ๊ณ„ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ (์ขŒํ‘œ๊ณ„: ์ค‘์‹ฌ์ด (0,0))
    let rect = app.window_rect();
    let mut particles = Vec::new(); // ํŒŒํ‹ฐํด ์ €์žฅ ๋ฒกํ„ฐ ์ดˆ๊ธฐํ™”

    // 2000๊ฐœ์˜ ํŒŒํ‹ฐํด์„ ํ™”๋ฉด ์ „์ฒด์— ๋žœ๋ค ๋ฐฐ์น˜
    for _ in 0..2000 {
        // x ์ขŒํ‘œ: ์ฐฝ์˜ ์™ผ์ชฝ(rect.left()) ~ ์˜ค๋ฅธ์ชฝ(rect.right()) ์‚ฌ์ด ๋žœ๋ค
        let x = random_range(rect.left(), rect.right());
        // y ์ขŒํ‘œ: ์ฐฝ์˜ ์•„๋ž˜(rect.bottom()) ~ ์œ„(rect.top()) ์‚ฌ์ด ๋žœ๋ค
        let y = random_range(rect.bottom(), rect.top());
        // ์ƒˆ ํŒŒํ‹ฐํด ์ƒ์„ฑ ํ›„ ๋ฒกํ„ฐ์— ์ถ”๊ฐ€
        particles.push(Particle::new(x, y));
    }

    // ์ดˆ๊ธฐ Model ๋ฐ˜ํ™˜
    Model { particles }
}

// ------------------ ์ƒํƒœ ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜ ------------------

/// `update` ํ•จ์ˆ˜: ๋งค ํ”„๋ ˆ์ž„ ํ˜ธ์ถœ๋˜์–ด ํŒŒํ‹ฐํด ์›€์ง์ž„ ๊ณ„์‚ฐ
fn update(app: &App, model: &mut Model, _update: Update) {
    // 3D ํผ๋ฆฐ ๋…ธ์ด์ฆˆ ์ƒ์„ฑ๊ธฐ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
    // โ†’ ๋ถ€๋“œ๋Ÿฝ๊ณ  ์ž์—ฐ์Šค๋Ÿฌ์šด ์œ ๊ธฐ์  ํŒจํ„ด ์ƒ์„ฑ์— ์‚ฌ์šฉ
    let noise = nannou::noise::Perlin::new();
    
    // ์‹œ๊ฐ„ ๋ณ€์ˆ˜: ํ”„๋ ˆ์ž„ ์ˆ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ถ€๋“œ๋Ÿฌ์šด ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์œ„ํ•œ ์‹œ๊ฐ„ ํ๋ฆ„
    // - 100.0์œผ๋กœ ๋‚˜๋ˆ„์–ด ์†๋„ ์กฐ์ ˆ (๊ฐ’์ด ์ž‘์„์ˆ˜๋ก ๋А๋ฆผ)
    let t = app.elapsed_frames() as f64 / 100.0;

    // ๋ชจ๋“  ํŒŒํ‹ฐํด์„ ์ˆœํšŒํ•˜๋ฉฐ ์—…๋ฐ์ดํŠธ
    for (i, p) in model.particles.iter_mut().enumerate() {
        // X ๋ฐฉํ–ฅ ๋…ธ์ด์ฆˆ ๊ฐ’ ๊ณ„์‚ฐ:
        // - ๊ณต๊ฐ„ ์ขŒํ‘œ(p.pos.x, p.pos.y)๋ฅผ ์Šค์ผ€์ผ๋ง(128, 137)ํ•˜์—ฌ ๋…ธ์ด์ฆˆ ์ž…๋ ฅ
        // - ์‹œ๊ฐ„(t)๊ณผ ํŒŒํ‹ฐํด ๊ณ ์œ  ID(i)๋ฅผ ์ถ”๊ฐ€ํ•ด ์‹œ๊ฐ„์— ๋”ฐ๋ผ ๋ณ€ํ™”ํ•˜๊ณ , ๊ฐ ํŒŒํ‹ฐํด์ด ๊ณ ์œ ํ•œ ์›€์ง์ž„์„ ๊ฐ€์ง€๋„๋ก ํ•จ
        let x = noise.get([
            p.pos.x as f64 / 128.0,
            p.pos.y as f64 / 137.0,
            t + i as f64 / 1000.0,
        ]);
        
        // Y ๋ฐฉํ–ฅ ๋…ธ์ด์ฆˆ ๊ฐ’ ๊ณ„์‚ฐ:
        // - ์ขŒํ‘œ๋ฅผ ํšŒ์ „(-y, x)ํ•˜์—ฌ X์™€ ๋‹ค๋ฅธ ๋ฐฉํ–ฅ์˜ ํ๋ฆ„ ์ƒ์„ฑ โ†’ ์œ ๊ธฐ์  ์†Œ์šฉ๋Œ์ด ํšจ๊ณผ
        let y = noise.get([
            -p.pos.y as f64 / 128.0,
            p.pos.x as f64 / 137.0,
            t + i as f64 / 1000.0,
        ]);

        // ๋…ธ์ด์ฆˆ ๊ฐ’์„ f32 ๋ฒกํ„ฐ๋กœ ๋ณ€ํ™˜ (๋…ธ์ด์ฆˆ๋Š” -1.0 ~ +1.0 ๋ฒ”์œ„)
        let force = vec2(x as f32, y as f32);
        // ํŒŒํ‹ฐํด ์—…๋ฐ์ดํŠธ: ๊ณ„์‚ฐ๋œ ํž˜(force)์„ ์ ์šฉ
        p.update(force);
    }
}

// ------------------ ๋ Œ๋”๋ง ํ•จ์ˆ˜ ------------------

/// `view` ํ•จ์ˆ˜: ๋งค ํ”„๋ ˆ์ž„ ํ™”๋ฉด์„ ๊ทธ๋ฆฌ๋Š” ํ•จ์ˆ˜
fn view(app: &App, model: &Model, frame: Frame) {
    // ๊ทธ๋ฆฌ๊ธฐ ๋ช…๋ น์„ ๋‹ด์„ Draw ๊ฐ์ฒด ์ƒ์„ฑ
    let draw = app.draw();

    // โš ๏ธ ๋ฐฐ๊ฒฝ์„ ๋ช…์‹œ์ ์œผ๋กœ ๊ฒ€์€์ƒ‰์œผ๋กœ ์ง€์šฐ์ง€ ์•Š์Œ โ†’ ์ด์ „ ํ”„๋ ˆ์ž„์ด ๋‚จ์•„ ์ž”์ƒ ํšจ๊ณผ ๋ฐœ์ƒ
    // ๋งŒ์•ฝ ์ž”์ƒ ์—†์ด ๊น”๋”ํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์›ํ•˜๋ฉด ๋‹ค์Œ ์ค„ ์ฃผ์„ ํ•ด์ œ:
    // draw.background().color(BLACK);

    // ๋ชจ๋“  ํŒŒํ‹ฐํด์„ ์ž‘์€ ์›์œผ๋กœ ๊ทธ๋ฆฌ๊ธฐ
    for p in &model.particles {
        draw.ellipse()
            .xy(p.pos)               // ์› ์ค‘์‹ฌ ์œ„์น˜
            .w(1.0)                  // ๋„ˆ๋น„: 1.0 ํ”ฝ์…€
            .h(1.0)                  // ๋†’์ด: 1.0 ํ”ฝ์…€ (์›ํ˜•)
            .color(hsla(0.5, 1.0, 0.6, 0.2)); // ์ƒ‰์ƒ: ์ฒญ๋ก์ƒ‰(H=0.5), ์ฑ„๋„ 100%, ๋ฐ๊ธฐ 60%, ํˆฌ๋ช…๋„ 20%
    }

    // ๋ชจ๋“  ๊ทธ๋ฆฌ๊ธฐ ๋ช…๋ น์„ ์‹ค์ œ ํ”„๋ ˆ์ž„ ๋ฒ„ํผ์— ์ ์šฉ
    draw.to_frame(app, &frame).unwrap();
}

๐Ÿ“ Rust Code + ์‹œํ€€์Šค ์ €์žฅ

[dependencies]
nannou = "0.19"
gif = "0.13"
image = "0.24"
use gif::{Encoder, Frame as GifFrame, Repeat};
use image::io::Reader as ImageReader;
use nannou::noise::NoiseFn;
use nannou::prelude::*;
use std::fs::{self, File};
use std::sync::{Arc, Mutex};
use std::thread;

// ------------------ Particle ------------------

struct Particle {
    pos: Vec2,
    vel: Vec2,
}

impl Particle {
    fn new(x: f32, y: f32) -> Particle {
        Particle {
            pos: vec2(x, y),
            vel: vec2(0.0, 0.0),
        }
    }

    fn update(&mut self, dir: Vec2) {
        self.pos += self.vel;
        self.vel += dir;
        self.vel *= 0.8;
    }
}

// ------------------ Model ------------------

struct Model {
    particles: Vec<Particle>,
    gif_created: Arc<Mutex<bool>>,
}

fn main() {
    // frames ํด๋” ์ƒ์„ฑ ๋ฐ ์ •๋ฆฌ
    let frames_dir = std::path::Path::new("frames");
    if frames_dir.exists() {
        fs::remove_dir_all(frames_dir).unwrap();
    }
    fs::create_dir_all(frames_dir).unwrap();

    nannou::app(model).update(update).exit(on_exit).run();
}

fn model(app: &App) -> Model {
    app.new_window().size(800, 800).view(view).build().unwrap();

    let rect = app.window_rect();
    let r = rect.right();
    let l = rect.left();
    let w = l - r;

    let t = rect.top();
    let b = rect.bottom();
    let h = t - b;

    let mut p = vec![];
    for _i in 0..2000 {
        let x = random::<f32>() * w + r;
        let y = random::<f32>() * h + b;
        p.push(Particle::new(x, y));
    }

    Model { 
        particles: p,
        gif_created: Arc::new(Mutex::new(false)),
    }
}

// ------------------ Update ------------------

fn update(app: &App, model: &mut Model, _update: Update) {
    let noise = nannou::noise::Perlin::new();
    let t = app.elapsed_frames() as f64 / 100.0;

    for (i, p) in model.particles.iter_mut().enumerate() {
        let x = noise.get([
            p.pos.x as f64 / 128.0,
            p.pos.y as f64 / 137.0,
            t + i as f64 / 1000.0,
        ]);
        let y = noise.get([
            -p.pos.y as f64 / 128.0,
            p.pos.x as f64 / 137.0,
            t + i as f64 / 1000.0,
        ]);

        let a = vec2(x as f32, y as f32);
        p.update(a);
    }

    // 200 ํ”„๋ ˆ์ž„ ์ดํ›„ ์ข…๋ฃŒ
    if app.elapsed_frames() >= 300 {
        app.quit();
    }
}

// ------------------ View ------------------

fn view(app: &App, model: &Model, frame: nannou::Frame) {
    let draw = app.draw();
    // draw.background().color(BLACK);

    for p in &model.particles {
        draw.ellipse()
            .xy(p.pos)
            .w(1.0)
            .h(1.0)
            .color(hsla(0.5, 1.0, 0.6, 0.2));
    }

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

    // PNG ์ €์žฅ
    let file_path = captured_frame_path(app, &frame);
    app.main_window().capture_frame(file_path);
}

// ------------------ Exit handler ------------------

fn on_exit(_app: &App, model: Model) {
    println!("์•ฑ ์ข…๋ฃŒ ์ค‘... GIF ์ƒ์„ฑ์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.");
    
    // GIF ์ƒ์„ฑ์„ ๋ณ„๋„ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰
    let gif_created = model.gif_created.clone();
    let handle = thread::spawn(move || {
        pngs_to_gif("frames", "output.gif", 800, 800);
        let mut created = gif_created.lock().unwrap();
        *created = true;
        println!("โœ… GIF saved as output.gif");
    });
    
    // GIF ์ƒ์„ฑ ์™„๋ฃŒ๊นŒ์ง€ ๋Œ€๊ธฐ
    handle.join().unwrap();
}

// ------------------ Helper functions ------------------

fn captured_frame_path(app: &App, frame: &nannou::Frame) -> std::path::PathBuf {
    app.project_path()
        .expect("failed to locate `project_path`")
        .join("frames")
        .join(format!("{:04}", frame.nth()))
        .with_extension("png")
}

fn pngs_to_gif(folder: &str, output: &str, width: u16, height: u16) {
    println!("GIF ์ƒ์„ฑ ์ค‘...");
    
    // PNG ํŒŒ์ผ๋“ค ์ฝ๊ธฐ
    let mut paths: Vec<_> = match fs::read_dir(folder) {
        Ok(entries) => entries
            .filter_map(|e| e.ok())
            .map(|e| e.path())
            .filter(|p| p.extension().unwrap_or_default() == "png")
            .collect(),
        Err(e) => {
            eprintln!("ํด๋” ์ฝ๊ธฐ ์‹คํŒจ: {}", e);
            return;
        }
    };
    
    if paths.is_empty() {
        eprintln!("PNG ํŒŒ์ผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");
        return;
    }
    
    paths.sort(); // ํ”„๋ ˆ์ž„ ์ˆœ์„œ๋Œ€๋กœ ์ •๋ ฌ
    println!("{}๊ฐœ์˜ PNG ํŒŒ์ผ์„ ์ฐพ์•˜์Šต๋‹ˆ๋‹ค.", paths.len());

    // GIF ํŒŒ์ผ ์ƒ์„ฑ
    let mut image = match File::create(output) {
        Ok(file) => file,
        Err(e) => {
            eprintln!("GIF ํŒŒ์ผ ์ƒ์„ฑ ์‹คํŒจ: {}", e);
            return;
        }
    };
    
    let mut encoder = match Encoder::new(&mut image, width, height, &[]) {
        Ok(enc) => enc,
        Err(e) => {
            eprintln!("์ธ์ฝ”๋” ์ƒ์„ฑ ์‹คํŒจ: {}", e);
            return;
        }
    };
    
    if let Err(e) = encoder.set_repeat(Repeat::Infinite) {
        eprintln!("๋ฐ˜๋ณต ์„ค์ • ์‹คํŒจ: {}", e);
        return;
    }

    // ๊ฐ PNG๋ฅผ GIF ํ”„๋ ˆ์ž„์œผ๋กœ ๋ณ€ํ™˜
    for (i, path) in paths.iter().enumerate() {
        match process_frame(&path, &mut encoder, width, height) {
            Ok(_) => {
                if (i + 1) % 50 == 0 || i == paths.len() - 1 {
                    println!("์ง„ํ–‰์ƒํ™ฉ: {}/{}", i + 1, paths.len());
                }
            }
            Err(e) => {
                eprintln!("ํ”„๋ ˆ์ž„ ์ฒ˜๋ฆฌ ์‹คํŒจ {:?}: {}", path, e);
            }
        }
    }
    
    println!("GIF ์ธ์ฝ”๋”ฉ ์™„๋ฃŒ");
}

fn process_frame(
    path: &std::path::Path, 
    encoder: &mut Encoder<&mut File>, 
    width: u16, 
    height: u16
) -> Result<(), Box<dyn std::error::Error>> {
    let img = ImageReader::open(path)?.decode()?.to_rgba8();
    let mut pixels = img.into_raw();
    
    let mut frame = GifFrame::from_rgba_speed(width, height, &mut pixels, 10);
    frame.delay = 3; // ์•ฝ 30fps (100/3 โ‰ˆ 33ms)
    
    encoder.write_frame(&frame)?;
    Ok(())
}

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

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