# AI 꾸러미 — 화이트보드 그리기 애니메이션 — Whiteboard animation (draw-on strokes)
> 손떨림 · 굵기 변화가 있는 마커 선을 길이 비율만큼 그어 펜이 따라가며 그리게 하고, 글씨는 한 글자씩, 장면 끝은 지우개로 지우며 자막과 맞춘다.  
> 견본: https://ai-techstudio.web.app/#t/i79

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

## 주문서

### 만들어 줘: 화이트보드 그리기 애니메이션 — Whiteboard animation (draw-on strokes)

#### 1. 목표
「왜 그럴까?」 짧은 원리 영상을 화이트보드 그리기 애니메이션으로 만들어 줘 — 손떨림 마커 선이 펜을 따라 그려지고, 글씨는 한 글자씩, 빗금 색칠 · 선 살랑임, 장면 끝은 지우개로 쓱쓱, 내레이션 자막이 낱말씩 맞춰 나오게. 분위기는 흰 보드 + 검정 · 빨강 · 파랑 마커.

#### 2. 핵심 기술 용어
- **Whiteboard animation (draw-on strokes)** — 선이 그려지는 화이트보드 설명 영상
- **Variable-width marker stroke** — 시작 · 끝이 가늘고 펜 누름이 흔들리는 선
- **Line boil** — 다 그린 선도 0.14초마다 살짝 다시 떨림
- **Hatch fill · caption sync** — 지그재그 빗금 색칠 · 낱말 단위 자막

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

#### 4. 조건
- 종이 층(한 번 그림)과 잉크 층(매 프레임) 두 캔버스로 나누기
- 떨린 선 점 목록은 캐시 — 같은 씨앗이면 같은 떨림 (프레임마다 무작위 금지, 선이 지글거린다)
- 선 살랑임은 0.14초마다 씨앗 3개를 돌려 쓰기 — 다 그린 선에만
- 펜은 지금 그리는 요소의 끝점에, 그리는 것이 없으면 다음 요소 시작점으로 옮겨 가기
- 자막 · 읽어 주기(한국어 음성) · 펜 · 겹선 · 빗금 · 손떨림을 켜고 끌 수 있게

#### 5. 완성 기준 (이게 보이면 성공)
- 선이 시작은 가늘게, 끝은 빠지듯 그려지고 펜 끝이 선 끝을 따라간다
- 다 그린 그림도 아주 살짝 살랑이며 살아 있는 낙서처럼 보인다
- 장면 끝에 지우개가 지그재그로 지나가며 그림이 지워지고 다음 장면이 시작된다
- 자막이 내레이션 길이에 맞춰 낱말씩 늘어난다

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

## 원리
- 그림은 장면 목록: 장면마다 { 길이, 내레이션 글, 요소들(선 · 빗금 · 글씨) }, 요소마다 시작 시각 t0 · 걸리는 시간 dur.
- 선은 점들에 손떨림(wob)을 더해 Catmull-Rom 으로 2.5 간격 점을 만들고, 누적 길이표를 둔다. 진행 f 만큼의 길이까지만 그리고 그 끝점에 펜을 둔다.
- 마커 굵기 = 기본 × 시작 가늘기(√) × 끝 빠짐(√) × 누름 흔들림(0.86 + 0.14·sin·sin) — 조각마다 굵기를 바꿔 짧은 선으로 이어 긋는다. 겹선은 같은 선을 한 번 더 반 굵기 · 55% 로.
- 색칠은 도형을 기울여 일정 간격 가로줄과의 교점을 지그재그로 이은 빗금, 다 그린 선은 0.14초마다 다른 떨림 씨앗(boil 0 · 1 · 2)으로 다시 그려 살아 있는 낙서처럼.
- 장면 끝에는 굵기 150 의 지그재그 선을 destination-out 으로 그어 지우개처럼 지우고, 자막은 지난 시간 비율만큼 낱말을 보여 준다 (읽어 주기는 speechSynthesis).

