# AI 꾸러미 — 움직이는 오로라 그라디언트 — Animated mesh / aurora gradient
> 색 원 여러 개를 정수 박자로 떠돌게 하고 큰 방사 그러데이션을 screen 으로 겹쳐, 이음새 없이 천천히 흐르는 오로라 배경을 만든다.  
> 견본: https://ai-techstudio.web.app/#t/i110

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

## 주문서

### 만들어 줘: 움직이는 오로라 그라디언트 — Animated mesh / aurora gradient

#### 1. 목표
결과 · 축하 화면 배경에 천천히 흐르는 오로라 그라디언트 배경을 깔아 줘 — 색 원 6개가 정수 박자로 떠돌고, 흐린 원을 screen 으로 섞어서. 분위기는 분홍 · 보라 · 하늘 · 민트 오로라.

#### 2. 핵심 기술 용어
- **Animated mesh / aurora gradient** — 색 덩어리가 흐르는 배경
- **Radial gradient blobs + screen blend** — 흐린 원을 밝게 겹쳐 섞기
- **Lissajous motion (integer frequencies)** — 정수 박자 사인 — 한 주기 뒤 제자리

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

#### 4. 조건
- 움직임은 정수 박자 사인 · 코사인만 — 한 바퀴 뒤 정확히 제자리
- 한 바퀴는 길게 (견본 24초) — 배경은 느려야 한다
- 섞기는 screen (또는 lighter 를 약하게) — 그냥 덮어 그리면 색이 탁해진다
- 글씨가 잘 읽히게 배경 바탕색은 짙게 (#1a1440)
- 「흐르는 빠르기」 · 「부드러움」 조절

#### 5. 완성 기준 (이게 보이면 성공)
- 왼쪽에 색 원 6개의 속 구조, 오른쪽에 그것을 흐리게 섞은 오로라가 나란히 보인다
- 색 덩어리가 천천히 흘러 섞이고, 오래 봐도 끊기거나 튀는 순간이 없다
- 「부드러움」 을 올리면 경계가 더 번지고, 0 이면 동그라미가 드러난다

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

## 원리
- 색 원 i 의 자리: x = 0.5 + 0.36·sin(2πp·a + i·1.7), y = 0.5 + 0.34·cos(2πp·b + i·2.3). a · b 는 정수(1~3).
- 정수 박자라 p 가 0 → 1 을 돌면 모든 원이 제자리로 → 이음새 없는 반복 (견본 한 바퀴 24초).
- 원마다 가운데 진하고 바깥 투명한 방사 그러데이션을 화면 전체에 칠하고, globalCompositeOperation = 'screen' 으로 겹친다 — 겹칠수록 밝아지되 하얗게 타지 않는다.
- 반지름도 천천히 숨 쉬게(0.32 + 0.07·sin) 하면 덩어리가 살아 움직인다.

## 핵심 코드 — 정수 박자로 떠도는 색 원 + screen 섞기
(발췌: demos/demosMotionB.ts i110 을 정리)
```ts
const TAU = Math.PI * 2;
const COL = ['#ff6fb5', '#8b5cff', '#3fd0ff', '#44f0a8', '#ffd166', '#ff8a5b'];
const FR = [[1, 2], [2, 1], [1, 1], [3, 2], [2, 3], [1, 3]]; // 정수 박자 = 이음새 없음
let ph = 0;
function draw(g: CanvasRenderingContext2D, w: number, h: number, dt: number) {
  ph += (dt * speed) / 24;              // 한 바퀴 24초
  const p = ph - Math.floor(ph);
  g.fillStyle = '#1a1440';
  g.fillRect(0, 0, w, h);
  g.globalCompositeOperation = 'screen'; // 겹칠수록 밝게, 하얗게 타지 않게
  COL.forEach((c, i) => {
    const [a, b] = FR[i];
    const x = w * (0.5 + 0.36 * Math.sin(TAU * p * a + i * 1.7));
    const y = h * (0.5 + 0.34 * Math.cos(TAU * p * b + i * 2.3));
    const r = Math.max(w, h) * (0.32 + 0.07 * Math.sin(TAU * p * 2 + i));
    const R = r * (0.55 + 0.6 * soft);
    const gr = g.createRadialGradient(x, y, 0, x, y, R);
    gr.addColorStop(0, c + 'ee');
    gr.addColorStop(0.45 / (0.4 + soft * 0.6), c + '77');
    gr.addColorStop(1, c + '00');
    g.fillStyle = gr;
    g.fillRect(0, 0, w, h);
  });
  g.globalCompositeOperation = 'source-over';
}
```

## 흔한 실수 · 확인 목록
- [ ] **박자를 0.7 처럼 정수가 아니게 주면 한 바퀴마다 툭 튄다** — 시간 p(0~1)에 곱하는 수는 모두 정수로 (이음새 없는 반복 고리 참고).
- [ ] **source-over 로 겹치면 색이 탁하고 뿌옇다** — screen 으로 섞는다. lighter 는 금방 하얗게 타므로 쓰려면 투명도를 낮춘다.
- [ ] **빠르게 흐르면 어지럽고 글씨가 안 읽힌다** — 배경은 한 바퀴 20초 이상으로 느리게.

## 완성 기준 체크리스트
- [ ] 왼쪽에 색 원 6개의 속 구조, 오른쪽에 그것을 흐리게 섞은 오로라가 나란히 보인다
- [ ] 색 덩어리가 천천히 흘러 섞이고, 오래 봐도 끊기거나 튀는 순간이 없다
- [ ] 「부드러움」 을 올리면 경계가 더 번지고, 0 이면 동그라미가 드러난다

## 이 기술 정보
- id: `i110` · 분류: 이펙트 · 연출 › 모션 그래픽 · 2D · 난이도 쉬움 · 폰 부담 가벼움 (폰 OK) — 화면 전체 그러데이션 6장. 큰 화면에선 픽셀 채우기가 늘어나니 낮은 해상도 캔버스에 그려 늘려도 된다 (흐린 그림이라 티가 안 난다).
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i110
- 쓰면 좋을 때: 글씨 · 별 같은 주인공 뒤의 차분한 배경 / 정해진 그림 없이 화면을 화사하게 채울 때
- 쓰지 말 때: 게임 판 바로 뒤 — 판보다 눈에 띄면 안 된다. 대신 단색 + 은은한 무늬 / 배터리를 아껴야 하는 오래 켜 두는 화면 — 아주 느리게 하거나 멈춘 그림으로

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i110 견본 항목 — `src/demos/demosMotionB.ts:350`
```ts
  i110: {
    kind: '2d',
    caption: '왼쪽은 속 구조(색 원 6개) · 오른쪽은 그 원을 흐리게 섞은 결과 — 천천히 흐르는 오로라 배경',
    make() {
      const COL = ['#ff6fb5', '#8b5cff', '#3fd0ff', '#44f0a8', '#ffd166', '#ff8a5b'];
      const FR = [
        [1, 2],
        [2, 1],
        [1, 1],
        [3, 2],
        [2, 3],
        [1, 3],
      ];
      let speed = 1;
      let soft = 1;
      let ph = 0;
      const blobs = (p: number, cw: number, h: number): { x: number; y: number; r: number; c: string }[] =>
        COL.map((c, i) => {
          const [a, b] = FR[i]!;
          return {
            x: cw * (0.5 + 0.36 * Math.sin(TAU * p * a! + i * 1.7)),
            y: h * (0.5 + 0.34 * Math.cos(TAU * p * b! + i * 2.3)),
            r: Math.max(cw, h) * (0.32 + 0.07 * Math.sin(TAU * p * 2 + i)),
            c,
          };
        });
      return {
        draw(g, w, h, _t, dt) {
          const u = scaleOf(w, h);
          ph += (dt * speed) / 24;
          const p = fract(ph);
          const hw = w / 2;
          // 왼쪽: 속 구조
          g.save();
          g.beginPath();
          g.rect(0, 0, hw, h);
          g.clip();
          g.fillStyle = '#17123a';
          g.fillRect(0, 0, hw, h);
          for (const b of blobs(p, hw, h)) {
            g.beginPath();
            g.arc(b.x, b.y, b.r * 0.55, 0, TAU);
            g.fillStyle = b.c + '88';
            g.fill();
            g.lineWidth = 2 * u;
            g.strokeStyle = b.c;
            g.stroke();
            g.fillStyle = '#fff';
            g.beginPath();
            g.arc(b.x, b.y, 2.5 * u, 0, TAU);
            g.fill();
          }
          g.restore();
          // 오른쪽: 흐리게 섞기
          g.save();
          g.beginPath();
          g.rect(hw, 0, hw, h);
          g.clip();
          g.translate(hw, 0);
          g.fillStyle = '#1a1440';
          g.fillRect(0, 0, hw, h);
          g.globalCompositeOperation = 'screen';
          for (const b of blobs(p, hw, h)) {
            const R = b.r * (0.55 + 0.6 * soft);
            const gr = g.createRadialGradient(b.x, b.y, 0, b.x, b.y, R);
            gr.addColorStop(0, b.c + 'ee');
            gr.addColorStop(0.45 / (0.4 + soft * 0.6), b.c + '77');
            gr.addColorStop(1, b.c + '00');
            g.fillStyle = gr;
            g.fillRect(0, 0, hw, h);
          }
          g.globalCompositeOperation = 'source-over';
          // 결과 화면처럼 글씨 · 별
          const cx = hw / 2;
          txt(g, '참 잘했어요!', cx, h * 0.5, 17 * u, '#fff', 'center', TF, 400);
          for (let i = 0; i < 3; i++) {
            const bob = Math.sin(TAU * p * 3 + i) * 2 * u;
            star(g, cx + (i - 1) * 20 * u, h * 0.66 + bob, 8 * u);
            g.fillStyle = '#ffe27a';
            g.fill();
            g.lineWidth = 1.5 * u;
            g.strokeStyle = '#fff';
            g.stroke();
          }
          g.restore();
          splitLabels(g, w, h, u, '속: 색 원 6개', '흐리게 섞기');
        },
        controls: [
          { type: 'range', label: '흐르는 빠르기', min: 0, max: 4, step: 0.1, value: 1, on: (v) => (speed = v) },
          { type: 'range', label: '부드러움', min: 0, max: 1.5, step: 0.05, value: 1, on: (v) => (soft = v) },
        ] as Control[],
      };
    },
  }
```

## 관련 기술
- 먼저 알면 좋은 기술: [캔버스 그러데이션 · 흐림 · 빛 섞기](https://ai-techstudio.web.app/ai/t/u52.md) `u52`
- 다음에 해 볼 기술: [이음새 없는 반복 고리](https://ai-techstudio.web.app/ai/t/i120.md) `i120` · [생성 패턴 (흐름장 · 보로노이 · 트루쳇)](https://ai-techstudio.web.app/ai/t/i122.md) `i122`
- 참고 문서: [MDN — globalCompositeOperation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation)
