# AI 꾸러미 — 코드로 그린 SVG 아이콘 · 도장 — Inline SVG
> 아이콘 · 마스코트 · 도장을 그림 파일 대신 SVG 코드로 그려, 글자가 번역되고 크기가 바뀌어도 또렷하며 속성만 바꿔 움직인다.  
> 견본: https://ai-techstudio.web.app/#t/u57

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

## 주문서

### 만들어 줘: 코드로 그린 SVG 아이콘 · 도장 — Inline SVG

#### 1. 목표
통과 · 반려 도장을 그림 파일 없이 SVG 코드로 그려 줘 — 글자는 <text> 로 두어 번역되게, 모양은 코드로 계산해서. 그림체는 굵은 선 귀여운 아이콘.

#### 2. 핵심 기술 용어
- **Inline SVG** — HTML 안에 직접 쓴 벡터 그림
- **Procedural SVG path (d attribute)** — 코드로 계산해 만든 경로 — 톱니 · 별
- **SVG transform · setAttribute animation** — 속성 값만 바꿔 움직이기
- **viewBox · preserveAspectRatio** — 그림 좌표계 · 맞춤 방식

#### 3. 환경
- 플랫폼: TypeScript (브라우저), 라이브러리 없이 — 화면과 떨어진 순수 함수로
- 화면: 2D · 브라우저 — PC · 폰 모두

#### 4. 조건
- 그림에 글자를 넣지 않는다 — 글자는 모두 <text> (번역 키로 바꿀 수 있게)
- viewBox 를 정하고 width · height 는 100% (크기는 바깥 상자가 정함)
- 규칙 있는 모양은 코드로 점을 계산 (손으로 좌표를 늘어놓지 않기)
- 글자 · 색 같은 값은 바뀔 때만 다시 넣기 (매 프레임 textContent 쓰기 금지)

#### 5. 완성 기준 (이게 보이면 성공)
- 화면을 크게 · 작게 해도 선이 또렷하다
- 언어를 바꾸면 도장 글자가 그 언어로 바뀐다
- 별의 꼭짓점 수 n 을 바꾸면 모양이 바로 다시 계산된다
- 도장이 쾅 찍힐 때 크기 1.9 → 1 로 줄며 살짝 흔들린다

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

## 원리
- SVG 는 점 · 선 · 곡선으로 그린 벡터라 크게 해도 흐려지지 않는다. viewBox 로 그림 좌표를 정하면 화면 크기와 상관없이 맞춰진다.
- 글자를 그림에 굽지 않고 <text> 로 두면 번역 사전으로 바로 바뀐다 — 도장의 「통과/반려」 처럼.
- 톱니 · 별 같은 규칙 있는 모양은 각도를 돌며 점을 계산해 path 의 d · polygon 의 points 문자열로 만든다.
- 움직임은 매 프레임 setAttribute 로 transform · ry · points 값만 바꾼다 — 바뀐 글자 · 색은 바뀔 때만 넣는다.

## 핵심 코드 — 코드로 만든 톱니 · 별 경로 + 쾅 찍히는 도장
(발췌: demos/demosDraw2d.ts u57 (톱니 경로 · update) 를 정리)
```ts
// 톱니: 이 4개 중 2개는 바깥(24), 2개는 안쪽(17) 반지름
function gearPath(teeth = 8, rOut = 24, rIn = 17) {
  let d = '';
  for (let i = 0; i < teeth * 4; i++) {
    const a = (i / (teeth * 4)) * Math.PI * 2;
    const r = i % 4 < 2 ? rOut : rIn;
    d += (i ? 'L' : 'M') + (Math.cos(a) * r).toFixed(2) + ' ' + (Math.sin(a) * r).toFixed(2);
  }
  return d + 'Z';
}
// 별: 꼭짓점 n 개, 바깥 30 · 안쪽 13
function starPoints(n: number, spin: number) {
  const pts: string[] = [];
  for (let i = 0; i < n * 2; i++) {
    const a = -Math.PI / 2 + (i * Math.PI) / n + spin;
    const r = i % 2 ? 13 : 30;
    pts.push((Math.cos(a) * r).toFixed(2) + ',' + (Math.sin(a) * r).toFixed(2));
  }
  return pts.join(' ');
}
gear.setAttribute('d', gearPath());

let lastWord = '';
function update(t: number) {
  // 도장: 2.4초마다 1.9 배 → 1 배로 쾅, 닿은 뒤 0.18초 흔들
  const ph = t % 2.4;
  const k = Math.min(1, ph / 0.22);
  const sc = 1.9 - 0.9 * (1 - (1 - k) * (1 - k));
  const shake = ph > 0.22 && ph < 0.4 ? Math.sin(ph * 120) * 2 : 0;
  stamp.setAttribute('transform', 'translate(' + shake + ' 0) rotate(-12) scale(' + sc + ')');
  const word = Math.floor(t / 2.4) % 2 ? $t('반려') : $t('통과'); // 글자는 번역 키로
  if (word !== lastWord) { lastWord = word; stampText.textContent = word; }
  star.setAttribute('points', starPoints(5 + (Math.floor(t / 1.5) % 4), t * 0.3));
  gear.setAttribute('transform', 'rotate(' + ((t * 60) % 360) + ')');
}
```

