# AI 꾸러미 — 생성 패턴 (흐름장 · 보로노이 · 트루쳇) — Flow field
> 규칙 몇 줄로 무늬를 만든다 — 흐름장 각도를 따라 흐르는 입자 선, 가장 가까운 점의 세포(보로노이), 사분원 두 개 타일을 돌려 놓는 트루쳇 미로.  
> 견본: https://ai-techstudio.web.app/#t/i122

이 문서를 코드 도우미(Claude Code · Cursor · ChatGPT 등)에 그대로 주면 돼요. 「## 주문서」 가 할 일, 나머지는 참고 자료예요.

## 주문서

### 만들어 줘: 생성 패턴 (흐름장 · 보로노이 · 트루쳇) — Flow field

#### 1. 목표
메뉴 · 결과 화면 배경에 생성 패턴을 그려 줘 — 흐름장(털처럼 흐르는 선) · 보로노이(세포) · 트루쳇(뒤집히는 타일 미로)를 규칙으로. 분위기는 네온 무지개.

#### 2. 핵심 기술 용어
- **Flow field** — 자리마다 정해진 방향(각도)을 따라 흐르는 선
- **Voronoi diagram (half-plane clipping)** — 가장 가까운 점끼리 나눈 세포 — 수직 이등분선으로 자르기
- **Truchet tiles** — 사분원 둘 그린 타일을 0° / 90° 로 놓아 이어지는 미로
- **Generative art** — 규칙으로 그리는 그림

#### 3. 환경
- 플랫폼: HTML Canvas 2D (TypeScript), requestAnimationFrame 루프, 라이브러리 없이
- 화면: 2D · 브라우저 — PC 와 폰(390px 폭) 모두, devicePixelRatio 맞춰 또렷하게, 60fps 목표

#### 4. 조건
- 흐름장 꼬리는 따로 둔 캔버스에 쌓고, 매 프레임 바탕색을 아주 옅게(0.06) 덮어 서서히 지운다
- 입자는 수명 · 화면 밖이면 새 자리로 — 한곳에 몰리지 않게
- 입자 수는 화면 크기에 맞춰 (최대 700)
- 보로노이는 수직 이등분 반평면 자르기로 다각형을 구한다 (픽셀마다 거리 재기 금지 — 느림)
- 트루쳇 타일은 사분원 둘을 마주 보는 모서리에 — 회전해도 이웃과 이어지게

#### 5. 완성 기준 (이게 보이면 성공)
- 흐름장: 색 선들이 털 결처럼 같은 방향으로 흐르고 천천히 결이 바뀐다
- 보로노이: 떠도는 점마다 색 세포가 있고, 경계는 두 점의 한가운데에 있다
- 트루쳇: 선이 끊김 없이 이어진 미로가 가운데부터 물결처럼 뒤집힌다
- 「무늬」 조절로 하나만 고정해 볼 수 있다

#### 6. 진행 방식
- 핵심 코드 위주로, 설명은 짧게. 내 프로젝트에 끼워 넣기 쉬운 함수 · 클래스로 나눠 줘.
- 처음 화면에 바로 결과가 보이게, 그리고 켬/끔(또는 전/후) 비교를 할 수 있게 만들어 줘.
- 그림 · 소리 · 모델 파일이 필요하면 코드로 만든 임시 대체물로 먼저 돌아가게 하고, 진짜 파일로 바꿀 자리를 표시해 줘.
- 마지막에 「확인 방법」(무엇을 보면 성공인지)과 「조절할 값」 목록을 짧게 정리해 줘.
- 답변과 코드 주석은 한국어로 해 줘.

## 원리
- 흐름장: 자리 (x, y) 마다 각도 a = (sin(2.3x + sin(1.9y + 0.25t)·1.7) + sin(2.9y − 0.2t + cos(1.6x)·1.3))·0.6π. 입자는 그 방향으로 조금씩 가고, 지나간 자리를 옅은 바탕색으로 덮어(투명도 0.06) 꼬리가 남는다.
- 보로노이: 씨앗 점마다 화면 사각형에서 시작해, 다른 씨앗과의 수직 이등분선으로 「나에게 더 가까운 쪽」 만 남기며 자른다. 남은 다각형이 그 점의 세포.
- 트루쳇: 타일 한 장에 마주 보는 두 모서리의 사분원 둘. 0° 또는 90° 로 놓으면 선이 늘 이웃 타일로 이어진다. 가운데에서 거리만큼 늦게(4초마다) 90° 씩 뒤집힌다.
- 세 무늬를 6초씩 차례로, 원이 커지며 다음 무늬로 바뀐다.

