GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

L시스템 프랙탈 나무 9종 — 문자 하나가 가지를 뻗는 캔버스

프랙탈 나무는 몇 글자의 문자열을 규칙대로 다시 쓰고 그 결과를 거북이가 걸어 그리는 L시스템 그림입니다. F 하나는 앞으로, + 하나는 회전입니다. 이 몇 글자를 몇 번 다시 읽기만 해도 나뭇가지가 저절로 뻗는데, 그 재귀 엔진을 통째로 복붙으로 가져갑니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

데모

아홉 장면 전부 파일 하나씩으로 열리자마자 세대 0 에서 최대 세대까지 자라기를 시작하고, 화면의 모든 변화는 CSS 키프레임이 아니라 다시 쓴 문자열을 거북이가 처음부터 순회한 결과를 다시 그린 것입니다. 24프레임 녹화에서 일곱 장면이 23프레임, 05 카펫과 07 식물이 22프레임 움직였는데 세대 경계에서 진행률이 한 프레임 쉬는 지점이 있는 것이지 멈춘 것이 아닙니다. 성장은 주기 2초 안에 차오르고, 다 자란 뒤에도 가지 끝의 호흡·바람·스포트라이트로 화면은 계속 살아 있습니다.

24프레임 실측 한눈에

항목 초점 누적면적(%) 강도 평균
01 재귀 가지 F[+F][-F]F · 26° · 세대 0→6 2.123 82.9
02 피타고라스 나무 정사각형 0.707 배 축소 · 세대 0→8 1.792 104.6
03 코흐 눈송이 F+F--F+F · 60° · 세대 0→4 8.315 45.0
04 셸핀스키 삼각형 A→B-A-B · B→A+B+A · 60° 5.87 83.7
05 셸핀스키 카펫 1→111101111 · getImageData 실측 16.167 141.3
06 용 곡선 X+YF+ · -FX-Y · 90° · 세대 0→11 5.192 88.4
07 매개변수 식물 25° ± 6° 사인파 흔들림 6.449 89.2
08 바람 나무 깊이 제곱 사인파 · 진폭 30° 1.706 70.2
09 대문 숲 규칙 3종 · 나무 5그루 동시 성장 11.5 63.6

수치는 구성의 성격으로 읽는다

누적면적이 대부분 1~8% 대에 머무는 것은 이 편의 성격입니다. 거북이 그리는 것이 두께 1~5px 안팎의 선분이라 화면을 스치는 정도일 수밖에 없고, 면적이 커지는 두 장면은 예외가 아니라 다른 방식입니다 — 05 카펫은 채운 칸을 fillRect 로 그려 남은 넓이가 그대로 픽셀 점유율이 되어 16.167%, 09 대문 숲은 나무 다섯 그루가 동시에 자라 겹쳐 11.5% 까지 오릅니다. 강도 평균은 05 카펫이 141.3 으로 가장 밝은데 채운 사각형과 배경의 대비가 크기 때문이고, 02 피타고라스 나무 104.6 이 계절 색 순환으로 뒤따릅니다. 면적최대는 05 가 한 프레임에 14.143% 까지 차오르고 09 는 9.198% 입니다.

F 하나의 재귀에서 곡선·매개변수까지 번진 L-system 순서

순서는 화려함의 서열이 아니라 문자 재쓰기가 어디까지 밀려나는가를 한 단계씩 넓히는 서열입니다. 먼저 문자 한 종류 F 하나를 규칙 하나로 다시 써 최소의 두 갈래 나무를 얻고(01), 같은 재귀를 정사각형 위에 올려 관 모양 숲을 만듭니다(02). 세 번째·네 번째는 재쓰기가 닫힌 도형에 닿습니다 — 변을 들어올리는 코흐 눈송이(03)와 두 문자가 서로를 다시 쓰는 셸핀스키 삼각형(04). 다섯째는 거북 해석을 벗어나 칸을 비우는 규칙으로 바뀌고 남은 넓이를 실측합니다(05). 여섯째부터 일곱 번째는 재쓰기가 곡선(06)과 매개변수(07)로 확장되고, 여덟 번째는 세대 번호가 진동 파라미터로 쓰입니다(08). 마지막에 규칙 세 가지를 나무 다섯 그루에 돌려써 대문 장면으로 조립합니다(09). 문자열을 벡터처럼 적분해 그림을 얻는다는 점에서 발산 횟수를 세어 색을 매기는 만델브로 줌과 같은 프랙탈 축의 이웃이고, 실 감기 한 올 한 올을 계산하는 스트링아트 캔버스와도 측정의 대상이 다를 뿐 규칙을 프레임마다 다시 계산한다는 계보입니다.

