# AI 꾸러미 — 격자 물결 (stagger grid) — Staggered grid animation (distance-based delay)
> 격자 칸마다 「가운데에서의 거리 × 지연」 만큼 늦게 뒤집혀, 곱셈표 칸이 물결처럼 퍼지며 답을 드러내게 한다.  
> 견본: https://ai-techstudio.web.app/#t/i101

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

## 주문서

### 만들어 줘: 격자 물결 (stagger grid) — Staggered grid animation (distance-based delay)

#### 1. 목표
곱셈표 9×9 정답 공개를 격자 물결로 뒤집어 보여 줘 — 칸마다 가운데에서의 거리에 비례해 늦게 뒤집히고, 앞면 → 뒷면(답). 분위기는 짙은 남색 + 무지개 칸.

#### 2. 핵심 기술 용어
- **Staggered grid animation (distance-based delay)** — 거리에 비례한 차례 지연
- **Card flip (scaleX = |cos θ|)** — 가로 크기를 줄였다 늘려 뒤집기 흉내
- **Ripple reveal** — 물결처럼 번지는 공개

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

#### 4. 조건
- 지연 = 거리 × 간격 (견본 0.09초, 0.02 ~ 0.2 조절)
- 뒤집기는 scaleX = |cos θ| 로, 0 이 되는 순간 앞 · 뒷면을 바꾼다 (scale 0 은 0.02 로 막기)
- 앞면 → 뒷면 → 앞면 두 번의 물결로 끝없이 반복
- 물결 고리(지연에 맞춰 커지는 원)를 함께 그려 「번진다」 를 보여 주기

#### 5. 완성 기준 (이게 보이면 성공)
- 가운데 노란 칸부터 바깥쪽으로 동그랗게 칸이 뒤집히며 곱셈 답이 나온다
- 「거리당 지연」 을 키우면 물결이 느리게 번지고, 줄이면 한꺼번에 뒤집힌다
- 잠시 뒤 같은 물결로 다시 앞면이 된다

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

## 원리
- 칸 (r, c) 의 거리 d = √((r − 4)² + (c − 4)²) — 가운데(5 × 5) 칸에서 잰다.
- 그 칸의 뒤집기 진행 f = seg(p, 0.3 + d × 지연, 0.3 + d × 지연 + 0.45). 거리가 같은 칸들이 함께 → 동그란 물결.
- 뒤집기 흉내: 각도 θ = f·π, 가로 크기 = |cos θ|. 절반(θ > 90°)을 넘으면 뒷면(답)을 그린다.
- 뒤집는 동안 sin θ 만큼 살짝 들어 올리면 입체감이 생긴다.

## 핵심 코드 — 거리 지연 · 뒤집기(|cos θ|) · 앞뒤 면
(발췌: demos/demosMotionA.ts i101 의 draw 를 정리)
```ts
const seg = (p: number, a: number, b: number) => Math.max(0, Math.min(1, (p - a) / (b - a)));
const N = 9, CS = 17, D = 0.45;
const p = tt % 6.4;
for (let r = 0; r < N; r++)
  for (let c = 0; c < N; c++) {
    const d = Math.hypot(r - 4, c - 4);                       // 가운데에서의 거리
    const f1 = seg(p, 0.3 + d * delay, 0.3 + d * delay + D);   // 앞 → 뒤
    const f2 = seg(p, 3.6 + d * delay, 3.6 + d * delay + D);   // 뒤 → 앞
    const ang = (p < 3.6 ? f1 : 1 + f2) * Math.PI;
    const sx = Math.abs(Math.cos(ang));                        // 가로로 줄었다 늘어남 = 뒤집기
    const back = f1 - f2 > 0.5 || (p >= 3.6 && f2 < 0.5 && f1 >= 1);
    const x = x0 + c * CS + CS / 2, y = y0 + r * CS + CS / 2;
    g.save();
    g.translate(x, y - Math.abs(Math.sin(ang) * 2));          // 뒤집는 동안 살짝 들림
    g.scale(Math.max(0.02, sx), 1);
    g.beginPath(); g.roundRect(-CS / 2 + 1, -CS / 2 + 1, CS - 2, CS - 2, 3);
    if (back) {
      const v = (r + 1) * (c + 1);
      g.fillStyle = 'hsl(' + ((330 - v * 3.2) % 360) + ',85%,' + (62 - v * 0.15) + '%)';
      g.fill();
      g.fillStyle = '#fff'; g.fillText(String(v), 0, 0.5);
    } else {
      g.fillStyle = r === 4 && c === 4 ? '#ffd166' : '#3a438c';
      g.fill();
    }
    g.restore();
  }
```

## 흔한 실수 · 확인 목록
- [ ] **순서(번호) 대로 지연하면 줄 단위로 넘어가 물결이 안 된다** — 지연을 「가운데(또는 누른 칸)에서의 거리」 로 주어야 동그랗게 번진다.
- [ ] **scale 0 에서 그리면 경고 · 깜박임** — Math.max(0.02, |cos θ|) 로 아주 얇게 남긴다.
- [ ] **뒤집기 중간에 앞뒤 면을 바꾸지 않으면 글자가 거울처럼 보인다** — θ 가 90° 를 넘는 순간(진행 0.5) 뒷면으로 바꿔 그리고, 뒷면은 뒤집힌 채가 아니게 그린다.

