# AI 꾸러미 — 픽셀 아트 확대 (계단 그대로 · 서브픽셀) — Integer scaling (nearest neighbor)
> 도트 그림을 정수 배로만 키우고 위치도 정수 칸에 맞추며, 필요하면 scale2x(EPX)로 대각선만 매끈하게 해 흐림 · 떨림 없이 또렷하게 보이게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i304

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

## 주문서

### 만들어 줘: 픽셀 아트 확대 (계단 그대로 · 서브픽셀) — Integer scaling (nearest neighbor)

#### 1. 목표
16×16 도트 캐릭터를 또렷하게 키워 줘 — 정수 배 가까운 점 확대, 위치는 정수 칸에 맞추고, 원하면 scale2x 로 대각선만 매끈하게. 결과는 계단 그대로 또렷하게.

#### 2. 핵심 기술 용어
- **Integer scaling (nearest neighbor)** — 정수 배 · 가까운 점 확대
- **Scale2x / EPX pixel-art scaling** — 이웃 색을 보고 대각 계단만 다듬는 2배 확대
- **imageSmoothingEnabled = false** — 캔버스 부드럽게 그리기 끄기
- **Sub-pixel jitter (pixel snapping)** — 반 칸 위치 때문에 번지고 떨리는 것 · 칸에 맞추기

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

#### 4. 조건
- imageSmoothingEnabled = false (캔버스를 다시 만들거나 크기가 바뀌면 다시 설정)
- 배수는 정수, 그리는 위치도 Math.round 로 정수
- scale2x 는 미리 한 번 계산해 둔 그림을 쓰기
- 카메라 위치도 칸 단위로 맞추기 (소수 이동은 내부 값으로만)

#### 5. 완성 기준 (이게 보이면 성공)
- 가까운 점 · scale2x · 부드럽게 세 가지를 나란히 보면 각각 계단 · 매끈한 대각선 · 흐릿함이 보인다
- 정수 칸에 맞춘 줄은 움직여도 또렷하고, 반 칸 줄은 번지고 떨린다
- 확대해도 칸 크기가 모두 같다
- 폰 고해상도 화면에서도 흐리지 않다

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

## 원리
- 부드럽게 키우면(보간) 칸 경계가 섞여 흐릿해진다. imageSmoothingEnabled = false 로 가까운 점 확대를 하면 계단이 그대로 또렷하다.
- 배수는 정수(×2 · ×3 · ×4)로. ×2.5 같은 배수는 칸마다 2화소 · 3화소가 섞여 크기가 들쭉날쭉하다.
- scale2x(EPX): 화소 P 와 위 A · 오른쪽 B · 왼쪽 C · 아래 D 를 보고, 예를 들어 C = A 이고 C ≠ D, A ≠ B 이면 왼쪽 위 칸을 A 로 — 대각 계단 모서리만 채운다. 두 번 하면 ×4.
- 움직일 때 위치가 반 칸(소수)이면 칸이 번지고 떨린다 — 그리는 자리는 Math.round 로 정수 칸에.

## 핵심 코드 — scale2x(EPX) + 정수 칸 위치로 또렷하게
(발췌: demos/demos2dLook.ts epx() · mkI304() 를 정리)
```ts
// src: 화소 색 (Uint32) · W × H → 2W × 2H
function epx(src: Uint32Array, W: number, H: number): Uint32Array {
  const o = new Uint32Array(W * 2 * H * 2);
  const at = (x: number, y: number) => src[Math.min(H - 1, Math.max(0, y)) * W + Math.min(W - 1, Math.max(0, x))]!;
  const W2 = W * 2;
  for (let y = 0; y < H; y++)
    for (let x = 0; x < W; x++) {
      const P = at(x, y), A = at(x, y - 1), B = at(x + 1, y), C = at(x - 1, y), D = at(x, y + 1);
      let p1 = P, p2 = P, p3 = P, p4 = P;
      if (C === A && C !== D && A !== B) p1 = A; // 왼쪽 위
      if (A === B && A !== C && B !== D) p2 = B; // 오른쪽 위
      if (D === C && D !== B && C !== A) p3 = C; // 왼쪽 아래
      if (B === D && B !== A && D !== C) p4 = D; // 오른쪽 아래
      o[y * 2 * W2 + x * 2] = p1;
      o[y * 2 * W2 + x * 2 + 1] = p2;
      o[(y * 2 + 1) * W2 + x * 2] = p3;
      o[(y * 2 + 1) * W2 + x * 2 + 1] = p4;
    }
  return o;
}
// ×4 = epx 두 번 (미리 한 번만)
const d0 = new Uint32Array(base.getContext('2d')!.getImageData(0, 0, 16, 16).data.buffer.slice(0));
const e2 = epx(epx(d0, 16, 16), 32, 32);

function drawSprite(g: CanvasRenderingContext2D, img: CanvasImageSource, x: number, y: number, scale: number) {
  g.imageSmoothingEnabled = false;             // 가까운 점
  g.drawImage(img, Math.round(x), Math.round(y), 16 * scale, 16 * scale); // 정수 칸 · 정수 배
}
```