항목별 코드

01재귀 가지 — 두 갈래로 갈라지는 최소 나무

axiom F 하나를 규칙 F→F[+F][-F]F 로 세대마다 다시 쓰고 거북이가 그 문자열을 그대로 걸으면 두 갈래로 갈라지는 나무가 나오는, 프랙탈 나무의 모든 규칙이 시작되는 최소 구현입니다. 세대마다 길이는 0.74 배로 줄고 6세대까지 2초 주기 안 약 1.5초에 걸쳐 자라며, 유지 구간엔 가지 끝이 숨 호흡으로 진동합니다.

재귀 가지 — 두 갈래로 갈라지는 최소 나무
새 창에서 열기

01 · 재귀 가지 — 두 갈래로 갈라지는 최소 나무 — 측정 누적면적 2.123% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.sinMath.coslineTobeginPath

규칙 전개와 세대 사전 계산

  // L-시스템 문자열 재쓰기 — axiom F 를 규칙으로 n세대 전개
  var RULE = { 'F': 'F[+F][-F]F' };
  function expand(axiom, gen) {
    var s = axiom;
    for (var g = 0; g < gen; g++) {
      var out = '';
      for (var i = 0; i < s.length; i++) {
        var ch = s.charAt(i);
        out += RULE[ch] !== undefined ? RULE[ch] : ch;
      }
      s = out;
    }
    return s;
  }

  // 세대별 문자열을 미리 전개해 둔다 — 매 프레임 재계산하지 않는다(문자 길이 3^gen)
  var GENS = [];
  for (var g = 0; g <= 6; g++) GENS.push(expand('F', g));

