# AI 꾸러미 — SDF 메타볼 (물방울 · 슬라임)
> 둥근 것끼리 녹아 붙음  
> 견본: https://ai-techstudio.web.app/#t/i29

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

## 주문서

### 만들어 줘: SDF 메타볼 (물방울 · 슬라임)

#### 1. 목표
수박 합치기에 「SDF 메타볼 (물방울 · 슬라임)」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 둥근 것끼리 녹아 붙음

#### 2. 핵심 기술 용어
- **SDF**

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

#### 4. 조건
- devicePixelRatio 를 맞춰 또렷하게
- 매 프레임 다시 그릴 필요 없는 그림은 화면 밖 캔버스에 미리 구워 두기
- 한 프레임 16ms 안 (폰 포함)

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「둥근 것끼리 녹아 붙음」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 둥근 것끼리 녹아 붙음


## 흔한 실수 · 확인 목록
- (아직 정리된 것이 없어요)

## 완성 기준 체크리스트
- [ ] 견본처럼 「둥근 것끼리 녹아 붙음」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i29` · 분류: 2D · 화면 › 2D 그리기 · 2D · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i29
- 쓰면 좋을 때: 쓰면 좋을 곳 — 수박 합치기 · 물방울

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i29 견본 항목 — `src/demos/demosDraw2d.ts:2497`
```ts
  i29: {
    kind: '2d',
    caption: '왼쪽 그냥 원 겹치기 · 오른쪽 메타볼(거리 장의 합) — 가까워지면 물방울처럼 녹아 붙고 멀어지면 쭉 늘어나 떨어져요',
    make() {
      let field: HTMLCanvasElement | null = null;
      let img: ImageData | null = null;
      let thr = 1;
      const balls = (t: number): [number, number, number][] => {
        const out: [number, number, number][] = [];
        for (let i = 0; i < 5; i++) {
          const a = t * 0.55 * (i % 2 ? 1 : -1) + i * 1.26;
          const rr2 = 0.18 + 0.12 * Math.sin(t * 0.8 + i * 1.7);
          out.push([0.5 + Math.cos(a) * rr2, 0.5 + Math.sin(a) * rr2 * 1.1, [0.13, 0.1, 0.12, 0.085, 0.11][i]!]);
        }
        return out;
      };
      return {
        draw(g, w, h, t) {
          const u = scaleOf(w, h);
          const hw = w / 2;
          const m = Math.min(hw, h);
          const B = balls(t);
          for (let s = 0; s < 2; s++) {
            const ox = s * hw;
            const gr = g.createLinearGradient(0, 0, 0, h);
            gr.addColorStop(0, '#1a2a5c');
            gr.addColorStop(1, '#0b1230');
            g.fillStyle = gr;
            g.fillRect(ox, 0, hw, h);
          }
          // 왼쪽: 그냥 원
          for (const [x, y, r] of B) {
            g.fillStyle = 'rgba(60,220,160,.85)';
            g.strokeStyle = '#0d6a4a';
            g.lineWidth = 1.5 * u;
            g.beginPath();
            g.arc(x * hw, y * h, r * m, 0, Math.PI * 2);
            g.fill();
            g.stroke();
          }
          // 오른쪽: 메타볼
          const gw = 100;
          const gh = Math.max(10, Math.round((gw * h) / hw));
          if (!field || field.width !== gw || field.height !== gh) {
            field = mkCanvas(gw, gh);
            img = ctx(field).createImageData(gw, gh);
          }
          const d = img!.data;
          const sx = hw / gw;
          const sy = h / gh;
          const P = B.map(([x, y, r]) => [x * hw, y * h, (r * m) ** 2] as const);
          for (let j = 0; j < gh; j++)
            for (let i = 0; i < gw; i++) {
              const px = (i + 0.5) * sx;
              const py = (j + 0.5) * sy;
              let f = 0;
              for (const [bx, by, r2] of P) f += r2 / ((px - bx) ** 2 + (py - by) ** 2 + 1e-3);
              const a = clamp01((f - thr * 0.9) / (thr * 0.2));
              const c = clamp01((f - thr) / 2.2);
              const rim = clamp01(1 - Math.abs(f - thr * 1.15) / 0.25) * 0.35;
              const o = (j * gw + i) * 4;
              d[o] = 30 + 120 * c + 200 * rim;
              d[o + 1] = 170 + 75 * c + 80 * rim;
              d[o + 2] = 140 + 90 * c + 80 * rim;
              d[o + 3] = a * 255;
            }
          ctx(field).putImageData(img!, 0, 0);
          g.save();
          g.beginPath();
          g.rect(hw, 0, hw, h);
          g.clip();
          g.imageSmoothingEnabled = true;
          g.drawImage(field, hw, 0, hw, h);
          for (const [x, y, r] of B) {
            g.fillStyle = 'rgba(255,255,255,.55)';
            g.beginPath();
            g.ellipse(hw + x * hw - r * m * 0.35, y * h - r * m * 0.4, r * m * 0.22, r * m * 0.12, -0.6, 0, Math.PI * 2);
            g.fill();
          }
          g.restore();
          splitLabels(g, w, h, u, '원 겹치기', '메타볼');
        },
        controls: [{ type: 'range', label: '경계 값 (작을수록 더 잘 붙음)', min: 0.5, max: 2, step: 0.05, value: 1, on: (v) => (thr = v) }] as Control[],
      };
    },
  }
```

## 관련 기술
- 이 효과를 만든 기술: [메타볼 몸 (말랑한 덩어리)](https://ai-techstudio.web.app/ai/t/i447.md) `i447`
