# AI 꾸러미 — 픽셀 그림 한 장 살리기 (부위 겹 · 칸 단위 움직임) — Cutout animation for pixel art (part layers)
> 도트 그림 한 장을 부위별로 오려 겹으로 나누고 통째로 칸 단위로 옮겨, 외곽선을 깨지 않고 가만히 · 걸음 · 공격 · 맞음 · 사라지기까지 움직인다.  
> 견본: https://ai-techstudio.web.app/#t/i501

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

## 주문서

### 만들어 줘: 픽셀 그림 한 장 살리기 (부위 겹 · 칸 단위 움직임) — Cutout animation for pixel art (part layers)

#### 1. 목표
정면을 보는 보스 · 적 도트 캐릭터를 그림 한 장만으로 움직이게 해 줘 — 부위를 오려 겹으로 나누고 칸 단위로 통째로 옮기고, 연기 · 불꽃은 색 순환으로. 상태는 가만히 · 공격 · 맞음 · 사라지기 · 분노 상태 세트.

#### 2. 핵심 기술 용어
- **Cutout animation for pixel art (part layers)** — 부위 오려 내기 애니메이션 — 픽셀 그림용
- **Integer pixel offsets · stepped timing (8–12 fps)** — 칸 단위 이동 · 끊어서 움직이기
- **Palette cycling (color cycling)** — 색 순환 — 칸은 그대로, 색만 흘러 연기 · 불꽃
- **Hit flash · dither dissolve · afterimage** — 맞음 번쩍 · 칸 흩어짐 사라지기 · 디더 잔상

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

#### 4. 조건
- 그림을 휘지(warp · mesh deform) 말 것 — 부위마다 통째로, 정수 칸 단위로만 옮긴다
- 부위 영역은 그림 칸 단위 마스크(부위 번호 그림)로, 몸 뒤 · 몸 앞을 나눠서
- 연기 · 불꽃은 색 순환으로 (칸 위치는 그대로)
- 1초에 8~12장으로 끊고, 모든 움직임 주기를 한 바퀴 장 수에 맞춰 시트가 이어지게
- 효과(음파 고리 · 음표 · 불티)도 같은 팔레트 · 같은 칸 크기로 그리기 — 매끈한 선 · 반투명 금지

#### 5. 완성 기준 (이게 보이면 성공)
- 한 장씩 넘겨 봐도 외곽선이 끊기거나 칸 크기가 달라지는 곳이 없다
- 「나눈 부위 보기」로 띠 · 머리카락 · 장식 · 손 · 연기 영역이 색으로 보인다
- 상태 단추(가만히 · 공격 · 맞음 · 사라지기 · 나타나기 · 분노)를 누르면 바로 바뀌고, 한 번짜리 상태는 끝나면 가만히로 돌아온다
- 스프라이트 시트로 구워 8장/초로 돌리면 이음새 없이 반복된다

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

## 원리
- 도트 그림을 칸마다 휘게(warp) 하면 휘는 양이 다른 곳에서 칸이 겹치거나 빠져 1칸짜리 외곽선이 끊긴다 — 도트는 「통째로 · 칸 단위로」 움직여야 한다.
- 부위를 몸 뒤(띠 · 머리카락 끝)와 몸 앞(손 · 장식 · 꽃)으로 나눈다. 뒤 부위는 몸통 아래에 그리고, 앞 부위가 비킨 자리는 옆 칸으로 메운다.
- 이음매는 가려지는 곳에 둔다 — 띠는 소매 뒤를 기준으로 위 고리 · 아래 가닥으로 나누면 끊김이 소매에 숨는다.
- 연기 · 불꽃처럼 모양이 흐르는 것은 칸을 옮기지 말고 색만 돌린다(색 순환): 밝기 단계 + (높이 + 시간) 의 물결.
- 1초에 8장처럼 끊어서 움직이고, 모든 주기를 한 바퀴 장 수의 약수로 맞추면 스프라이트 시트가 이음새 없이 돈다.

