


use nannou::prelude::*;
use rand::Rng;
use std::time::{SystemTime, UNIX_EPOCH};
use std::collections::HashMap;
const CONNECT_DISTANCE: f32 = 150.0;
const PARTICLE_MIN_SIZE: f32 = 1.0;
const PARTICLE_MAX_SIZE: f32 = 5.0;
const PARTICLE_MIN_SPEED: f32 = 10.0;
const PARTICLE_MAX_SPEED: f32 = 80.0;
const PARTICLE_MIN_LIFETIME: f32 = 7.0;
const PARTICLE_MAX_LIFETIME: f32 = 10.0;
const VELOCITY_DAMPING: f32 = 0.995;
const MIN_LINE_ALPHA: f32 = 0.01;
struct Particle {
pos: Vec2,
vel: Vec2,
born: f32,
lifetime: f32,
size: f32,
color: Rgba,
}
impl Particle {
fn new(origin: Vec2, now: f32) -> Self {
let mut rng = rand::thread_rng();
let angle = rng.gen_range(0.0..TAU);
let speed = rng.gen_range(PARTICLE_MIN_SPEED..PARTICLE_MAX_SPEED);
let vel = vec2(angle.cos() * speed, angle.sin() * speed);
let lifetime = rng.gen_range(PARTICLE_MIN_LIFETIME..PARTICLE_MAX_LIFETIME);
let size = rng.gen_range(PARTICLE_MIN_SIZE..PARTICLE_MAX_SIZE);
let hue = rng.gen_range(0.5..0.72);
let color = hsla(hue, 0.7, 0.5, 1.0).into();
Self {
pos: origin,
vel,
born: now,
lifetime,
size,
color,
}
}
fn age(&self, now: f32) -> f32 {
now - self.born
}
fn life_ratio(&self, now: f32) -> f32 {
1.0 - (self.age(now) / self.lifetime).clamp(0.0, 1.0)
}
fn alive(&self, now: f32) -> bool {
self.age(now) < self.lifetime
}
fn update(&mut self, dt: f32) {
self.pos += self.vel * dt;
self.vel *= VELOCITY_DAMPING;
}
}
struct Model {
particles: Vec<Particle>,
show_title: bool,
is_fullscreen: bool,
grid: HashMap<(i32, i32), Vec<usize>>,
cell_size: f32,
}
impl Model {
fn new() -> Self {
Self {
particles: Vec::new(),
show_title: true,
is_fullscreen: false,
grid: HashMap::new(),
cell_size: CONNECT_DISTANCE,
}
}
fn update_grid(&mut self) {
self.grid.clear();
for (i, particle) in self.particles.iter().enumerate() {
let cell_x = (particle.pos.x / self.cell_size).floor() as i32;
let cell_y = (particle.pos.y / self.cell_size).floor() as i32;
self.grid.entry((cell_x, cell_y)).or_insert_with(Vec::new).push(i);
}
}
fn clear_particles(&mut self) {
self.particles.clear();
self.grid.clear();
self.show_title = true;
}
}
fn main() {
nannou::app(model).update(update).run();
}
fn model(app: &App) -> Model {
app.new_window()
.size(1080, 1080)
.key_pressed(key_pressed)
.mouse_pressed(mouse_pressed)
.view(view)
.title("๋ณ์๋ฆฌ๋ง๋ค๊ธฐ")
.build()
.unwrap();
Model::new()
}
fn key_pressed(app: &App, model: &mut Model, key: Key) {
match key {
Key::F => {
model.is_fullscreen = !model.is_fullscreen;
app.main_window().set_fullscreen(model.is_fullscreen);
}
Key::S => {
let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let filename = format!("constellation_{}.png", ts);
app.main_window().capture_frame(filename);
println!("๐ธ Screenshot saved!");
}
Key::N => {
model.clear_particles();
}
Key::C => {
let now = app.time as f32;
model.particles.retain(|p| p.alive(now));
model.update_grid();
}
_ => (),
}
}
fn mouse_pressed(app: &App, model: &mut Model, _button: MouseButton) {
if model.show_title {
model.show_title = false;
}
let mut rng = rand::thread_rng();
let count = rng.gen_range(15..=30);
let now = app.time as f32;
let origin = app.mouse.position();
for _ in 0..count {
model.particles.push(Particle::new(origin, now));
}
model.update_grid();
}
fn update(app: &App, model: &mut Model, _update: Update) {
let dt = app.duration.since_prev_update.as_secs_f32();
let now = app.time as f32;
for particle in &mut model.particles {
particle.update(dt);
}
model.particles.retain(|p| p.alive(now));
model.update_grid();
}
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(BLACK);
let now = app.time as f32;
let win_rect = app.window_rect();
for p in &model.particles {
let lr = p.life_ratio(now);
let mut col = p.color;
col.alpha *= lr;
draw.ellipse().xy(p.pos).radius(p.size).color(col);
}
let grid = &model.grid;
let connect_distance_sq = CONNECT_DISTANCE * CONNECT_DISTANCE;
for (cell_pos, indices) in grid {
for &i in indices {
let a = &model.particles[i];
if !a.alive(now) { continue; }
for dx in -1..=1 {
for dy in -1..=1 {
let neighbor_cell = (cell_pos.0 + dx, cell_pos.1 + dy);
if let Some(neighbor_indices) = grid.get(&neighbor_cell) {
for &j in neighbor_indices {
if j <= i { continue; }
let b = &model.particles[j];
if !b.alive(now) { continue; }
let d_sq = a.pos.distance_squared(b.pos);
if d_sq <= connect_distance_sq {
let d = d_sq.sqrt();
let dist_alpha = 1.0 - d / CONNECT_DISTANCE;
let alpha = dist_alpha * a.life_ratio(now) * b.life_ratio(now);
if alpha > MIN_LINE_ALPHA {
let col = rgba(
(a.color.red + b.color.red) * 0.5,
(a.color.green + b.color.green) * 0.5,
(a.color.blue + b.color.blue) * 0.5,
alpha
);
draw.line().start(a.pos).end(b.pos).weight(0.8).color(col);
}
}
}
}
}
}
}
}
if model.show_title {
draw.text("CONSTELLATION")
.color(WHITE)
.font_size(120)
.x_y(0.0, 150.0)
.center_justify()
.wh(win_rect.wh());
draw.text("Create your own star constellations")
.color(SILVER)
.font_size(36)
.x_y(0.0, 0.0)
.center_justify()
.wh(win_rect.wh());
draw.text("CLICK TO CREATE STARS โข F: FULLSCREEN โข S: SCREENSHOT โข N: NEW")
.color(GRAY)
.font_size(24)
.x_y(0.0, -150.0)
.center_justify()
.wh(win_rect.wh());
}
if !model.show_title {
draw.text(&format!("Particles: {}", model.particles.len()))
.color(GRAY)
.font_size(18)
.x_y(-500.0, 500.0);
}
draw.to_frame(app, &frame).unwrap();
}
use nannou::prelude::*; // Nannou ๊ทธ๋ํฝ ํ๋ ์์ํฌ์ ๊ธฐ๋ณธ ๊ธฐ๋ฅ๋ค์ ๊ฐ์ ธ์ด
use rand::Rng; // ๋๋ค ์ซ์ ์์ฑ์ ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
use std::time::{SystemTime, UNIX_EPOCH}; // ์์คํ
์๊ฐ ๊ด๋ จ ๊ธฐ๋ฅ
use std::collections::HashMap; // ํด์๋งต ์๋ฃ๊ตฌ์กฐ
////////////////////////////////////////////////////////////////////////////////
// ์์ ์ ์ - ํ๋ก๊ทธ๋จ ์ ๋ฐ์์ ์ฌ์ฉ๋๋ ์ค์ ๊ฐ๋ค
////////////////////////////////////////////////////////////////////////////////
const CONNECT_DISTANCE: f32 = 150.0; // ๋ณ๋ค ์ฌ์ด๋ฅผ ์ฐ๊ฒฐํ๋ ์ต๋ ๊ฑฐ๋ฆฌ
const PARTICLE_MIN_SIZE: f32 = 2.0; // ๋ณ์ ์ต์ ํฌ๊ธฐ
const PARTICLE_MAX_SIZE: f32 = 8.0; // ๋ณ์ ์ต๋ ํฌ๊ธฐ
const PARTICLE_MIN_SPEED: f32 = 10.0; // ๋ณ์ ์ต์ ์ด๋ ์๋
const PARTICLE_MAX_SPEED: f32 = 80.0; // ๋ณ์ ์ต๋ ์ด๋ ์๋
const PARTICLE_MIN_LIFETIME: f32 = 7.0; // ๋ณ์ ์ต์ ์๋ช
(์ด)
const PARTICLE_MAX_LIFETIME: f32 = 10.0; // ๋ณ์ ์ต๋ ์๋ช
(์ด)
const VELOCITY_DAMPING: f32 = 0.995; // ๋งค ํ๋ ์๋ณ ์๋ ๊ฐ์์จ (๋ง์ฐฐ ํจ๊ณผ)
const MIN_LINE_ALPHA: f32 = 0.01; // ์ฐ๊ฒฐ์ ์ ๊ทธ๋ฆด ์ต์ ํฌ๋ช
๋ ๊ฐ
////////////////////////////////////////////////////////////////////////////////
// Particle ๊ตฌ์กฐ์ฒด - ๊ฐ๋ณ ๋ณ์ ๋ํ๋ด๋ ๋ฐ์ดํฐ
////////////////////////////////////////////////////////////////////////////////
struct Particle {
pos: Vec2, // ๋ณ์ ํ์ฌ ์์น (x, y ์ขํ)
vel: Vec2, // ๋ณ์ ์ด๋ ์๋์ ๋ฐฉํฅ
born: f32, // ๋ณ์ด ์์ฑ๋ ์๊ฐ
lifetime: f32, // ๋ณ์ ์ด ์๋ช
size: f32, // ๋ณ์ ํฌ๊ธฐ (๋ฐ์ง๋ฆ)
color: Rgba, // ๋ณ์ ์์ (RGBA ๊ฐ)
}
impl Particle {
// ์๋ก์ด ๋ณ ์์ฑ ํจ์
fn new(origin: Vec2, now: f32) -> Self {
let mut rng = rand::thread_rng(); // ๋๋ค ์ซ์ ์์ฑ๊ธฐ ์ด๊ธฐํ
let angle = rng.gen_range(0.0..TAU); // 0~2ฯ ์ฌ์ด์ ๋๋ค ๊ฐ๋
let speed = rng.gen_range(PARTICLE_MIN_SPEED..PARTICLE_MAX_SPEED); // ๋๋ค ์๋
let vel = vec2(angle.cos() * speed, angle.sin() * speed); // ๊ฐ๋์ ์๋๋ฅผ x,y ์๋๋ก ๋ณํ
let lifetime = rng.gen_range(PARTICLE_MIN_LIFETIME..PARTICLE_MAX_LIFETIME); // ๋๋ค ์๋ช
let size = rng.gen_range(PARTICLE_MIN_SIZE..PARTICLE_MAX_SIZE); // ๋๋ค ํฌ๊ธฐ
let hue = rng.gen_range(0.5..0.72); // ํ๋์ ๊ณ์ด์ ๋๋ค ์์
let color = hsla(hue, 0.7, 0.5, 1.0).into(); // HSL ์์์ RGBA๋ก ๋ณํ
// ์์ฑ๋ ๊ฐ๋ค๋ก ์๋ก์ด Particle ์ธ์คํด์ค ๋ฐํ
Self {
pos: origin, // ์์ ์์น๋ ๋ง์ฐ์ค ํด๋ฆญ ์์น
vel, // ๊ณ์ฐ๋ ์๋ ๋ฒกํฐ
born: now, // ํ์ฌ ์๊ฐ์ ์์ฑ ์๊ฐ์ผ๋ก ์ ์ฅ
lifetime, // ๋๋คํ๊ฒ ๊ฒฐ์ ๋ ์๋ช
size, // ๋๋คํ๊ฒ ๊ฒฐ์ ๋ ํฌ๊ธฐ
color, // ๊ฒฐ์ ๋ ์์
}
}
// ๋ณ์ ํ์ฌ ๋์ด ๊ณ์ฐ (ํ์ฌ ์๊ฐ - ์์ฑ ์๊ฐ)
fn age(&self, now: f32) -> f32 {
now - self.born
}
// ๋ณ์ ์๋ช
๋น์จ ๊ณ์ฐ (1.0 = ์๋ก ์์ฑ, 0.0 = ์๋ช
์ข
๋ฃ)
fn life_ratio(&self, now: f32) -> f32 {
1.0 - (self.age(now) / self.lifetime).clamp(0.0, 1.0) // 0~1 ์ฌ์ด๋ก ์ ํ
}
// ๋ณ์ด ์์ง ์ด์์๋์ง ํ์ธ
fn alive(&self, now: f32) -> bool {
self.age(now) < self.lifetime // ๋์ด๊ฐ ์๋ช
๋ณด๋ค ์์ผ๋ฉด ์ด์์์
}
// ๋ณ์ ์์น์ ์๋ ์
๋ฐ์ดํธ
fn update(&mut self, dt: f32) {
self.pos += self.vel * dt; // ์๋์ ์๊ฐ์ ๊ณฑํด์ ์์น ์ด๋
self.vel *= VELOCITY_DAMPING; // ์๋์ ๊ฐ์ ์จ์ ๊ณฑํด์ ์ ์ ๋๋ ค์ง๊ฒ ํจ
}
}
////////////////////////////////////////////////////////////////////////////////
// Model ๊ตฌ์กฐ์ฒด - ํ๋ก๊ทธ๋จ์ ์ ์ฒด ์ํ๋ฅผ ๊ด๋ฆฌ
////////////////////////////////////////////////////////////////////////////////
struct Model {
particles: Vec<Particle>, // ๋ชจ๋ ๋ณ๋ค์ ์ ์ฅํ๋ ๋ฒกํฐ
show_title: bool, // ์ ๋ชฉ ํ๋ฉด ํ์ ์ฌ๋ถ
is_fullscreen: bool, // ์ ์ฒด ํ๋ฉด ๋ชจ๋ ์ฌ๋ถ
grid: HashMap<(i32, i32), Vec<usize>>, // ๊ณต๊ฐ ๋ถํ ์ ์ํ ๊ทธ๋ฆฌ๋ ์์คํ
cell_size: f32, // ๊ทธ๋ฆฌ๋์ ๊ฐ ์
ํฌ๊ธฐ
}
impl Model {
// ์๋ก์ด ๋ชจ๋ธ ์ธ์คํด์ค ์์ฑ
fn new() -> Self {
Self {
particles: Vec::new(), // ๋น ๋ณ ๋ชฉ๋ก์ผ๋ก ์์
show_title: true, // ์ฒ์์๋ ์ ๋ชฉ ํ๋ฉด ํ์
is_fullscreen: false, // ์ ์ฒด ํ๋ฉด ๋ชจ๋๋ ๊บผ์ง ์ํ๋ก ์์
grid: HashMap::new(), // ๋น ๊ทธ๋ฆฌ๋๋ก ์์
cell_size: CONNECT_DISTANCE, // ์
ํฌ๊ธฐ๋ฅผ ์ฐ๊ฒฐ ๊ฑฐ๋ฆฌ์ ๋์ผํ๊ฒ ์ค์
}
}
// ๊ณต๊ฐ ๊ทธ๋ฆฌ๋ ์
๋ฐ์ดํธ - ์ฑ๋ฅ ์ต์ ํ๋ฅผ ์ํด
fn update_grid(&mut self) {
self.grid.clear(); // ๊ธฐ์กด ๊ทธ๋ฆฌ๋ ๋น์ฐ๊ธฐ
// ๋ชจ๋ ๋ณ๋ค์ ๊ทธ๋ฆฌ๋์ ๋ฐฐ์น
for (i, particle) in self.particles.iter().enumerate() {
// ๋ณ์ ์์น๋ฅผ ์
์ขํ๋ก ๋ณํ
let cell_x = (particle.pos.x / self.cell_size).floor() as i32;
let cell_y = (particle.pos.y / self.cell_size).floor() as i32;
// ํด๋น ์
์ ๋ณ์ ์ธ๋ฑ์ค ์ถ๊ฐ (์์ผ๋ฉด ์ ๋ฒกํฐ ์์ฑ)
self.grid.entry((cell_x, cell_y)).or_insert_with(Vec::new).push(i);
}
}
// ๋ชจ๋ ๋ณ๋ค ์ ๊ฑฐํ๊ณ ์ด๊ธฐ ์ํ๋ก ๋ฆฌ์
fn clear_particles(&mut self) {
self.particles.clear(); // ๋ณ ๋ชฉ๋ก ๋น์ฐ๊ธฐ
self.grid.clear(); // ๊ทธ๋ฆฌ๋ ๋น์ฐ๊ธฐ
self.show_title = true; // ์ ๋ชฉ ํ๋ฉด ๋ค์ ํ์
}
}
////////////////////////////////////////////////////////////////////////////////
// ๋ฉ์ธ ํจ์ ๋ฐ ํ๋ก๊ทธ๋จ ์ด๊ธฐํ
////////////////////////////////////////////////////////////////////////////////
fn main() {
// Nannou ์ฑ ์์: ๋ชจ๋ธ ์์ฑ โ ์
๋ฐ์ดํธ ํจ์ โ ๋ทฐ ํจ์ ์์ผ๋ก ์คํ
nannou::app(model).update(update).run();
}
// ํ๋ก๊ทธ๋จ ๋ชจ๋ธ ์ด๊ธฐํ ํจ์
fn model(app: &App) -> Model {
// ์๋ก์ด ์๋์ฐ ์์ฑ ๋ฐ ์ค์
app<.new_window()
.size(1080, 1080) // ์๋์ฐ ํฌ๊ธฐ 1080x1080 ํฝ์
.key_pressed(key_pressed) // ํค ์
๋ ฅ ์ด๋ฒคํธ ์ฒ๋ฆฌ ํจ์ ์ฐ๊ฒฐ
.mouse_pressed(mouse_pressed) // ๋ง์ฐ์ค ํด๋ฆญ ์ด๋ฒคํธ ์ฒ๋ฆฌ ํจ์ ์ฐ๊ฒฐ
.view(view) // ํ๋ฉด ๊ทธ๋ฆฌ๊ธฐ ํจ์ ์ฐ๊ฒฐ
.title("๋ณ์๋ฆฌ๋ง๋ค๊ธฐ") // ์๋์ฐ ์ ๋ชฉ ์ค์
.build() // ์๋์ฐ ์์ฑ
.unwrap(); // ์์ฑ ์คํจ ์ ํ๋ก๊ทธ๋จ ์ข
๋ฃ
Model::new() // ์๋ก์ด ๋ชจ๋ธ ์ธ์คํด์ค ๋ฐํ
}
////////////////////////////////////////////////////////////////////////////////
// ํค๋ณด๋ ์
๋ ฅ ์ฒ๋ฆฌ ํจ์
////////////////////////////////////////////////////////////////////////////////
fn key_pressed(app: &App, model: &mut Model, key: Key) {
match key {
Key::F => {
// F ํค: ์ ์ฒด ํ๋ฉด ํ ๊ธ
model.is_fullscreen = !model.is_fullscreen;
app.main_window().set_fullscreen(model.is_fullscreen);
}
Key::S => {
// S ํค: ์คํฌ๋ฆฐ์ท ์ ์ฅ
let ts = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
let filename = format!("constellation_{}.png", ts); // ์๊ฐ ๊ธฐ๋ฐ์ผ๋ก ํ์ผ๋ช
์์ฑ
app.main_window().capture_frame(filename); // ํ์ฌ ํ๋ ์ ์บก์ฒ
println!("๐ธ Screenshot saved!"); // ์ฑ๊ณต ๋ฉ์์ง ์ถ๋ ฅ
}
Key::N => {
// N ํค: ์๋ก์ด ๋ณ์๋ฆฌ ์์ (๋ชจ๋ ๋ณ ์ ๊ฑฐ)
model.clear_particles();
}
Key::C => {
// C ํค: ์ฃฝ์ ๋ณ๋ค ์ ๋ฆฌ (๋๋ฒ๊น
์ฉ)
let now = app.time as f32;
model.particles.retain(|p| p.alive(now)); // ์ด์์๋ ๋ณ๋ค๋ง ๋จ๊ธฐ๊ธฐ
model.update_grid(); // ๊ทธ๋ฆฌ๋ ๋ค์ ๊ณ์ฐ
}
_ => (), // ๋ค๋ฅธ ํค๋ ๋ฌด์
}
}
////////////////////////////////////////////////////////////////////////////////
// ๋ง์ฐ์ค ์
๋ ฅ ์ฒ๋ฆฌ ํจ์
////////////////////////////////////////////////////////////////////////////////
fn mouse_pressed(app: &App, model: &mut Model, _button: MouseButton) {
// ๋ง์ฐ์ค ํด๋ฆญ ์ ์ ๋ชฉ ํ๋ฉด ์จ๊ธฐ๊ธฐ
if model.show_title {
model.show_title = false;
}
let mut rng = rand::thread_rng(); // ๋๋ค ์ซ์ ์์ฑ๊ธฐ
let count = rng.gen_range(15..=30); // ์์ฑํ ๋ณ ๊ฐ์ (15~30๊ฐ)
let now = app.time as f32; // ํ์ฌ ์๊ฐ
let origin = app.mouse.position(); // ๋ง์ฐ์ค ํด๋ฆญ ์์น
// ์ง์ ๋ ๊ฐ์๋งํผ ๋ณ ์์ฑ
for _ in 0..count {
model.particles.push(Particle::new(origin, now)); // ์ ๋ณ์ ๋ชฉ๋ก์ ์ถ๊ฐ
}
model.update_grid(); // ๊ทธ๋ฆฌ๋ ์
๋ฐ์ดํธ (์๋ก์ด ๋ณ๋ค์ ๋ฐ์)
}
////////////////////////////////////////////////////////////////////////////////
// ๊ฒ์ ์ํ ์
๋ฐ์ดํธ ํจ์ (๋งค ํ๋ ์ ํธ์ถ๋จ)
////////////////////////////////////////////////////////////////////////////////
fn update(app: &App, model: &mut Model, _update: Update) {
let dt = app.duration.since_prev_update.as_secs_f32(); // ์ด์ ํ๋ ์๋ถํฐ์ ์๊ฐ ๊ฐ๊ฒฉ
let now = app.time as f32; // ํ์ฌ ์๊ฐ
// ๋ชจ๋ ๋ณ๋ค ์
๋ฐ์ดํธ
for particle in &mut model.particles {
particle.update(dt); // ๊ฐ ๋ณ์ ์์น์ ์๋ ์
๋ฐ์ดํธ
}
// ์๋ช
์ด ๋๋ ๋ณ๋ค ์ ๊ฑฐ
model.particles.retain(|p| p.alive(now));
model.update_grid(); // ๋ณ๊ฒฝ๋ ์์น๋ฅผ ๋ฐ์ํ์ฌ ๊ทธ๋ฆฌ๋ ์
๋ฐ์ดํธ
}
////////////////////////////////////////////////////////////////////////////////
// ํ๋ฉด ๊ทธ๋ฆฌ๊ธฐ ํจ์ (๋งค ํ๋ ์ ํธ์ถ๋จ)
////////////////////////////////////////////////////////////////////////////////
fn view(app: &App, model: &Model, frame: Frame) {
let draw = app.draw(); // ๊ทธ๋ฆผ ๊ทธ๋ฆฌ๊ธฐ ๊ฐ์ฒด ์์ฑ
draw.background().color(BLACK); // ๋ฐฐ๊ฒฝ์ ๊ฒ์์์ผ๋ก ์ง์ฐ๊ธฐ
let now = app.time as f32; // ํ์ฌ ์๊ฐ
let win_rect = app.window_rect(); // ์๋์ฐ ํฌ๊ธฐ ์ ๋ณด
////////////////////////////////////////////////////////////////////////////
// 1. ๋ชจ๋ ๋ณ๋ค ๊ทธ๋ฆฌ๊ธฐ
////////////////////////////////////////////////////////////////////////////
for p in &model.particles {
let lr = p.life_ratio(now); // ๋ณ์ ํ์ฌ ์๋ช
๋น์จ ๊ณ์ฐ (1.0~0.0)
let mut col = p.color; // ๋ณ์ ๊ธฐ๋ณธ ์์ ๋ณต์ฌ
col.alpha *= lr; // ์๋ช
์ ๋ฐ๋ผ ํฌ๋ช
๋ ์กฐ์ (์ ์ ์ฌ๋ผ์ง)
draw.ellipse() // ์ํ์ผ๋ก ๋ณ ๊ทธ๋ฆฌ๊ธฐ
.xy(p.pos) // ๋ณ์ ์์น์
.radius(p.size) // ๋ณ์ ํฌ๊ธฐ๋ก
.color(col); // ์กฐ์ ๋ ์์์ผ๋ก
}
////////////////////////////////////////////////////////////////////////////
// 2. ๋ณ๋ค ์ฌ์ด ์ฐ๊ฒฐ์ ๊ทธ๋ฆฌ๊ธฐ (์ฑ๋ฅ ์ต์ ํ๋ ๋ฒ์ )
////////////////////////////////////////////////////////////////////////////
let grid = &model.grid; // ๊ณต๊ฐ ๊ทธ๋ฆฌ๋ ์ฐธ์กฐ
let connect_distance_sq = CONNECT_DISTANCE.powi(2); // ์ ๊ณฑ ๊ฑฐ๋ฆฌ (์ฑ๋ฅ์ ์ํด)
// ๊ฐ ๊ทธ๋ฆฌ๋ ์
์ ์ํ
for (cell_pos, indices) in grid {
// ํ์ฌ ์
์ ๊ฐ ๋ณ์ ๋ํด
for &i in indices {
let a = &model.particles[i]; // ์ฒซ ๋ฒ์งธ ๋ณ
if !a.alive(now) { continue; } // ์ฃฝ์ ๋ณ์ ๊ฑด๋๋ฐ๊ธฐ
// ์ฃผ๋ณ 3x3 ์
๋ค ๊ฒ์ฌ (์ธ์ ํ ์
๋ค๋ง)
for dx in -1..=1 {
for dy in -1..=1 {
let neighbor_cell = (cell_pos.0 + dx, cell_pos.1 + dy); // ์ด์ ์
์ขํ
// ์ด์ ์
์ด ์กด์ฌํ๋์ง ํ์ธ
if let Some(neighbor_indices) = grid.get(&neighbor_cell) {
// ์ด์ ์
์ ๊ฐ ๋ณ์ ๋ํด
for &j in neighbor_indices {
if j <= i { continue; } // ์ค๋ณต ๊ฒ์ฌ ๋ฐฉ์ง (i < j ์กฐ๊ฑด)
let b = &model.particles[j]; // ๋ ๋ฒ์งธ ๋ณ
if !b.alive(now) { continue; } // ์ฃฝ์ ๋ณ์ ๊ฑด๋๋ฐ๊ธฐ
// ๋ ๋ณ ์ฌ์ด์ ์ ๊ณฑ ๊ฑฐ๋ฆฌ ๊ณ์ฐ (์ฑ๋ฅ์ ์ํด sqrt ํํผ)
let d_sq = a.pos.distance_squared(b.pos);
// ์ฐ๊ฒฐ ๊ฐ๋ฅํ ๊ฑฐ๋ฆฌ์ธ์ง ํ์ธ
if d_sq <= connect_distance_sq {
let d = d_sq.sqrt(); // ์ค์ ๊ฑฐ๋ฆฌ ๊ณ์ฐ
let dist_alpha = 1.0 - d / CONNECT_DISTANCE; // ๊ฑฐ๋ฆฌ์ ๋ฐ๋ฅธ ํฌ๋ช
๋
let alpha = dist_alpha * a.life_ratio(now) * b.life_ratio(now); // ์ต์ข
ํฌ๋ช
๋
// ์ถฉ๋ถํ ๋ณด์ด๋ ์ ๋ง ๊ทธ๋ฆฌ๊ธฐ
if alpha > MIN_LINE_ALPHA {
// ๋ ๋ณ ์์์ ํ๊ท ๊ฐ์ผ๋ก ์ ์์ ๊ฒฐ์
let col = rgba(
(a.color.red + b.color.red) * 0.5,
(a.color.green + b.color.green) * 0.5,
(a.color.blue + b.color.blue) * 0.5,
alpha // ๊ณ์ฐ๋ ํฌ๋ช
๋ ์ ์ฉ
);
// ์ฐ๊ฒฐ์ ๊ทธ๋ฆฌ๊ธฐ
draw.line()
.start(a.pos) // ์ฒซ ๋ฒ์งธ ๋ณ์์
.end(b.pos) // ๋ ๋ฒ์งธ ๋ณ๊น์ง
.weight(0.8) // ์ ๋๊ป
.color(col); // ๊ฒฐ์ ๋ ์์์ผ๋ก
}
}
}
}
}
}
}
}
////////////////////////////////////////////////////////////////////////////
// 3. ์ ๋ชฉ ํ๋ฉด ๊ทธ๋ฆฌ๊ธฐ
////////////////////////////////////////////////////////////////////////////
if model.show_title {
// ๋ฉ์ธ ์ ๋ชฉ - ์ค์ ์๋จ
draw.text("CONSTELLATION")
.color(WHITE) // ํฐ์ ํ
์คํธ
.font_size(120) // ํฐ ๊ธ์จ ํฌ๊ธฐ
.x_y(0.0, 150.0) // ํ๋ฉด ์ค์ ์๋จ ์์น
.center_justify() // ๊ฐ์ด๋ฐ ์ ๋ ฌ
.wh(win_rect.wh()); // ์ฐฝ ์ ์ฒด ๋๋น ์ฌ์ฉ (์ค๋ฐ๊ฟ ๋ฐฉ์ง)
// ๋ถ์ ๋ชฉ - ํ๋ฉด ์ค์
draw.text("Create your own star constellations")
.color(SILVER) // ์์ ํ
์คํธ
.font_size(36) // ์ค๊ฐ ๊ธ์จ ํฌ๊ธฐ
.x_y(0.0, 0.0) // ํ๋ฉด ์ ์ค์ ์์น
.center_justify() // ๊ฐ์ด๋ฐ ์ ๋ ฌ
.wh(win_rect.wh()); // ์ฐฝ ์ ์ฒด ๋๋น ์ฌ์ฉ
// ์กฐ์๋ฒ ์๋ด - ์ค์ ํ๋จ
draw.text("CLICK TO CREATE STARS โข F: FULLSCREEN โข S: SCREENSHOT โข N: NEW")
.color(GRAY) // ํ์ ํ
์คํธ
.font_size(24) // ์์ ๊ธ์จ ํฌ๊ธฐ
.x_y(0.0, -150.0) // ํ๋ฉด ์ค์ ํ๋จ ์์น
.center_justify() // ๊ฐ์ด๋ฐ ์ ๋ ฌ
.wh(win_rect.wh()); // ์ฐฝ ์ ์ฒด ๋๋น ์ฌ์ฉ (ํ ์ค๋ก ํ์)
}
////////////////////////////////////////////////////////////////////////////
// 4. ๋๋ฒ๊น
์ ๋ณด ํ์
////////////////////////////////////////////////////////////////////////////
if !model.show_title {
// ํ์ฌ ๋ณ ๊ฐ์ ํ์ (ํ๋ฉด ์ข์ธก ์๋จ)
draw.text(&format!("Particles: {}", model.particles.len()))
.color(GRAY) // ํ์ ํ
์คํธ
.font_size(18) // ๋งค์ฐ ์์ ๊ธ์จ
.x_y(-500.0, 500.0); // ์ข์ธก ์๋จ ์ฝ๋ ์์น
}
// ๊ทธ๋ฆฌ๊ธฐ ๋ช
๋ น๋ค์ ์ค์ ํ๋ ์์ ์ ์ฉ
draw.to_frame(app, &frame).unwrap();
}