# AI 꾸러미 — 카메라 킥 · 펀치 줌 — Camera kick (screen push)
> 맞는 순간 화면 전체를 때린 쪽으로 툭 밀고 2~5% 확대했다가, 임계 감쇠 충격 곡선으로 넘침 없이 돌아오게 해 타격감을 준다.  
> 견본: https://ai-techstudio.web.app/#t/i199

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

## 주문서

### 만들어 줘: 카메라 킥 · 펀치 줌 — Camera kick (screen push)

#### 1. 목표
블록이 자리에 딱 들어가는 순간에 카메라 킥 · 펀치 줌을 넣어 줘 — 맞는 순간 화면이 때린 쪽으로 툭 밀렸다 돌아오고 순간 4% 확대, 넘침 없는 감쇠로. 분위기는 짧고 경쾌한 퍼즐.

#### 2. 핵심 기술 용어
- **Camera kick (screen push)** — 때린 쪽으로 화면이 툭 밀림
- **Punch zoom** — 순간 확대했다 돌아오기
- **Critically damped impulse (a·W·e·e^(−aW))** — 임계 감쇠 충격 곡선 — 한 번 솟고 넘침 없이 0 으로

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

#### 4. 조건
- 곡선은 임계 감쇠 충격(a·W·e·e^(−aW)) — 사인 흔들림처럼 왔다 갔다 넘치지 않게
- 확대는 2~5% (견본 4.5%) — 그 이상이면 멀미
- 밀리는 방향 = 맞은 방향
- 세기 조절 하나로 이동 · 확대를 함께 (0 ~ 3)
- 켬 / 끔을 나란히 비교

#### 5. 완성 기준 (이게 보이면 성공)
- 블록이 판에 딱 들어가는 순간, 오른쪽(켬)은 화면 전체가 오른쪽 아래로 툭 밀리며 살짝 커졌다가 0.3초 안에 넘침 없이 돌아온다
- 왼쪽(끔)은 같은 장면이 그냥 멈춰 있어 차이가 바로 보인다
- 「세기」 를 0 으로 하면 움직임이 없고, 3 으로 하면 크게 밀린다

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

## 원리
- 맞은 뒤 시간 a 에 대해 k(a) = a·W·e·e^(−a·W) — 0 에서 시작해 a = 1/W 에서 정확히 1, 그 뒤 넘침 없이 0 으로 (W = 13 이면 약 77ms 에 최고).
- 화면 이동 = 방향 × 9 × k (견본 x 9 · y 3), 확대 = 1 + 0.045 × k. 세기 하나로 모두 곱한다.
- 확대는 화면 가운데를 축으로: 가운데로 옮김 → 키움 → 되돌림.
- 흔들기(무작위 떨림)와 다르게 한 방향으로 한 번 — 「어디서 맞았는지」 가 느껴진다.

## 핵심 코드 — 임계 감쇠 충격 곡선으로 화면 밀기 + 확대
(발췌: demos/demosJuice.ts D199 의 scene() 을 정리)
```ts
const W = 13;                                   // 클수록 빨리 솟고 빨리 끝남 (최고점 1/W 초)
const a = ph - HIT;                            // 맞은 뒤 시간
const k = a >= 0 ? a * W * Math.E * Math.exp(-a * W) : 0; // 0 → 1 (1/W 에서) → 넘침 없이 0

g.save();
if (on) {
  const z = 1 + 0.045 * power * k;             // 순간 확대 4.5%
  g.translate(w / 2 + 9 * u * power * k, h / 2 + 3 * u * power * k); // 맞은 쪽으로 밀림
  g.scale(z, z);                               // 가운데를 축으로
  g.translate(-w / 2, -h / 2);
}
drawScene(g, w, h);
g.restore();

// three.js 라면 같은 k 로:
// camera.position.copy(base).addScaledVector(hitDir, 0.09 * power * k);
// camera.fov = baseFov / (1 + 0.045 * power * k); camera.updateProjectionMatrix();
```

## 흔한 실수 · 확인 목록
- [ ] **사인으로 흔들면 왔다 갔다 해서 「툭」 이 아니라 「덜덜」 이 된다** — 임계 감쇠 곡선은 한 번 솟고 넘침 없이 돌아온다 — 방향 있는 한 방이 된다.
- [ ] **확대 축이 왼쪽 위면 화면이 대각선으로 미끄러진다** — 가운데로 옮기고 키운 뒤 되돌린다.
- [ ] **모든 동작에 킥을 주면 정작 중요한 순간이 묻힌다** — 정답 · 완성 같은 큰 순간에만.

## 완성 기준 체크리스트
- [ ] 블록이 판에 딱 들어가는 순간, 오른쪽(켬)은 화면 전체가 오른쪽 아래로 툭 밀리며 살짝 커졌다가 0.3초 안에 넘침 없이 돌아온다
- [ ] 왼쪽(끔)은 같은 장면이 그냥 멈춰 있어 차이가 바로 보인다
- [ ] 「세기」 를 0 으로 하면 움직임이 없고, 3 으로 하면 크게 밀린다

