# AI 꾸러미 — 영상으로 굽기 (헤드리스 브라우저 → mp4)
> 장면 코드를 헤드리스 브라우저로 1/30초씩 시계를 돌려 프레임을 찍고 · 목소리 · 음악과 ffmpeg 로 묶어 mp4 — 홍보 영상 · 설명 영상  
> 견본: https://ai-techstudio.web.app/#t/i82

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

## 주문서

### 만들어 줘: 영상으로 굽기 (헤드리스 브라우저 → mp4)

#### 1. 목표
게임 소개 영상에 「영상으로 굽기 (헤드리스 브라우저 → mp4)」 기술을 깔끔한 기본 느낌으로 넣어 줘. — 장면 코드를 헤드리스 브라우저로 1/30초씩 시계를 돌려 프레임을 찍고 · 목소리 · 음악과 ffmpeg 로 묶어 mp4 — 홍보 영상 · 설명 영상

#### 2. 핵심 기술 용어
- **mp4**
- **ffmpeg**

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

#### 4. 조건
- 화면 코드와 계산 코드를 나눠서 (계산은 순수 함수)
- 한 프레임 16ms 안 — 오래 걸리는 계산은 나눠서

#### 5. 완성 기준 (이게 보이면 성공)
- 견본처럼 「장면 코드를 헤드리스 브라우저로 1/30초씩 시계를 돌려 프레임을 찍고 · 목소리 · 음악과 ffmpeg 로 묶어 mp4 — 홍보 영상 · 설명 영상」 이(가) 첫 화면에서 바로 보인다
- 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

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

## 원리
- 장면 코드를 헤드리스 브라우저로 1/30초씩 시계를 돌려 프레임을 찍고 · 목소리 · 음악과 ffmpeg 로 묶어 mp4 — 홍보 영상 · 설명 영상


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

## 완성 기준 체크리스트
- [ ] 견본처럼 「장면 코드를 헤드리스 브라우저로 1/30초씩 시계를 돌려 프레임을 찍고 · 목소리 · 음악과 ffmpeg 로 묶어 mp4 — 홍보 영상 · 설명 영상」 이(가) 첫 화면에서 바로 보인다
- [ ] 켬/끔(또는 전/후) 비교 단추로 이 기술이 만드는 차이를 확인할 수 있다
- [ ] 중요한 값 1~3개를 슬라이더로 바꿔 볼 수 있다
- [ ] 콘솔 오류 0, 폰 폭(390px)에서도 동작한다

## 이 기술 정보
- id: `i82` · 분류: 물리 · 수학 원리 › 원리 설명 · 구조 · 공통 · 난이도 보통 · 폰 부담 보통 (폰 주의)
- 라이브 견본 (브라우저에서 직접 조작): https://ai-techstudio.web.app/#t/i82
- 쓰면 좋을 때: 쓰면 좋을 곳 — 게임 소개 영상 · 축제 홍보 영상 · SNS

