๐ŸŽ‡ Particle on Sphere

BamgasiJMยท2025๋…„ 12์›” 26์ผ

p5.js Art

๋ชฉ๋ก ๋ณด๊ธฐ
42/65
post-thumbnail

๐Ÿ“ p5.js

// ==========================================
// [์„ค์ • ์˜์—ญ] ์ „์—ญ ์ƒ์ˆ˜ ๋ฐ ๋ณ€์ˆ˜
// ==========================================

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);
  }
}

profile
Coding Art with Blender / oF / Processing / p5.js / nannou

0๊ฐœ์˜ ๋Œ“๊ธ€