# AI 꾸러미 — 임팩트 프레임 (반전 번쩍) — Impact frame
> 맞는 순간 게임 시간을 멈추고 두 프레임 동안 흑백 반전 실루엣 + 집중선을 보여 준 뒤 이어 가, 애니메이션 필살기 같은 임팩트를 낸다.  
> 견본: https://ai-techstudio.web.app/#t/i200

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

## 주문서

### 만들어 줘: 임팩트 프레임 (반전 번쩍) — Impact frame

#### 1. 목표
필살 투구가 블록에 꽂히는 순간에 임팩트 프레임을 넣어 줘 — 맞는 순간 시간을 멈추고 두 프레임(각 70ms) 흑백 반전 실루엣 + 집중선, 그 뒤 이어서. 분위기는 애니메이션 필살기.

#### 2. 핵심 기술 용어
- **Impact frame** — 맞는 순간 한두 장 흑백 · 반전 그림
- **Hit freeze (time hold)** — 그 동안 게임 시간 멈춤
- **Speed lines (focus lines)** — 집중선 — 한 점으로 모이는 쐐기

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

#### 4. 조건
- 임팩트 프레임 동안 게임 시간을 멈춘다 — 물체가 계속 움직이면 그냥 깜박임으로 보인다
- 두 프레임: 흰 바탕 검은 실루엣 → 검은 바탕 흰 실루엣 (한 프레임 16 ~ 200ms, 견본 70)
- 실루엣 모드에서는 그라데이션 · 빛 효과를 끄고 한 색만
- 집중선은 맞은 점을 향해 — 쐐기 40개, 프레임마다 조금 흔들림
- 연달아 쓰지 않기 (번쩍임 주의)

#### 5. 완성 기준 (이게 보이면 성공)
- 공이 블록에 꽂히는 순간 화면이 흰 바탕 · 검은 그림자 → 검은 바탕 · 흰 그림자로 두 번 바뀌고 「쾅!」 이 보인다
- 그 두 프레임 동안 공 · 조각이 멈춰 있다가 이어서 블록 조각이 흩어진다
- 「한 프레임 길이」 를 늘리면 정지가 길어지고, 집중선을 끄면 실루엣만 남는다
- 왼쪽(끔)과 비교하면 같은 타격이 훨씬 세게 느껴진다

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

## 원리
- 맞는 순간 HIT 부터 프레임 길이 fd(견본 70ms) 동안 모드 1(흰 바탕 · 검은 실루엣), 다음 fd 동안 모드 2(검은 바탕 · 흰 실루엣).
- 그 두 프레임 동안 게임 시간 gt 는 HIT 에 멈춰 있고, 끝나면 2·fd 만큼 늦춰 이어 간다 — 모든 물체가 같은 자리에서 「얼었다」 풀린다.
- 모드일 때는 모든 그림을 한 색(C = 검정 또는 흰색)으로만 칠한다 → 실루엣.
- 집중선: 맞은 점을 향해 좁아지는 쐐기 40개를 바탕과 반대 색으로.

## 핵심 코드 — 시간 멈춤 + 두 프레임 반전 실루엣 + 집중선
(발췌: demos/demosJuice.ts D200 의 scene() 을 정리)
```ts
const fd = frameMs / 1000;            // 한 프레임 길이 (견본 70ms)
let mode = 0;                           // 0 보통 · 1 흰 바탕 검은 실루엣 · 2 검은 바탕 흰 실루엣
let gt = ph;                            // 게임 시간
if (ph >= HIT && ph < HIT + fd) mode = 1;
else if (ph >= HIT + fd && ph < HIT + 2 * fd) mode = 2;
if (ph >= HIT && ph < HIT + 2 * fd) gt = HIT;      // 두 프레임 동안 시간 멈춤
else if (ph >= HIT + 2 * fd) gt = ph - 2 * fd;     // 멈춘 만큼 늦춰 이어 가기

const C = (c: string) => (mode === 0 ? c : mode === 1 ? '#000' : '#fff'); // 실루엣 색
if (mode === 0) drawBackground(g, w, h);
else { g.fillStyle = mode === 1 ? '#fff' : '#000'; g.fillRect(0, 0, w, h); }

if (mode > 0) {                          // 집중선: 맞은 점(ix, iy)을 향해 좁아지는 쐐기 40개
  g.fillStyle = mode === 1 ? '#000' : '#fff';
  for (let i = 0; i < 40; i++) {
    const an = (i / 40) * Math.PI * 2 + hash(i, Math.floor(T * 30)) * 0.12;
    const r0 = (26 + hash(i, 9) * 30) * u, R = 240 * u, wd = 0.035 + hash(i, 4) * 0.03;
    g.beginPath();
    g.moveTo(ix + Math.cos(an) * r0, iy + Math.sin(an) * r0);
    g.lineTo(ix + Math.cos(an - wd) * R, iy + Math.sin(an - wd) * R);
    g.lineTo(ix + Math.cos(an + wd) * R, iy + Math.sin(an + wd) * R);
    g.fill();
  }
}
drawBall(g, gt, mode ? C('') : null);     // 모든 물체는 gt 로, 모드면 한 색으로
drawBlock(g, gt, mode ? C('') : null);
```

