# AI 꾸러미 — 손글씨 글자 흔들기 — Handwriting jitter (per-glyph transform)
> 글자를 하나씩 따로 그리며 기울기 · 크기 · 기준선 · 간격을 시드 난수로 살짝 달리해, 같은 글꼴이 손으로 쓴 글씨처럼 보이게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i337

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

## 주문서

### 만들어 줘: 손글씨 글자 흔들기 — Handwriting jitter (per-glyph transform)

#### 1. 목표
말풍선 대사의 글자를 손글씨처럼 바꿔 줘 — 글자마다 기울기 · 크기 · 기준선 · 간격을 조금씩 달리하고, 같은 글자도 매번 다르게. 분위기는 공책에 쓴 아이 글씨.

#### 2. 핵심 기술 용어
- **Handwriting jitter (per-glyph transform)** — 글자마다 따로 돌리고 키우고 올리기
- **Seeded random (deterministic jitter)** — 시드 고정 난수 — 매 프레임 같은 흔들림
- **Canvas fillText + transform (skew)** — 캔버스 글자 + 기울임 변환
- **Wobbly outline** — 말풍선 테두리도 손으로 그린 듯 흔들기

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

#### 4. 조건
- 난수는 시드 고정 (매 프레임 Math.random 금지 — 글자가 떨린다)
- 흔들림 폭은 작게: 회전 ±0.11 라디안 · 크기 ±12% 안팎 — 넘으면 읽기 어렵다
- 글자 간격은 실제 글자 폭(measureText) 기준으로 계산
- 가운데 · 오른쪽 맞춤도 흔든 뒤의 전체 폭으로 계산

#### 5. 완성 기준 (이게 보이면 성공)
- 같은 「7」 이 줄 안에서 매번 다르게 보인다
- 세기를 0 으로 하면 반듯한 타자 글씨, 1 이면 손글씨로 부드럽게 바뀐다
- 「다른 손글씨」 단추로 시드를 바꾸면 다른 글씨체처럼 보인다
- 기울기 · 크기 · 기준선 슬라이더를 따로 조절할 수 있다

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

## 원리
- 문자열을 글자 하나씩 나눠, 글자마다 난수 6개를 뽑는다: 회전(±0.11 라디안) · 크기(±12%) · 기준선(±글자 크기 8.5%) · 기울임 · 가로 폭 · 다음 글자까지 간격.
- 난수는 시드 고정 — 같은 시드면 매 프레임 똑같은 손글씨, 시드를 바꾸면 다른 사람 글씨.
- 줄 전체에도 작은 오르막 · 내리막(slope)을 줘 기준선이 한 방향으로 살짝 흐르게 한다.
- 세기 k(0~1)로 반듯한 글자 ↔ 손글씨 사이를 섞을 수 있다 — 글자마다 k 를 늦게 주면 앞에서부터 흐트러진다.

## 핵심 코드 — 글자마다 흔들어 쓰기
(발췌: demos/demosHandB.ts handText() 를 정리)
```ts
function rng(seed: number): () => number {
  let s = Math.abs(Math.floor(seed * 9301 + 49297)) % 2147483647;
  if (s <= 0) s += 2147483646;
  return () => (s = (s * 16807) % 2147483647) / 2147483647;
}

function handText(g: CanvasRenderingContext2D, str: string, x: number, y: number, size: number, seed: number, k = 1, ink = '#24324a') {
  g.font = '600 ' + size + 'px Pretendard, sans-serif';
  const r = rng(seed);
  const slope = (r() - 0.5) * 0.05;                  // 줄 전체가 살짝 오르막 · 내리막
  const gl = [...str].map((ch) => {
    const w = g.measureText(ch).width;
    return {
      ch, w,
      rot: (r() - 0.5) * 0.22,                       // 기울기
      sc: 1 + (r() - 0.5) * 0.24,                    // 크기
      dy: (r() - 0.5) * size * 0.17,                 // 기준선
      sk: (r() - 0.5) * 0.26,                        // 기울임(skew)
      adv: w * (1 + (r() - 0.5) * 0.2),              // 다음 글자까지
      sx: 1 + (r() - 0.5) * 0.12,                    // 가로 폭
    };
  });
  const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
  g.textAlign = 'center';
  let cx = x;
  for (const q of gl) {
    const adv = lerp(q.w, q.adv, k);
    const mx = cx + adv / 2;
    if (q.ch !== ' ') {
      g.save();
      g.translate(mx, y + (q.dy + slope * (mx - x)) * k);
      g.rotate(q.rot * k);
      g.transform(lerp(1, q.sc * q.sx, k), 0, q.sk * k, lerp(1, q.sc, k), 0, 0);
      g.fillStyle = ink;
      g.fillText(q.ch, 0, 0);
      g.restore();
    }
    cx += adv;
  }
  g.textAlign = 'left';
}
```

