

use nannou::prelude::*;
const SCREEN_SIZE: u32 = 800;
const GRID_COUNT: usize = 20;
fn main() {
nannou::app(model).update(update).simple_window(view).size(SCREEN_SIZE, SCREEN_SIZE).run();
}
struct Model;
fn model(_app: &App) -> Model {
Model
}
fn update(_app: &App, _model: &mut Model, _update: Update) {
// ์
๋ฐ์ดํธ ๋ก์ง์ด ํ์ํ๋ฉด ์ฌ๊ธฐ์ ์ถ๊ฐ
}
fn ease_in_out(t: f32) -> f32 {
3.0 * t.powi(2) - 2.0 * t.powi(3)
}
fn view(app: &App, _model: &Model, frame: Frame) {
let draw = app.draw();
draw.background().color(rgba(0.05, 0.05, 0.08, 1.0));
let color = rgb(0.1, 0.8, 0.7);
// ๊ทธ๋ฆฌ๋ ์
ํฌ๊ธฐ์ ์ต๋ ๋ฐ์ง๋ฆ ๊ณ์ฐ
let grid_cell_size = (SCREEN_SIZE as f32) / (GRID_COUNT as f32);
let max_radius = grid_cell_size * 0.5;
let delay_per_circle = 10.0 / 60.0;
for row in 0..GRID_COUNT {
for col in 0..GRID_COUNT {
// ๊ฐ ์์ ์์น๋ฅผ ๊ทธ๋ฆฌ๋ ์ค์์ ๋ฐฐ์น
let x = ((col as f32) + 0.5) * grid_cell_size - (SCREEN_SIZE as f32) * 0.5;
let y = ((row as f32) + 0.5) * grid_cell_size - (SCREEN_SIZE as f32) * 0.5;
// ๊ฐ ์๋ง๋ค ๊ณ ์ ํ ์ธ๋ฑ์ค๋ก ์ง์ฐ์๊ฐ ๊ณ์ฐ
let circle_index = row * GRID_COUNT + col;
let t = app.time - (circle_index as f32) * delay_per_circle;
let cycle = t.rem_euclid(4.0) / 4.0;
let raw = if cycle < 0.5 { cycle * 2.0 } else { (1.0 - cycle) * 2.0 };
let eased = ease_in_out(raw);
let radius = max_radius * eased;
draw.ellipse().x_y(x, y).radius(radius).color(color);
}
}
draw.to_frame(app, &frame).unwrap();
}
// `nannou` ํฌ๋ ์ดํธ์ `prelude` ๋ชจ๋์์ ์์ฃผ ์ฌ์ฉ๋๋ ํ์
๊ณผ ํจ์๋ค์ ๊ฐ์ ธ์ต๋๋ค.
// ์ด๋ ๋ฒกํฐ, ์์, ์ฑ ์ค์ ๋ฑ ํต์ฌ ๊ธฐ๋ฅ์ ํฌํจํฉ๋๋ค.
use nannou::prelude::*;
// ํ๋ฉด์ ํฌ๊ธฐ๋ฅผ ํฝ์
๋จ์๋ก ์ ์ํฉ๋๋ค. ์ ์ฌ๊ฐํ ํ๋ฉด ๊ธฐ์ค.
const SCREEN_SIZE: u32 = 800;
// ๊ทธ๋ฆฌ๋(๊ฒฉ์)์ ํ/์ด ๊ฐ์. ์ด GRID_COUNT ร GRID_COUNT ๊ฐ์ ์์ด ๊ทธ๋ ค์ง๋๋ค.
const GRID_COUNT: usize = 20;
// ๊ฐ ์์ด ์ ๋๋ฉ์ด์
์์ ์์ ์์ ์ผ๋ง๋ ์ง์ฐ๋ ์ง๋ฅผ ์ ์ํฉ๋๋ค.
// 10ํ๋ ์(60fps ๊ธฐ์ค) ๋งํผ ์ง์ฐ๋๋ฏ๋ก, ์๋ค์ด ์์ฐจ์ ์ผ๋ก ์์ง์
๋๋ค.
const DELAY_PER_CIRCLE: f32 = 10.0 / 60.0;
// ๋ง์ฐ์ค ์ํฅ ๋ฒ์: ๋ง์ฐ์ค ์์น์์ ์ด ๊ฑฐ๋ฆฌ ๋ด์ ์๋ ์๋ง ์ํฅ์ ๋ฐ์ต๋๋ค.
const MOUSE_INFLUENCE_RANGE: f32 = 300.0;
// ๋ง์ฐ์ค ๊ทผ์ฒ์ ์์ ๋ ์์ ํฌ๊ธฐ๊ฐ ์ผ๋ง๋ ์ปค์ง์ง๋ฅผ ๊ฒฐ์ ํ๋ ๋ฐฐ์จ.
// 0.5์ด๋ฉด ์ต๋ 50% ๋ ์ปค์ง๋๋ค.
const MOUSE_INFLUENCE_SCALE: f32 = 0.5;
// ๋ง์ฐ์ค ํด๋ฆญ ์ ์๊ฐ ์คํ์
(time_offset)์ ๋ํด์ง๋ ๊ฐ.
// ์ด ๊ฐ์ด ํด์๋ก ์ ๋๋ฉ์ด์
์ ์ ์ฒด ํ์ด๋ฐ์ด ๋นจ๋ผ์ง๋๋ค.
const TIME_OFFSET_INCREMENT: f32 = 0.05;
// ๋ง์ฐ์ค๋ฅผ ๋ผ๋ฉด ์๊ฐ ์คํ์
์ด ์์ํ ๊ฐ์ํ๋๋ก ๋ง๋๋ ๊ฐ์ ๋น์จ.
// 0.95๋ ๋งค ํ๋ ์๋ง๋ค 5%์ฉ ์ค์ด๋ ๋ค๋ ์๋ฏธ์
๋๋ค.
const TIME_OFFSET_DECAY: f32 = 0.95;
// ํ๋ก๊ทธ๋จ ์ง์
์ : nannou ์ฑ์ ์ด๊ธฐํํ๊ณ ์คํํฉ๋๋ค.
// `model` ํจ์๋ก ์ด๊ธฐ ์ํ๋ฅผ ๋ง๋ค๊ณ , `update`๋ก ๋งค ํ๋ ์ ์ํ๋ฅผ ๊ฐฑ์ ํ๋ฉฐ, `run()`์ผ๋ก ์คํํฉ๋๋ค.
fn main() {
nannou::app(model).update(update).run();
}
// ์ฑ์ ์ํ๋ฅผ ์ ์ฅํ๋ ๊ตฌ์กฐ์ฒด.
// ๋ง์ฐ์ค ์์น, ๋ง์ฐ์ค ํด๋ฆญ ์ฌ๋ถ, ์๊ฐ ์คํ์
์ ๊ด๋ฆฌํฉ๋๋ค.
struct Model {
mouse_pos: Vec2, // ํ์ฌ ๋ง์ฐ์ค ์ขํ (ํ๋ฉด ์ค์ ๊ธฐ์ค)
mouse_pressed: bool, // ๋ง์ฐ์ค ์ผ์ชฝ ๋ฒํผ์ด ๋๋ ค ์๋์ง ์ฌ๋ถ
time_offset: f32, // ์ ๋๋ฉ์ด์
ํ์ด๋ฐ์ ์กฐ์ ํ๋ ์คํ์
๊ฐ
}
// Model ๊ตฌ์กฐ์ฒด์ ๋ํ ๋ฉ์๋ ๊ตฌํ
impl Model {
// ์ด๊ธฐ ์ํ๋ฅผ ์์ฑํ๋ ํจ์
fn new() -> Self {
Self {
mouse_pos: Vec2::ZERO, // ์ด๊ธฐ ๋ง์ฐ์ค ์์น๋ (0, 0) โ ํ๋ฉด ์ค์
mouse_pressed: false, // ์ฒ์์ ๋ง์ฐ์ค ์ ๋๋ฆผ
time_offset: 0.0, // ์๊ฐ ์คํ์
๋ 0๋ถํฐ ์์
}
}
// ๋งค ํ๋ ์๋ง๋ค ๋ง์ฐ์ค ์ํ๋ฅผ ์
๋ฐ์ดํธํ๋ ํจ์
fn update_mouse_state(&mut self, app: &App) {
// ํ์ฌ ๋ง์ฐ์ค ์์น๋ฅผ ๊ฐ์ ธ์ด (ํ๋ฉด ์ค์์ด (0, 0)์ธ ์ขํ๊ณ)
self.mouse_pos = app.mouse.position();
// ๋ง์ฐ์ค ์ผ์ชฝ ๋ฒํผ์ด ๋๋ ค ์๋์ง ํ์ธ
// `is_down()`์ nannou์ MouseButtonState ๋ฉ์๋๋ก,
// ํด๋น ๋ฒํผ์ด ํ์ฌ ๋๋ ค ์๋์ง ์ฌ๋ถ๋ฅผ bool๋ก ๋ฐํํฉ๋๋ค.
if app.mouse.buttons.left().is_down() {
self.mouse_pressed = true;
// ๋ง์ฐ์ค๋ฅผ ๋๋ฅด๋ฉด time_offset ์ฆ๊ฐ โ ์ ๋๋ฉ์ด์
๊ฐ์
self.time_offset += TIME_OFFSET_INCREMENT;
} else {
self.mouse_pressed = false;
// ๋ง์ฐ์ค๋ฅผ ๋ผ๋ฉด time_offset์ด ์์ํ ๊ฐ์ (๊ฐ์ ํจ๊ณผ)
self.time_offset *= TIME_OFFSET_DECAY;
}
}
// ๋ง์ฐ์ค X ์ขํ์ ๋ฐ๋ผ ์์์ ๊ณ์ฐํ๋ ํจ์
fn calculate_color(&self) -> Hsl {
// `map_range`: ๊ฐ ํ๋๋ฅผ ํน์ ๋ฒ์์์ ๋ค๋ฅธ ๋ฒ์๋ก ์ ํ ๋ณํํฉ๋๋ค.
// ์ฌ๊ธฐ์๋ ๋ง์ฐ์ค X์ขํ(-400 ~ 400)๋ฅผ ์์ ํด(hue) ๋ฒ์(0.0 ~ 1.0)๋ก ๋งคํ.
let hue = map_range(self.mouse_pos.x, -400.0, 400.0, 0.0, 1.0);
// `fract()`: ์ค์์ ์์ ๋ถ๋ถ๋ง ๋ฐํํฉ๋๋ค. (์: 1.7 โ 0.7)
// hue๊ฐ 0~1 ๋ฒ์๋ฅผ ๋ฒ์ด๋ ์ ์์ผ๋ฏ๋ก, ์ด๋ฅผ ์ํ์ํค๊ธฐ ์ํด ์ฌ์ฉ.
// ์ด๋ ์์์ด ๋นจ๊ฐ โ ์ด๋ก โ ํ๋ โ ๋นจ๊ฐ... ์ํ๋๋๋ก ๋ง๋ญ๋๋ค.
hsl(hue.fract(), 0.8, 0.6) // ์ฑ๋ 80%, ๋ช
๋ 60% ๊ณ ์
}
// ๋ง์ฐ์ค์ ์ ์ฌ์ด ๊ฑฐ๋ฆฌ์ ๋ฐ๋ผ ์์ ๋ฐ์ง๋ฆ์ ์กฐ์ ํ๋ ํจ์
fn calculate_circle_radius(&self, position: Vec2, base_radius: f32) -> f32 {
// `distance()`: ๋ Vec2 ์ฌ์ด์ ์ ํด๋ฆฌ๋ ๊ฑฐ๋ฆฌ๋ฅผ ๊ณ์ฐํฉ๋๋ค.
let dist = self.mouse_pos.distance(position);
// ๊ฑฐ๋ฆฌ์ ๋ฐ๋ผ ์ํฅ๋(influence) ๊ณ์ฐ: ๊ฑฐ๋ฆฌ๊ฐ ๋ฉ์๋ก 0์ ๊ฐ๊น์์ง
// `max(0.0)`์ ์์๊ฐ ๋์ง ์๋๋ก ๋ณด์ฅ (๊ฑฐ๋ฆฌ๊ฐ ๋ฒ์ ๋ฐ์ด๋ฉด ์ํฅ ์์)
let influence = (1.0 - (dist / MOUSE_INFLUENCE_RANGE)).max(0.0);
// ๊ธฐ๋ณธ ๋ฐ์ง๋ฆ์ ์ํฅ๋๋ฅผ ๊ณฑํด ํ์ฅ๋ ๋ฐ์ง๋ฆ ๋ฐํ
base_radius * (1.0 + influence * MOUSE_INFLUENCE_SCALE)
}
}
// ๊ทธ๋ฆฌ๋์ ๋ฐฐ์น๋ ๊ฐ ์์ ๋ํ๋ด๋ ๊ตฌ์กฐ์ฒด
struct GridCircle {
position: Vec2, // ์์ ์ค์ฌ ์ขํ
index: usize, // ๊ทธ๋ฆฌ๋ ๋ด ๊ณ ์ ์ธ๋ฑ์ค (0๋ถํฐ ์์)
}
impl GridCircle {
// ์ GridCircle ์ธ์คํด์ค๋ฅผ ์์ฑํ๋ ํจ์
fn new(row: usize, col: usize, grid_cell_size: f32, screen_size: f32) -> Self {
// ๊ฐ ์
์ ์ค์ฌ์ ์์ ๋ฐฐ์น: (col + 0.5) * ์
ํฌ๊ธฐ
// ํ๋ฉด ์ค์์ด (0,0)์ด ๋๋๋ก screen_size * 0.5๋ฅผ ๋บ
let x = ((col as f32) + 0.5) * grid_cell_size - screen_size * 0.5;
let y = ((row as f32) + 0.5) * grid_cell_size - screen_size * 0.5;
// 1์ฐจ์ ์ธ๋ฑ์ค ๊ณ์ฐ: row * GRID_COUNT + col
let index = row * GRID_COUNT + col;
Self {
position: Vec2::new(x, y),
index,
}
}
// ์๊ฐ์ ๋ฐ๋ผ ๋ถ๋๋ฝ๊ฒ ํ์ฅ-์์ถํ๋ ๋ฐ์ง๋ฆ์ ๊ณ์ฐํ๋ ํจ์
fn calculate_animated_radius(&self, time: f32, time_offset: f32, max_radius: f32) -> f32 {
// ๊ฐ ์๋ง๋ค ์ง์ฐ์ ์ฃผ๊ธฐ ์ํด ์ธ๋ฑ์ค์ ๋ฐ๋ผ ์๊ฐ์ ์คํ์
let t = time - (self.index as f32) * DELAY_PER_CIRCLE + time_offset;
// `rem_euclid()`: ์ ํด๋ฆฌ๋ ๋๋จธ์ง ์ฐ์ฐ. ์์๋ ์์ ๊ฒฐ๊ณผ๋ฅผ ๋ณด์ฅ.
// ์: -0.5.rem_euclid(4.0) โ 3.5
// ์ด๋ฅผ ํตํด t๋ฅผ 0~4 ์ฌ์ด์ ์ฃผ๊ธฐ์ ๊ฐ์ผ๋ก ๋ณํ โ 4์ด ์ฃผ๊ธฐ ์ ๋๋ฉ์ด์
let cycle = t.rem_euclid(4.0) / 4.0; // 0.0 ~ 1.0 ์ฌ์ด๋ก ์ ๊ทํ
// ์ผ๊ฐํ(โฒ ๋ชจ์) ํํ์ raw ์ ํธ ์์ฑ:
// 0~0.5: ์ฆ๊ฐ (0 โ 1), 0.5~1.0: ๊ฐ์ (1 โ 0)
let raw = if cycle < 0.5 {
cycle * 2.0
} else {
(1.0 - cycle) * 2.0
};
// `ease_in_out`: ๋ถ๋๋ฌ์ด ์
/์ถ๋ ฅ ์ด์ง ํจ์ (์๋ ์ ์๋จ)
let eased = ease_in_out(raw);
// ์ต๋ ๋ฐ์ง๋ฆ์ ์ด์ง ์ ์ฉ
max_radius * eased
}
}
// ์ฑ ์์ ์ ํธ์ถ๋์ด ์ด๊ธฐ ๋ชจ๋ธ๊ณผ ์ฐฝ์ ์ค์ ํ๋ ํจ์
fn model(app: &App) -> Model {
// ์๋ก์ด ์ฐฝ ์์ฑ: ํฌ๊ธฐ, ๋ทฐ ํจ์ ์ง์
app.new_window()
.size(SCREEN_SIZE, SCREEN_SIZE) // ์ฐฝ ํฌ๊ธฐ ์ค์
.view(view) // ๋ ๋๋งํ ํจ์ ์ง์
.build() // ์ฐฝ ๋น๋
.unwrap(); // ์ค๋ฅ ๋ฐ์ ์ ํจ๋ (๊ฐ๋จํ ์์ ์ฉ)
// ์ด๊ธฐ ๋ชจ๋ธ ๋ฐํ
Model::new()
}
// ๋งค ํ๋ ์๋ง๋ค ํธ์ถ๋์ด ๋ชจ๋ธ ์ํ๋ฅผ ์
๋ฐ์ดํธํ๋ ํจ์
fn update(app: &App, model: &mut Model, _update: Update) {
// ๋ง์ฐ์ค ์ํ ๊ฐฑ์
model.update_mouse_state(app);
}
// ๋ถ๋๋ฌ์ด ์
/์ถ๋ ฅ ์ด์ง(easing) ํจ์: t โ [0,1] โ [0,1]
// ์์๊ณผ ๋์์ ๊ฐ์/๊ฐ์ ํจ๊ณผ๋ฅผ ์ค (์์ฐ์ค๋ฌ์ด ์์ง์)
fn ease_in_out(t: f32) -> f32 {
// 3tยฒ - 2tยณ: ํ๋ฐ(Hermite) ๋ณด๊ฐ ๊ธฐ๋ฐ์ ๋ถ๋๋ฌ์ด ์ด์ง
3.0 * t.powi(2) - 2.0 * t.powi(3)
}
// ๋งค ํ๋ ์๋ง๋ค ํ๋ฉด์ ๊ทธ๋ฆฌ๋ ํจ์
fn view(app: &App, model: &Model, frame: Frame) {
// ๊ทธ๋ฆฌ๊ธฐ ๊ฐ์ฒด ์์ฑ
let draw = app.draw();
// ๋ฐฐ๊ฒฝ์ ์ค์ : ์งํ ๋จ์ ๊ณ์ด
draw.background().color(rgba(0.05, 0.05, 0.08, 1.0));
// ํ์ฌ ๋ง์ฐ์ค ์์น์ ๋ฐ๋ผ ์์ ๊ณ์ฐ
let color = model.calculate_color();
// ๊ทธ๋ฆฌ๋ ์
ํฌ๊ธฐ ๊ณ์ฐ (ํ๋ฉด ํฌ๊ธฐ / ๊ทธ๋ฆฌ๋ ์)
let grid_cell_size = (SCREEN_SIZE as f32) / (GRID_COUNT as f32);
// ์์ ์ต๋ ๋ฐ์ง๋ฆ: ์
ํฌ๊ธฐ์ ์ ๋ฐ (์
์์ ๊ฝ ์ฐจ๊ฒ)
let max_radius = grid_cell_size * 0.5;
// ๊ทธ๋ฆฌ๋ ์ ์ฒด ์ํ
for row in 0..GRID_COUNT {
for col in 0..GRID_COUNT {
// ํ์ฌ ์
์ ๋ํ GridCircle ์์ฑ
let circle = GridCircle::new(row, col, grid_cell_size, SCREEN_SIZE as f32);
// ์๊ฐ ๊ธฐ๋ฐ ์ ๋๋ฉ์ด์
๋ฐ์ง๋ฆ ๊ณ์ฐ
let base_radius = circle.calculate_animated_radius(
app.time, // ์ ์ฒด ๊ฒฝ๊ณผ ์๊ฐ (์ด ๋จ์)
model.time_offset, // ๋ง์ฐ์ค ํด๋ฆญ์ ๋ฐ๋ฅธ ์๊ฐ ์คํ์
max_radius,
);
// ๋ง์ฐ์ค ์์น์ ๋ฐ๋ผ ๋ฐ์ง๋ฆ ์กฐ์
let adjusted_radius = model.calculate_circle_radius(circle.position, base_radius);
// ์ ๊ทธ๋ฆฌ๊ธฐ
draw.ellipse()
.xy(circle.position) // ์ค์ฌ ์ขํ
.radius(adjusted_radius) // ์กฐ์ ๋ ๋ฐ์ง๋ฆ
.color(color); // ๋์ ์์
}
}
// ๊ทธ๋ฆฐ ๋ด์ฉ์ ํ๋ ์์ ์ถ๋ ฅ
draw.to_frame(app, &frame).unwrap();
}
f32 ๋๋ f64 ํ์
์ ๋ฉ์๋3.7.fract() โ 0.7, -1.3.fract() โ 0.7 (Rust๋ ์์์์๋ ์์ ์์ ๋ฐํ)Vec2 (2D ๋ฒกํฐ)์ ๋ฉ์๋โ((x2-x1)ยฒ + (y2-y1)ยฒ)f32 ๋๋ f64์ ๋ฉ์๋%์ ์ฐจ์ด: -1.5 % 4.0 = -1.5์ง๋ง, -1.5.rem_euclid(4.0) = 2.5MouseButtonState (์: app.mouse.buttons.left())bool๋ก ๋ฐํ.map_range(value, in_min, in_max, out_min, out_max)