# AI 꾸러미 — SVG 선 그리기 (stroke-dashoffset) · 손떨림 — SVG line drawing animation (stroke-dashoffset)
> SVG 선에 「선 길이만큼의 점선 간격」을 주고 시작점을 길이 → 0 으로 밀어 선이 그려지는 것처럼 보이게 하고, 점을 살짝 흔들어 손그림 느낌을 낸다.  
> 견본: https://ai-techstudio.web.app/#t/i80

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

## 주문서

### 만들어 줘: SVG 선 그리기 (stroke-dashoffset) · 손떨림 — SVG line drawing animation (stroke-dashoffset)

#### 1. 목표
도형 · 작도 단계를 펜이 그리듯 한 획씩 그려지게 해 줘 — stroke-dasharray = 선 길이, stroke-dashoffset 을 길이 → 0 으로. 점은 살짝 흔들어 화이트보드 손그림.

#### 2. 핵심 기술 용어
- **SVG line drawing animation (stroke-dashoffset)** — 점선 시작점을 밀어 선이 그려지는 효과
- **stroke-dasharray** — 점선의 선 · 빈칸 길이
- **getTotalLength · getPointAtLength** — 경로 전체 길이 · 길이 위치의 점 (펜 끝 따라가기)
- **Hand-drawn jitter (wobble)** — 점을 조금씩 흔들어 손떨림

#### 3. 환경
- 플랫폼: TypeScript (브라우저), 라이브러리 없이 — 화면과 떨어진 순수 함수로
- 화면: 2D · 브라우저 — PC · 폰 모두

#### 4. 조건
- dasharray 와 시작 dashoffset 은 getTotalLength() 로 잰 실제 길이
- 손떨림은 씨앗 고정 난수 — 프레임마다 다시 흔들면 선이 떤다
- 진행은 처음 빠르고 끝에서 느리게 (1 - (1-k)^1.6)
- 펜 그림이 있으면 getPointAtLength 로 선 끝을 따라가게

#### 5. 완성 기준 (이게 보이면 성공)
- 별 → 나선 → 직선이 차례로 펜 끝을 따라 그려진다
- 「손떨림」 0 → 8 로 선이 점점 삐뚤빼뚤해지고, 그 사이에도 떨리지는 않는다
- 그리는 중 선 끝에 펜이 붙어 다닌다
- 다 그린 뒤 다시 처음부터 반복된다

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

## 원리
- 점선 간격(dasharray)을 선 전체 길이로 주면 「선 하나 + 같은 길이의 빈칸」이 된다.
- 점선 시작점(dashoffset)을 길이만큼 밀면 빈칸만 보이고, 0 으로 줄이면 선이 처음부터 끝까지 드러난다 — 그려지는 것처럼.
- 길이는 path.getTotalLength() 로 잰다. 펜 끝은 getPointAtLength(길이 × 진행) 자리에 둔다.
- 손떨림: 점마다 씨앗 고정 난수로 ±wob/2 흔들고(끝점은 절반만), 점 사이는 이차 곡선(Q)으로 부드럽게 잇는다.

## 핵심 코드 — 손떨림 경로 만들기 + 길이만큼 점선을 밀어 그리기
(발췌: demos/demosWhiteboard.ts path() · board() build/update 를 정리)
```ts
type P = [number, number];
function rng(seed: number) { let s = seed >>> 0 || 1; return () => ((s = (s * 16807) % 2147483647) / 2147483647); }

// 점들을 손떨림 + 부드러운 곡선 경로로 (씨앗 고정 — 떨지 않는다)
function wobblyPath(pts: P[], wob: number, seed: number) {
  const r = rng(seed);
  const q = pts.map(([x, y], i) => {
    const k = i === 0 || i === pts.length - 1 ? 0.5 : 1; // 끝점은 덜 흔들기
    return [x + (r() - 0.5) * wob * k, y + (r() - 0.5) * wob * k] as P;
  });
  let d = 'M' + q[0]![0].toFixed(1) + ',' + q[0]![1].toFixed(1);
  for (let i = 1; i < q.length - 1; i++) {
    const [x, y] = q[i]!, [nx, ny] = q[i + 1]!;
    d += ' Q' + x.toFixed(1) + ',' + y.toFixed(1) + ' ' + ((x + nx) / 2).toFixed(1) + ',' + ((y + ny) / 2).toFixed(1);
  }
  const l = q[q.length - 1]!;
  return d + ' L' + l[0].toFixed(1) + ',' + l[1].toFixed(1);
}

const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', wobblyPath(starPts, 2, 40));
path.setAttribute('fill', 'none');
svg.appendChild(path);
const len = path.getTotalLength();
path.style.strokeDasharray = String(len);
path.style.strokeDashoffset = String(len); // 처음엔 빈칸만

function update(lt: number, t0 = 0.4, dur = 2) {
  const k = Math.max(0, Math.min(1, (lt - t0) / dur));
  const e = k < 1 ? 1 - Math.pow(1 - k, 1.6) : 1;
  path.style.strokeDashoffset = String(len * (1 - e));
  if (k > 0 && k < 1) {
    const p = path.getPointAtLength(len * e); // 펜 끝
    pen.setAttribute('transform', 'translate(' + p.x + ' ' + p.y + ')');
  }
}
```

