L-시스템

L-system

문자를 규칙에 따라 계속 치환해서 식물처럼 가지 치는 구조를 만드는 문법. 생물학자 Aristid Lindenmayer가 1968년 식물의 성장을 모델링하려고 고안했습니다.

다른 이름: Lindenmayer systemTurtle graphics fractal
···
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 ACCENT3 = cs.getPropertyValue('--accent-3').trim() || '#18c29c';
let w, h;
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);
}
addEventListener('resize', resize);
resize();

function expand(axiom, rules, gens) {
  let s = axiom;
  for (let g = 0; g < gens; g++) { let next = ''; for (const ch of s) next += rules[ch] ?? ch; s = next; }
  return s;
}
const RULES = { X: 'F+[[X]-X]-F[-FX]+X', F: 'FF' };
const STR = expand('X', RULES, 4);
const ANGLE = (25 * Math.PI) / 180, STEP = 6.2;

function buildSegments() {
  let x = 0, y = 0, ang = -Math.PI / 2, stack = [];
  const segs = [];
  let minX = 0, maxX = 0, minY = 0, maxY = 0;
  for (const ch of STR) {
    if (ch === 'F') {
      const nx = x + Math.cos(ang) * STEP, ny = y + Math.sin(ang) * STEP;
      segs.push([x, y, nx, ny]);
      x = nx; y = ny;
      minX = Math.min(minX, x); maxX = Math.max(maxX, x); minY = Math.min(minY, y); maxY = Math.max(maxY, y);
    } else if (ch === '+') ang += ANGLE;
    else if (ch === '-') ang -= ANGLE;
    else if (ch === '[') stack.push([x, y, ang]);
    else if (ch === ']') [x, y, ang] = stack.pop();
  }
  return { segs, bounds: [minX, maxX, minY, maxY] };
}
const { segs, bounds } = buildSegments();
const treeW = bounds[1] - bounds[0], treeH = bounds[3] - bounds[2];

function draw(frac) {
  const dpr = Math.min(devicePixelRatio || 1, 2);
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  ctx.clearRect(0, 0, w, h);
  const scale = Math.min(w / treeW, h / treeH) * 0.82;
  ctx.save();
  ctx.translate(w / 2 - ((bounds[0] + bounds[1]) / 2) * scale, h * 0.94 - bounds[3] * scale);
  ctx.scale(scale, scale);
  ctx.strokeStyle = ACCENT3; ctx.lineWidth = 1.4 / scale; ctx.lineCap = 'round';
  const count = Math.floor(segs.length * frac);
  for (let i = 0; i < count; i++) { const s = segs[i]; ctx.beginPath(); ctx.moveTo(s[0], s[1]); ctx.lineTo(s[2], s[3]); ctx.stroke(); }
  ctx.restore();
}
let start = performance.now();
const GROW_MS = 1100, HOLD_MS = 2200;
(function loop(t) {
  const elapsed = (t - start) % (GROW_MS + HOLD_MS);
  draw(Math.min(1, elapsed / GROW_MS));
  requestAnimationFrame(loop);
})(performance.now());

"X"라는 문자 하나에서 시작해서, "X를 발견하면 F+[[X]-X]-F[-FX]+X로 바꿔라" 같은 규칙을 반복 적용하면 문자열이 매 세대 기하급수적으로 자랍니다. 이 문자열을 "터틀 그래픽"으로 읽으면 그림이 됩니다 — F는 앞으로 전진하며 선 긋기, +와 -는 좌우로 방향 틀기, [와 ]는 지금 위치·방향을 저장하고 나중에 되돌아오기(가지를 치고 원래 줄기로 복귀)입니다.

이 규칙 하나만으로 고사리, 나무, 관목처럼 "부분이 전체를 닮은" 프랙탈 구조가 나옵니다. 코흐 눈송이 같은 순수 기하학적 프랙탈도 같은 방식(F → F+F--F+F)으로 만들 수 있습니다.

이 데모는 문자열을 4세대까지 미리 펼친 뒤, 터틀이 그 경로를 그려나가는 과정을 애니메이션으로 보여줍니다. 절차적 식생 생성, 배경의 나뭇가지 패턴, 프랙탈 아트 포스터에 널리 쓰입니다.

언제 쓰나

절차적으로 나무·식물을 생성할 때, 배경에 자연스러운 나뭇가지 패턴이 필요할 때. 각도와 규칙 문자열을 바꾸면 완전히 다른 종의 식물처럼 보입니다.