## 핵심 코드 — 손떨림 선 → 길이 비율까지 굵기 변하는 마커로 긋기
(발췌: demos/whiteboardSketch.ts rough · upTo · marker 를 정리 (catmull · withLen · rng 는 같은 파일))
```ts
type P = [number, number];
type Poly = { p: P[]; L: number[] }; // 점들 + 누적 길이
// 손떨림 + 살랑임을 얹은 부드러운 선 (같은 seed = 같은 떨림)
function rough(pts: P[], wob: number, seed: number, boil: number, sharp: boolean): Poly {
  const r = rng(seed), r2 = rng(seed * 31 + boil * 977 + 5);
  const q = pts.map(([x, y]) => [x + (r() - 0.5) * wob + (r2() - 0.5) * (boil ? 1.6 : 0),
                                 y + (r() - 0.5) * wob + (r2() - 0.5) * (boil ? 1.6 : 0)] as P);
  return withLen(catmull(q, 2.5, sharp));
}
// 길이 비율 frac 까지의 점 개수와 끝점
function upTo(pl: Poly, frac: number): { n: number; end: P } {
  const want = pl.L[pl.L.length - 1] * frac;
  let i = 1;
  while (i < pl.L.length && pl.L[i] < want) i++;
  if (i >= pl.L.length) return { n: pl.p.length, end: pl.p[pl.p.length - 1] };
  const a = pl.p[i - 1], b = pl.p[i], k = (want - pl.L[i - 1]) / Math.max(1e-6, pl.L[i] - pl.L[i - 1]);
  return { n: i, end: [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k] };
}
// 굵기가 변하는 마커 선 — 끝점(펜 자리)을 돌려준다
function marker(g: CanvasRenderingContext2D, pl: Poly, frac: number, color: string, w: number, seed: number): P | null {
  if (frac <= 0) return null;
  const { n, end } = upTo(pl, frac);
  const len = pl.L[pl.L.length - 1], ph = seed * 1.7;
  g.strokeStyle = color; g.lineCap = 'round';
  let prev = pl.p[0];
  for (let i = 1; i <= n; i++) {
    const p = i === n ? end : pl.p[i];
    const s = pl.L[Math.min(i, pl.L.length - 1)];
    const t0 = Math.min(1, s / Math.min(14, len * 0.3));                          // 시작은 가늘게
    const t1 = frac >= 1 ? Math.min(1, (len - s) / Math.min(18, len * 0.3)) : 1;  // 끝은 빠지듯
    const press = 0.86 + 0.14 * Math.sin(s * 0.045 + ph) * Math.sin(s * 0.011 + ph * 2); // 펜 누름
    g.lineWidth = Math.max(0.6, w * (0.35 + 0.65 * Math.sqrt(t0)) * (0.3 + 0.7 * Math.sqrt(Math.max(0, t1))) * press);
    g.beginPath(); g.moveTo(prev[0], prev[1]); g.lineTo(p[0], p[1]); g.stroke();
    prev = p;
  }
  return end;
}
// 매 프레임: f = (장면 시각 − t0) / dur, ease = 1 − (1 − f)^1.5
// const boil = Math.floor(t / 0.14) % 3;  → 다 그린 선만 boil 씨앗으로 다시
```

## 흔한 실수 · 확인 목록
- [ ] **프레임마다 손떨림을 새 무작위로 주면 선이 지글지글 끓는다** — 요소마다 씨앗을 고정하고 점 목록을 캐시한다. 살랑임은 0.14초마다 씨앗 3개만 돌린다.
- [ ] **선 전체를 한 굵기로 그으면 컴퓨터 선처럼 보인다** — 짧은 조각마다 시작 가늘기 · 끝 빠짐 · 펜 누름을 곱한 굵기로 긋는다.
- [ ] **지우개를 clearRect 로 한 번에 지우면 「지우는 맛」이 없다** — 굵은 지그재그 선을 destination-out 으로 진행 비율만큼 그어 쓱쓱 지운다.
- [ ] **읽어 주기가 장면이 바뀐 뒤에도 계속 말한다** — 장면이 바뀔 때 speechSynthesis.cancel() 후 새로 말한다.