## 핵심 코드 — 부위 겹 합치기 — 뒤 부위 → 몸통 → 앞 부위, 모두 정수 칸 이동
(발췌: demos/demosPixelArt.ts charLayer() 를 정리)
```ts
// src: 원본 RGBA(256×256), base: 앞 부위 자리를 메우고 뒤 부위를 비운 몸통, parts[k]: 부위 k 의 칸 번호 목록
// off[k] = [dx, dy] (정수) — 매 장 sin 을 반올림해 -1 · 0 · 1 로
function compose(out: Uint8ClampedArray, src: Uint8ClampedArray, base: Uint8ClampedArray,
                 parts: Record<number, number[]>, off: Record<number, [number, number]>,
                 back: number[], front: number[], N = 256) {
  out.fill(0);
  const put = (x: number, y: number, s: number) => {
    if (x < 0 || y < 0 || x >= N || y >= N) return;
    const p = (y * N + x) * 4;
    out[p] = src[s * 4]; out[p + 1] = src[s * 4 + 1]; out[p + 2] = src[s * 4 + 2]; out[p + 3] = 255;
  };
  for (const k of back) for (const i of parts[k] ?? []) put((i % N) + off[k][0], ((i / N) | 0) + off[k][1], i);
  for (let i = 0; i < N * N; i++) if (base[i * 4 + 3]) out.set(base.subarray(i * 4, i * 4 + 4), i * 4); // 몸통이 뒤 부위를 덮는다
  for (const k of front) for (const i of parts[k] ?? []) put((i % N) + off[k][0], ((i / N) | 0) + off[k][1], i);
}
const sq = (f: number, period: number, ph = 0) => Math.round(Math.sin((2 * Math.PI * f) / period + ph)); // -1 · 0 · 1
```

## 흔한 실수 · 확인 목록
- [ ] **그림을 칸마다 휘게 하면 외곽선이 끊기고 지글거린다** — 처음 시험판이 이랬다 — 휘는 양이 다른 곳에서 칸이 겹치거나 빠진다. 부위를 오려 통째로 정수 칸만큼 옮긴다.
- [ ] **긴 부위를 두 조각으로 나누면 이음매가 찢어진다** — 이음매를 소매 · 몸통 뒤처럼 가려지는 곳에 둔다. 띠는 위 고리 · 아래 가닥으로.
- [ ] **부위 상자를 네모로 잡으면 뒤의 치마까지 같이 움직인다** — 상자 안에서도 그 부위 색 · 이어진 덩어리만 고른다(연꽃 상자가 치마를 같이 잡았던 일).
- [ ] **사라지기를 무작위 칸으로 흩으면 모래알처럼 지저분하다** — 아래 → 위 순서에 작은 무작위만 섞어 경계선이 보이게 하고, 날아가는 알갱이는 일부 칸만.
- [ ] **0.5 칸처럼 소수점 위치에 그리면 칸 크기가 제각각이 된다(믹셀)** — 모든 위치를 정수 칸으로 반올림하고, 화면 확대도 정수 배로.

## 완성 기준 체크리스트
- [ ] 한 장씩 넘겨 봐도 외곽선이 끊기거나 칸 크기가 달라지는 곳이 없다
- [ ] 「나눈 부위 보기」로 띠 · 머리카락 · 장식 · 손 · 연기 영역이 색으로 보인다
- [ ] 상태 단추(가만히 · 공격 · 맞음 · 사라지기 · 나타나기 · 분노)를 누르면 바로 바뀌고, 한 번짜리 상태는 끝나면 가만히로 돌아온다
- [ ] 스프라이트 시트로 구워 8장/초로 돌리면 이음새 없이 반복된다