## 흔한 실수 · 확인 목록
- [ ] **매 프레임 Math.random 으로 흔들면 글자가 덜덜 떨린다** — 시드 고정 난수로 글자마다 값을 정해 두고, 바꿀 때만 시드를 바꾼다.
- [ ] **문자열을 한 번에 fillText 하면 글자별로 못 흔든다** — [...str] 로 글자를 나누고(한글 · 이모지도 한 글자씩) 하나씩 그린다.
- [ ] **흔든 뒤 폭을 모르면 가운데 맞춤이 어긋난다** — 먼저 글자마다 간격을 더해 전체 폭을 구하고, 그 절반만큼 왼쪽에서 시작한다.
- [ ] **흔들림을 너무 크게 하면 아이들이 못 읽는다** — 회전 · 크기 흔들림은 작게, 글자 수가 많을수록 더 줄인다.

## 완성 기준 체크리스트
- [ ] 같은 「7」 이 줄 안에서 매번 다르게 보인다
- [ ] 세기를 0 으로 하면 반듯한 타자 글씨, 1 이면 손글씨로 부드럽게 바뀐다
- [ ] 「다른 손글씨」 단추로 시드를 바꾸면 다른 글씨체처럼 보인다
- [ ] 기울기 · 크기 · 기준선 슬라이더를 따로 조절할 수 있다

## 이 기술 정보
- id: `i337` · 분류: 2D · 화면 › 손그림 그림체 (2D) · 2D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 글자마다 save · transform · fillText 한 번. 수백 글자까지는 폰도 문제없다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i337
- 쓰면 좋을 때: 말풍선 · 메모 · 칠판처럼 사람이 쓴 글 / 손글씨 글꼴을 따로 받지 않고 여러 언어에 같은 느낌을 낼 때
- 쓰지 말 때: 긴 설명 글 · 규칙 글 — 읽기 힘들다. 제목 · 짧은 대사에만 / 점수처럼 매 프레임 바뀌는 숫자 — 바뀔 때마다 글자가 튀어 보인다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### handText — `src/demos/demosHandB.ts:594`
```ts
function handText(g: G, str: string, x: number, y: number, size: number, seed: number, j: Jit | number, st: HandStyle): { x0: number; x1: number } {
  const jj: Jit = typeof j === 'number' ? { rot: j, size: j, base: j, space: j } : j;
  const chars = [...str];
  g.font = `${st.weight ?? 600} ${size}px ${F}`;
  const r = rng(seed);
  const slope = (r() - 0.5) * 0.05;
  const n = chars.length;
  const kOf = (i: number): number => (typeof st.k === 'function' ? st.k(i, n) : st.k ?? 1);
  const gl = chars.map((ch) => {
    const w = g.measureText(ch).width;
    const a = r();
    const b = r();
    const c = r();
    const d = r();
    const e = r();
    const q = r();
    return {
      ch,
      w,
      rot: (a - 0.5) * 0.22 * jj.rot,
      sc: 1 + (b - 0.5) * 0.24 * jj.size,
      dy: (c - 0.5) * size * 0.17 * jj.base,
      sk: (d - 0.5) * 0.26 * jj.rot,
      adv: w * (1 + (e - 0.5) * 0.2 * (jj.space ?? jj.size)),
      sx: 1 + (q - 0.5) * 0.12 * jj.size,
    };
  });
  let total = 0;
  gl.forEach((q, i) => (total += lerp(q.w, q.adv, kOf(i))));
  const x0 = st.al === 'center' ? x - total / 2 : st.al === 'right' ? x - total : x;
  g.textAlign = 'center';
  g.textBaseline = 'alphabetic';
  let cx = x0;
  gl.forEach((q, i) => {
    const k = kOf(i);
    const adv = lerp(q.w, q.adv, k);
    const mx = cx + adv / 2;
    if (q.ch !== ' ') {
      const dy = (q.dy + slope * (mx - x0) * jj.base) * k;
      g.save();
      g.translate(mx, y + dy);
      g.rotate(q.rot * k);
      g.transform(lerp(1, q.sc * q.sx, k), 0, q.sk * k, lerp(1, q.sc, k), 0, 0);
      g.fillStyle = st.fill;
      g.fillText(q.ch, 0, 0);
      if (st.stroke) {
        g.strokeStyle = st.fill;
        g.lineWidth = st.stroke;
        g.lineJoin = 'round';
        g.strokeText(q.ch, 0, 0);
      }
      g.restore();
    }
    cx += adv;
  });
  g.textAlign = 'left';
  return { x0, x1: cx };
}
```

## 관련 기술
- 먼저 알면 좋은 기술: [선 들끓기 (line boil)](https://ai-techstudio.web.app/ai/t/i323.md) `i323`
- 다음에 해 볼 기술: [칠판 · 분필 (번짐 · 지운 자국)](https://ai-techstudio.web.app/ai/t/i333.md) `i333` · [설계도 · 모눈 도면 (블루프린트)](https://ai-techstudio.web.app/ai/t/i339.md) `i339`