## 핵심 코드 — 보로노이 반평면 자르기 + 흐름장 한 걸음 + 트루쳇 타일
(발췌: demos/demosMotionB.ts clipHalf() · i122 의 drawFlow · drawTruchet 을 정리)
```ts
/** poly 에서 a 에 더 가까운 쪽(수직 이등분선 기준)만 남긴다 */
function clipHalf(poly: [number, number][], ax: number, ay: number, bx: number, by: number) {
  const mx = (ax + bx) / 2, my = (ay + by) / 2, nx = bx - ax, ny = by - ay;
  const side = (p: [number, number]) => (p[0] - mx) * nx + (p[1] - my) * ny;
  const out: [number, number][] = [];
  for (let i = 0; i < poly.length; i++) {
    const p = poly[i], q = poly[(i + 1) % poly.length];
    const sp = side(p), sq = side(q);
    if (sp <= 0) out.push(p);
    if (sp <= 0 !== sq <= 0) { const k = sp / (sp - sq); out.push([p[0] + (q[0] - p[0]) * k, p[1] + (q[1] - p[1]) * k]); }
  }
  return out;
}
// 세포 i = 화면 사각형을 다른 모든 씨앗과 잘라 남은 것
let cell: [number, number][] = [[0, 0], [w, 0], [w, h], [0, h]];
pts.forEach((o, j) => { if (j !== i && cell.length) cell = clipHalf(cell, pts[i][0], pts[i][1], o[0], o[1]); });

/** 흐름장: 자리마다 각도, 입자는 그 방향으로 */
const field = (x: number, y: number, t: number) =>
  (Math.sin(x * 2.3 + Math.sin(y * 1.9 + t * 0.25) * 1.7) + Math.sin(y * 2.9 - t * 0.2 + Math.cos(x * 1.6) * 1.3)) * Math.PI * 0.6;
trailCtx.fillStyle = 'rgba(13,16,48,.06)'; trailCtx.fillRect(0, 0, w, h); // 옅게 덮어 꼬리
for (const p of parts) {
  const a = field(p.x * 3 * asp, p.y * 3, t), sp = 0.0028 * Math.min(3, dt * 60);
  const nx = p.x + (Math.cos(a) * sp) / asp, ny = p.y + Math.sin(a) * sp;
  trailCtx.beginPath(); trailCtx.moveTo(p.x * w, p.y * h); trailCtx.lineTo(nx * w, ny * h); trailCtx.stroke();
  p.x = nx; p.y = ny;
}

/** 트루쳇 타일: 마주 보는 모서리 사분원 둘, 0° 또는 90° */
g.save(); g.translate(x + s / 2, y + s / 2); g.rotate((ty + fk) * (Math.PI / 2));
g.beginPath(); g.arc(-s / 2, -s / 2, s / 2, 0, Math.PI / 2); g.stroke();
g.beginPath(); g.arc(s / 2, s / 2, s / 2, Math.PI, Math.PI * 1.5); g.stroke();
g.restore();
```

## 흔한 실수 · 확인 목록
- [ ] **흐름장 꼬리를 화면에 바로 쌓으면 다른 그림까지 남는다** — 꼬리 전용 캔버스를 따로 두고, 크기 · 픽셀 비율이 바뀌면 새로 만든다. 다른 무늬로 넘어가면 비운다.
- [ ] **입자 속도를 dt 와 상관없이 두면 느린 기기에서 무늬가 달라진다** — 한 걸음을 dt × 60 (최대 3배)에 비례하게.
- [ ] **보로노이를 픽셀마다 가장 가까운 점으로 칠하면 매우 느리다** — 반평면 자르기로 다각형을 구해 한 번에 채운다 (씨앗 수십 개까지 OK).
- [ ] **트루쳇 사분원을 엉뚱한 모서리에 그리면 선이 이웃과 안 이어진다** — 마주 보는 두 모서리를 중심으로, 반지름 = 타일 반.

## 완성 기준 체크리스트
- [ ] 흐름장: 색 선들이 털 결처럼 같은 방향으로 흐르고 천천히 결이 바뀐다
- [ ] 보로노이: 떠도는 점마다 색 세포가 있고, 경계는 두 점의 한가운데에 있다
- [ ] 트루쳇: 선이 끊김 없이 이어진 미로가 가운데부터 물결처럼 뒤집힌다
- [ ] 「무늬」 조절로 하나만 고정해 볼 수 있다

## 이 기술 정보
- id: `i122` · 분류: 이펙트 · 연출 › 모션 그래픽 · 2D · 난이도 보통 · 폰 부담 보통 (폰 주의) — 흐름장 입자는 화면 크기에 따라 최대 700 (220u² + 60). 보로노이는 씨앗 15개 × 14번 자르기. 트루쳇은 칸 수만큼 호 2개.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i122
- 쓰면 좋을 때: 정해진 그림 없이 화면을 개성 있게 채울 때 / 수학 무늬(타일 · 세포 · 벡터장) 자체를 보여 줄 때
- 쓰지 말 때: 판 바로 뒤 배경 — 무늬가 판보다 시끄럽다. 대신 아주 옅게 · 느리게 / 세포 수천 개 보로노이 — 이 방법은 n² 이다. 대신 들로네 라이브러리 · 셰이더

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### clipHalf — `src/demos/demosMotionB.ts:265`
```ts
function clipHalf(poly: [number, number][], ax: number, ay: number, bx: number, by: number): [number, number][] {
  // a 에 더 가까운 쪽만 남김
  const mx = (ax + bx) / 2;
  const my = (ay + by) / 2;
  const nx = bx - ax;
  const ny = by - ay;
  const side = (p: [number, number]): number => (p[0] - mx) * nx + (p[1] - my) * ny;
  const out: [number, number][] = [];
  for (let i = 0; i < poly.length; i++) {
    const p = poly[i]!;
    const qd = poly[(i + 1) % poly.length]!;
    const sp = side(p);
    const sq = side(qd);
    if (sp <= 0) out.push(p);
    if (sp <= 0 !== sq <= 0) {
      const k = sp / (sp - sq);
      out.push([p[0] + (qd[0] - p[0]) * k, p[1] + (qd[1] - p[1]) * k]);
    }
  }
  return out;
}
```

## 관련 기술
- 먼저 알면 좋은 기술: [움직이는 오로라 그라디언트](https://ai-techstudio.web.app/ai/t/i110.md) `i110` · [잡음으로 그린 성운 (fbm)](https://ai-techstudio.web.app/ai/t/u54.md) `u54`
- 다음에 해 볼 기술: [격자 물결 (stagger grid)](https://ai-techstudio.web.app/ai/t/i101.md) `i101`
- 참고 문서: [Wikipedia — Voronoi diagram](https://en.wikipedia.org/wiki/Voronoi_diagram) · [Wikipedia — Truchet tiles](https://en.wikipedia.org/wiki/Truchet_tiles)
