# AI 꾸러미 — 휩 팬 · 줌 통과 · 매치 컷 — Whip pan transition
> 휙 지나가며 한 방향으로 번지는 휩 팬, 숫자 0 의 구멍으로 빨려 드는 줌 통과, 같은 자리 · 크기의 모양으로 잇는 매치 컷으로 장면을 바꾼다.  
> 견본: https://ai-techstudio.web.app/#t/i106

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

## 주문서

### 만들어 줘: 휩 팬 · 줌 통과 · 매치 컷 — Whip pan transition

#### 1. 목표
1일차 → 2일차 → 3일차 넘기기에 영화식 장면 전환을 넣어 줘 — 휩 팬(한 방향 흐림), 숫자 구멍으로 줌 통과, 같은 자리 모양으로 매치 컷. 분위기는 밝은 만화 영화.

#### 2. 핵심 기술 용어
- **Whip pan transition** — 카메라를 휙 돌리듯 한 방향 흐림 전환
- **Zoom-through (hole reveal)** — 모양의 구멍으로 확대해 들어가 다음 장면
- **Match cut** — 같은 자리 · 크기 · 모양으로 이어 붙이는 컷
- **Even-odd clip path** — 구멍 뚫린 모양으로 자르기 (evenodd)

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

#### 4. 조건
- 장면은 「그리기 함수」로 두어 복사본 · 잘린 영역 안에서 다시 그릴 수 있게
- 휩 팬은 가운데서 가장 빠르게 — 복사본 수와 속도선 투명도를 vel 에 맞추기
- 줌 통과 확대는 처음엔 천천히, 끝에 빠르게 (k^2.2) — 구멍 가운데를 축으로
- 매치 컷의 두 모양은 화면 자리 · 크기를 똑같이 (견본 반지름 30)
- 어느 전환인지 작은 이름표로 보여 주기

#### 5. 완성 기준 (이게 보이면 성공)
- 1일차가 휙 옆으로 흐려지며 2일차가 들어온다 (한 방향 흐림 + 가로 속도선)
- 2일차의 주황 숫자 0 구멍으로 확대해 들어가면 그 속에 3일차 밤하늘이 보인다
- 3일차 달이 같은 자리의 1일차 해로 딱 바뀌며 흰빛이 번쩍한다

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

## 원리
- 휩 팬: 두 장면을 나란히 붙여 함께 옆으로 민다. 가운데서 가장 빠르게(vel = sin πk), 빠를수록 반투명 복사본을 여러 장(1 + 8·vel) 조금씩 밀려 겹쳐 그려 흐림을 흉내 낸다.
- 줌 통과: 다음 장면을 먼저 그리고, 그 위에 이전 장면을 「구멍만 뚫린」 모양(큰 사각형 + 타원, evenodd)으로 잘라 그린 채 16^(k^2.2) 배로 키운다 — 구멍이 화면을 덮으며 다음 장면이 드러난다.
- 매치 컷: 달과 해처럼 같은 자리 · 같은 크기의 모양을 두고 바로 바꾼 뒤, 흰 원이 번쩍(e^(−7t)) 하며 이어 준다.

## 핵심 코드 — 휩 팬 복사본 흐림 · evenodd 구멍 줌 통과
(발췌: demos/demosMotionA.ts i106 의 draw 를 정리)
```ts
// 휩 팬 — whip 0 → 1 (0.55초)
const k = inOut(whip);
const vel = Math.sin(whip * Math.PI);          // 가운데서 가장 빠름
const nCopies = 1 + Math.round(vel * 8);       // 빠를수록 복사본 여러 장 = 흐림
for (let c = 0; c < nCopies; c++) {
  const off = (k + (c / Math.max(1, nCopies)) * 0.12 * vel) * w;
  const a = c === 0 ? 1 : 0.5 / nCopies + 0.05;
  for (const [dx, scene] of [[-off, sceneA], [w - off, sceneB]] as const) {
    g.save(); g.globalAlpha = a; g.translate(dx, 0);
    g.beginPath(); g.rect(0, 0, w, h); g.clip();
    scene(g, w, h);
    g.restore();
  }
}

// 줌 통과 — 다음 장면 위에 「구멍 뚫린 이전 장면」 을 키운다
const s = Math.pow(16, Math.pow(zoom, 2.2));    // 처음엔 천천히, 끝에 빠르게
sceneNext(g, w, h);
g.save();
g.translate(w / 2, h / 2); g.scale(s, s); g.translate(-w / 2, -h / 2);
const hole = new Path2D();
hole.rect(-w, -h, w * 3, h * 3);               // 큰 바깥
hole.ellipse(w / 2, h / 2, 17 * u, 29 * u, 0, 0, Math.PI * 2); // 숫자 0 의 구멍
g.clip(hole, 'evenodd');                       // 구멍만 빼고 그린다
scenePrev(g, w, h);
g.restore();

// 매치 컷 — 같은 자리 · 크기의 해로 바꾼 뒤 번쩍
const flash = Math.exp(-(p - 6.2) * 7);
g.fillStyle = 'rgba(255,255,255,' + flash * 0.8 + ')';
g.beginPath(); g.arc(w / 2, h / 2, 30 * u * (1 + (1 - flash) * 0.5), 0, Math.PI * 2); g.fill();
```

## 흔한 실수 · 확인 목록
- [ ] **복사본 수를 늘 많게 두면 느릴 때도 흐리다** — 복사본 수 · 투명도를 속도(vel)에 맞춰 — 처음 · 끝은 한 장.
- [ ] **구멍을 clip 하려고 타원만 쓰면 반대(구멍 안)만 남는다** — 큰 사각형 + 타원을 한 Path2D 에 넣고 clip(path, 'evenodd').
- [ ] **매치 컷 두 모양의 자리가 조금만 달라도 튄다** — 같은 중심 · 같은 반지름 값을 공유하는 상수로 그린다.

