# AI 꾸러미 — 점선 행진 — dashoffset
> 점선이 경로를 따라 끝없이 흐름 — dashoffset 반복  
> 견본: https://ai-techstudio.web.app/#t/i98

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

## 주문서

### 만들어 줘: 점선 행진 — dashoffset

#### 1. 목표
이동 경로에 「점선 행진」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 점선이 경로를 따라 끝없이 흐름 — dashoffset 반복

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

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

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

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「점선이 경로를 따라 끝없이 흐름 — dashoffset 반복」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 점선이 경로를 따라 끝없이 흐름 — dashoffset 반복


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

## 완성 기준 체크리스트
- [ ] 견본처럼 「점선이 경로를 따라 끝없이 흐름 — dashoffset 반복」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i98` · 분류: 이펙트 · 연출 › 모션 그래픽 · 2D · 난이도 쉬움 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i98
- 쓰면 좋을 때: 쓰면 좋을 곳 — 이동 경로 · 물길 · 전류 흐름

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i98 — `src/demos/demosMotionA.ts:974`
```ts
const i98 = {
  kind: '2d' as const,
  caption: '점선의 시작 위치(dashoffset)만 계속 옮기면 길 · 물길 · 전류가 흐르는 것처럼 보여요',
  make() {
    const o = { s: 1 };
    let tt = 0;
    return {
      controls: [speedCtl(o, '흐름 빠르기')],
      draw(g: G, w: number, h: number, _t: number, dt: number) {
        tt += Math.max(0, dt) * o.s;
        bg(g, w, h, '#a8e07a', '#6fbf55');
        view(g, w, h);
        // 물길
        const river = (): void => {
          g.beginPath();
          for (let x = -10; x <= 290; x += 5) {
            const y = 24 + Math.sin(x * 0.04) * 7;
            if (x === -10) g.moveTo(x, y);
            else g.lineTo(x, y);
          }
        };
        river();
        g.strokeStyle = '#4aa8ea';
        g.lineWidth = 16;
        g.lineCap = 'round';
        g.stroke();
        river();
        g.strokeStyle = '#d6f2ff';
        g.lineWidth = 2;
        g.setLineDash([10, 12]);
        g.lineDashOffset = -tt * 40;
        g.stroke();
        g.setLineDash([]);
        txt(g, '물길', 252, 44, 8, '#1d5d8f', 'center', F, 800);
        // 길 (집 → 학교)
        const road = (): void => {
          g.beginPath();
          g.moveTo(34, 142);
          g.bezierCurveTo(90, 150, 60, 80, 110, 82);
          g.bezierCurveTo(150, 84, 130, 58, 168, 58);
        };
        road();
        g.strokeStyle = '#f2d8a4';
        g.lineWidth = 12;
        g.stroke();
        road();
        g.strokeStyle = '#ffffff';
        g.lineWidth = 2.5;
        g.setLineDash([6, 6]);
        g.lineDashOffset = -tt * 24;
        g.stroke();
        g.setLineDash([]);
        // 집
        g.fillStyle = '#ffe9c7';
        g.fillRect(20, 128, 26, 20);
        g.fillStyle = '#e8584c';
        g.beginPath();
        g.moveTo(16, 130);
        g.lineTo(33, 114);
        g.lineTo(50, 130);
        g.fill();
        g.fillStyle = '#8a5a2e';
        g.fillRect(29, 137, 8, 11);
        // 학교
        g.fillStyle = '#fff4d9';
        g.fillRect(160, 40, 34, 26);
        g.fillStyle = '#5b7be0';
        g.fillRect(158, 36, 38, 6);
        g.fillStyle = '#ffd166';
        g.beginPath();
        g.arc(177, 52, 5, 0, TAU);
        g.fill();
        txt(g, '길', 110, 98, 8, '#7a4f1c', 'center', F, 800);
        // 회로
        const cx0 = 204;
        const cy0 = 92;
        const CW = 62;
        const CH = 58;
        rr(g, cx0 - 6, cy0 - 6, CW + 12, CH + 12, 8);
        g.fillStyle = 'rgba(30,40,70,.85)';
        g.fill();
        g.beginPath();
        g.rect(cx0, cy0, CW, CH);
        g.strokeStyle = '#6b7488';
        g.lineWidth = 3;
        g.stroke();
        g.beginPath();
        g.rect(cx0, cy0, CW, CH);
        g.strokeStyle = '#ffe14d';
        g.lineWidth = 2;
        g.setLineDash([4, 5]);
        g.lineDashOffset = -tt * 30;
        g.stroke();
        g.setLineDash([]);
        // 전지
        g.fillStyle = '#3a3f55';
        g.fillRect(cx0 - 5, cy0 + CH / 2 - 9, 10, 18);
        g.fillStyle = '#ff5a5a';
        g.fillRect(cx0 - 5, cy0 + CH / 2 - 9, 10, 6);
        // 전구
        const glow = 0.75 + 0.25 * Math.sin(tt * 8);
        const rg = g.createRadialGradient(cx0 + CW, cy0 + CH / 2, 1, cx0 + CW, cy0 + CH / 2, 16);
        rg.addColorStop(0, `rgba(255,240,140,${glow})`);
        rg.addColorStop(1, 'rgba(255,240,140,0)');
        g.fillStyle = rg;
        g.beginPath();
        g.arc(cx0 + CW, cy0 + CH / 2, 16, 0, TAU);
        g.fill();
        g.fillStyle = '#fff7b0';
        g.beginPath();
        g.arc(cx0 + CW, cy0 + CH / 2, 6, 0, TAU);
        g.fill();
        txt(g, '전류', cx0 + CW / 2, cy0 + CH / 2, 9, '#ffe14d', 'center', F, 800);
        g.restore();
      },
    };
  },
};
```

## 관련 기술
- 이 효과를 만든 기술: [SVG 선 그리기 (stroke-dashoffset) · 손떨림](https://ai-techstudio.web.app/ai/t/i80.md) `i80`
