# AI 꾸러미 — 매개 모핑 (이음새 없는 변신)
> 숫자 「3」이 점으로 줄었다 사과 3개로 펴짐 — 중간 매개를 거쳐, 셋이 한 화면에 겹치지 않게  
> 견본: https://ai-techstudio.web.app/#t/i97

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

## 주문서

### 만들어 줘: 매개 모핑 (이음새 없는 변신)

#### 1. 목표
수 ↔ 양 ↔ 기호 대응에 「매개 모핑 (이음새 없는 변신)」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 숫자 「3」이 점으로 줄었다 사과 3개로 펴짐 — 중간 매개를 거쳐, 셋이 한 화면에 겹치지 않게

#### 2. 핵심 기술 용어
- 매개 모핑 (이음새 없는 변신) — 숫자 「3」이 점으로 줄었다 사과 3개로 펴짐 — 중간 매개를 거쳐, 셋이 한 화면에 겹치지 않게

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

#### 4. 조건
- devicePixelRatio 를 맞춰 또렷하게
- 매 프레임 다시 그릴 필요 없는 그림은 화면 밖 캔버스에 미리 구워 두기
- 한 프레임 16ms 안 (폰 포함)

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「숫자 「3」이 점으로 줄었다 사과 3개로 펴짐 — 중간 매개를 거쳐, 셋이 한 화면에 겹치지 않게」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 숫자 「3」이 점으로 줄었다 사과 3개로 펴짐 — 중간 매개를 거쳐, 셋이 한 화면에 겹치지 않게


## 흔한 실수 · 확인 목록
- (아직 정리된 것이 없어요)

## 완성 기준 체크리스트
- [ ] 견본처럼 「숫자 「3」이 점으로 줄었다 사과 3개로 펴짐 — 중간 매개를 거쳐, 셋이 한 화면에 겹치지 않게」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i97` · 분류: 이펙트 · 연출 › 모션 그래픽 · 2D · 난이도 어려움 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i97
- 쓰면 좋을 때: 쓰면 좋을 곳 — 수 ↔ 양 ↔ 기호 대응 · 분수 ↔ 그림

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i97 — `src/demos/demosMotionA.ts:886`
```ts
const i97 = {
  kind: '2d' as const,
  caption: '숫자 「3」이 작은 점으로 줄었다가 사과 3개로 펴져요 — 셋이 한꺼번에 겹치지 않게 「점」을 다리로',
  make() {
    const o = { s: 1 };
    let tt = 0;
    return {
      controls: [speedCtl(o)],
      draw(g: G, w: number, h: number, _t: number, dt: number) {
        tt += Math.max(0, dt) * o.s;
        const T = 6.4;
        const p = tt % T;
        bg(g, w, h, '#fff3df', '#ffd9b3');
        view(g, w, h);
        const cx = VW / 2;
        const cy = 74;
        // 단계: 0-0.9 숫자, 0.9-1.6 숫자→점, 1.6-2.5 점→사과, 2.5-3.6 사과, 3.6-4.4 사과→점, 4.4-5.2 점→숫자, 5.2-6.4 숫자
        let stage = 0;
        const numShrink = seg(p, 0.9, 1.6);
        const split = seg(p, 1.6, 2.5);
        const merge = seg(p, 3.6, 4.4);
        const grow = seg(p, 4.4, 5.2);
        const showNum = p < 1.6 || p >= 4.4;
        if (showNum) {
          const s = p < 1.6 ? lerp(1, 0.08, inOut(numShrink)) : lerp(0.08, 1, outBack(grow, 1.4));
          const crossDot = p < 1.6 ? seg(numShrink, 0.65, 1) : 1 - seg(grow, 0, 0.35);
          stage = s > 0.3 ? 0 : 1;
          g.save();
          g.translate(cx, cy);
          g.scale(s, s);
          g.globalAlpha = 1 - crossDot;
          g.font = `400 96px ${TF}`;
          g.textAlign = 'center';
          g.textBaseline = 'middle';
          g.fillStyle = 'rgba(160,60,20,.18)';
          g.fillText('3', 4, 8);
          g.fillStyle = '#ff5a3c';
          g.fillText('3', 0, 4);
          g.restore();
          if (crossDot > 0) {
            g.fillStyle = `rgba(230,50,50,${crossDot})`;
            g.beginPath();
            g.arc(cx, cy, 6, 0, TAU);
            g.fill();
          }
        } else {
          const spread = p < 3.6 ? inOut(split) : 1 - inOut(merge);
          const r = lerp(6, 19, spread);
          stage = spread > 0.6 ? 2 : 1;
          for (let q = 0; q < 3; q++) {
            const delay = q * 0.08;
            const sp = p < 3.6 ? outBack(seg(split, delay, 0.75 + delay), 1.3) : 1 - inOut(seg(merge, 0.25 - delay, 1 - delay));
            const x = cx + (q - 1) * 60 * sp;
            const y = cy + 6 * sp - Math.sin(sp * Math.PI) * 18;
            const bob = spread > 0.95 ? Math.sin(tt * 3 + q) * 2 : 0;
            if (spread < 0.25) {
              g.fillStyle = '#e63232';
              g.beginPath();
              g.arc(x, y, r, 0, TAU);
              g.fill();
            } else apple(g, x, y + bob, r, seg(spread, 0.4, 0.9));
            const lab = seg(p, 2.4 + q * 0.2, 2.6 + q * 0.2) * (1 - merge * 3);
            if (lab > 0) {
              g.save();
              g.globalAlpha = clamp01(lab);
              txt(g, String(q + 1), x, y - 34 - lab * 2, 12, '#b5321c', 'center', TF, 400);
              g.restore();
            }
          }
        }
        // 아래 세 단계 표
        const chips = ['기호 3', '매개 ●', '양 (사과 셋)'];
        chips.forEach((c, q) => {
          const x = 60 + q * 80;
          const on = q === stage;
          g.save();
          g.globalAlpha = on ? 1 : 0.45;
          pill(g, c, x, 150, on ? 10 : 9, on ? '#ff5a3c' : '#c99a7a');
          g.restore();
          if (q < 2) txt(g, '→', x + 40, 150, 10, '#b5784e');
        });
        g.restore();
      },
    };
  },
};
```

## 관련 기술
- 이 효과를 만든 기술: [도형 모핑 (모양 바꾸기)](https://ai-techstudio.web.app/ai/t/i96.md) `i96`
