

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();
}
// 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();
}
[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(())
}