
๐ Rust Code
project/
โโ Cargo.toml
โโ src/main.rs
โโ assets/image.jpg โ ์ฌ๊ธฐ์ ์ด๋ฏธ์ง
use nannou::prelude::*;
use nannou::image as nimage;
const WIN_W: u32 = 1920;
const WIN_H: u32 = 1080;
const IMAGE_TARGET_WIDTH: f32 = 1000.0;
const PIXEL_STEP: u32 = 10;
const RETURN_FORCE: f32 = 0.03;
const DAMPING: f32 = 0.88;
const MOUSE_RADIUS: f32 = 150.0;
const ATTRACTION_FORCE: f32 = 4.0;
const EXPLOSION_FORCE: f32 = 100.0;
const GAUSSIAN_SIGMA: f32 = 0.55;
const MOUSE_RADIUS_SQ: f32 = MOUSE_RADIUS * MOUSE_RADIUS;
struct Particle {
origin: Vec2,
pos: Vec2,
vel: Vec2,
color: Rgba,
}
struct Model {
particles: Vec<Particle>,
mouse_pos: Vec2,
mouse_down: bool,
}
fn main() {
nannou::app(model)
.update(update)
.event(event)
.view(view)
.run();
}
fn model(app: &App) -> Model {
app.new_window()
.size(WIN_W, WIN_H)
.title("Blackhole Explosion")
.build()
.unwrap();
let assets = app.assets_path().expect("assets ๊ฒฝ๋ก๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค");
let img_path = assets.join("galaxy.jpg");
let img = nimage::open(&img_path)
.expect("์ด๋ฏธ์ง๋ฅผ ๋ก๋ํ ์ ์์ต๋๋ค")
.to_rgba8();
let (iw, ih) = img.dimensions();
let scale = IMAGE_TARGET_WIDTH / iw as f32;
let target_h = ih as f32 * scale;
let offset_x = -IMAGE_TARGET_WIDTH / 2.0;
let offset_y = -target_h / 2.0;
let estimated_size = ((iw / PIXEL_STEP) * (ih / PIXEL_STEP)) as usize;
let mut particles = Vec::with_capacity(estimated_size);
for y in (0..ih).step_by(PIXEL_STEP as usize) {
for x in (0..iw).step_by(PIXEL_STEP as usize) {
let p = img.get_pixel(x, y);
let alpha = p[3] as f32 / 255.0;
if alpha < 0.1 {
continue;
}
let px = x as f32 * scale + offset_x;
let py = (ih - y) as f32 * scale + offset_y;
particles.push(Particle {
origin: vec2(px, py),
pos: vec2(px, py),
vel: Vec2::ZERO,
color: rgba(
p[0] as f32 / 255.0,
p[1] as f32 / 255.0,
p[2] as f32 / 255.0,
alpha,
),
});
}
}
particles.shrink_to_fit();
Model {
particles,
mouse_pos: Vec2::ZERO,
mouse_down: false,
}
}
fn event(_app: &App, model: &mut Model, event: Event) {
match event {
Event::WindowEvent { simple: Some(ev), .. } => match ev {
MouseMoved(pos) => {
model.mouse_pos = pos;
}
MousePressed(MouseButton::Left) => {
model.mouse_down = true;
for p in model.particles.iter_mut() {
let to_mouse = model.mouse_pos - p.pos;
let dist_sq = to_mouse.length_squared();
if dist_sq < MOUSE_RADIUS_SQ && dist_sq > 0.01 {
let dist = dist_sq.sqrt();
let g = gaussian(dist, MOUSE_RADIUS);
let dir = to_mouse / dist;
p.vel -= dir * g * EXPLOSION_FORCE;
}
}
}
MouseReleased(MouseButton::Left) => {
model.mouse_down = false;
}
_ => {}
},
_ => {}
}
}
#[inline]
fn gaussian(dist: f32, radius: f32) -> f32 {
let x = dist / radius;
(-x * x / GAUSSIAN_SIGMA).exp()
}
fn update(_app: &App, model: &mut Model, _update: Update) {
for p in model.particles.iter_mut() {
let to_origin = p.origin - p.pos;
p.vel += to_origin * RETURN_FORCE;
if !model.mouse_down {
let to_mouse = model.mouse_pos - p.pos;
let dist_sq = to_mouse.length_squared();
if dist_sq < MOUSE_RADIUS_SQ && dist_sq > 0.01 {
let dist = dist_sq.sqrt();
let g = gaussian(dist, MOUSE_RADIUS);
let dir = to_mouse / dist;
p.vel += dir * g * ATTRACTION_FORCE;
}
}
p.vel *= DAMPING;
p.pos += p.vel;
}
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(BLACK);
for p in model.particles.iter() {
draw.ellipse()
.xy(p.pos)
.radius(3.0)
.color(p.color);
}
draw.to_frame(app, &frame).unwrap();
}