## 흔한 실수 · 확인 목록
- [ ] **캔버스 크기를 바꾸면 imageSmoothingEnabled 가 다시 켜진다** — 크기를 바꾼 뒤마다 다시 false 로 설정한다.
- [ ] **위치가 소수면 움직일 때 도트가 번지고 떨린다** — 그리는 자리를 Math.round 로. 속도 · 위치 계산은 소수로 두고 그릴 때만 정수.
- [ ] **×1.5 · ×2.5 처럼 키우면 칸 크기가 들쭉날쭉하다** — 정수 배로만 키우고 남는 화면은 테두리로 채운다.
- [ ] **scale2x 를 매 프레임 계산하면 쓸데없이 바쁘다** — 그림마다 한 번 계산해 캔버스로 구워 둔다.

## 완성 기준 체크리스트
- [ ] 가까운 점 · scale2x · 부드럽게 세 가지를 나란히 보면 각각 계단 · 매끈한 대각선 · 흐릿함이 보인다
- [ ] 정수 칸에 맞춘 줄은 움직여도 또렷하고, 반 칸 줄은 번지고 떨린다
- [ ] 확대해도 칸 크기가 모두 같다
- [ ] 폰 고해상도 화면에서도 흐리지 않다

## 이 기술 정보
- id: `i304` · 분류: 2D · 화면 › 2D 그림 효과 · 2D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 가까운 점 확대는 공짜. scale2x 는 그림마다 한 번 (16×16 → 64×64) 미리 계산.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i304
- 쓰면 좋을 때: 도트 그림 게임을 화면에 크게 키워 보일 때 / 카메라가 움직이는 도트 게임에서 떨림 · 번짐이 보일 때
- 쓰지 말 때: 매끈한 그림 · 사진 — 가까운 점 확대는 계단을 드러낸다. 대신 부드럽게 / 아주 작은 배수 차이가 필요한 화면 맞춤 — 정수 배 + 남는 곳은 테두리로 채운다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### mkI304 — `src/demos/demos2dLook.ts:2198`
```ts
function mkI304() {
  const base = pixCanvas(SLIME, SL_PAL.orig!);
  const d0 = new Uint32Array(c2(base).getImageData(0, 0, PN, PN).data.buffer.slice(0));
  const e1 = epx(d0, PN, PN);
  const e2 = epx(e1, PN * 2, PN * 2);
  const ep = mkCanvas(PN * 4, PN * 4);
  {
    const x = c2(ep);
    const im = x.createImageData(PN * 4, PN * 4);
    new Uint32Array(im.data.buffer).set(e2);
    x.putImageData(im, 0, 0);
  }
  const LW = 120;
  const LH = 18;
  const lanes = [mkCanvas(LW, LH), mkCanvas(LW, LH)];
  let gridOn = true;
  let spd = 1;
  let along = 0;
  const controls: Control[] = [
    { type: 'toggle', label: '원래 칸 격자', value: gridOn, on: (v) => (gridOn = v) },
    { type: 'range', label: '카메라 속도', min: 0, max: 3, step: 0.1, value: spd, on: (v) => (spd = v) },
  ];
  return {
    controls,
    draw(g: G, w: number, h: number, _t: number, dt: number) {
      reset(g);
      const f = frame(g, w, h);
      vbg(g, f, '#1b2040', '#0c0f22');
      along += dt * spd;
      const PS = 72;
      const xs = [14, 104, 194];
      const labs = ['가장 가까운 점 ×4', 'scale2x(EPX) ×4', '부드럽게 ×4'];
      for (let i = 0; i < 3; i++) {
        const x = xs[i]!;
        const y = 22;
        rr(g, x - 2, y - 2, PS + 4, PS + 4, 5);
        g.fillStyle = '#e9f2ff';
        g.fill();
        g.fillStyle = '#cfe0f5';
        for (let a = 0; a < 8; a++) for (let b = 0; b < 8; b++) if ((a + b) % 2) g.fillRect(x + a * 9, y + b * 9, 9, 9);
        g.imageSmoothingEnabled = i === 2;
        if (i === 2) g.imageSmoothingQuality = 'high';
        g.drawImage(i === 1 ? ep : base, x, y, PS, PS);
        g.imageSmoothingEnabled = true;
        if (gridOn && i === 0) {
          g.strokeStyle = 'rgba(0,0,0,.12)';
          g.lineWidth = 0.4;
          for (let k = 0; k <= 16; k++) {
            g.beginPath();
            g.moveTo(x + k * 4.5, y);
            g.lineTo(x + k * 4.5, y + PS);
            g.moveTo(x, y + k * 4.5);
            g.lineTo(x + PS, y + k * 4.5);
            g.stroke();
          }
        }
        txt(g, labs[i]!, x + PS / 2, 103, 7.5, ['#9fe0ff', '#9fffc0', '#ffb3c8'][i]!);
      }
      txt(g, '계단 그대로', xs[0]! + PS / 2, 13, 6.5, '#7f88b8');
      txt(g, '대각선만 매끈', xs[1]! + PS / 2, 13, 6.5, '#7f88b8');
      txt(g, '흐릿해짐', xs[2]! + PS / 2, 13, 6.5, '#7f88b8');
      // 서브픽셀 줄
      const pos = 8 + (Math.sin(along * 0.9) * 0.5 + 0.5) * (LW - 34);
      for (let k = 0; k < 2; k++) {
        const c = lanes[k]!;
        const x = c2(c);
        x.imageSmoothingEnabled = k === 1;
        x.fillStyle = '#2d3566';
        x.fillRect(0, 0, LW, LH);
        x.fillStyle = '#3a4580';
        for (let i = 0; i < LW; i += 8) x.fillRect(i, LH - 2, 4, 2);
        const px = k === 0 ? Math.round(pos) : pos;
        x.drawImage(base, px, 1, 16, 16);
        const y = 114 + k * 30;
        g.imageSmoothingEnabled = false;
        g.drawImage(c, 78, y, LW * 1.6, LH * 1.6);
        g.imageSmoothingEnabled = true;
        txt(g, k === 0 ? '정수 칸에 맞춤' : '반 칸 위치', 40, y + 10, 7.5, k === 0 ? '#9fffc0' : '#ffb3c8');
        txt(g, k === 0 ? '또렷' : '번지고 떨림', 40, y + 20, 6.5, '#7f88b8');
      }
      g.restore();
    },
  };
}
```

### i304 견본 항목 — `src/demos/demos2dLook.ts:3787`
```ts
  i304: { kind: '2d', caption: '16×16 도트를 ×4: 가까운 점 · scale2x(대각선만 매끈) · 부드럽게(흐림) — 아래는 정수 칸 vs 반 칸 위치', make: mkI304 }
```

## 관련 기술
- 먼저 알면 좋은 기술: [AI 픽셀풍 그림 정리 (격자 · 팔레트 되찾기)](https://ai-techstudio.web.app/ai/t/i500.md) `i500`
- 다음에 해 볼 기술: [픽셀 그림 한 장 살리기 (부위 겹 · 칸 단위 움직임)](https://ai-techstudio.web.app/ai/t/i501.md) `i501` · [색 바꾸기 · 팔레트 교체 (2D)](https://ai-techstudio.web.app/ai/t/i296.md) `i296`
- 참고 문서: [Wikipedia — Pixel-art scaling algorithms](https://en.wikipedia.org/wiki/Pixel-art_scaling_algorithms) · [MDN — imageSmoothingEnabled](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/imageSmoothingEnabled)