## 흔한 실수 · 확인 목록
- [ ] **시간을 멈추지 않으면 그냥 화면이 깜박인 것처럼 보인다** — 두 프레임 동안 게임 시간을 HIT 에 고정하고, 끝나면 그만큼 늦춰 이어 간다.
- [ ] **실루엣 모드에서도 빛 · 꼬리를 그리면 지저분하다** — 모드일 때는 그라데이션 · 빛 번짐 · 불꽃 꼬리를 건너뛰고 한 색만.
- [ ] **프레임 길이를 ms 대신 「렌더 프레임 수」 로 세면 기기마다 길이가 다르다** — 시간(ms)으로 정한다 — 120Hz 화면에서도 같은 길이.

## 완성 기준 체크리스트
- [ ] 공이 블록에 꽂히는 순간 화면이 흰 바탕 · 검은 그림자 → 검은 바탕 · 흰 그림자로 두 번 바뀌고 「쾅!」 이 보인다
- [ ] 그 두 프레임 동안 공 · 조각이 멈춰 있다가 이어서 블록 조각이 흩어진다
- [ ] 「한 프레임 길이」 를 늘리면 정지가 길어지고, 집중선을 끄면 실루엣만 남는다
- [ ] 왼쪽(끔)과 비교하면 같은 타격이 훨씬 세게 느껴진다