## 이 기술 정보
- id: `i199` · 분류: 이펙트 · 연출 › 손맛 · 주스 · 공통 · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 화면 변환 하나. 계산은 거의 없다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i199
- 쓰면 좋을 때: 딱 맞는 순간 · 맞히는 순간에 짧은 손맛 / 방향이 있는 충격 (왼쪽에서 날아와 꽂힘)
- 쓰지 말 때: 자주 일어나는 작은 동작(칸 하나 옮기기) — 매번 밀리면 어지럽다 / 글씨를 읽어야 하는 순간 — 대신 대상만 찌그러지기

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### D199 — `src/demos/demosJuice.ts:561`
```ts
const D199: Demo = {
  kind: '2d',
  caption: '블록이 자리에 딱 — 오른쪽은 화면이 때린 쪽으로 툭 밀렸다 스프링처럼 돌아오고 순간 4% 확대',
  make() {
    let compare = true;
    let power = 1;
    const P = 2.1;
    const HIT = 0.72;
    let T = 0;
    const scene = (g: G, w: number, h: number, on: boolean): void => {
      const u = uh(w, h);
      const ph = T % P;
      const a = ph - HIT;
      const W = 13;
      const k = a >= 0 ? a * W * Math.E * Math.exp(-a * W) : 0;
      g.save();
      if (on) {
        const z = 1 + 0.045 * power * k;
        g.translate(w / 2 + 9 * u * power * k, h / 2 + 3 * u * power * k);
        g.scale(z, z);
        g.translate(-w / 2, -h / 2);
      }
      bg(g, w, h, '#18306a', '#0a1430');
      // 모눈 점 (움직임이 보이게)
      g.fillStyle = 'rgba(160,190,255,.25)';
      for (let y = 8 * u; y < h; y += 14 * u) for (let x = 6 * u; x < w; x += 14 * u) g.fillRect(x, y, 1.6 * u, 1.6 * u);
      // 판
      const bx = w / 2 - 60 * u;
      const by = h * 0.5;
      rr(g, bx, by, 120 * u, 50 * u, 10 * u);
      g.fillStyle = '#5c3d1e';
      g.fill();
      rr(g, bx + 3 * u, by + 3 * u, 114 * u, 44 * u, 8 * u);
      g.fillStyle = '#8b5a2b';
      g.fill();
      const slots = ['4', '5', '6'];
      const sx0 = [w / 2 - 36 * u, w / 2, w / 2 + 36 * u];
      const s = 28 * u;
      slots.forEach((n, i) => {
        const x = sx0[i]!;
        rr(g, x - s / 2 - 2 * u, by + 9 * u, s + 4 * u, s + 4 * u, 7 * u);
        g.fillStyle = i === 1 && a >= 0 ? '#2f9e44' : '#3d2410';
        g.fill();
        txt(g, n, x, by + 25 * u, 12 * u, 'rgba(255,230,190,.45)', 'center', 900);
      });
      if (a >= 0) {
        const gl = Math.exp(-a * 3);
        g.save();
        g.globalCompositeOperation = 'lighter';
        glow(g, w / 2, by + 25 * u, 40 * u, '#8ce99a', 0.7 * gl);
        g.restore();
      }
      // 블록: 위에서 미끄러져 와 쏙
      const tx = w / 2;
      const ty = by + 41 * u;
      let x: number;
      let y: number;
      let sq = 1;
      if (a < 0) {
        const kk = clamp01(ph / HIT);
        const e = kk * kk * kk;
        x = lerp(-30 * u, tx, e);
        y = lerp(h * 0.2, ty, e) - Math.sin(kk * Math.PI) * 30 * u;
      } else {
        x = tx;
        y = ty;
        sq = 1 - 0.18 * Math.exp(-a * 10) * Math.cos(a * 30);
      }
      block(g, x, y, s, '#ff6b8a', '5', { sx: 2 - sq, sy: sq });
      if (a >= 0) {
        sparks(g, tx, by + 25 * u, a, 14, 11, 44 * u, ['#fff', '#b2f2bb', '#ffe066'], 0.5, 0, 2.6 * u);
        const pk = backOut(a / 0.3);
        if (a < 1.1) {
          g.save();
          g.globalAlpha = 1 - smooth(0.8, 1.1, a);
          g.translate(w / 2, h * 0.3 - a * 8 * u);
          g.scale(pk, pk);
          otext(g, '딱!', 0, 0, 22 * u, '#ffe066', '#7a3b00', 4 * u);
          g.restore();
        }
      }
      g.restore();
    };
    return {
      draw(g, w, h, t) {
        T = t;
        split(g, w, h, compare, scene);
      },
      controls: [tgl('나란히 비교 (끔 | 켬)', compare, (v) => (compare = v)), rng('세기', 0, 3, 0.1, power, (v) => (power = v))],
    };
  },
};
```

## 관련 기술
- 먼저 알면 좋은 기술: [스프링 · 예비 동작 · 찌그러짐](https://ai-techstudio.web.app/ai/t/i102.md) `i102`
- 다음에 해 볼 기술: [임팩트 프레임 (반전 번쩍)](https://ai-techstudio.web.app/ai/t/i200.md) `i200` · [슬로모션 · 시간 배율 곡선](https://ai-techstudio.web.app/ai/t/i204.md) `i204`