## 완성 기준 체크리스트
- [ ] 1일차가 휙 옆으로 흐려지며 2일차가 들어온다 (한 방향 흐림 + 가로 속도선)
- [ ] 2일차의 주황 숫자 0 구멍으로 확대해 들어가면 그 속에 3일차 밤하늘이 보인다
- [ ] 3일차 달이 같은 자리의 1일차 해로 딱 바뀌며 흰빛이 번쩍한다

## 이 기술 정보
- id: `i106` · 분류: 이펙트 · 연출 › 모션 그래픽 · 2D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 휩 팬 순간에만 장면을 최대 9 × 2 번 그린다 (0.55초). 장면이 무거우면 미리 오프스크린 캔버스에 찍어 두고 그 그림을 민다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i106
- 쓰면 좋을 때: 일차 · 단계처럼 「다음 것으로 넘어간다」 를 힘 있게 보여 줄 때 / 두 장면 사이에 공통된 모양(숫자 0 · 해 · 달)이 있을 때
- 쓰지 말 때: 자주 오가는 메뉴 · 탭 — 매번 길면 답답하다. 대신 바뀌는 부분만 바로 / 멀미가 날 만큼 빠른 휩 팬을 연달아

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i106 — `src/demos/demosMotionA.ts:2020`
```ts
const i106 = {
  kind: '2d' as const,
  caption: '1일차 → 휙(휩 팬, 한 방향 흐림) → 2일차 → 숫자 0 구멍으로 쑥(줌 통과) → 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 = 7.6;
        const p = tt % T;
        const u = Math.min(w / VW, h / VH);
        let label = '';
        const whip = seg(p, 1.4, 1.95);
        const zoom = seg(p, 3.4, 4.3);
        const flash = p >= 6.2 ? Math.exp(-(p - 6.2) * 7) : 0;
        if (p < 1.4) scDay(g, w, h, u, 0, tt);
        else if (p < 1.95) {
          label = '휩 팬';
          const k = inOut(whip);
          const vel = Math.sin(whip * Math.PI); // 가운데서 가장 빠름
          const nCopies = 1 + Math.round(vel * 8);
          for (let c = 0; c < nCopies; c++) {
            const off = (k + (c / Math.max(1, nCopies)) * 0.12 * vel) * w;
            g.save();
            g.globalAlpha = c === 0 ? 1 : 0.5 / nCopies + 0.05;
            g.translate(-off, 0);
            g.beginPath();
            g.rect(0, 0, w, h);
            g.clip();
            scDay(g, w, h, u, 0, tt);
            g.restore();
            g.save();
            g.globalAlpha = c === 0 ? 1 : 0.5 / nCopies + 0.05;
            g.translate(w - off, 0);
            g.beginPath();
            g.rect(0, 0, w, h);
            g.clip();
            scDay(g, w, h, u, 1, tt);
            ring0(g, w / 2, h / 2, u);
            g.restore();
          }
          // 가로 속도선
          g.strokeStyle = `rgba(255,255,255,${0.5 * vel})`;
          g.lineWidth = 1.5 * u;
          for (let q = 0; q < 10; q++) {
            const yy = hsh(q) * h;
            g.beginPath();
            g.moveTo(hsh(q + 3) * w, yy);
            g.lineTo(hsh(q + 3) * w + 60 * u, yy);
            g.stroke();
          }
        } else if (p < 3.4) {
          scDay(g, w, h, u, 1, tt);
          const bob = 1 + 0.03 * Math.sin(tt * 4);
          ring0(g, w / 2, h / 2, u * bob);
        } else if (p < 4.3) {
          label = '줌 통과';
          const k = Math.pow(zoom, 2.2);
          const s = Math.pow(16, k);
          scDay(g, w, h, u, 2, tt);
          g.save();
          g.translate(w / 2, h / 2);
          g.scale(s, s);
          g.translate(-w / 2, -h / 2);
          const hole = new Path2D();
          hole.rect(-w, -h, w * 3, h * 3);
          hole.ellipse(w / 2, h / 2, 17 * u, 29 * u, 0, 0, TAU);
          g.save();
          g.clip(hole, 'evenodd');
          scDay(g, w, h, u, 1, tt);
          ring0(g, w / 2, h / 2, u);
          g.restore();
          g.restore();
        } else if (p < 6.2) {
          scDay(g, w, h, u, 2, tt);
          if (p > 5.6) label = '매치 컷';
        } else {
          label = '매치 컷';
          scDay(g, w, h, u, 0, tt);
          g.fillStyle = `rgba(255,255,255,${flash * 0.8})`;
          g.beginPath();
          g.arc(w / 2, h / 2, 30 * u * (1 + (1 - flash) * 0.5), 0, TAU);
          g.fill();
        }
        if (label) {
          view(g, w, h);
          pill(g, label, VW - 40, 18, 10, 'rgba(255,60,110,.9)');
          g.restore();
        }
      },
    };
  },
};
```

## 관련 기술
- 먼저 알면 좋은 기술: [마스크 와이프 · 원 조리개](https://ai-techstudio.web.app/ai/t/i99.md) `i99`
- 다음에 해 볼 기술: [타임라인 연출 + 프레임 효과음](https://ai-techstudio.web.app/ai/t/i127.md) `i127` · [2D 시차 층 · 무한 확대](https://ai-techstudio.web.app/ai/t/i104.md) `i104`
- 참고 문서: [Wikipedia — Match cut](https://en.wikipedia.org/wiki/Match_cut) · [MDN — Path2D](https://developer.mozilla.org/en-US/docs/Web/API/Path2D)