## 흔한 실수 · 확인 목록
- [ ] **손떨림을 매 프레임 새 난수로 만들면 선이 부들부들 떤다** — 획마다 씨앗을 고정해 한 번만 경로를 만든다. 떨림 값을 바꿀 때만 다시 만든다.
- [ ] **길이를 어림으로 주면 선이 덜 그려지거나 점선이 보인다** — getTotalLength() 로 잰 값을 dasharray · dashoffset 둘 다에.
- [ ] **stroke-linecap round 면 시작 전에 점 하나가 보인다** — 진행 0 일 때는 opacity 0 으로 숨기거나 offset 을 길이보다 조금 더 크게.
- [ ] **화면 크기에 따라 선 굵기가 바뀌어 보인다** — viewBox 좌표로 그리면 굵기도 함께 맞춰진다. 고정 화소 굵기가 필요하면 vector-effect="non-scaling-stroke".

## 완성 기준 체크리스트
- [ ] 별 → 나선 → 직선이 차례로 펜 끝을 따라 그려진다
- [ ] 「손떨림」 0 → 8 로 선이 점점 삐뚤빼뚤해지고, 그 사이에도 떨리지는 않는다
- [ ] 그리는 중 선 끝에 펜이 붙어 다닌다
- [ ] 다 그린 뒤 다시 처음부터 반복된다

## 이 기술 정보
- id: `i80` · 분류: 2D · 화면 › 2D 그리기 · 2D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 선마다 속성 하나를 매 프레임 바꾸는 정도. 선 수십 개는 가볍다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i80
- 쓰면 좋을 때: 작도 · 풀이 과정을 순서대로 보여 줄 때 / 선 그림을 손으로 그리는 듯 등장시키고 싶을 때
- 쓰지 말 때: 채운 도형 · 그림 — 선(stroke)에만 된다. 대신 채움은 선이 끝난 뒤 투명도로 / 아주 많은 선을 동시에 — DOM 이 무거워진다. 대신 캔버스

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i80 견본 항목 — `src/demos/demosWhiteboard.ts:301`
```ts
  i80: {
    kind: 'dom',
    caption: '선의 길이만큼 점선 간격을 주고(dasharray) 그 시작점을 밀어(dashoffset) 선이 그려지는 것처럼 — 손떨림은 점을 살짝 흔들어서',
    make(box) {
      let sd = 40;
      const S = (pts: P[], color: string, w: number, t0: number, dur: number): Stroke => ({ pts, color, w, t0, dur, seed: sd++ });
      const star: P[] = Array.from({ length: 11 }, (_, i) => {
        const a = -Math.PI / 2 + (i * Math.PI * 2) / 10;
        const r = i % 2 ? 48 : 110;
        return [200 + Math.cos(a) * r, 250 + Math.sin(a) * r] as P;
      });
      const spiral: P[] = Array.from({ length: 80 }, (_, i) => {
        const a = i * 0.22;
        return [560 + Math.cos(a) * i * 1.5, 250 + Math.sin(a) * i * 1.5] as P;
      });
      const scenes: Scene[] = [
        {
          t0: 0,
          t1: 8,
          say: '선이 그려지는 원리 — 점선 간격을 선 길이만큼, 시작점을 밀어서.',
          texts: [{ x: 400, y: 450, text: 'stroke-dasharray = 길이 · stroke-dashoffset: 길이 → 0', size: 22, color: K, t0: 4.5, dur: 1.6, anchor: 'middle' }],
          strokes: [S(star, RED, 4, 0.4, 2), S(spiral, BLUE, 4, 2.2, 2.2), S(line([90, 400], [710, 400], 10), K, 3, 4.2, 0.8)],
        },
      ];
      const b = board(box, { scenes, loop: 8, captions: false, hand: true, wob: 2 });
      return {
        update: (t) => b.update(t),
        controls: [
          { type: 'range', label: '손떨림', min: 0, max: 8, step: 0.2, value: 2, on: (v) => b.setWob(v) },
          { type: 'toggle', label: '펜 보이기', value: true, on: (v) => b.setHand(v) },
        ],
        dispose: () => b.dispose(),
      };
    },
  }
```

## 관련 기술
- 같은 방법으로 만든 효과 예시: [점선 행진](https://ai-techstudio.web.app/ai/t/i98.md) `i98` · [지도 길 그리기 연출 (점선 따라)](https://ai-techstudio.web.app/ai/t/i280.md) `i280`
- 먼저 알면 좋은 기술: [코드로 그린 SVG 아이콘 · 도장](https://ai-techstudio.web.app/ai/t/u57.md) `u57`
- 다음에 해 볼 기술: [화이트보드 그리기 애니메이션](https://ai-techstudio.web.app/ai/t/i79.md) `i79` · [선 들끓기 (line boil)](https://ai-techstudio.web.app/ai/t/i323.md) `i323`
- 참고 문서: [MDN — stroke-dashoffset](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dashoffset) · [MDN — SVGGeometryElement.getTotalLength()](https://developer.mozilla.org/en-US/docs/Web/API/SVGGeometryElement/getTotalLength)
