# AI 꾸러미 — 글자 차례 등장 (키네틱 타이포) — Kinetic typography
> 글자를 하나씩 따로 그리고 차례 지연(stagger)을 주어, 튀어 올라 통 내려앉았다가 차례로 떨어지는 움직이는 제목을 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i90

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

## 주문서

### 만들어 줘: 글자 차례 등장 (키네틱 타이포) — Kinetic typography

#### 1. 목표
「참 잘했어요!」 같은 정답 문구를 글자마다 차례로 튀어 오르는 키네틱 타이포로 보여 줘 — 글자 사이 지연을 조절할 수 있게. 분위기는 밝고 통통 튀는 만화.

#### 2. 핵심 기술 용어
- **Kinetic typography** — 움직이는 글자 연출
- **Stagger (per-character delay)** — 글자마다 조금씩 늦게 시작
- **easeOutBack overshoot** — 목표를 살짝 넘었다 돌아오는 이징

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

#### 4. 조건
- 줄 자리는 글자를 다 놓았을 때 기준으로 미리 정한다 (움직이는 중에 옆 글자가 밀리지 않게)
- 글자마다 지연 = 순서 × 간격, 간격은 조절판으로 (견본 0.09초, 0.02 ~ 0.25)
- 크기 · 회전은 글자 아래쪽 근처를 축으로 (내려앉는 느낌)
- 한 주기(견본 4.3초)로 끝없이 반복 — 처음과 끝 모두 빈 화면

#### 5. 완성 기준 (이게 보이면 성공)
- 글자가 왼쪽부터 하나씩 튀어 올라 살짝 넘쳤다 자리 잡는다
- 내려앉는 순간 글자가 가로로 퍼졌다 통통 돌아온다
- 「글자 사이 지연」 슬라이더를 키우면 물결이 느리게, 줄이면 한꺼번에 들어온다
- 다 들어오면 부제 「정답!」 이 톡 나오고, 잠시 뒤 글자가 차례로 떨어진다

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

## 원리
- 문장을 글자 배열로 나누고, 글자 폭(measureText)을 더해 가운데 정렬 자리를 먼저 정한다.
- 글자 i 는 시작 시간이 0.2 + i × 지연 — 같은 움직임이 차례로 번져 물결처럼 보인다.
- 들어올 때: 위에서 내려오며 크기 0.2 → 1 (easeOutBack 로 살짝 넘침), 비스듬히 기울었다 바로 섬.
- 내려앉은 뒤 0.5초: 지수로 줄어드는 사인(e^(−9t)·sin 30t)으로 가로로 퍼지고 세로로 눌렸다 통통.
- 나갈 때: 시간의 제곱만큼 아래로 떨어지며(중력) 돌고 흐려진다.

## 핵심 코드 — 글자마다 지연 · 넘침 · 착지 찌그러짐 · 떨어지기
(발췌: demos/demosMotionA.ts i90 의 draw 를 정리)
```ts
const outBack = (x: number, s = 1.9) => { const v = Math.min(1, Math.max(0, x)) - 1; return 1 + (s + 1) * v * v * v + s * v * v; };
const p = tt % 4.3; // 한 주기
chars.forEach((c, i) => {
  const cx = xs[i];                          // 미리 정한 글자 가운데 자리
  const tin = p - (0.2 + i * gap);           // 들어오기 시작 (gap = 0.09초)
  const tout = p - (3.4 + i * gap * 0.7);    // 나가기 시작
  if (tin < 0) return;
  const k = Math.min(1, tin / 0.55);
  let y = 78 - (1 - outBack(k, 2.4)) * 50;
  let sc = 0.2 + (1 - 0.2) * outBack(k, 2.2);
  let rot = (1 - (1 - Math.pow(1 - k, 3))) * (i % 2 ? 0.6 : -0.6);
  let a = Math.min(1, tin / 0.12);
  let sx = 1, sy = 1;
  const land = tin - 0.35;                   // 착지 뒤 0.5초 찌그러짐
  if (land > 0 && land < 0.5) { const q = Math.exp(-land * 9) * Math.sin(land * 30); sx = 1 + q * 0.25; sy = 1 - q * 0.25; }
  if (tout > 0) { y += tout * tout * 260; rot += tout * (i % 2 ? 2.2 : -2.2); a *= Math.max(0, 1 - tout * 1.6); sc *= 1 - tout * 0.2; }
  if (a <= 0) return;
  g.save();
  g.globalAlpha = a;
  g.translate(cx, y + 18); g.rotate(rot); g.scale(sc * sx, sc * sy); g.translate(0, -18); // 아래쪽 축
  g.lineJoin = 'round';
  g.strokeStyle = '#1a1446'; g.lineWidth = 6; g.strokeText(c, 0, 0);
  g.fillStyle = cols[i % cols.length]; g.fillText(c, 0, 0);
  g.restore();
});
```

