# AI 꾸러미 — 선 들끓기 (line boil)
> 같은 그림을 살짝 다르게 떨리는 선으로 2~4장 그려 10fps 로 번갈아 보여 줘, 손으로 매번 다시 그린 애니처럼 선이 살아 움직이게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i323

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

## 주문서

### 만들어 줘: 선 들끓기 (line boil)

#### 1. 목표
손그림 캐릭터 · 소품에 선 들끓기 효과를 넣어 줘 — 같은 그림을 살짝 다르게 떨린 선으로 3장 구워 두고 10fps 로 번갈아. 그림체는 연필 스케치.

#### 2. 핵심 기술 용어
- **Line boil** — 선 들끓기 — 손그림 애니의 꿈틀거리는 선
- **Hand-drawn jitter (seeded noise)** — 씨앗이 정해진 무작위로 떨림
- **Frame stepping (animate on twos)** — 매 프레임이 아니라 몇 프레임마다 장 바꾸기
- **Offscreen canvas cache** — 화면 밖 캔버스에 미리 구워 두기

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

#### 4. 조건
- 떨림은 씨앗이 정해진 의사 난수(mulberry32 등) — Math.random 금지 (장마다 늘 같은 그림이어야 한다)
- 장 2~4장을 화면 밖 캔버스에 미리 구워 두고, 그리기는 drawImage 만
- 바꾸는 빠르기 8~12fps (화면은 60fps 로 돌아도 장은 천천히)
- 켬/끔 비교 — 왼쪽 한 장 그대로, 오른쪽 번갈아

#### 5. 완성 기준 (이게 보이면 성공)
- 오른쪽 그림의 선이 꿈틀거리며 「손으로 다시 그린」 느낌이 나고, 왼쪽은 그대로다
- 장 수 · 빠르기(fps) · 떨림 세기 슬라이더로 느낌을 바꿀 수 있다
- 떨림 세기를 바꾸면 다시 굽고, 그 외에는 매 프레임 그리기 비용이 거의 없다

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

## 원리
- 손그림 애니는 장마다 사람이 다시 그려서 선이 조금씩 다르다 — 그게 「살아 있는」 느낌을 준다.
- 선의 점마다 씨앗(seed)이 정해진 작은 무작위 떨림을 주면, 같은 씨앗은 늘 같은 장이 나온다.
- 씨앗만 바꾼 2~4장을 미리 구워 두고, 8~12fps 로 순서대로 번갈아 보여 준다.
- 매 프레임 새로 떨리게 하면 「지글지글」 소음이 되므로, 장 수와 바꾸는 빠르기를 제한하는 것이 핵심.

## 핵심 코드 — 씨앗 떨림 선을 3장 구워 10fps 로 번갈아
(발췌: 새로 씀 (demos/demosHandA.ts bakeBoilFrame · Bakery 와 같은 방식, 연필 질감은 뺌))
```ts
type P = [number, number];
/** 씨앗이 같으면 늘 같은 수열 */
function mulberry(a: number) {
  return () => {
    a |= 0; a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
/** 선을 잘게 나눠 점마다 살짝 흔든 손그림 선 */
function wobblyLine(g: CanvasRenderingContext2D, pts: P[], seed: number, jit: number) {
  const r = mulberry(seed);
  g.beginPath();
  for (let i = 0; i < pts.length - 1; i++) {
    const [x0, y0] = pts[i]!, [x1, y1] = pts[i + 1]!;
    const n = Math.max(2, Math.ceil(Math.hypot(x1 - x0, y1 - y0) / 8));
    for (let k = i ? 1 : 0; k <= n; k++) {
      const x = x0 + ((x1 - x0) * k) / n + (r() - 0.5) * 2 * jit;
      const y = y0 + ((y1 - y0) * k) / n + (r() - 0.5) * 2 * jit;
      i || k ? g.lineTo(x, y) : g.moveTo(x, y);
    }
  }
  g.stroke();
}
/** 장 f 를 화면 밖 캔버스에 굽기 */
function bake(lines: P[][], w: number, h: number, f: number, jit = 1.2): HTMLCanvasElement {
  const c = document.createElement('canvas');
  c.width = w; c.height = h;
  const g = c.getContext('2d')!;
  g.strokeStyle = '#2b2734'; g.lineWidth = 2.2; g.lineCap = g.lineJoin = 'round';
  lines.forEach((l, i) => wobblyLine(g, l, f * 131 + i * 17, jit));
  return c;
}
const frames = [0, 1, 2].map((f) => bake(LINES, 400, 300, f));
function draw(g: CanvasRenderingContext2D, t: number) {
  g.drawImage(frames[Math.floor(t * 10) % frames.length]!, 0, 0); // 10fps 로 번갈아
}
```

