# AI 꾸러미 — 색 바꾸기 · 팔레트 교체 (2D) — Palette swap
> 도트 그림을 색 번호(0~7)로 한 번만 그리고 색 표만 갈아 끼워, 같은 그림으로 팀 색 · 피격 깜빡임 · 얼음 · 무지개 돌리기를 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i296

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

## 주문서

### 만들어 줘: 색 바꾸기 · 팔레트 교체 (2D) — Palette swap

#### 1. 목표
16×16 도트 슬라임을 색 번호 그림 + 색 표로 바꿔 줘 — 같은 그림에 색 표만 갈아 끼워 팀 색 · 피격 깜빡임 · 얼음 상태를 낸다. 그림체는 선명한 도트.

#### 2. 핵심 기술 용어
- **Palette swap** — 색 표 바꾸기 — 같은 그림, 다른 색
- **Indexed sprite** — 색 번호로 그린 스프라이트
- **Hit flash** — 맞았을 때 흰색 · 빨강 번갈아 깜빡
- **Hue rotation palette** — 색상(hue)을 돌린 색 표

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

#### 4. 조건
- 그림은 번호 배열 하나, 색은 표에서만 — 그림에 색을 직접 칠하지 않기
- 번호의 뜻(외곽선 · 몸 · 그늘 …)을 정해 두고 모든 표가 같은 뜻을 따르기
- 표마다 한 번 구워 캐시, 키울 땐 imageSmoothingEnabled = false
- 피격 깜빡임은 0.07초 간격으로 표 두 개 번갈아

#### 5. 완성 기준 (이게 보이면 성공)
- 같은 슬라임이 원래 · 파랑 팀 · 피격 · 얼음 · 무지개로 나란히 보인다
- 피격은 0.45초 동안 빨강 ↔ 흰색으로 깜빡이며 살짝 흔들린다
- 색 표 보기를 켜면 각 슬라임 아래 7칸 색 표가 보인다
- 확대해도 칸 경계가 번지지 않는다

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

## 원리
- 그림은 화소마다 번호 0~7 만 가진다: 0 투명 · 1 외곽선 · 2 몸 · 3 그늘 · 4 빛 · 5 눈 흰자 · 6 장식 · 7 볼.
- 색 표는 번호 → 색 배열 8칸. 원래 · 파랑 팀 · 피격(빨강) · 흰색 · 얼음 표를 따로 둔다.
- 표마다 한 번 칠해 작은 캔버스로 구워 두고 골라 쓴다 — 그림을 새로 그릴 일이 없다.
- 피격: 0.45초 동안 0.07초마다 「피격 ↔ 흰색」 표를 번갈아. 무지개는 hue 가 바뀔 때만 표를 새로 만들어 다시 굽는다.

## 핵심 코드 — 색 번호 그림 × 색 표 → 구워 두고 골라 쓰기
(발췌: demos/demos2dLook.ts pixCanvas() · SL_PAL · mkI296() 를 정리)
```ts
const PN = 16;
type Pal = (string | null)[]; // 번호 → 색 (0 = 투명)
const PALS: Record<string, Pal> = {
  orig:  [null, '#1d3b2a', '#7ad65a', '#3f9c46', '#e4ffd2', '#ffffff', '#ffb43c', '#ff8fa8'],
  team:  [null, '#16213e', '#5ea8ff', '#2f5fd0', '#dff0ff', '#ffffff', '#ffd43c', '#ff9ac0'],
  hit:   [null, '#5a0f14', '#ff4d5e', '#c21f35', '#ffd0d6', '#ffffff', '#ffe0a0', '#ffffff'],
  white: [null, '#ffffff', '#ffffff', '#ffe9ec', '#ffffff', '#ffffff', '#ffffff', '#ffffff'],
  ice:   [null, '#1c4b66', '#bdf4ff', '#7ccde8', '#ffffff', '#ffffff', '#9be7ff', '#ffc9e0'],
};
function pixCanvas(idx: Uint8Array, pal: Pal) {
  const c = document.createElement('canvas');
  c.width = c.height = PN;
  const x = c.getContext('2d')!;
  const im = x.createImageData(PN, PN);
  for (let i = 0; i < PN * PN; i++) {
    const col = pal[idx[i]!];
    if (!col) continue;
    const n = parseInt(col.slice(1), 16);
    im.data.set([(n >> 16) & 255, (n >> 8) & 255, n & 255, 255], i * 4);
  }
  x.putImageData(im, 0, 0);
  return c;
}
// 표마다 한 번 굽기
const baked = Object.fromEntries(Object.entries(PALS).map(([k, p]) => [k, pixCanvas(SLIME, p)]));

function spriteFor(state: 'orig' | 'team' | 'ice' | 'hit', hitT: number) {
  if (state === 'hit' && hitT < 0.45) return Math.floor(hitT / 0.07) % 2 ? baked.hit! : baked.white!; // 깜빡
  return baked[state]!;
}
// 그리기: g.imageSmoothingEnabled = false; g.drawImage(spriteFor(s, t % 2.2), x, y, 32, 32);
```

