# AI 꾸러미 — 살 · 범람 · 시계 와이프 — conic
> 블라인드처럼 덮기 · 물이 차오르는 전환 · 시계바늘처럼 쓸기(conic 마스크)  
> 견본: https://ai-techstudio.web.app/#t/i107

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

## 주문서

### 만들어 줘: 살 · 범람 · 시계 와이프 — conic

#### 1. 목표
시간 문제에 「살 · 범람 · 시계 와이프」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 블라인드처럼 덮기 · 물이 차오르는 전환 · 시계바늘처럼 쓸기(conic 마스크)

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

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

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

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「블라인드처럼 덮기 · 물이 차오르는 전환 · 시계바늘처럼 쓸기(conic 마스크)」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 블라인드처럼 덮기 · 물이 차오르는 전환 · 시계바늘처럼 쓸기(conic 마스크)


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

## 완성 기준 체크리스트
- [ ] 견본처럼 「블라인드처럼 덮기 · 물이 차오르는 전환 · 시계바늘처럼 쓸기(conic 마스크)」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i107 — `src/demos/demosMotionA.ts:2117`
```ts
const i107 = {
  kind: '2d' as const,
  caption: '블라인드 살이 차례로 덮고, 물이 차오르며 바뀌고, 시계바늘이 한 바퀴 쓸며 바뀌어요',
  make() {
    const o = { s: 1 };
    let tt = 0;
    const scenes: Scene[] = [
      sceneQ,
      sceneA,
      (g, w, h, t) => {
        bg(g, w, h, '#7b5cff', '#3a2a9e');
        view(g, w, h);
        // 시계
        const cx = VW / 2;
        const cy = VH / 2;
        g.fillStyle = '#fff';
        g.beginPath();
        g.arc(cx, cy, 44, 0, TAU);
        g.fill();
        g.fillStyle = '#ffd166';
        g.beginPath();
        g.moveTo(cx, cy);
        g.arc(cx, cy, 40, -Math.PI / 2, 0);
        g.fill();
        for (let i = 0; i < 12; i++) {
          const a = (i / 12) * TAU;
          g.fillStyle = '#3a2a9e';
          g.fillRect(cx + Math.cos(a) * 38 - 1, cy + Math.sin(a) * 38 - 1, 2, 2);
        }
        g.strokeStyle = '#3a2a9e';
        g.lineWidth = 3;
        g.lineCap = 'round';
        g.beginPath();
        g.moveTo(cx, cy);
        g.lineTo(cx + 30, cy);
        g.moveTo(cx, cy);
        g.lineTo(cx, cy - 22);
        g.stroke();
        txt(g, '15분 = ¼ 바퀴', cx, cy + 58, 11, '#fff');
        void t;
        g.restore();
      },
    ];
    return {
      controls: [speedCtl(o)],
      draw(g: G, w: number, h: number, _t: number, dt: number) {
        tt += Math.max(0, dt) * o.s;
        const SEG = 2.6;
        const TR = 1.1;
        const T = SEG * 3;
        const p = tt % T;
        const i = Math.min(2, Math.floor(p / SEG));
        const lp = p - i * SEG;
        const k = seg(lp, SEG - TR, SEG);
        const A = scenes[i]!;
        const B = scenes[(i + 1) % 3]!;
        A(g, w, h, tt);
        const names = ['살 와이프', '범람 와이프', '시계 와이프'];
        if (k > 0) {
          g.save();
          const path = new Path2D();
          if (i === 0) {
            const N = 7;
            const sh = h / N;
            for (let s = 0; s < N; s++) {
              const f = inOut(seg(k, s * 0.07, s * 0.07 + 0.55));
              path.rect(0, s * sh + (sh * (1 - f)) / 2, w, sh * f + 0.6);
            }
          } else if (i === 1) {
            const lvl = h * (1.08 - inOut(k) * 1.2);
            path.moveTo(0, h);
            for (let x = 0; x <= w + 4; x += 4) path.lineTo(x, lvl + Math.sin(x * 0.04 + tt * 6) * 6 * Math.min(w / VW, h / VH));
            path.lineTo(w, h);
            path.closePath();
          } else {
            const a = inOut(k) * TAU;
            const R = Math.hypot(w, h);
            path.moveTo(w / 2, h / 2);
            path.arc(w / 2, h / 2, R, -Math.PI / 2, -Math.PI / 2 + Math.max(0.0001, a));
            path.closePath();
          }
          g.clip(path);
          B(g, w, h, tt);
          g.restore();
          const u = Math.min(w / VW, h / VH);
          if (i === 1) {
            const lvl = h * (1.08 - inOut(k) * 1.2);
            g.strokeStyle = 'rgba(255,255,255,.85)';
            g.lineWidth = 3 * u;
            g.beginPath();
            for (let x = 0; x <= w + 4; x += 4) {
              const y = lvl + Math.sin(x * 0.04 + tt * 6) * 6 * u;
              if (x) g.lineTo(x, y);
              else g.moveTo(x, y);
            }
            g.stroke();
            for (let q = 0; q < 8; q++) {
              g.fillStyle = 'rgba(255,255,255,.6)';
              g.beginPath();
              g.arc(hsh(q) * w, lvl + (10 + mod(hsh(q + 2) * 60 - tt * 30, 60)) * u, 2 * u, 0, TAU);
              g.fill();
            }
          } else if (i === 2) {
            const a = -Math.PI / 2 + inOut(k) * TAU;
            g.strokeStyle = '#fff';
            g.lineWidth = 3 * u;
            g.lineCap = 'round';
            g.beginPath();
            g.moveTo(w / 2, h / 2);
            g.lineTo(w / 2 + Math.cos(a) * Math.hypot(w, h), h / 2 + Math.sin(a) * Math.hypot(w, h));
            g.stroke();
            g.fillStyle = '#fff';
            g.beginPath();
            g.arc(w / 2, h / 2, 4 * u, 0, TAU);
            g.fill();
          }
          view(g, w, h);
          pill(g, names[i]!, VW / 2, 16, 10, 'rgba(20,20,50,.78)');
          if (i === 2) txt(g, `${Math.round(inOut(k) * 60)}분`, VW / 2, 32, 10, '#fff');
          g.restore();
        }
      },
    };
  },
};
```

## 관련 기술
- 이 효과를 만든 기술: [마스크 와이프 · 원 조리개](https://ai-techstudio.web.app/ai/t/i99.md) `i99`
