
// ==========================================
// [์ค์ ์์ญ] ์ ์ญ ์์ ๋ฐ ๋ณ์
// ==========================================
const PARTICLE_COUNT = 8000; // ํํฐํด ๊ฐ์ (๋ง์์๋ก ๋ฐ๋ ๋์ ๊ตฌ์ฒด)
const ATTRACTION = 0.01; // ์๋ ์์น๋ก ๋์ด๋น๊ธฐ๋ ํ (0~1, ํด์๋ก ๋น ๋ฅด๊ฒ ๋ณต๊ท)
const DAMPING = 0.9; // ๋ง์ฐฐ๋ ฅ/๊ฐ์ ๊ณ์ (0~1, 1์ ๊ฐ๊น์ธ์๋ก ๋ถ๋๋ฝ๊ฒ ๋ฏธ๋๋ฌ์ง)
const REPEL_STRENGTH = 28; // ๋ง์ฐ์ค ๋ฐ๋ฐ๋ ฅ ๊ฐ๋ (ํฝ์
๋จ์ ๊ฐ์๋)
const CANVAS_WIDTH = 1200; // ์บ๋ฒ์ค ๋๋น
const CANVAS_HEIGHT = 900; // ์บ๋ฒ์ค ๋์ด
const SPHERE_RADIUS = 350; // ๊ฐ์ ๊ตฌ์ฒด์ ๋ฐ์ง๋ฆ (์ค์ ๋ก๋ 2D ํ์)
const REPEL_RADIUS = 120; // ๋ง์ฐ์ค ์ฃผ๋ณ ์ด ๊ฑฐ๋ฆฌ ์์ ์๋ ํํฐํด๋ง ๋ฐ๋ฐ
let angle = 0; // ์๊ฐ์ ๋ฐ๋ผ ์ฆ๊ฐํ๋ ํ์ ๊ฐ๋ (๋ผ๋์)
let points = []; // ํํฐํด ๊ฐ์ฒด ๋ฐฐ์ด {index, pos, vel}
// ==========================================
// [p5.js ๋ผ์ดํ์ฌ์ดํด]
// ==========================================
function setup() {
// ์บ๋ฒ์ค ์์ฑ
createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
// pixelDensity(1): ๊ณ ํด์๋ ๋์คํ๋ ์ด์์๋ 1:1 ํฝ์
๋งคํ (์ฑ๋ฅ ์ต์ ํ)
pixelDensity(1);
// ํํฐํด ๋ ๋๋ง ์คํ์ผ ์ค์
stroke(255); // ํฐ์ ์
strokeWeight(2); // ์ ํฌ๊ธฐ 2px
// ํํฐํด ๋ฐฐ์ด ์ด๊ธฐํ (๋น ๊ฐ์ฒด๋ค ์์ฑ)
initializeParticles();
// ์ด๊ธฐ ํ์ ๊ฐ๋๋ฅผ 0์ผ๋ก ์ค์
angle = 0;
// ๊ฐ ํํฐํด์ ์ด๊ธฐ "ํ" ์์น๋ฅผ ๊ตฌ์ฒด ํ๋ฉด์ ๋ฐฐ์น
updateParticleTargets();
// ๋ชจ๋ ํํฐํด์ ์๋๋ฅผ 0์ผ๋ก ์ด๊ธฐํ (์ ์ง ์ํ์์ ์์)
for (let p of points) {
p.vel.set(0, 0);
}
}
function draw() {
// ๋งค ํ๋ ์๋ง๋ค ๊ฒ์ ๋ฐฐ๊ฒฝ์ผ๋ก ํ๋ฉด ์ง์ฐ๊ธฐ
background(0);
// ์ขํ๊ณ ์์ ์ ์บ๋ฒ์ค ์ค์์ผ๋ก ์ด๋
// ์ด์ (0, 0)์ด ํ๋ฉด ์ ์ค์์ด ๋จ
translate(width / 2, height / 2);
// ๋ง์ฐ์ค ์์น๋ฅผ ์บ๋ฒ์ค ์ค์ฌ ๊ธฐ์ค ์ขํ๋ก ๋ณํ
// ์: ๋ง์ฐ์ค๊ฐ ์บ๋ฒ์ค ์ผ์ชฝ ์๋จ์ ์์ผ๋ฉด (-600, -450)
const mousePos = createVector(mouseX - width / 2, mouseY - height / 2);
// ๋ชจ๋ ํํฐํด์ ๋ํด ๋ฌผ๋ฆฌ ๊ณ์ฐ ๋ฐ ๋ ๋๋ง ์ํ
updateAndRenderParticles(mousePos);
// ์๊ฐ ๊ฒฝ๊ณผ์ ๋ฐ๋ผ ๊ฐ๋ ์ฆ๊ฐ (์ด๋น ์ฝ 0.6 ๋ผ๋์ = 34๋)
// ์ด ๊ฐ๋๊ฐ ์ฆ๊ฐํ๋ฉด์ ๊ตฌ์ฒด๊ฐ ํ์ ํ๋ ํจ๊ณผ ๋ฐ์
angle += 0.01;
}
// ==========================================
// [์ด๊ธฐํ ํจ์]
// ==========================================
function initializeParticles() {
// ํํฐํด ๋ฐฐ์ด ์ด๊ธฐํ
points = [];
// PARTICLE_COUNT๊ฐ์ ํํฐํด ์์ฑ
for (let i = 0; i < PARTICLE_COUNT; i++) {
points.push({
index: i, // ํํฐํด ๊ณ ์ ๋ฒํธ (0 ~ 7999)
pos: createVector(0, 0), // ํ์ฌ ์์น (x, y)
vel: createVector(0, 0), // ํ์ฌ ์๋ (vx, vy)
});
}
}
// ํํฐํด์ ์ด๊ธฐ "ํ" ์์น๋ฅผ ์ค์ ํ๋ ํจ์
function updateParticleTargets() {
for (let p of points) {
const i = p.index;
// 2D ํ๋ฉด์์ 3D ๊ตฌ์ฒด๋ฅผ ํ๋ด๋ด๋ ์์
// sin(i + angle): i์ ๋ฐ๋ผ X์ถ ๋ฐฉํฅ ํ์
// sin(i * i): i์ ์ ๊ณฑ์ sin์ ์ ์ฉํ์ฌ ๋ถ๊ท์นํ ๋ถํฌ ์์ฑ
// ๋ sin์ ๊ณฑํ๋ฉด ๊ตฌ์ฒด ํ๋ฉด์ฒ๋ผ ๋ณด์ด๋ ํจํด ์์ฑ
const x = sin(i + angle) * sin(i * i) * SPHERE_RADIUS;
// cos(i * i): Y์ถ ์์น (์์๋ ๋ถํฌ)
// i * i๋ฅผ ์ฌ์ฉํ์ฌ ํํฐํด๋ง๋ค ๋ค๋ฅธ ๋์ด ๋ถ์ฌ
const y = cos(i * i) * SPHERE_RADIUS;
// ๊ณ์ฐ๋ ์์น๋ฅผ ํํฐํด์ pos์ ์ค์
p.pos.set(x, y);
}
}
// ==========================================
// [ํํฐํด ๋ฌผ๋ฆฌ ์๋ฎฌ๋ ์ด์
]
// ==========================================
function updateAndRenderParticles(mousePos) {
// ๋ชจ๋ ํํฐํด์ ๋ํด ๋ฐ๋ณต
for (let p of points) {
const i = p.index;
// --------------------------------------------------
// 1. ํ์ ํ๋ "ํ" ์์น ๊ณ์ฐ
// --------------------------------------------------
// angle์ด ๊ณ์ ์ฆ๊ฐํ๋ฏ๋ก ํ ์์น๊ฐ ์๊ฐ์ ๋ฐ๋ผ ํ์ ํจ
// sin(i + angle): angle์ด ๋ณํ๋ฉด์ X ์ขํ๊ฐ ์ข์ฐ๋ก ์ด๋
const homeX = sin(i + angle) * sin(i * i) * SPHERE_RADIUS;
// Y ์ขํ๋ angle์ ๋
๋ฆฝ์ ์ด๋ฏ๋ก ์ํ ์๋ณต๋ง ํจ
const homeY = cos(i * i) * SPHERE_RADIUS;
// ํ ์์น๋ฅผ ๋ฒกํฐ๋ก ์์ฑ
const home = createVector(homeX, homeY);
// --------------------------------------------------
// 2. ์คํ๋ง ํ (ํ์ผ๋ก ๋์ด๋น๊น)
// --------------------------------------------------
// ํ์ฌ ์์น์์ ํ ์์น๋ก ๊ฐ๋ ๋ฒกํฐ ๊ณ์ฐ
// ์: ํํฐํด์ด ํ์์ ์ค๋ฅธ์ชฝ์ผ๋ก 10px ๋จ์ด์ ธ ์์ผ๋ฉด toHome = (-10, 0)
const toHome = p5.Vector.sub(home, p.pos);
// ์คํ๋ง ํ = ๊ฑฐ๋ฆฌ ร ATTRACTION
// ๋ฉ๋ฆฌ ๋จ์ด์ง์๋ก ๊ฐํ ํ์ด ์์ฉ (ํํฌ์ ๋ฒ์น)
const springForce = toHome.mult(ATTRACTION);
// ์๋์ ์คํ๋ง ํ ์ถ๊ฐ (๊ฐ์๋ ์ ์ฉ)
p.vel.add(springForce);
// --------------------------------------------------
// 3. ๋ง์ฐ์ค ๋ฐ๋ฐ๋ ฅ
// --------------------------------------------------
applyMouseRepulsion(p, mousePos);
// --------------------------------------------------
// 4. ๊ฐ์ ๋ฐ ์์น ์
๋ฐ์ดํธ
// --------------------------------------------------
// ์๋์ DAMPING(0.9)๋ฅผ ๊ณฑํด ๋งค ํ๋ ์๋ง๋ค 10%์ฉ ๊ฐ์
// ์ด๊ฒ์ด ์์ผ๋ฉด ํํฐํด์ด ๋์์ด ๊ฐ์๋จ
p.vel.mult(DAMPING);
// ๋ดํด์ ์ด๋ ๋ฒ์น: ์์น += ์๋
p.pos.add(p.vel);
// --------------------------------------------------
// 5. ํํฐํด ๋ ๋๋ง
// --------------------------------------------------
// ํ์ฌ ์์น์ ์ ํ๋ ๊ทธ๋ฆฌ๊ธฐ
point(p.pos.x, p.pos.y);
}
}
function applyMouseRepulsion(particle, mousePos) {
// ํํฐํด์์ ๋ง์ฐ์ค๋ก๋ถํฐ ๋ฉ์ด์ง๋ ๋ฐฉํฅ ๋ฒกํฐ ๊ณ์ฐ
// ์: ํํฐํด(100, 100), ๋ง์ฐ์ค(90, 100) โ awayFromMouse = (10, 0)
const awayFromMouse = p5.Vector.sub(particle.pos, mousePos);
// ๊ฑฐ๋ฆฌ์ ์ ๊ณฑ ๊ณ์ฐ (sqrt ์ฐ์ฐ ์๋ต์ผ๋ก ์ฑ๋ฅ ์ต์ ํ)
// ์: (10, 0)์ magSq = 10ยฒ + 0ยฒ = 100
const distSq = awayFromMouse.magSq();
// --------------------------------------------------
// ๋ฐ๋ฐ๋ ฅ ์ ์ฉ ์กฐ๊ฑด ์ฒดํฌ
// --------------------------------------------------
// ์กฐ๊ฑด 1: distSq > 0.1 โ ํํฐํด๊ณผ ๋ง์ฐ์ค๊ฐ ๋๋ฌด ๊ฐ๊น์ง ์์ (0์ผ๋ก ๋๋๊ธฐ ๋ฐฉ์ง)
// ์กฐ๊ฑด 2: distSq < REPEL_RADIUSยฒ โ ํํฐํด์ด ๋ง์ฐ์ค ์ํฅ๊ถ ์์ ์์
if (distSq > 0.1 && distSq < REPEL_RADIUS * REPEL_RADIUS) {
// ์ค์ ๊ฑฐ๋ฆฌ ๊ณ์ฐ (ํผํ๊ณ ๋ผ์ค ์ ๋ฆฌ)
const distance = sqrt(distSq);
// ๋ฒกํฐ ์ ๊ทํ: ํฌ๊ธฐ๋ฅผ 1๋ก ๋ง๋ค์ด ๋ฐฉํฅ๋ง ๋จ๊น
// ์: (10, 0) โ (1, 0)
awayFromMouse.normalize();
// --------------------------------------------------
// ๊ฑฐ๋ฆฌ์ ๋ฐ๋ฅธ ์์ฐ์ค๋ฌ์ด ๊ฐ์ ๊ณ์ฐ
// --------------------------------------------------
// (1 - distance / REPEL_RADIUS): ๊ฐ๊น์ธ์๋ก 1์ ๊ฐ๊น์, ๋ฉ์๋ก 0์ ๊ฐ๊น์
// ์: distance=60, REPEL_RADIUS=120 โ (1 - 60/120) = 0.5
// ์ต์ข
ํ = 28 ร 0.5 = 14
const repelForce = REPEL_STRENGTH * (1 - distance / REPEL_RADIUS);
// ์ ๊ทํ๋ ๋ฐฉํฅ ๋ฒกํฐ์ ํ์ ํฌ๊ธฐ๋ฅผ ๊ณฑํจ
// ์: (1, 0) ร 14 = (14, 0)
awayFromMouse.mult(repelForce);
// ํํฐํด์ ์๋์ ๋ฐ๋ฐ๋ ฅ ์ถ๊ฐ
particle.vel.add(awayFromMouse);
}
}