## 이 기술 정보
- id: `i200` · 분류: 이펙트 · 연출 › 손맛 · 주스 · 공통 · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 두 프레임 동안 단색으로 그리기만. 오히려 가볍다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i200
- 쓰면 좋을 때: 게임에서 가장 큰 한 방 (필살기 · 결정타 · 모순 발견) / 만화 · 애니 풍 연출
- 쓰지 말 때: 자주 일어나는 타격 — 번쩍임이 잦으면 눈이 아프다. 대신 흰 번쩍(맞은 물체만) / 빛에 민감한 아이 — 한 번에 두 프레임만, 연달아 쓰지 않기

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### D200 — `src/demos/demosJuice.ts:655`
```ts
const D200: Demo = {
  kind: '2d',
  caption: '필살 투구가 블록에 꽂히는 순간 두 프레임 — 흑백 반전 · 실루엣 + 집중선, 애니 필살기 느낌',
  make() {
    let compare = true;
    let frameMs = 70;
    let lines = true;
    const P = 2.4;
    const HIT = 0.85;
    let T = 0;
    const scene = (g: G, w: number, h: number, on: boolean): void => {
      const u = uh(w, h);
      const ph = T % P;
      const fd = frameMs / 1000;
      let mode = 0;
      let gt = ph;
      if (on) {
        if (ph >= HIT && ph < HIT + fd) mode = 1;
        else if (ph >= HIT + fd && ph < HIT + 2 * fd) mode = 2;
        if (ph >= HIT && ph < HIT + 2 * fd) gt = HIT;
        else if (ph >= HIT + 2 * fd) gt = ph - 2 * fd;
      }
      const C = (c: string): string => (mode === 0 ? c : mode === 1 ? '#000' : '#fff');
      if (mode === 0) bg(g, w, h, '#2b1650', '#0d0620');
      else {
        g.fillStyle = mode === 1 ? '#fff' : '#000';
        g.fillRect(0, 0, w, h);
      }
      const ix = w * 0.66;
      const iy = h * 0.52;
      if (mode === 0) {
        for (let i = 0; i < 18; i++) {
          g.fillStyle = `rgba(255,255,255,${0.25 + hash(i, 5) * 0.5})`;
          g.fillRect(hash(i, 1) * w, hash(i, 2) * h * 0.7, 1.4 * u, 1.4 * u);
        }
      }
      // 집중선
      if (mode > 0 && lines) {
        g.fillStyle = mode === 1 ? '#000' : '#fff';
        for (let i = 0; i < 40; i++) {
          const an = (i / 40) * TAU + hash(i, Math.floor(T * 30)) * 0.12;
          const r0 = (26 + hash(i, 9) * 30) * u;
          const R = 240 * u;
          const wd = 0.035 + hash(i, 4) * 0.03;
          g.beginPath();
          g.moveTo(ix + Math.cos(an) * r0, iy + Math.sin(an) * r0);
          g.lineTo(ix + Math.cos(an - wd) * R, iy + Math.sin(an - wd) * R);
          g.lineTo(ix + Math.cos(an + wd) * R, iy + Math.sin(an + wd) * R);
          g.fill();
        }
      }
      const gy = h * 0.8;
      g.fillStyle = mode === 0 ? '#22113f' : C('');
      if (mode === 0) g.fillRect(0, gy, w, h - gy);
      const a = gt - HIT;
      const bs = 40 * u;
      if (a <= 0) {
        block(g, ix, iy + bs / 2, bs, '#9775fa', '9', mode ? { ink: C('') } : {});
      } else {
        // 조각
        for (let i = 0; i < 9; i++) {
          const an = -Math.PI * 0.15 + (hash(i, 21) - 0.5) * 2.4;
          const sp = (50 + hash(i, 22) * 70) * u;
          const px = ix + Math.cos(an) * sp * a + 10 * u;
          const py = iy + Math.sin(an) * sp * a - 40 * u * a + 160 * u * a * a;
          const sz = (8 + hash(i, 23) * 8) * u;
          g.save();
          g.translate(px, py);
          g.rotate(a * (hash(i, 24) - 0.5) * 14);
          g.fillStyle = mode ? C('') : BLK[i % 6]!;
          g.globalAlpha = 1 - smooth(0.6, 1.0, a);
          rr(g, -sz / 2, -sz / 2, sz, sz, 2 * u);
          g.fill();
          g.restore();
        }
      }
      // 공 (불꽃 꼬리)
      const br = 11 * u;
      let bxp: number;
      let byp: number;
      if (a < 0) {
        const kk = clamp01(gt / HIT);
        const e = kk * kk;
        bxp = lerp(-20 * u, ix - bs / 2 - br * 0.3, e);
        byp = lerp(h * 0.85, iy, e) - Math.sin(kk * Math.PI) * 20 * u;
      } else {
        bxp = ix + a * 140 * u;
        byp = iy - a * 50 * u;
      }
      if (mode === 0 && a < 0.3) {
        g.save();
        g.globalCompositeOperation = 'lighter';
        for (let j = 1; j < 9; j++) {
          const kk = clamp01((gt - j * 0.025) / HIT);
          const e = kk * kk;
          const px = lerp(-20 * u, ix - bs / 2 - br * 0.3, e);
          const py = lerp(h * 0.85, iy, e) - Math.sin(kk * Math.PI) * 20 * u;
          glow(g, px, py, br * (1.8 - j * 0.12), j < 3 ? '#ffe066' : '#ff6b2b', 0.55 - j * 0.05);
        }
        g.restore();
      }
      if (mode) {
        g.beginPath();
        g.arc(bxp, byp, br, 0, TAU);
        g.fillStyle = C('');
        g.fill();
      } else {
        g.beginPath();
        g.arc(bxp, byp, br, 0, TAU);
        g.fillStyle = '#fbfbf6';
        g.fill();
        g.strokeStyle = '#e03131';
        g.lineWidth = 1.6 * u;
        g.beginPath();
        g.arc(bxp - br * 1.25, byp, br, -0.8, 0.8);
        g.stroke();
        g.beginPath();
        g.arc(bxp + br * 1.25, byp, br, Math.PI - 0.8, Math.PI + 0.8);
        g.stroke();
      }
      if (a >= 0 && mode === 0) {
        sparks(g, ix - 10 * u, iy, a, 20, 31, 70 * u, ['#fff', '#ffe066', '#ff922b'], 0.5, 0, 3 * u);
        ringFx(g, ix - 10 * u, iy, a, 0.3, 10 * u, 60 * u, '#fff', 5 * u);
      }
      if (mode) otext(g, '쾅!', w * 0.36, h * 0.28, 26 * u, mode === 1 ? '#fff' : '#000', mode === 1 ? '#000' : '#fff', 5 * u);
    };
    return {
      draw(g, w, h, t) {
        T = t;
        split(g, w, h, compare, scene);
      },
      controls: [
        tgl('나란히 비교 (끔 | 켬)', compare, (v) => (compare = v)),
        rng('한 프레임 길이 (ms)', 16, 200, 2, frameMs, (v) => (frameMs = v)),
        tgl('집중선', lines, (v) => (lines = v)),
      ],
    };
  },
};
```

## 관련 기술
- 먼저 알면 좋은 기술: [카메라 킥 · 펀치 줌](https://ai-techstudio.web.app/ai/t/i199.md) `i199` · [필살기 연출 (빛꼬리 · 잔상)](https://ai-techstudio.web.app/ai/t/u37.md) `u37`
- 다음에 해 볼 기술: [피격 흰 번쩍](https://ai-techstudio.web.app/ai/t/i201.md) `i201` · [슬로모션 · 시간 배율 곡선](https://ai-techstudio.web.app/ai/t/i204.md) `i204`