거북 해석 — 문자열을 선분 목록으로

  // 거북 해석 — 문자열을 선분 목록 {x1,y1,x2,y2,depth} 로 적분
  function turtle(str, x0, y0, len0, angDeg) {
    var segs = [];
    var x = x0, y = y0, a = -Math.PI / 2;   // 위를 향해 시작(캔버스 y축 아래방향 보정)
    var stackX = [], stackY = [], stackA = [];
    var len = len0;
    for (var i = 0; i < str.length; i++) {

각도 26° 는 25도에 가까운 낮은 각이라 침엽수 실루엣이 나오는 값입니다.

02피타고라스 나무 — 정사각형 위에 정사각형

정사각형 한 변 위에 빗변을 밑변으로 삼는 정사각형 둘을 얹는 재귀를 세대 0→8 로 반복하면 관 모양 숲이 자라는, 실측 제안에도 있는 클래식 프랙탈입니다. 한 변은 매 세대 0.707(√2/2) 배로 줄어 8세대면 사각형이 약 250개까지 늘어나고, 다 자란 뒤엔 초록 여름에서 호박 가을을 지나 다시 봄으로 계절 색이 순환합니다.

피타고라스 나무 — 정사각형 위에 정사각형
새 창에서 열기

02 · 피타고라스 나무 — 정사각형 위에 정사각형 — 측정 누적면적 1.792% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.sinMath.coslineTofillRect

꼭짓점을 직접 계산하는 정사각형 재귀

  function drawTree(ctx, x, y, s, gen, genF, seasonT) {
    if (s < 0.9 || gen > genF) return;
    var thisF = Math.min(1, genF - gen);           // 현재 세대 안 진행률 — 부분 크기로 스르륵
    var drawS = s * thisF;
    if (drawS < 0.5) return;
    // 색: 몸통(베이지) → 잎(계절 보간). 깊이 비율로 혼합
    var depthT = Math.min(1, gen / MAXGEN);
    var leaf = mix(C_LEAFA, C_LEAFB, seasonT);
    var col = mix(C_TRUNK, leaf, depthT);
    ctx.fillStyle = rgb(col);
    // ctx.translate+rotate 대신 꼭짓점 직접 계산 — 정사각형이 기울어져도 fillRect 회전 근사가 아닌 정확 좌표
    ctx.beginPath();
    ctx.moveTo(x, y + s);
    ctx.lineTo(x + s, y + s);
    ctx.lineTo(x + s, y);
    ctx.lineTo(x, y);
    ctx.closePath();

빗변 위에 두 사각형 올리기

    if (gen + 1 > genF) return;
    // 위에 직각삼각형(45-45-90) — 빗변이 이 변. 두 정사각형을 빗변에 얹는다.
    // 빗변 양끝 (x, y) (x+s, y). 45도 삼각형의 꼭대기는 중점 위 s/2.
    var mx = x + s / 2, my = y - s / 2;
    var NS = s / Math.SQRT2;
    // 왼쪽 사각형: 빗변 (x,y)-(mx,my). 변 NS.
    var lx = x, ly = y;
    // 왼쪽 사각형의 왼쪽 위 꼭짓점 = 빗변 시작점에서 밖으로 NS 만큼
    var lcx = lx + Math.cos(-Math.PI * 3 / 4) * NS;   // 225도 방향
    var lcy = ly + Math.sin(-Math.PI * 3 / 4) * NS;
    drawTree(ctx, lcx, lcy, NS, gen + 1, genF, seasonT);

첫 정사각형 변은 무대 배율 0.21 이고, 완성 뒤엔 밑동을 고정한 미세 진동으로 화면이 죽지 않게 합니다.

03코흐 눈송이 — 변을 들어올려 무한 해안선

axiom F--F--F(정삼각형 세 변)에 규칙 F→F+F--F+F 를 세대마다 적용하면 매번 변 하나가 더 잘게 접혀 눈송이 해안선이 길어지는, 프랙탈 하면 떠오르는 대표 곡선입니다. 각도는 60°, 세대 0→4 로 보간해 이전 세대 점과 다음 세대 점을 섞어 부드럽게 넘어갑니다.

코흐 눈송이 — 변을 들어올려 무한 해안선
새 창에서 열기

03 · 코흐 눈송이 — 변을 들어올려 무한 해안선 — 측정 누적면적 8.315% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.sinMath.coslineTostrokeStyle

닫힌 다각형으로 해석하는 거북

  var RULE = { 'F': 'F+F--F+F' };
  function expand(axiom, gen) {
    var s = axiom;
    for (var g = 0; g < gen; g++) {
      var out = '';
      for (var i = 0; i < s.length; i++) {
        var ch = s.charAt(i);
        out += RULE[ch] !== undefined ? RULE[ch] : ch;
      }
      s = out;
    }
    return s;
  }

  // 거북 해석 — 문자열을 점 목록(닫힌 다각형)으로 적분
  function turtlePoly(str, step) {
    var x = 0, y = 0, a = 0;
    var pts = [{ x: x, y: y }];

axiom 이 F--F--F 세 변이어야 정삼각형에서 시작해 해안선이 안으로 접히는데, 다른 axiom 이면 같은 이름의 도형이 나오지 않습니다.

04셸핀스키 삼각형 — 숨은 삼각형의 삼각형

axiom A 에 A→B-A-B, B→A+B+A 두 문자가 서로를 다시 쓰는 규칙을 60° 거북 걸음으로 풀면 한붓그리기 화살촉 곡선이 되는, 빈틈이 메워져 가는 과정이 눈에 보이는 항목입니다. 세대 0→6 으로 늘수록 그 곡선이 셸핀스키 삼각형의 빈틈을 그대로 메워 갑니다.

셸핀스키 삼각형 — 숨은 삼각형의 삼각형
새 창에서 열기

04 · 셸핀스키 삼각형 — 숨은 삼각형의 삼각형 — 측정 누적면적 5.87% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.sinMath.coslineTobeginPath

두 문자가 서로를 다시 쓰는 규칙

  var RULE = { 'A': 'B-A-B', 'B': 'A+B+A' };
  function expand(axiom, gen) {
    var s = axiom;
    for (var g = 0; g < gen; g++) {
      var out = '';
      for (var i = 0; i < s.length; i++) {
        var ch = s.charAt(i);
        out += RULE[ch] !== undefined ? RULE[ch] : ch;
      }
      s = out;
    }
    return s;
  }

A·B 모두 전진하는 열린 곡선

  // 거북 해석 — A,B 모두 전진 한 걸음, +,- 는 60도 회전. 열린 곡선 점 목록을 만든다.
  function turtlePath(str, step) {
    var x = 0, y = 0, a = 0;
    var pts = [{ x: x, y: y }];
    for (var i = 0; i < str.length; i++) {
      var ch = str.charAt(i);
      if (ch === 'A' || ch === 'B') {

두 규칙을 하나로 합치면 서로가 서로를 다시 쓰는 구조가 사라져 다른 곡선이 나옵니다.

05셸핀스키 카펫 — 칸을 비우며 파고드는 구멍

채움 칸 하나(문자 1)를 3×3 아홉 글자로 다시 쓰되 가운데만 구멍(문자 0)으로 남기는 규칙 1→111101111 을 세대 0→4 로 적용해 정사각형을 fillRect 격자로 그리는, 면이 줄고 가장자리가 늘어나는 과정을 실측으로 보여주는 항목입니다. getImageData 로 실제 채워진 픽셀 비율을 세어 남은 넓이 % 를 화면에 그대로 표시하는데 이는 트윈 값이 아니라 실측값입니다.

셸핀스키 카펫 — 칸을 비우며 파고드는 구멍
새 창에서 열기

05 · 셸핀스키 카펫 — 칸을 비우며 파고드는 구멍 — 측정 누적면적 16.167% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

fillRectgetImageDatarequestAnimationFrame

3^gen 격자 해석과 fillRect

  // 문자 재쓰기 규칙 — '1' 한 칸이 3x3 아홉 칸(가운데만 구멍)으로 다시 쓰인다.
  var RULE = { '1': '111101111', '0': '000000000' };
  function expand(axiom, gen) {
    var s = axiom;
    for (var g = 0; g < gen; g++) {
      var out = '';
      for (var i = 0; i < s.length; i++) out += RULE[s.charAt(i)];
      s = out;
    }
    return s;
  }

  // gen 세대 문자열을 3^gen x 3^gen 격자로 해석해 채워진 칸만 fillRect
  function drawCarpet(ctx, str, gen, ox, oy, side, thisF, bright) {
    var n = Math.pow(3, gen);
    var cell = side / n;

남은 넓이를 직접 세는 getImageData

    if (areaEl && gA !== lastAreaTick) {
      lastAreaTick = gA;
      try {
        var img = ctx.getImageData(Math.round(ox), Math.round(oy), Math.round(side), Math.round(side));
        var data = img.data, filled = 0, total = data.length / 4;
        for (var p = 0; p < data.length; p += 4) {
          if (data[p] !== C_INK[0] || data[p + 1] !== C_INK[1] || data[p + 2] !== C_INK[2]) filled++;
        }
        areaEl.textContent = Math.round((filled / total) * 100) + '%';
      } catch (e) { /* 캔버스 판독 실패해도 화면은 그대로 동작 */ }
    }

세대가 바뀔 때만 다시 세어 계산량을 아끼는데, 세대 4 면 채워진 칸 비율이 (8/9)⁴ ≈ 0.62 로 화면의 % 표시와 맞아떨어집니다.

06용 곡선 — 접기 규칙이 만드는 종이공룡

axiom FX 한 줄에 X→X+YF+, Y→-FX-Y 두 규칙을 되풀이 적용하면 종이를 반으로 접었다 편 자국처럼 겹치지 않고 빼곡히 접히는 용 곡선이 자라는, L시스템 두 문자의 상호 재쓰기로 그리는 대표 응용입니다. 각도는 90°, 세대 0→11 까지 늘려 접힘 밀도를 끝까지 채웁니다.

용 곡선 — 접기 규칙이 만드는 종이공룡
새 창에서 열기

06 · 용 곡선 — 접기 규칙이 만드는 종이공룡 — 측정 누적면적 5.192% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.sinMath.coslineTorequestAnimationFrame

X·Y 자리표시자와 90도 회전

  var RULE = { 'X': 'X+YF+', 'Y': '-FX-Y' };
  function expand(axiom, gen) {
    var s = axiom;
    for (var g = 0; g < gen; g++) {
      var out = '';
      for (var i = 0; i < s.length; i++) {
        var ch = s.charAt(i);
        out += RULE[ch] !== undefined ? RULE[ch] : ch;
      }
      s = out;
    }
    return s;
  }

  // 거북 해석 — F 만 전진(점 추가), X·Y 는 자리표시자(전진 없음), +/- 는 90도 회전.
  function turtlePath(str, step) {

세대 11 이면 문자열 길이가 수십만 글자에 이르러, 세대별 사전 계산이 없으면 한 프레임에 전개를 다시 하는 순간 계산이 폭주합니다.

07매개변수 식물 — F+-[] 문자열이 자라는 잎

axiom X 에 X→F+[[X]-X]-F[-FX]+X, F→FF 두 규칙을 세대 0→5 로 적용하면 관엽식물 잎이 펼쳐지는, 몇 글자 고치면 완전히 다른 식물이 되는 생성문법 실험판입니다. 기준 분기각 25° 가 다 자란 뒤 ±6° 폭으로 사인파처럼 흔들려 같은 문법·다른 매개변수가 다른 식물임을 실시간으로 보여주고, 잎은 깊이에 따라 globalAlpha 로 옅어집니다.

매개변수 식물 — F+-[] 문자열이 자라는 잎
새 창에서 열기

07 · 매개변수 식물 — F+-[] 문자열이 자라는 잎 — 측정 누적면적 6.449% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.sinMath.coslineToglobalAlpha

매 프레임 바뀌는 각도 매개변수

  var RULE = { 'X': 'F+[[X]-X]-F[-FX]+X', 'F': 'FF' };
  function expand(axiom, gen) {
    var s = axiom;
    for (var g = 0; g < gen; g++) {
      var out = '';
      for (var i = 0; i < s.length; i++) {
        var ch = s.charAt(i);
        out += RULE[ch] !== undefined ? RULE[ch] : ch;
      }
      s = out;
    }
    return s;
  }
  var STRS = [];
  for (var g0 = 0; g0 <= MAXGEN; g0++) STRS.push(expand('X', g0));

깊이별 색 보간과 스택

  // 거북 렌더러 — angleDeg 는 매 프레임 바뀌는 매개변수(같은 문자열, 다른 각도 → 다른 식물)
  function drawPlant(ctx, str, x0, y0, stepLen, angleDeg, ox, oy, sc, alphaMul, drawFrac) {
    var x = x0, y = y0, a = -Math.PI / 2;
    var stack = [];
    var n = str.length;
    var drawUpto = Math.floor(n * drawFrac);
    for (var i = 0; i < drawUpto; i++) {
      var ch = str.charAt(i);
      if (ch === 'F') {

문자열은 그대로 두고 angleDeg 만 흔들어도 잎 전체의 실루엣이 숨 쉬는 것이 이 판의 계산 겹입니다.

08바람에 흔들리는 나무 — 세대마다 위상이 어긋나는 진동

규칙 F→F[+F]F-F는 그대로 두고, 거북이 매 걸음마다 꺾는 각도에 깊이 비례하는 사인파(진동수 1.6회/주기, 최대 진폭 30°)를 곱해 더하는 바람 판입니다. 가지 길이도 깊이마다 0.78 배씩 줄어 뿌리는 거의 안 움직이고 잔가지 끝만 크게 흔들립니다.

바람에 흔들리는 나무 — 세대마다 위상이 어긋나는 진동
새 창에서 열기

08 · 바람에 흔들리는 나무 — 세대마다 위상이 어긋나는 진동 — 측정 누적면적 1.706% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.sinlineTorequestAnimationFrameglobalAlpha

깊이 비례 사인파를 각도에 더하기

      var ch = str.charAt(i);
      if (ch === 'F') {
        var depthT = depth / maxDepth;
        // 세대 깊이에 비례(선형)해 커지는 사인파 — 잔가지뿐 아니라 중간 가지도 함께 휘어야
        // 프레임마다 바뀌는 픽셀 면적이 뿌리 근처 몇 개 선분에만 갇히지 않는다.
        var sway = windAmp * depthT * Math.sin(windPhase + depth * 0.8) * Math.PI / 180;
        a += sway;
        var segLen = stepLen * Math.pow(0.78, depth);
        var nx = x + Math.cos(a) * segLen;
        var ny = y + Math.sin(a) * segLen;

깊이로 굵기·명도 내기

        var cr = C_BARK[0] + (C_LEAF[0] - C_BARK[0]) * depthT;
        var cg = C_BARK[1] + (C_LEAF[1] - C_BARK[1]) * depthT;
        var cb = C_BARK[2] + (C_LEAF[2] - C_BARK[2]) * depthT;
        ctx.globalAlpha = 0.6 + 0.4 * depthT;
        ctx.strokeStyle = 'rgb(' + (cr | 0) + ',' + (cg | 0) + ',' + (cb | 0) + ')';
        ctx.lineWidth = Math.max(1.5, 5.2 * Math.pow(0.66, depth));
        ctx.beginPath();
        ctx.moveTo(ox + x * sc, oy + y * sc);
        ctx.lineTo(ox + nx * sc, oy + ny * sc);
        ctx.stroke();

바람 진폭을 깊이와 무관하게 고정하면 전체가 같은 폭으로 흔들려 뻣뻣한 진동일 뿐이라, 위상에 depth * 0.8 을 더해 가지마다 어긋나게 하는 것이 정해입니다.

09대문 숲 — 브랜드 자막 위로 자라는 프랙탈 숲

씨앗 하나에 서로 다른 재쓰기 규칙 세 가지를 돌려써 나무 다섯 그루를 세대 0→4 로 동시에 키우는 대문 장면입니다. 나무마다 각도(20+(i%3)5)·크기(0.85+(i%2)0.22)·성장 지연(0.06×i)이 달라 같은 씨앗이 아니라 정말 다른 규칙으로 자라는 것처럼 보이고, 뒤편 createRadialGradient 스포트라이트가 성장이 차오를수록 밝아집니다.

대문 숲 — 브랜드 자막 위로 자라는 프랙탈 숲
새 창에서 열기

09 · 대문 숲 — 브랜드 자막 위로 자라는 프랙탈 숲 — 측정 누적면적 11.5% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.sinlineTocreateRadialGradientrequestAnimationFrame

규칙 세 가지를 돌려쓰는 씨앗 뿌리기

  // 나무마다 다른 재쓰기 규칙 세 가지를 돌려쓴다 — "각기 다른 규칙으로 자란다"를 문자 그대로 구현
  var RULES = [
    { 'F': 'F[+F]F[-F]' },
    { 'F': 'F[+F][-F]F' },
    { 'F': 'FF[+F][-F]' }
  ];
  function expand(rule, gen) {
    var s = 'F';
    for (var g = 0; g < gen; g++) {
      var out = '';
      for (var i = 0; i < s.length; i++) {
        var ch = s.charAt(i);
        out += rule[ch] !== undefined ? rule[ch] : ch;
      }
      s = out;
    }
    return s;
  }

나무마다 다른 각도·크기·지연

  var TREES = [];
  (function seed() {
    for (var i = 0; i < N_TREE; i++) {
      var rule = RULES[i % RULES.length];
      TREES.push({
        xFrac: (i + 0.5) / N_TREE,
        str: expand(rule, MAXGEN),
        angle: 20 + (i % 3) * 5,
        scale: 0.85 + (i % 2) * 0.22,
        phase: i * 0.9,
        genDelay: (i % N_TREE) * 0.06
      });
    }
  })();

규칙을 하나로 통일하면 다섯 그루가 복제된 나무로 보이니, 세 가지를 돌려쓰는 것이 이 장면의 전제입니다.

expand 를 렌더 루프 안에서 부르면 함정이 시작된다

세대별 문자열을 매 프레임 다시 전개하면 폭주한다

가장 자주 만나는 깨짐은 expand 를 렌더 루프 안에서 부르는 것입니다. 규칙에 따라 문자열이 세대마다 3배~2배씩 늘어 06 용 곡선의 세대 11 이면 한 프레임에 수십만 글자를 다시 써야 하니 프레임이 끊깁니다. 세대별 문자열을 미리 배열에 저장해 두고 렌더는 순회만 하는 것이 정해인데, 세대 경계의 진행률 보간으로 낮은 세대의 짧은 문자열에서 다음 세대의 긴 문자열로 점을 섞어 넘어가야 부드럽습니다. 08 의 바람 진폭을 깊이와 무관하게 고정하면 전체가 같은 폭으로 흔들려 바람이 아니라 진동이 되고, 04 의 두 문자 규칙을 하나로 합치면 서로를 다시 쓰는 구조가 사라져 삼각형 빈틈이 메워지지 않습니다. 이 아홉 장면은 규칙 전개부터 거북 순회까지 통째로 index.html 하나씩으로 들어 있는데 그 원본을 그대로 묶은 암호 zip 의 비밀번호는 z8hwyvrw 이고 풀면 본문의 코드 조각과 줄 단위로 나란히 대조해 보실 수 있습니다.

왜 몇 글자의 규칙이 나무가 되는가 — 재쓰기와 자기유사성

L시스템의 나무는 그린 것이 아니라 다시 쓴 것입니다. 규칙 F→F[+F][-F]F 는 F 하나를 만나면 F[+F][-F]F 로 바꾸라는 뜻인데, 세대를 거듭할 때마다 이 바꾸기가 결과 문자열 전체에 다시 적용되어 F 가 포함된 조각마다 똑같은 구조가 한 단계 작게 복제됩니다. 거북이가 그 문자열을 처음부터 걸으면 [ 를 만날 때마다 현재 위치와 방향을 스택에 저장했다가 ] 에서 되돌아오므로, 한 갈래를 다 그리고 갈라지기 직전 자리로 돌아와 반대 갈래를 그립니다. 곡선 호출은 한 줄도 없는데 나뭇가지가 나오는 것은 결과 문자열 전체가 거북의 걸음 순서이기 때문이고, 가지마다 같은 규칙의 복제가 들어 있으니 전체가 부분을 닮은 자기유사 모양이 됩니다. 세대를 얼마나 반복할지와 각도를 몇 도로 꺾을지 두 값만 바꿔도 침엽수가 활엽수가 되고 눈송이가 해안선이 되는 것도 이 때문입니다.

곡선을 직접 그리는 순간 재쓰기가 사라진다

여기에 arc 나 2차 곡선 호출을 한 줄 섞는 순간 화면은 여전히 예뻐 보이지만 L시스템이 아닙니다. 이 장면의 곡선은 문자열 재쓰기가 만든 걸음 순서의 결과인데 곡선 함수가 그 근거를 대신 그려버리면 남은 선분들의 규칙성도 함께 죽습니다. 그래서 아홉 장면의 구조선을 전부 moveTo·lineTo 만으로 그리고, arc·quadraticCurveTo 는 씨앗 점·잎눈 같은 장식용 점 강조에만 썼습니다.

이런 화면에서 쓴다 — 브랜드 인트로와 로딩 화면

아홉 판 중 가장 먼저 붙는 자리는 09 대문 숲 같은 히어로 섹션입니다. 로고나 브랜드 문구 위로 나무가 자라 올라오는 동안 방문자는 몇 초를 자연스럽게 붙잡히고, 그 사이 뒤 페이지가 로드됩니다. 01 재귀 가지처럼 가장 단순한 판은 로딩 스피너 대신 쓰기 좋은데, 규칙 두어 줄만 바꿔도 나무 모양이 달라지기 때문에 데모를 그대로 복붙해도 다른 사이트처럼 보이지 않습니다.

07 매개변수 식물은 반대로 상호작용이 있는 자리에 어울립니다. 슬라이더로 각도나 세대를 바꾸게 열어 두면 방문자가 직접 식물을 키우는 미니 도구가 되는데, 원예·식물 커머스 사이트의 제품 소개 옆이나 교육용 코딩 페이지의 실습 예제로 붙이면 같은 코드가 완전히 다른 역할을 합니다.

세 변형 값과 reduced-motion — 나무가 멈추는 자리

값 하나를 바꾼 세 변형

이름 바꾼 값 느낌
가지 각도 01 각도 25° → 45° 가지가 넓게 벌어지며 침엽수에서 활엽수로 옷을 갈아입는다
재귀 깊이 07 깊이 4 → 8 세대가 두 배로 늘면 잎이 화면을 채우고 계산량은 2의 거듭제곱으로 뛴다
축소 비율 08 길이 비율 0.72 → 0.85 가지가 덜 줄어들수록 숲이 빽빽해지고 흔들림이 무겁게 느려진다

동작을 줄인 사용자에게 주는 장면

모든 판은 시작할 때 prefers-reduced-motion: reduce 를 검사해 켜져 있으면 애니메이션 루프를 돌리지 않고 고정 장면 하나를 그린 뒤 루트 요소에 .is-still 클래스를 붙입니다. 여덟 판은 최대 세대까지 다 자란 모습을 고정하고 01 만 한 세대 못 미친 가지를 고정하니, 정지 사용자에게도 빈 캔버스가 아니라 자란 나무가 남습니다.

화면 밖 읽는 사람과 화면 낭독

아홉 판 모두 자동 재생이고 사람이 알 수 있는 aria-label(재귀 가지 실험판 류)과 tabindex 를 가지며, 규칙·각도·세대 범위 같은 측정 메타가 화면 아래 한 줄로 항상 표시됩니다.

L 시스템 나무를 처음 붙일 때 자주 나오는 질문

프랙탈 나무 그리기를 이 데모로 처음 시작해도 따라할 수 있나요?

핵심은 규칙 객체 하나와 문자열을 한 글자씩 치환하는 expand 함수, 그리고 F 전진·+/- 회전·[ ] 스택의 거북 해석 세 조각인데 01 판의 스무 줄 남짓이 그 전부입니다. zip 안에는 아홉 장면을 같은 조건으로 다시 짜라고 시키는 지시 문서가 들어 있어 코드 도구에 붙여 넣으면 같은 화면부터 시작하고, 그다음은 각도와 세대 수를 하나씩 바꿔 보는 순서가 가장 빠릅니다.

프랙탈 구조 나무처럼 내 규칙을 만들어 다른 나무를 자라나게 할 수 있나요?

07 이 정확히 그 구조입니다. RULE 객체의 치환 문자열 몇 글자와 대괄호 위치를 바꾸면 잎이 침엽으로 바뀌고 갈래 수가 달라지는데, 각도와 길이 비율까지 움직이면 같은 문법으로 완전히 다른 식물이 자랍니다. 09 처럼 규칙 두세 개를 나무마다 돌려쓰면 한 장면에 숲이 들어섭니다.

피타고라스 나무 프랙탈의 남은 넓이처럼 수치도 화면에서 확인할 수 있나요?

05 카펫이 세대마다 getImageData 로 실제 채워진 픽셀을 세어 남은 넓이 % 를 화면에 그대로 띄웁니다. 세대가 하나 늘 때마다 남은 넓이는 8/9 씩 줄어 세대 4 에서 약 62% 인데, 이는 장식 수치가 아니라 캔버스에서 방금 읽은 실측값이라 표시된 숫자와 손계산이 맞아떨어집니다.

더 읽을거리

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.