## 완성 기준 체크리스트
- [ ] 선이 시작은 가늘게, 끝은 빠지듯 그려지고 펜 끝이 선 끝을 따라간다
- [ ] 다 그린 그림도 아주 살짝 살랑이며 살아 있는 낙서처럼 보인다
- [ ] 장면 끝에 지우개가 지그재그로 지나가며 그림이 지워지고 다음 장면이 시작된다
- [ ] 자막이 내레이션 길이에 맞춰 낱말씩 늘어난다

## 이 기술 정보
- id: `i79` · 분류: 물리 · 수학 원리 › 원리 설명 · 구조 · 2D · 난이도 보통 · 폰 부담 가벼움 (폰 OK) — 선마다 짧은 조각 수백 개를 긋지만 장면 하나에 요소 수십 개라 가볍다. 떨린 선 점은 (요소 · 겹 · boil · 손떨림) 열쇠로 캐시해 다시 계산하지 않는다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i79
- 쓰면 좋을 때: 원리 · 이유를 짧은 영상처럼 설명할 때 / 그림이 그려지는 과정 자체가 설명이 될 때 / 그림 파일 없이 코드만으로 설명 영상을 만들 때
- 쓰지 말 때: 정확한 도형 · 그래프 — 손떨림이 값을 흐린다, 대신 깔끔한 선(i80 SVG 선 그리기) / 사람이 직접 조작해야 하는 체험 — 영상은 보기만 한다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i79 견본 항목 — `src/demos/demosWhiteboard.ts:284`
```ts
  i79: {
    kind: 'dom',
    caption: '라이덴프로스트 효과 — 굵기가 변하는 마커 선 · 겹선 · 빗금 색칠 · 얼굴 있는 물방울 · 지우개로 다음 장면 (세 장면 33초)',
    make(box) {
      const b = sketchBoard(box, { wob: 2.4, dbl: true, fill: true, boil: true, hand: true, captions: true, speak: false });
      const controls: Control[] = [
        { type: 'toggle', label: '겹선 (스케치 느낌)', value: true, on: (v) => b.set({ dbl: v }) },
        { type: 'toggle', label: '빗금 색칠', value: true, on: (v) => b.set({ fill: v }) },
        { type: 'toggle', label: '선 살랑임 (line boil)', value: true, on: (v) => b.set({ boil: v }) },
        { type: 'range', label: '손떨림', min: 0, max: 6, step: 0.2, value: 2.4, on: (v) => b.set({ wob: v }) },
        { type: 'toggle', label: '펜 보이기', value: true, on: (v) => b.set({ hand: v }) },
        { type: 'toggle', label: '자막', value: true, on: (v) => b.set({ captions: v }) },
        { type: 'toggle', label: '읽어 주기 (한국어 음성)', value: false, on: (v) => b.set({ speak: v }) },
      ];
      return { update: (t) => b.update(t), controls, dispose: () => b.dispose() };
    },
  }
```

## 관련 기술
- 같은 방법으로 만든 효과 예시: [그려지는 애니 (손이 그리는 순서)](https://ai-techstudio.web.app/ai/t/i338.md) `i338`
- 먼저 알면 좋은 기술: [단계 이야기 (scrollytelling · 다음 단추)](https://ai-techstudio.web.app/ai/t/i45.md) `i45`
- 다음에 해 볼 기술: [SVG 선 그리기 (stroke-dashoffset) · 손떨림](https://ai-techstudio.web.app/ai/t/i80.md) `i80`
- 참고 문서: [MDN — CanvasRenderingContext2D.globalCompositeOperation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation) · [MDN — SpeechSynthesisUtterance](https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesisUtterance)