## 흔한 실수 · 확인 목록
- [ ] **매 프레임 Math.random 으로 떨면 지글지글 소음이 된다** — 씨앗 고정 · 장 2~4장 · 8~12fps. 이 세 가지가 「들끓기」와 「잡음」의 차이.
- [ ] **매 프레임 떨린 선을 새로 그리면 폰에서 무겁다** — 장마다 한 번 구워 두고 drawImage 만. 크기 · 떨림 세기가 바뀔 때만 다시 굽는다.
- [ ] **떨림이 선 굵기보다 크면 그림이 무너진다** — 떨림은 선 굵기의 0.5~1배 정도. 큰 화면에선 화면 비율에 맞춰 키운다.
- [ ] **칠한 색과 선이 같이 안 떨리면 어색하다** — 칠도 장마다 1~2px 어긋나게 (견본은 칠을 장마다 조금 밀어 손으로 칠한 느낌).

## 완성 기준 체크리스트
- [ ] 오른쪽 그림의 선이 꿈틀거리며 「손으로 다시 그린」 느낌이 나고, 왼쪽은 그대로다
- [ ] 장 수 · 빠르기(fps) · 떨림 세기 슬라이더로 느낌을 바꿀 수 있다
- [ ] 떨림 세기를 바꾸면 다시 굽고, 그 외에는 매 프레임 그리기 비용이 거의 없다

## 이 기술 정보
- id: `i323` · 분류: 2D · 화면 › 손그림 그림체 (2D) · 2D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 장을 미리 구워 두면 매 프레임 drawImage 한 번. 굽는 비용은 처음 한 번 (크기 바뀔 때만 다시).
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i323
- 쓰면 좋을 때: 손그림 · 동화책 그림체 게임에서 멈춘 화면도 생기 있게 / 정지 그림이 많은 설명 화면
- 쓰지 말 때: 글씨를 읽어야 하는 작은 UI — 떨리면 읽기 힘들다 / 이미 많이 움직이는 화면 — 산만해진다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### bakeBoilFrame — `src/demos/demosHandA.ts:704`
```ts
function bakeBoilFrame(b: BK, s: Subject, f: number, jit: number): void {
  const sc = b.S / 100;
  const r = mulberry(f * 7 + 3);
  // 색: 판마다 살짝 다르게 어긋난 칠 (손으로 매번 칠한 느낌)
  const [lc, lg] = layer(b, 0);
  const dx = (r() - 0.5) * 2.2 * sc * jit;
  const dy = (r() - 0.5) * 2.2 * sc * jit;
  s.fills.forEach((fl, i) => {
    const hp = handPath(mapPts(b, fl.pts).map((p): P => [p[0] + dx, p[1] + dy]), true, f * 131 + i * 17, jit * 1.1 * sc, 0, sc, 2.5);
    lg.fillStyle = fl.color;
    lg.globalAlpha = 0.85;
    linePath(lg, hp.p, true);
    lg.fill();
  });
  lg.globalAlpha = 1;
  grainOut(lg, 0.16, f * 41, f * 23);
  blit(b.g, lc, 1, 'multiply');
  bakePencil(b, s, { seed: f * 13 + 1, jit: jit * 1.15, over: 0.6 * jit, passes: 1, w: 1.9, col: '#2b2734', grain: 0.42 });
}
```

## 관련 기술
- 먼저 알면 좋은 기술: [스케치 선 (넘침 · 두 번 긋기)](https://ai-techstudio.web.app/ai/t/i324.md) `i324`
- 다음에 해 볼 기술: [연필 빗금 명암 (해칭 · 교차 빗금)](https://ai-techstudio.web.app/ai/t/i325.md) `i325` · [색연필 · 크레파스 결 (종이 이)](https://ai-techstudio.web.app/ai/t/i330.md) `i330`