## 완성 기준 체크리스트
- [ ] 가운데 노란 칸부터 바깥쪽으로 동그랗게 칸이 뒤집히며 곱셈 답이 나온다
- [ ] 「거리당 지연」 을 키우면 물결이 느리게 번지고, 줄이면 한꺼번에 뒤집힌다
- [ ] 잠시 뒤 같은 물결로 다시 앞면이 된다

## 이 기술 정보
- id: `i101` · 분류: 이펙트 · 연출 › 모션 그래픽 · 2D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 칸 81개 × 둥근 네모 하나. 가볍다.
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i101
- 쓰면 좋을 때: 판 전체가 한꺼번에 바뀌는 순간 (정답 공개 · 완성) / 바뀌는 차례에 뜻이 있을 때 (가운데에서 바깥으로 · 누른 칸에서 번짐)
- 쓰지 말 때: 칸 하나만 바뀔 때 — 그 칸만 뒤집는다 / 수천 칸 — 캔버스 대신 셰이더로 같은 식을 쓴다

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i101 — `src/demos/demosMotionA.ts:1330`
```ts
const i101 = {
  kind: '2d' as const,
  caption: '곱셈표 칸이 가운데(5 × 5)에서부터 거리 순서대로 물결처럼 뒤집혀 답이 나와요',
  make() {
    const o = { s: 1 };
    let tt = 0;
    let delay = 0.09;
    return {
      controls: [speedCtl(o), { type: 'range' as const, label: '거리당 지연(초)', min: 0.02, max: 0.2, step: 0.01, value: 0.09, on: (v: number) => (delay = v) }],
      draw(g: G, w: number, h: number, _t: number, dt: number) {
        tt += Math.max(0, dt) * o.s;
        const T = 6.4;
        const p = tt % T;
        bg(g, w, h, '#22275e', '#0d1030');
        view(g, w, h);
        const N = 9;
        const CS = 17;
        const x0 = 14;
        const y0 = (VH - N * CS) / 2;
        const D = 0.45;
        for (let r = 0; r < N; r++)
          for (let c = 0; c < N; c++) {
            const d = Math.hypot(r - 4, c - 4);
            const f1 = seg(p, 0.3 + d * delay, 0.3 + d * delay + D);
            const f2 = seg(p, 3.6 + d * delay, 3.6 + d * delay + D);
            const f = f1 - f2; // 0 앞면 → 1 뒷면 → 0
            const ang = (p < 3.6 ? f1 : 1 + f2) * Math.PI;
            const sx = Math.abs(Math.cos(ang));
            const back = f > 0.5 || (p >= 3.6 && f2 < 0.5 && f1 >= 1);
            const x = x0 + c * CS + CS / 2;
            const y = y0 + r * CS + CS / 2;
            const lift = Math.sin(ang) * 2;
            g.save();
            g.translate(x, y - Math.abs(lift));
            g.scale(Math.max(0.02, sx), 1);
            const v = (r + 1) * (c + 1);
            rr(g, -CS / 2 + 1, -CS / 2 + 1, CS - 2, CS - 2, 3);
            if (back) {
              g.fillStyle = `hsl(${(330 - v * 3.2) % 360},85%,${62 - v * 0.15}%)`;
              g.fill();
              txt(g, String(v), 0, 0.5, v >= 10 ? 7.5 : 8.5, '#fff', 'center', F, 900);
            } else {
              g.fillStyle = r === 4 && c === 4 ? '#ffd166' : '#3a438c';
              g.fill();
              g.fillStyle = 'rgba(255,255,255,.18)';
              g.fillRect(-CS / 2 + 3, -CS / 2 + 3, CS - 6, 1.5);
            }
            g.restore();
          }
        // 물결 고리
        for (const s0 of [0.3, 3.6]) {
          const rad = (p - s0) / delay;
          if (rad > 0 && rad < 6.5) {
            g.strokeStyle = `rgba(255,255,255,${0.5 * (1 - rad / 6.5)})`;
            g.lineWidth = 2;
            g.beginPath();
            g.arc(x0 + 4.5 * CS, y0 + 4.5 * CS, rad * CS, 0, TAU);
            g.stroke();
          }
        }
        const tx = 220;
        txt(g, '곱셈표', tx, 50, 18, '#fff', 'center', TF, 400);
        txt(g, '가운데에서 바깥으로', tx, 76, 9, 'rgba(255,255,255,.75)', 'center', F, 700);
        txt(g, `지연 = 거리 × ${delay.toFixed(2)}초`, tx, 92, 9, '#ffd166', 'center', F, 700);
        // 작은 그림: 거리 고리
        for (let k = 3; k >= 1; k--) {
          g.fillStyle = ['#ffd166', '#ff7aa8', '#7ec8ff'][k - 1]!;
          g.globalAlpha = 0.85;
          g.beginPath();
          g.arc(tx, 126, k * 7, 0, TAU);
          g.fill();
        }
        g.globalAlpha = 1;
        g.restore();
      },
    };
  },
};
```

## 관련 기술
- 먼저 알면 좋은 기술: [글자 차례 등장 (키네틱 타이포)](https://ai-techstudio.web.app/ai/t/i90.md) `i90`
- 다음에 해 볼 기술: [CSS 3D (카드 뒤집기)](https://ai-techstudio.web.app/ai/t/u66.md) `u66` · [생성 패턴 (흐름장 · 보로노이 · 트루쳇)](https://ai-techstudio.web.app/ai/t/i122.md) `i122`