## 흔한 실수 · 확인 목록
- [ ] **색을 그림에 직접 칠해 두면 팀 색을 바꿀 때마다 그림을 새로 그려야 한다** — 그림은 번호로, 색은 표로 나눈다.
- [ ] **번호 뜻이 표마다 다르면 눈 · 볼 자리 색이 뒤죽박죽된다** — 번호마다 부위를 정해 두고(1 외곽선 · 2 몸 …) 모든 표가 따른다.
- [ ] **피격 깜빡임을 매 프레임 바꾸면 너무 빨라 회색으로 보인다** — 0.07초 간격으로 표 두 개를 번갈아 쓴다.
- [ ] **무지개 표를 매 프레임 다시 구우면 쓸데없이 바쁘다** — hue 정수가 바뀔 때만 다시 굽는다.

## 완성 기준 체크리스트
- [ ] 같은 슬라임이 원래 · 파랑 팀 · 피격 · 얼음 · 무지개로 나란히 보인다
- [ ] 피격은 0.45초 동안 빨강 ↔ 흰색으로 깜빡이며 살짝 흔들린다
- [ ] 색 표 보기를 켜면 각 슬라임 아래 7칸 색 표가 보인다
- [ ] 확대해도 칸 경계가 번지지 않는다

## 이 기술 정보
- id: `i296` · 분류: 2D · 화면 › 2D 그림 효과 · 2D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 색 표마다 16×16 한 번 굽기. 게임 중엔 drawImage 만. 무지개도 hue 가 바뀔 때만 다시 굽는다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i296
- 쓰면 좋을 때: 같은 캐릭터를 팀 · 상태별 색으로 여러 벌 쓸 때 / 맞았을 때 깜빡임 · 얼어붙음 같은 상태 표시
- 쓰지 말 때: 그러데이션 · 사진 그림 — 번호 몇 개로 나눌 수 없다. 대신 색 곱하기(source-atop) 물들이기 / 색만 살짝 어둡게 — filter brightness 가 더 간단

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### mkI296 — `src/demos/demos2dLook.ts:700`
```ts
function mkI296() {
  const base = {
    orig: pixCanvas(SLIME, SL_PAL.orig!),
    team: pixCanvas(SLIME, SL_PAL.team!),
    hit: pixCanvas(SLIME, SL_PAL.hit!),
    white: pixCanvas(SLIME, SL_PAL.white!),
    ice: pixCanvas(SLIME, SL_PAL.ice!),
  };
  const grayPal: PalArr = [null, '#202020', '#9a9a9a', '#6a6a6a', '#e6e6e6', '#ffffff', '#c4c4c4', '#b0b0b0'];
  const gray = pixCanvas(SLIME, grayPal);
  const rb = mkCanvas(PN, PN);
  let swatch = true;
  let speed = 1;
  let lastHue = -1;
  const controls: Control[] = [
    { type: 'toggle', label: '색 표 보기', value: swatch, on: (v) => (swatch = v) },
    { type: 'range', label: '무지개 돌리기 속도', min: 0, max: 3, step: 0.1, value: speed, on: (v) => (speed = v) },
  ];
  return {
    controls,
    draw(g: G, w: number, h: number, t: number) {
      reset(g);
      const f = frame(g, w, h);
      vbg(g, f, '#1c2340', '#0e1226');
      g.imageSmoothingEnabled = false;
      // 왼쪽: 색 번호 그림
      rr(g, 8, 24, 78, 110, 8);
      g.fillStyle = 'rgba(255,255,255,.06)';
      g.fill();
      g.drawImage(gray, 15, 38, 64, 64);
      g.strokeStyle = 'rgba(255,255,255,.08)';
      g.lineWidth = 0.4;
      for (let i = 0; i <= 16; i++) {
        g.beginPath();
        g.moveTo(15 + i * 4, 38);
        g.lineTo(15 + i * 4, 102);
        g.moveTo(15, 38 + i * 4);
        g.lineTo(79, 38 + i * 4);
        g.stroke();
      }
      txt(g, '색 번호 그림', 47, 31, 7.5, '#cfd8ff');
      txt(g, '(0~7 번호만)', 47, 112, 7, '#8f9ac4');
      txt(g, '1개 그림', 47, 124, 7, '#8f9ac4');
      // 화살표
      g.fillStyle = '#7f8cff';
      g.beginPath();
      g.moveTo(90, 66);
      g.lineTo(98, 70);
      g.lineTo(90, 74);
      g.fill();
      // 무지개 팔레트
      const hue = Math.floor(t * 120 * speed) % 360;
      if (hue !== lastHue) {
        lastHue = hue;
        const p: PalArr = [null, hslHex(hue + 200, 45, 18), hslHex(hue, 85, 62), hslHex(hue - 15, 70, 42), hslHex(hue, 90, 90), '#ffffff', hslHex(hue + 180, 85, 60), '#ff9ac0'];
        const nc = pixCanvas(SLIME, p);
        const x = c2(rb);
        x.clearRect(0, 0, PN, PN);
        x.drawImage(nc, 0, 0);
      }
      const hitT = t % 2.2;
      const hitting = hitT < 0.45;
      const flashPal = hitting ? (Math.floor(hitT / 0.07) % 2 ? 'hit' : 'white') : 'hit';
      const items: { c: HTMLCanvasElement; name: string; pal: PalArr }[] = [
        { c: base.orig, name: '원래', pal: SL_PAL.orig! },
        { c: base.team, name: '파랑 팀', pal: SL_PAL.team! },
        { c: hitting ? base[flashPal as 'hit' | 'white'] : base.hit, name: '피격', pal: SL_PAL.hit! },
        { c: base.ice, name: '얼음', pal: SL_PAL.ice! },
        { c: rb, name: '색 돌리기', pal: [] },
      ];
      for (let i = 0; i < items.length; i++) {
        const it = items[i]!;
        const cx = 120 + i * 36;
        const bounce = Math.abs(Math.sin(t * 3 + i * 0.7)) * 5;
        let sx = 0;
        if (i === 2 && hitting) sx = Math.sin(hitT * 90) * 2.5 * (1 - hitT / 0.45);
        g.fillStyle = 'rgba(0,0,0,.35)';
        g.beginPath();
        g.ellipse(cx, 99, 13 - bounce * 0.6, 3, 0, 0, TAU);
        g.fill();
        g.drawImage(it.c, Math.round(cx - 16 + sx), Math.round(98 - 32 - bounce), 32, 32);
        if (i === 2 && hitting) txt(g, '-3', cx + 10, 56 - hitT * 30, 10, '#ffde59', 'center', 900);
        txt(g, it.name, cx, 46, 7, '#e3e8ff');
        if (swatch) {
          const pal = i === 4 ? [null, hsl(hue + 200, 45, 18), hsl(hue, 85, 62), hsl(hue - 15, 70, 42), hsl(hue, 90, 90), '#fff', hsl(hue + 180, 85, 60), '#ff9ac0'] : it.pal;
          for (let k = 1; k < 8; k++) {
            const sy = 108 + (k - 1) * 7;
            g.fillStyle = pal[k] ?? '#000';
            g.fillRect(cx - 8, sy, 16, 6);
          }
          g.strokeStyle = 'rgba(255,255,255,.25)';
          g.lineWidth = 0.6;
          g.strokeRect(cx - 8, 108, 16, 49);
        }
      }
      if (swatch) for (let k = 1; k < 8; k++) txt(g, String(k), 98, 111 + (k - 1) * 7, 5.5, '#8f9ac4');
      pill(g, '같은 그림 · 색 표만 바꿈', 175, 14, 8.5, 'rgba(127,140,255,.22)', '#e8ebff');
      g.restore();
    },
  };
}
```

### i296 견본 항목 — `src/demos/demos2dLook.ts:3779`
```ts
  i296: { kind: '2d', caption: '번호(0~7)로 칠한 도트 그림 하나에 색 표만 갈아 끼움 — 팀 색 · 피격 깜빡 · 얼음 · 색 돌리기', make: mkI296 }
```

## 관련 기술
- 먼저 알면 좋은 기술: [미리 그린 3D 스프라이트 (pre-rendered)](https://ai-techstudio.web.app/ai/t/i73.md) `i73`
- 다음에 해 볼 기술: [팔레트 색 순환 (그림은 그대로, 색만 흐름)](https://ai-techstudio.web.app/ai/t/i236.md) `i236` · [스프라이트 외곽선 · 빛 테 (알파 넓히기)](https://ai-techstudio.web.app/ai/t/i294.md) `i294`
- 참고 문서: [Wikipedia — Palette swap](https://en.wikipedia.org/wiki/Palette_swap)
