디퍼렌셜 그로스

Differential growth

닫힌 곡선을 이루는 점들이 서로 밀어내면서도 이웃끼리는 당기며, 늘어난 구간에 점을 계속 끼워 넣어 산호나 뇌 주름처럼 구불구불 자라나는 시뮬레이션.

다른 이름: Differential line growth
···
js
const c = document.createElement('canvas');
document.body.appendChild(c);
c.style.width = '100%'; c.style.height = '100%';
const ctx = c.getContext('2d');
const cs = getComputedStyle(document.documentElement);
const ACCENT2 = cs.getPropertyValue('--accent-2').trim() || '#f25c8a';
let w, h, nodes;
function resize() {
  const dpr = Math.min(devicePixelRatio || 1, 2);
  w = innerWidth; h = innerHeight;
  c.width = w * dpr; c.height = h * dpr;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  const cx = w / 2, cy = h / 2, R = Math.min(w, h) * 0.12;
  nodes = Array.from({ length: 24 }, (_, i) => { const a = (i / 24) * Math.PI * 2; return { x: cx + Math.cos(a) * R, y: cy + Math.sin(a) * R }; });
}
addEventListener('resize', resize);
resize();

const MAX_NODES = 240, REPEL_R = 13, MAX_EDGE = 11;
function step() {
  const n = nodes.length;
  const fx = new Float32Array(n), fy = new Float32Array(n);
  for (let i = 0; i < n; i++) {
    for (let j = i + 1; j < n; j++) {
      const dx = nodes[j].x - nodes[i].x, dy = nodes[j].y - nodes[i].y;
      const d2 = dx * dx + dy * dy;
      if (d2 < REPEL_R * REPEL_R && d2 > 0.0001) {
        const d = Math.sqrt(d2), f = (REPEL_R - d) / d * 0.5;
        fx[i] -= dx * f; fy[i] -= dy * f; fx[j] += dx * f; fy[j] += dy * f;
      }
    }
  }
  for (let i = 0; i < n; i++) {
    const prev = nodes[(i - 1 + n) % n], next = nodes[(i + 1) % n];
    fx[i] += (prev.x + next.x - 2 * nodes[i].x) * 0.22;
    fy[i] += (prev.y + next.y - 2 * nodes[i].y) * 0.22;
  }
  for (let i = 0; i < n; i++) { nodes[i].x += fx[i]; nodes[i].y += fy[i]; }
  if (n < MAX_NODES) {
    const grown = [];
    for (let i = 0; i < n; i++) {
      grown.push(nodes[i]);
      const next = nodes[(i + 1) % n];
      if (Math.hypot(next.x - nodes[i].x, next.y - nodes[i].y) > MAX_EDGE) grown.push({ x: (nodes[i].x + next.x) / 2, y: (nodes[i].y + next.y) / 2 });
    }
    nodes = grown;
  }
}
function draw() {
  ctx.fillStyle = '#0d0d12'; ctx.fillRect(0, 0, w, h);
  ctx.strokeStyle = ACCENT2; ctx.lineWidth = 1.6; ctx.beginPath();
  ctx.moveTo(nodes[0].x, nodes[0].y);
  for (let i = 1; i <= nodes.length; i++) { const p = nodes[i % nodes.length]; ctx.lineTo(p.x, p.y); }
  ctx.stroke();
}
for (let i = 0; i < 130; i++) step(); // 미리 자라게 해서 처음부터 주름이 보이도록
draw();
(function loop() { step(); draw(); requestAnimationFrame(loop); })();

작은 원 모양으로 이어진 점들의 고리에서 시작합니다. 매 스텝마다 두 가지 힘이 동시에 작용합니다 — 가까운 점들끼리는 서로 밀어내는 척력(같은 자리에 뭉치지 않게), 곡선을 따라 이웃한 점끼리는 서로 당기는 인력(선이 끊어지지 않고 매끈하게 이어지게)입니다. 여기에 한 가지 규칙을 더합니다: 이웃한 두 점 사이의 간격이 일정 길이를 넘으면 그 사이에 새 점을 하나 끼워 넣습니다.

이 "밀어내며 자라기"를 반복하면 고리는 둥글게 유지되지 못하고 점점 구불구불 접히기 시작합니다. 공간은 제한적인데 곡선 위의 점(둘레)은 계속 늘어나기 때문에, 좁은 공간에 더 긴 선을 욱여넣으려면 주름이 잡힐 수밖에 없는 것입니다 — 산호의 표면, 뇌의 주름, 나뭇잎 가장자리가 비슷한 방식으로 형태를 얻는다는 점에서 생물학적 성장 패턴을 흉내 낸 시뮬레이션으로 창작자들 사이에 널리 알려져 있습니다.

포스터의 유기적인 윤곽선, 산호·뇌 주름 텍스처, "저절로 자라나는" 느낌의 로고 애니메이션에 씁니다. 점 개수가 계속 늘어나므로 상한을 두지 않으면 갈수록 느려집니다.

언제 쓰나

유기적으로 "자라나는" 느낌의 윤곽선·텍스처가 필요할 때. 점 개수 상한과 척력 반경을 함께 조절해야 성능과 밀도의 균형이 맞습니다.