## 흔한 실수 · 확인 목록
- [ ] **글자를 움직일 때마다 줄을 다시 재면 옆 글자가 출렁인다** — 자리는 다 놓인 문장 기준으로 한 번 정하고, 움직임은 그 자리를 축으로만 준다.
- [ ] **가운데를 축으로 크기를 바꾸면 공중에서 부푸는 것처럼 보인다** — 글자 아래쪽(견본 +18)으로 옮겨 놓고 크기 · 회전을 준 뒤 되돌린다.
- [ ] **모든 글자가 같은 쪽으로 기울면 단조롭다** — 홀짝 글자를 반대로 (i % 2 ? 0.6 : −0.6).

## 완성 기준 체크리스트
- [ ] 글자가 왼쪽부터 하나씩 튀어 올라 살짝 넘쳤다 자리 잡는다
- [ ] 내려앉는 순간 글자가 가로로 퍼졌다 통통 돌아온다
- [ ] 「글자 사이 지연」 슬라이더를 키우면 물결이 느리게, 줄이면 한꺼번에 들어온다
- [ ] 다 들어오면 부제 「정답!」 이 톡 나오고, 잠시 뒤 글자가 차례로 떨어진다

## 이 기술 정보
- id: `i90` · 분류: 이펙트 · 연출 › 모션 그래픽 · 2D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 글자 10개 안팎을 매 프레임 그리는 정도라 가볍다. 글자마다 테두리 · 그림자 · 하이라이트 4번 그리기.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i90
- 쓰면 좋을 때: 정답 · 성공 순간을 크게 축하할 때 / 제목 화면 · 단계 시작처럼 글자가 주인공인 순간
- 쓰지 말 때: 읽어야 하는 긴 설명 — 대신 타자기처럼 한 글자씩 쳐지기 / 자주 반복되는 작은 알림 — 매번 튀면 피곤하다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i90 — `src/demos/demosMotionA.ts:141`
```ts
const i90 = {
  kind: '2d' as const,
  caption: '글자마다 조금씩 늦게(stagger) 튀어 올라 통 — 잠시 뒤 차례로 떨어져요',
  make() {
    const o = { s: 1 };
    let tt = 0;
    let gap = 0.09;
    const word = '참 잘했어요!';
    const cols = ['#ffd84d', '#ff7aa8', '#7ee0ff', '#9dff8a', '#ffb05a', '#c9a2ff', '#ff6b6b'];
    return {
      controls: [speedCtl(o), { type: 'range' as const, label: '글자 사이 지연', min: 0.02, max: 0.25, step: 0.01, value: 0.09, on: (v: number) => (gap = v) }],
      draw(g: G, w: number, h: number, _t: number, dt: number) {
        tt += Math.max(0, dt) * o.s;
        const T = 4.3;
        const p = tt % T;
        bg(g, w, h, '#2b2f7a', '#120f38');
        stars(g, w, h, tt);
        view(g, w, h);
        const chars = [...word];
        const size = 40;
        g.font = `400 ${size}px ${TF}`;
        const ws = chars.map((c) => (c === ' ' ? size * 0.35 : g.measureText(c).width + 2));
        const total = ws.reduce((a, b) => a + b, 0);
        let x = VW / 2 - total / 2;
        let landed = 0;
        chars.forEach((c, i) => {
          const cw = ws[i]!;
          const cx = x + cw / 2;
          x += cw;
          if (c === ' ') return;
          const tin = p - (0.2 + i * gap);
          const tout = p - (3.4 + i * gap * 0.7);
          if (tin < 0) return;
          const k = clamp01(tin / 0.55);
          let y = 78 - (1 - outBack(k, 2.4)) * 50;
          let sc = lerp(0.2, 1, outBack(k, 2.2));
          let rot = (1 - outCubic(k)) * (i % 2 ? 0.6 : -0.6);
          let a = clamp01(tin / 0.12);
          // 착지 찌그러짐
          const land = tin - 0.35;
          let sx = 1;
          let sy = 1;
          if (land > 0 && land < 0.5) {
            const q = Math.exp(-land * 9) * Math.sin(land * 30);
            sx = 1 + q * 0.25;
            sy = 1 - q * 0.25;
          }
          if (k >= 1) landed++;
          if (tout > 0) {
            y += tout * tout * 260;
            rot += tout * (i % 2 ? 2.2 : -2.2);
            a *= clamp01(1 - tout * 1.6);
            sc *= 1 - tout * 0.2;
          }
          if (a <= 0) return;
          g.save();
          g.globalAlpha = a;
          g.translate(cx, y + 18);
          g.rotate(rot);
          g.scale(sc * sx, sc * sy);
          g.translate(0, -18);
          g.font = `400 ${size}px ${TF}`;
          g.textAlign = 'center';
          g.textBaseline = 'middle';
          g.lineJoin = 'round';
          g.fillStyle = 'rgba(0,0,0,.35)';
          g.fillText(c, 2, 4);
          g.strokeStyle = '#1a1446';
          g.lineWidth = 6;
          g.strokeText(c, 0, 0);
          g.fillStyle = cols[i % cols.length]!;
          g.fillText(c, 0, 0);
          g.fillStyle = 'rgba(255,255,255,.35)';
          g.fillText(c, 0, -1.5);
          g.restore();
        });
        // 다 내려앉으면 반짝 + 부제
        const allIn = landed >= chars.length - 1;
        const sub = seg(p, 0.2 + chars.length * gap + 0.4, 0.2 + chars.length * gap + 0.8) * (1 - seg(p, 3.3, 3.6));
        if (sub > 0) {
          g.save();
          g.globalAlpha = sub;
          g.translate(VW / 2, 128);
          g.scale(outBack(sub), outBack(sub));
          pill(g, '3 × 4 = 12  정답!', 0, 0, 12, '#ff5c8a');
          g.restore();
        }
        if (allIn) {
          for (let i = 0; i < 8; i++) {
            const a = (i / 8) * TAU + tt;
            const r = 105 + 10 * Math.sin(tt * 3 + i);
            const sx = VW / 2 + Math.cos(a) * r;
            const sy = 78 + Math.sin(a) * r * 0.42;
            const s = 2.5 + 1.5 * Math.sin(tt * 6 + i * 2);
            g.fillStyle = cols[i % cols.length]!;
            g.beginPath();
            for (let j = 0; j < 8; j++) {
              const rr2 = j % 2 ? s * 0.4 : s * 1.4;
              g.lineTo(sx + Math.cos((j * TAU) / 8) * rr2, sy + Math.sin((j * TAU) / 8) * rr2);
            }
            g.fill();
          }
        }
        txt(g, `지연 ${gap.toFixed(2)}초 × 글자 순서`, VW - 8, VH - 9, 8, 'rgba(255,255,255,.5)', 'right', F, 600);
        g.restore();
      },
    };
  },
};
```

## 관련 기술
- 같은 방법으로 만든 효과 예시: [뜻을 연기하는 글자](https://ai-techstudio.web.app/ai/t/i91.md) `i91` · [타자기 + 깜박이는 커서](https://ai-techstudio.web.app/ai/t/i95.md) `i95`
- 먼저 알면 좋은 기술: [글자 테 · 그러데이션 글자](https://ai-techstudio.web.app/ai/t/u64.md) `u64`
- 다음에 해 볼 기술: [암호 풀리는 글자 (scramble)](https://ai-techstudio.web.app/ai/t/i93.md) `i93` · [튀어 오르는 숫자](https://ai-techstudio.web.app/ai/t/i202.md) `i202`
