# AI 꾸러미 — 대칭 그리기 (만화경 · 거울) — Rotational symmetry (N-fold)
> 한 번 그은 토막을 중심 둘레로 N 번 돌리고 거울로 한 번 더 뒤집어 그려, 대칭 축 수에 따라 만화경 · 눈송이 무늬가 저절로 생기게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i318

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

## 주문서

### 만들어 줘: 대칭 그리기 (만화경 · 거울) — Rotational symmetry (N-fold)

#### 1. 목표
대칭 단원 무늬 만들기에 대칭 그리기를 넣어 줘 — 한 번 그으면 중심 둘레로 회전 · 거울 대칭으로 여러 개, 대칭 축 수 조절. 분위기는 무지개 색 만화경.

#### 2. 핵심 기술 용어
- **Rotational symmetry (N-fold)** — N 겹 회전 대칭 — 360° ÷ N 씩 돌려 복사
- **Reflection symmetry (kaleidoscope)** — 거울 대칭 — 위아래 뒤집어 한 번 더
- **2D rotation matrix** — x cos − y sin, x sin + y cos
- **Dihedral group D_N** — 회전 N 개 + 거울 N 개 = 2N 겹

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

#### 4. 조건
- 좌표는 중심 기준으로 바꿔 돌리고 다시 중심을 더하기
- 거울은 돌리기 전에 y 를 뒤집기 (돌린 뒤에 뒤집으면 축이 어긋난다)
- 획은 점 목록으로 기록 — 축 수를 바꾸면 다시 그리기
- 대칭 축을 점선으로 보여 주기

#### 5. 완성 기준 (이게 보이면 성공)
- 한 번 그으면 중심 둘레로 같은 모양이 N 개(거울이면 2N 개) 동시에 그려진다
- 대칭 축 수 1 ~ 12 를 바꾸면 이미 그린 무늬가 그 축 수로 다시 그려진다
- 거울을 끄면 바람개비처럼 한 방향으로 도는 무늬, 켜면 눈송이처럼 좌우가 맞는 무늬
- 오른쪽 아래에 「대칭 6 · 거울」 같은 표시

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

## 원리
- 중심 C 기준 좌표로 바꾼 토막 (a → b) 를 k = 0 … N−1 마다 각 k × 360° ÷ N 만큼 돌려 그린다.
- 회전: (x, y) → (x cos θ − y sin θ, x sin θ + y cos θ). 거울을 켜면 돌리기 전에 y 를 −y 로 뒤집은 토막도 함께.
- 토막이 추가될 때마다 그 토막만 2N 개로 그려 겹에 쌓는다. 축 수나 거울을 바꾸면 기록한 획을 처음부터 다시 그린다.
- 점마다 색상(hue)을 조금씩 바꾸면 만화경처럼 무지개 무늬.

## 핵심 코드 — 토막 하나를 회전 · 거울로 2N 개 그리기
(발췌: demos/demosDrawTools.ts symmetry() seg() 를 정리)
```ts
type P = { x: number; y: number };
let N = 6, mirror = true;
function seg(g: CanvasRenderingContext2D, C: P, a: P, b: P, hue: number) {
  g.strokeStyle = 'hsl(' + (hue % 360) + ',78%,50%)';
  g.lineWidth = 2.2; g.lineCap = 'round';
  g.beginPath();
  for (let k = 0; k < N; k++) {
    const an = (k / N) * Math.PI * 2;
    const c = Math.cos(an), s = Math.sin(an);
    for (const m of mirror ? [1, -1] : [1]) {   // 거울: 돌리기 전에 y 뒤집기
      const ax = a.x - C.x, ay = (a.y - C.y) * m;
      const bx = b.x - C.x, by = (b.y - C.y) * m;
      g.moveTo(C.x + ax * c - ay * s, C.y + ax * s + ay * c);
      g.lineTo(C.x + bx * c - by * s, C.y + bx * s + by * c);
    }
  }
  g.stroke();
}
// 그리는 중: 새 점이 오면 seg(layer, C, 앞 점, 새 점, 획 색 + 점 번호 × 0.8)
// N · 거울을 바꾸면 기록한 모든 획을 처음부터 seg 로 다시 그린다
```

## 흔한 실수 · 확인 목록
- [ ] **거울을 돌린 뒤에 뒤집으면 무늬가 중심에서 어긋난다** — 중심 기준 좌표에서 y 를 먼저 뒤집고 그 다음 돌린다.
- [ ] **토막마다 beginPath · stroke 를 2N 번 하면 느리다** — 한 토막의 2N 개 선을 한 경로에 moveTo · lineTo 로 모아 stroke 한 번.
- [ ] **축 수를 바꿨는데 이미 그린 무늬가 그대로다** — 획을 점 목록으로 기록해 두고, 바꿀 때 겹을 지우고 다시 그린다.

## 완성 기준 체크리스트
- [ ] 한 번 그으면 중심 둘레로 같은 모양이 N 개(거울이면 2N 개) 동시에 그려진다
- [ ] 대칭 축 수 1 ~ 12 를 바꾸면 이미 그린 무늬가 그 축 수로 다시 그려진다
- [ ] 거울을 끄면 바람개비처럼 한 방향으로 도는 무늬, 켜면 눈송이처럼 좌우가 맞는 무늬
- [ ] 오른쪽 아래에 「대칭 6 · 거울」 같은 표시