## 흔한 실수 · 확인 목록
- [ ] **도장 · 단추 글씨를 그림 파일에 넣으면 번역이 안 된다** — 그림에는 글자를 넣지 않고 SVG <text> 로 그린다 — 수학 검문소에서 사용자와 정한 규칙.
- [ ] **매 프레임 textContent 를 다시 쓰면 글자 레이아웃이 계속 다시 계산된다** — 값이 바뀔 때만 넣는다 (lastWord 비교).
- [ ] **toFixed 없이 긴 소수를 경로에 넣으면 문자열이 커진다** — 좌표는 toFixed(2) 정도로 줄인다.
- [ ] **SVG 글꼴이 늦게 와서 첫 화면 글자가 다른 글꼴로 보인다** — document.fonts.ready 뒤에 한 번 더 그리거나, 글꼴을 미리 불러 둔다.

## 완성 기준 체크리스트
- [ ] 화면을 크게 · 작게 해도 선이 또렷하다
- [ ] 언어를 바꾸면 도장 글자가 그 언어로 바뀐다
- [ ] 별의 꼭짓점 수 n 을 바꾸면 모양이 바로 다시 계산된다
- [ ] 도장이 쾅 찍힐 때 크기 1.9 → 1 로 줄며 살짝 흔들린다

## 이 기술 정보
- id: `u57` · 분류: 2D · 화면 › 2D 그리기 · 2D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 요소 수십 개 · 속성 몇 개를 바꾸는 정도는 가볍다. 매 프레임 수백 요소를 바꾸면 레이아웃 비용이 쌓인다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/u57
- 쓰면 좋을 때: 번역되는 글자가 든 도장 · 이름표 · 배너 / 크기가 여러 가지인 아이콘 / 모양이 수(꼭짓점 수 등)에 따라 바뀌는 그림
- 쓰지 말 때: 사진 · 붓질 질감 그림 — 벡터로는 무겁고 어색하다. 대신 그림 파일 / 요소 수천 개를 매 프레임 바꾸기 — DOM 이 느려진다. 대신 캔버스

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### u57 견본 항목 — `src/demos/demosDraw2d.ts:807`
```ts
  u57: {
    kind: 'dom',
    caption: '그림 파일 없이 코드로 만든 SVG — 눈 깜빡이는 마스코트 · 쾅 찍히는 도장 · 꼭짓점 수가 바뀌는 별 · 도는 톱니',
    make(box) {
      const root = domRoot(
        box,
        'd2-svg',
        `.d2-svg{background:radial-gradient(circle at 50% 30%,#2c3f86,#0d1230)}
         .d2-svg svg{width:100%;height:100%;display:block}
         .d2-svg text{font-family:${TF}}`,
        `<svg viewBox="0 0 320 200" preserveAspectRatio="xMidYMid meet">
          <g id="bot" transform="translate(70 104)">
            <line x1="0" y1="-52" x2="0" y2="-38" stroke="#cfd8ff" stroke-width="3"/>
            <circle class="ball" cx="0" cy="-56" r="6" fill="#ff6fa8"/>
            <rect x="-38" y="-40" width="76" height="62" rx="18" fill="#7fbcff" stroke="#fff" stroke-width="3"/>
            <rect x="-28" y="-28" width="56" height="34" rx="12" fill="#14204a"/>
            <ellipse class="eye" cx="-12" cy="-12" rx="6" ry="7" fill="#7cf0ff"/>
            <ellipse class="eye" cx="12" cy="-12" rx="6" ry="7" fill="#7cf0ff"/>
            <path class="mouth" d="M-9 0 Q0 7 9 0" stroke="#7cf0ff" stroke-width="3" fill="none" stroke-linecap="round"/>
            <circle cx="-30" cy="6" r="5" fill="#ff9ec4" opacity=".8"/><circle cx="30" cy="6" r="5" fill="#ff9ec4" opacity=".8"/>
            <rect x="-26" y="26" width="52" height="30" rx="10" fill="#5a9cf0" stroke="#fff" stroke-width="3"/>
          </g>
          <g id="stamp" transform="translate(170 100)">
            <g class="st">
              <circle r="40" fill="none" stroke="#e8364a" stroke-width="5"/>
              <circle r="32" fill="none" stroke="#e8364a" stroke-width="2"/>
              <text class="word" y="10" text-anchor="middle" font-size="28" fill="#e8364a">통과</text>
            </g>
          </g>
          <g transform="translate(262 62)"><polygon class="star" fill="#ffd23f" stroke="#fff" stroke-width="3" stroke-linejoin="round"/>
            <text class="n" y="44" text-anchor="middle" font-size="13" fill="#ffd23f">n = 5</text></g>
          <g transform="translate(262 150)"><path class="gear" fill="#9ae6b4" stroke="#fff" stroke-width="2.5" stroke-linejoin="round"/><circle r="7" fill="#0d1230"/></g>
        </svg>`,
      );
      const eyes = qa<SVGEllipseElement>(root, '.eye');
      const ball = q<SVGCircleElement>(root, '.ball');
      const st = q<SVGGElement>(root, '.st');
      const word = q<SVGTextElement>(root, '.word');
      const stampCircles = qa<SVGCircleElement>(root, '.st circle');
      const starEl = q<SVGPolygonElement>(root, '.star');
      const nEl = q<SVGTextElement>(root, '.n');
      const gear = q<SVGPathElement>(root, '.gear');
      let lastN = 0;
      let lastWord = '';
      // 톱니 경로를 코드로
      {
        let d = '';
        const teeth = 8;
        for (let i = 0; i < teeth * 4; i++) {
          const a = (i / (teeth * 4)) * Math.PI * 2;
          const r = i % 4 < 2 ? 24 : 17;
          d += `${i ? 'L' : 'M'}${(Math.cos(a) * r).toFixed(2)} ${(Math.sin(a) * r).toFixed(2)}`;
        }
        gear.setAttribute('d', d + 'Z');
      }
      return {
        update(t) {
          const blink = t % 3 < 0.14 ? 0.6 : 7;
          for (const e of eyes) e.setAttribute('ry', String(blink));
          ball.setAttribute('cy', String(-56 + Math.sin(t * 4) * 3));
          const ph = t % 2.4;
          const k = clamp01(ph / 0.22);
          const sc = 1.9 - 0.9 * (1 - (1 - k) * (1 - k));
          const shake = ph > 0.22 && ph < 0.4 ? Math.sin(ph * 120) * 2 : 0;
          st.setAttribute('transform', `translate(${shake} 0) rotate(-12) scale(${sc})`);
          st.setAttribute('opacity', String(ph > 2.1 ? clamp01((2.4 - ph) / 0.3) : clamp01(k * 1.5)));
          const w2 = Math.floor(t / 2.4) % 2 ? '반려' : '통과';
          if (w2 !== lastWord) {
            lastWord = w2;
            word.textContent = w2;
            const col = w2 === '통과' ? '#2fbf71' : '#e8364a';
            word.setAttribute('fill', col);
            for (const c of stampCircles) c.setAttribute('stroke', col);
          }
          const n = 5 + (Math.floor(t / 1.5) % 4);
          if (n !== lastN) {
            lastN = n;
            nEl.textContent = `n = ${n}`;
          }
          const pts: string[] = [];
          for (let i = 0; i < n * 2; i++) {
            const a = -Math.PI / 2 + (i * Math.PI) / n + t * 0.3;
            const r = i % 2 ? 13 : 30;
            pts.push(`${(Math.cos(a) * r).toFixed(2)},${(Math.sin(a) * r).toFixed(2)}`);
          }
          starEl.setAttribute('points', pts.join(' '));
          gear.setAttribute('transform', `rotate(${(t * 60) % 360})`);
        },
      };
    },
  }
```

## 관련 기술
- 다음에 해 볼 기술: [SVG 필터 (종이 결 feTurbulence)](https://ai-techstudio.web.app/ai/t/u58.md) `u58` · [SVG 선 그리기 (stroke-dashoffset) · 손떨림](https://ai-techstudio.web.app/ai/t/i80.md) `i80`
- 참고 문서: [MDN — SVG <path> d 속성](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d) · [MDN — SVG viewBox](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox)