## 이 기술 정보
- id: `i501` · 분류: 2D · 화면 › 2D 그림 효과 · 2D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 256×256 칸 한 장을 부위마다 옮겨 합치는 일 — 화면 크기와 상관없이 가볍다. 미리 스프라이트 시트로 구우면 실행 중 비용은 0.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i501
- 쓰면 좋을 때: 그림을 한 장만 받았고 프레임을 따로 그릴 수 없을 때 / 정면 전투의 적 · 보스, 이야기 장면의 인물처럼 크게 움직이지 않는 캐릭터
- 쓰지 말 때: 걷기 · 달리기처럼 팔다리가 크게 움직이는 캐릭터 — 자세마다 따로 그린 프레임이 필요하다 / 옆모습이 필요한 옆 스크롤 게임 — 정면 그림 하나로는 방향을 못 바꾼다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i501 견본 항목 — `src/demos/demosPixelArt.ts:414`
```ts
  i501: {
    kind: 'dom',
    caption: '그림 한 장을 부위별로 오려 통째로 칸 단위로 — 가만히 · 유령 걸음 · 공격 · 맞음 · 사라지기 · 나타나기 · 분노',
    make(box) {
      const { cv, g } = fitCanvas(box, '#16131f');
      const anim = makeGhost();
      let state: StateId = 'idle';
      let f = 0;
      let acc = 0;
      let fps = 8;
      let auto = true;
      let autoT = 0;
      let showParts = false;
      const tmp = document.createElement('canvas');
      tmp.width = SW;
      tmp.height = N;
      const order: StateId[] = ['idle', 'attack', 'idle', 'hit', 'walk', 'rage', 'vanish', 'appear'];
      let oi = 0;
      const set = (s: StateId): void => {
        state = s;
        f = 0;
      };
      const controls: Control[] = [
        ...GHOST_STATES.map((s): Control => ({ type: 'button', label: s.name, on: () => { auto = false; set(s.id); } })),
        { type: 'toggle', label: '상태 저절로 바꾸기', value: true, on: (v) => { auto = v; } },
        { type: 'toggle', label: '나눈 부위 색으로 보기', value: false, on: (v) => { showParts = v; } },
        { type: 'range', label: '장 / 초', min: 4, max: 12, step: 1, value: 8, on: (v) => { fps = v; } },
      ];
      return {
        controls,
        update(_t, dt) {
          const [w, h] = sizeTo(cv);
          g.imageSmoothingEnabled = false;
          g.fillStyle = '#16131f';
          g.fillRect(0, 0, w, h);
          if (!anim.ready()) {
            g.fillStyle = '#aaa';
            g.font = `600 ${Math.round(h * 0.05)}px ${FONT}`;
            g.textAlign = 'center';
            g.fillText('그림 불러오는 중…', w / 2, h / 2);
            return;
          }
          const st = GHOST_STATES.find((s) => s.id === state)!;
          acc += Math.min(0.25, dt);
          while (acc >= 1 / fps) {
            acc -= 1 / fps;
            f++;
            if (f >= st.len) {
              if (st.loop) f = 0;
              else if (state === 'vanish') f = st.len - 1;
              else set('idle');
            }
          }
          if (auto) {
            autoT += dt;
            const hold = state === 'idle' || state === 'walk' || state === 'rage' ? 4 : st.len / fps + 0.6;
            if (autoT > hold) {
              autoT = 0;
              oi = (oi + 1) % order.length;
              set(order[oi]!);
            }
          }
          const img = anim.render(state, f, showParts);
          tmp.getContext('2d')!.putImageData(new ImageData(new Uint8ClampedArray(img), SW, N), 0, 0);
          const k = Math.min(w / SW, h / N);
          const s = k >= 2 ? Math.floor(k) : k; // 두 배 이상이면 정수 배 (칸이 고르게)
          const dw = SW * s;
          const dh = N * s;
          g.drawImage(tmp, Math.round((w - dw) / 2), Math.round((h - dh) / 2), dw, dh);
          g.font = `700 ${Math.max(11, Math.round(h * 0.04))}px ${FONT}`;
          g.textAlign = 'left';
          g.fillStyle = 'rgba(255,255,255,0.75)';
          g.fillText(`${st.name} · ${f + 1}/${st.len}장`, 10, Math.max(16, h * 0.06));
        },
        dispose() {
          box.innerHTML = '';
        },
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [AI 픽셀풍 그림 정리 (격자 · 팔레트 되찾기)](https://ai-techstudio.web.app/ai/t/i500.md) `i500` · [플립북 (스프라이트 시트)](https://ai-techstudio.web.app/ai/t/i59.md) `i59`
- 다음에 해 볼 기술: [팔레트 색 순환 (그림은 그대로, 색만 흐름)](https://ai-techstudio.web.app/ai/t/i236.md) `i236` · [스프라이트 녹기 · 나타나기 (잡음 디졸브 2D)](https://ai-techstudio.web.app/ai/t/i295.md) `i295` · [피격 흰 번쩍](https://ai-techstudio.web.app/ai/t/i201.md) `i201` · [색 바꾸기 · 팔레트 교체 (2D)](https://ai-techstudio.web.app/ai/t/i296.md) `i296`
- 참고 문서: [Wikipedia — Color cycling](https://en.wikipedia.org/wiki/Color_cycling) · [Wikipedia — Cutout animation](https://en.wikipedia.org/wiki/Cutout_animation)