## 견본 실제 코드 (라이브 견본이 돌리는 코드 — three.js · TypeScript)
### i82 견본 항목 — `src/demos/demosWhiteboard.ts:446`
```ts
  i82: {
    kind: '2d',
    caption: '장면 코드 → 헤드리스 브라우저가 1/30초마다 한 장씩 찍기 → 그림 900장 + 목소리 · 음악 → ffmpeg → mp4 (1분 영상)',
    make() {
      return {
        draw(g, w, h, t) {
          g.fillStyle = '#0f1117';
          g.fillRect(0, 0, w, h);
          const fs = Math.max(9, Math.min(15, w / 48));
          const box = (x: number, y: number, bw: number, bh: number, title: string, sub: string, col: string, hot = false): void => {
            g.fillStyle = hot ? col + '33' : '#1a1d27';
            g.strokeStyle = hot ? col : '#33384a';
            g.lineWidth = 1.5;
            g.beginPath();
            g.roundRect(x, y, bw, bh, 8);
            g.fill();
            g.stroke();
            g.fillStyle = '#eceef3';
            g.textAlign = 'center';
            g.font = `700 ${fs}px "Pretendard Variable", system-ui`;
            g.fillText(title, x + bw / 2, y + bh * 0.45);
            g.fillStyle = '#8a90a2';
            g.font = `500 ${fs * 0.8}px "Pretendard Variable", system-ui`;
            g.fillText(sub, x + bw / 2, y + bh * 0.75);
          };
          const ph = (t % 6) / 6;
          const bw = w * 0.2;
          const bh = h * 0.22;
          const y = h * 0.16;
          box(w * 0.03, y, bw, bh, '장면 코드', 'SVG · 캔버스 · 시각표', '#5aa8ff', ph < 0.2);
          box(w * 0.28, y, bw, bh, '헤드리스 브라우저', '1/30초씩 시계를 돌려', '#9b7bff', ph >= 0.2 && ph < 0.55);
          box(w * 0.53, y, bw, bh, '그림 900장', 'frame_0001.png …', '#ffc35a', ph >= 0.45 && ph < 0.8);
          box(w * 0.77, y, bw, bh, 'mp4', 'ffmpeg 로 묶기', '#3ee29a', ph >= 0.75);
          // 화살표
          g.strokeStyle = '#555c70';
          g.lineWidth = 2;
          for (const ax of [0.23, 0.48, 0.73]) {
            g.beginPath();
            g.moveTo(w * ax + 2, y + bh / 2);
            g.lineTo(w * (ax + 0.05) - 4, y + bh / 2);
            g.stroke();
          }
          // 필름 띠 — 찍힌 장면이 흘러감
          const fy = h * 0.5;
          const fh = h * 0.2;
          g.fillStyle = '#1a1d27';
          g.fillRect(0, fy, w, fh);
          const fw = fh * 1.5;
          const off = ((t * 60) % fw) - fw;
          for (let x = off; x < w; x += fw) {
            g.fillStyle = '#fbfaf6';
            g.fillRect(x + 4, fy + 10, fw - 8, fh - 20);
            const k = ((x - off) / fw + Math.floor(t * 60 / fw)) % 30;
            g.strokeStyle = '#2f7be0';
            g.lineWidth = 2;
            g.beginPath();
            g.ellipse(x + fw / 2, fy + fh / 2, (fw - 20) * 0.3 * Math.min(1, (k + 1) / 15), (fh - 26) * 0.25, 0, 0, Math.PI * 2 * Math.min(1, (k + 1) / 12));
            g.stroke();
            g.fillStyle = '#0f1117';
            for (let q = 0; q < 6; q++) {
              g.fillRect(x + (q + 0.3) * (fw / 6), fy + 2, fw / 12, 5);
              g.fillRect(x + (q + 0.3) * (fw / 6), fy + fh - 7, fw / 12, 5);
            }
          }
          // 소리 줄
          const sy = h * 0.8;
          g.fillStyle = '#8a90a2';
          g.textAlign = 'left';
          g.font = `600 ${fs * 0.85}px "Pretendard Variable", system-ui`;
          g.fillText('목소리 (TTS)', w * 0.03, sy - 4);
          g.fillText('배경음악', w * 0.03, sy + h * 0.1 - 4);
          g.strokeStyle = '#e2483a';
          g.beginPath();
          for (let i = 0; i < 160; i++) {
            const x = w * 0.2 + (i / 160) * w * 0.75;
            const a = Math.abs(Math.sin(i * 0.5 + t * 3)) * (i % 23 < 18 ? 1 : 0.1) * h * 0.035;
            g.moveTo(x, sy - 10 - a);
            g.lineTo(x, sy - 10 + a);
          }
          g.stroke();
          g.strokeStyle = '#b48ad8';
          g.beginPath();
          for (let i = 0; i <= 160; i++) {
            const x = w * 0.2 + (i / 160) * w * 0.75;
            const yy = sy + h * 0.1 - 10 + Math.sin(i * 0.25 + t * 2) * h * 0.015;
            if (i) g.lineTo(x, yy);
            else g.moveTo(x, yy);
          }
          g.stroke();
        },
      };
    },
  }
```