## 이 기술 정보
- id: `i318` · 분류: 2D · 화면 › 그리기 도구 · 2D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 토막 하나당 선 2N 개 (N = 12, 거울이면 24). 그리는 중에는 새 토막만.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i318
- 쓰면 좋을 때: 대칭 · 도형 단원 — 회전 대칭과 선대칭 차이 보여 주기 / 아무렇게나 그어도 예쁜 무늬가 나오는 꾸미기 놀이
- 쓰지 말 때: 대칭이 한 축(좌우)만 필요한 그림 — N = 1 + 거울로 충분 / 획이 아주 많은 그림에서 축 수를 자주 바꾸기 — 다시 그리기가 축 수 × 2 배로 늘어난다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### symmetry — `src/demos/demosDrawTools.ts:2253`
```ts
function symmetry(env: Env): Tool {
  const layer = mkLayer(env.R);
  const C = pt(PAPER.x + PAPER.w / 2, PAPER.y + PAPER.h / 2 + 2);
  type St = { pts: P[]; hue: number };
  let strokes: St[] = [];
  let cur: St | null = null;
  let N = 6;
  let mirror = true;
  let userN = false;
  let hue = 200;
  function seg(g: G, a: P, b: P, h: number): void {
    g.strokeStyle = `hsl(${h % 360},78%,50%)`;
    g.lineWidth = 2.2;
    g.beginPath();
    for (let k = 0; k < N; k++) {
      const an = (k / N) * Math.PI * 2;
      const c = Math.cos(an);
      const s = Math.sin(an);
      for (const m of mirror ? [1, -1] : [1]) {
        const ax = a.x - C.x;
        const ay = (a.y - C.y) * m;
        const bx = b.x - C.x;
        const by = (b.y - C.y) * m;
        g.moveTo(C.x + ax * c - ay * s, C.y + ax * s + ay * c);
        g.lineTo(C.x + bx * c - by * s, C.y + bx * s + by * c);
      }
    }
    g.stroke();
  }
  function rebuild(): void {
    clearLayer(layer);
    for (const st of strokes) for (let i = 1; i < st.pts.length; i++) seg(layer.g, st.pts[i - 1]!, st.pts[i]!, st.hue + i * 0.8);
  }
  return {
    cursor: () => 'pencil',
    color: () => `hsl(${hue % 360},78%,50%)`,
    begin(p) {
      hue += 47;
      cur = { pts: [p], hue };
      strokes.push(cur);
    },
    drag(p) {
      if (!cur) return;
      const l = cur.pts[cur.pts.length - 1]!;
      if (dist(l, p) < 0.8) return;
      cur.pts.push(p);
      seg(layer.g, l, p, cur.hue + cur.pts.length * 0.8);
    },
    end() {
      cur = null;
    },
    clear() {
      strokes = [];
      cur = null;
      clearLayer(layer);
    },
    draw(g) {
      clipPaper(g, PAPER);
      g.strokeStyle = 'rgba(47,111,224,0.18)';
      g.lineWidth = 0.7;
      g.setLineDash([3, 3]);
      const lines = mirror ? N : N;
      for (let k = 0; k < lines; k++) {
        const an = mirror ? (k / N) * Math.PI : (k / N) * Math.PI * 2;
        g.beginPath();
        g.moveTo(C.x, C.y);
        g.lineTo(C.x + Math.cos(an) * 200, C.y + Math.sin(an) * 200);
        if (mirror) {
          g.moveTo(C.x, C.y);
          g.lineTo(C.x - Math.cos(an) * 200, C.y - Math.sin(an) * 200);
        }
        g.stroke();
      }
      g.setLineDash([]);
      g.drawImage(layer.c, 0, 0, W, H);
      g.fillStyle = '#2f6fe0';
      g.beginPath();
      g.arc(C.x, C.y, 1.6, 0, Math.PI * 2);
      g.fill();
      pill(g, 344, 224, `대칭 ${N}${mirror ? ' · 거울' : ''}`, 7.5, 'rgba(29,39,71,0.85)', '#fff');
    },
    script(loop) {
      if (!userN) {
        const cyc = [6, 8, 5, 12];
        N = cyc[loop % cyc.length]!;
        mirror = loop % 4 !== 2;
        rebuild();
      }
      const r = rng(loop * 5 + 3);
      const acts: Act[] = [WAIT(0.2)];
      for (let k = 0; k < 3; k++) {
        const ph = r() * 6;
        const th0 = r() * Math.PI * 2;
        const base = 30 + k * 22;
        const o: P[] = [];
        for (let s = 0; s <= 2.6; s += 0.04) {
          const rr = base + 26 * Math.sin(s * 1.7 + ph);
          const th = th0 + 0.75 * s + 0.35 * Math.sin(2.3 * s + ph);
          o.push(pt(C.x + Math.cos(th) * rr, C.y + Math.sin(th) * rr));
        }
        acts.push(S(o, 150), WAIT(0.15));
      }
      acts.push(WAIT(1.6));
      return acts;
    },
    controls: [
      {
        type: 'range',
        label: '대칭 축 수',
        min: 1,
        max: 12,
        step: 1,
        value: 6,
        on: (v) => {
          N = v;
          userN = true;
          rebuild();
        },
      },
      {
        type: 'toggle',
        label: '거울 (뒤집기)',
        value: true,
        on: (v) => {
          mirror = v;
          userN = true;
          rebuild();
        },
      },
    ],
  };
}
```

### i318 견본 항목 — `src/demos/demosDrawTools.ts:3440`
```ts
  i318: dom('한 번 그으면 회전 · 거울 대칭으로 여러 개 — 대칭 축 수에 따라 만화경 무늬', (e) => symmetry(e))
```

## 관련 기술
- 먼저 알면 좋은 기술: [되돌리기 · 다시 하기 (명령 기록)](https://ai-techstudio.web.app/ai/t/i317.md) `i317`
- 다음에 해 볼 기술: [펜 도구 (베지에 조절점)](https://ai-techstudio.web.app/ai/t/i319.md) `i319`
