이 문서는 피그마(Figma)에서 제작한 모션 그래픽을 투명 배경이 포함된 동영상 파일로 내보내고, 어도비 프리미어 프로(Adobe Premiere Pro)에서 곧바로 합성할 수 있도록 돕는 플러그인과 로컬 인코더 시스템의 구현 프롬프트를 다룬다. 피그마 환경에서 애니메이션 프레임을 추출하는 클라이언트와 이를 퀵타임(QuickTime) 동영상으로 변환하는 서버 간의 연동 구조를 설명한다. 영상 편집 소프트웨어와의 호환성을 확보하기 위한 코덱 설정과 바이너리 구조 검증 원리를 상세히 기술한다.

1 – 피그마 모션 투명배경으로 저장
피그마 모션을 제작한 동영상을 어도비 프로그램에서 알파채널을 인식할수 있게 내보내는 플러그인 이다.
피그마의 기본 내보내기 기능은 벡터 그래픽이나 단순 이미지 추출에 최적화되어 있어, 타임라인 기반 애니메이션을 투명 배경이 유지된 영상 파일로 직접 출력하는 데 한계가 있다. 영상 편집 소프트웨어에서 배경 없이 그래픽 요소만 얹어 합성하려면 알파 채널(Alpha Channel) 정보가 온전히 포함된 동영상 파일이 필요하다. 알파 채널은 이미지나 영상의 각 화소(Pixel)가 가지는 투명도 정보를 기록하는 데이터 영역이다. 불투명도 값을 0부터 1까지의 수치로 저장하여 렌더링 엔진이 아래 레이어를 비추도록 계산하며, 투명한 셀로판지에 그림을 겹쳐 올리는 것과 같은 방식으로 동작한다.
어도비 프리미어 프로와 같은 전문 편집 도구에서 알파 채널을 안정적으로 인식시키기 위해서는 퀵타임 애니메이션 코덱 규격을 정확히 준수해야 한다. 아래 링크는 해당 기능을 구현한 플러그인 및 서버 소스 코드 저장소다.
2 – 코칭 프롬프트
아래 코칭 프롬프트(Coaching Prompt)는 인공지능 모델에게 피그마 플러그인과 로컬 인코딩 서버를 밑바닥부터 설계하고 구현하도록 지시하기 위해 작성된 상세 사양서다. 두 개 프로세스의 통신 구조, 파일 규격, 영상 코덱 옵션, 그리고 바이너리 파서 구현 요구사항이 체계적으로 정의되어 있다.
## 역할
너는 30년차 풀스택 개발자다. Figma 플러그인, Node.js/Express, FFmpeg, TypeScript, QuickTime 컨테이너 내부 구조까지 다뤄본 사람으로서 이 프로젝트를 밑바닥부터 구현한다.
## 목표
Figma Motion으로 만든 애니메이션을 알파 채널이 살아있는 `MOV (QuickTime Animation, RGB+Alpha)`로 내보내, Premiere Pro에 그대로 임포트해 합성할 수 있게 하는 **개발용 Figma 플러그인 + 로컬 인코더 서버**를 만든다.
## 아키텍처
두 개의 프로세스로 구성된다.
1. **Figma 플러그인** (`src/code.ts` → `dist/code.js`, `src/ui.html` → `dist/ui.html`)
- Figma 문서에서 선택된 노드의 Motion 애니메이션을 시간축을 따라 샘플링해 프레임별로 PNG를 생성
- 각 PNG 바이트를 UI iframe으로 postMessage
- UI가 서버로 multipart form-data 업로드
2. **로컬 Node.js 인코더 서버** (`server.js`, port 8787)
- Express + multer + ffmpeg-static
- 업로드된 PNG 시퀀스를 `qtrle` 코덱 + `argb` 픽셀 포맷 + 전 프레임 키프레임(`-g 1`)으로 MOV 인코딩
- 인코딩 후 자체 검증: 스트림에 `argb|rgba|bgra` 픽셀 포맷이 있는가, MOV 컨테이너에 `stss` atom이 없는가
- 검증 통과 시 브라우저로 파일 반환
## 배경 (반드시 준수)
- Premiere Pro 12.1+는 **델타 프레임이 포함된 QuickTime Animation MOV를 임포트하지 못한다**. ffmpeg의 `qtrle` 인코더는 기본으로 프레임 간 델타 압축을 써서 일부만 키프레임으로 기록하고 MOV에 `stss` (sync sample) atom을 생성한다.
- 해결: `-g 1`로 **모든 프레임을 키프레임으로 강제**해서 `stss` atom이 아예 없게 만든다. 파일 크기는 커지지만 임포트가 안정된다.
- 픽셀 데이터에 우연히 `stss` 바이트가 나올 수 있으므로, 검증 로직은 문자열 검색이 아니라 **MOV atom 트리를 파싱**해야 한다.
## 프로젝트 파일 구성
```
manifest.json Figma 플러그인 매니페스트
package.json npm 스크립트, 의존성
tsconfig.json TypeScript 설정
.gitignore node_modules/, dist/, 로그, 개인 메모 제외
src/code.ts 플러그인 백그라운드 코드
src/ui.html 플러그인 UI (단일 파일, 스크립트 인라인)
scripts/cp.mjs build 시 src/ui.html → dist/ui.html 복사
server.js 로컬 FFmpeg 인코더
README.md 사용법
TESTING.md 회귀 검증 절차
premiere-test/ Premiere 임포트 비교용 샘플 3종 (선택)
```
## 의존성
- devDependencies: `@figma/plugin-typings`, `esbuild`
- dependencies: `express`, `cors`, `multer`, `ffmpeg-static`
## npm 스크립트
- `build`: `esbuild src/code.ts --bundle --target=es2018 --outfile=dist/code.js && node scripts/cp.mjs`
- `watch`: 코드와 UI 변경 모두 반영되도록 처음 한 번 `node scripts/cp.mjs` 실행 후 esbuild watch 시작
- `server`: `node server.js`
## manifest.json 요구사항
```json
{
"name": "Motion Alpha Exporter Premiere",
"id": "000000000000000000",
"api": "1.0.0",
"main": "dist/code.js",
"ui": "dist/ui.html",
"editorType": ["figma"],
"documentAccess": "dynamic-page"
}
```
## src/code.ts 구현 요구사항
### 진입점
- `figma.showUI(__html__, { width: 380, height: 520 })`
- 페이지 로드 시 선택 노드 정보를 UI로 전송 (`sendInfo`), `selectionchange` 이벤트에도 갱신
- `figma.ui.onmessage`로 `{ type: 'run', ... }` 메시지 수신
### 메시지 타입 정의
```ts
type Msg = {
type: 'run';
fps: number;
sec: number; // 0이면 모션 길이 자동
sc: number; // 배율
clear: boolean; // 루트 프레임 fill 제거
clearAll: boolean;// 내부 프레임 fill까지 제거
hideBg: boolean; // 배경 사각형 자동 숨김
name: string;
};
```
### run 흐름
1. 선택 노드가 있는지, clone 가능한지 확인
2. FPS는 1~60, sc는 0.25~4로 클램프
3. 모션 길이(`getDur`)를 재귀로 구해 자동 길이 계산
4. `src.clone()`으로 임시 노드 생성 → 원본에서 우측 300px 이동
5. 옵션에 따라 배경 처리 함수 호출 (아래 참고)
6. 원본과 클론의 노드 쌍(`Pair`)을 재귀로 매핑 (초기 x/y/w/h/opacity/rotation 기록)
7. `len = ceil(dur * fps)` 프레임 루프:
- 시간 t에 대해 모든 Pair에 애니메이션 값 적용 (`applyOne`)
- `tmp.exportAsync({ format: 'PNG', constraint: { type: 'SCALE', value: sc }, contentsOnly: true, useAbsoluteBounds: true, colorProfile: 'SRGB' })`
- 결과 바이트를 `figma.ui.postMessage({ type: 'fr', i, bytes })`로 전송
8. 종료 시 tmp 노드 삭제 (finally)
### 애니메이션 샘플링
노드 프로퍼티 `n.animations`가 `Record<string, Bind>` 형태로 존재한다고 가정하고 처리. 지원 채널:
- `TRANSLATION_X`, `TRANSLATION_Y`, `TRANSLATION_XY` (Vector)
- `OPACITY` (0~1)
- `ROTATION`
- `SCALE_X`, `SCALE_Y`, `SCALE_XY`, `WIDTH`, `HEIGHT`
각 Bind는 `baseValue`, `timelineDuration`, `tracks: Tr[]` 구조. 각 트랙은 `keyframeOperation: 'SET'|'OFFSET'|'SCALE'`과 정렬된 keyframes(`timelinePosition`, `easing`, `value`). 트랙을 순서대로 base에 겹쳐 적용하는 방식으로 최종값 산출.
### 이징 지원
`LINEAR`, `HOLD`, `EASE_IN`, `EASE_OUT`, `EASE_IN_AND_OUT`, `EASE_IN_BACK`, `EASE_OUT_BACK`, `CUSTOM_CUBIC_BEZIER`(cubic-bezier y1, y2 사용).
### 배경 처리 옵션
- `clear`: 선택 노드 자체의 `fills`/`backgrounds`/`fillStyleId`/`backgroundStyleId` 제거 (try/catch로 실패 무시)
- `clearAll`: 자식이 있는 모든 컨테이너 노드의 fill 제거 (walk)
- `hideBg`: 자식이 없는 RECTANGLE/VECTOR/ELLIPSE 중 아래 조건 만족 시 `visible = false`
- 이름이 `bg`/`background`/`backdrop`/`배경`/`백그라운드`/`흰배경`/`white bg` 매치
- 또는 부모 로컬 좌표계에서 부모 전체를 덮음
- 또는 root 절대좌표 기준 root 전체를 덮음
- 그리고 단색 fill이 opacity 0.95 초과로 보이는 상태
### 유틸리티
`clampNum`, `num` (숫자 아닐 때 default), `lerp`, `round`, `safeName`(`[\\/:*?"<>|]+`을 `-`로).
## src/ui.html 구현 요구사항
한 파일 안에 마크업+스크립트. CSS 장식 금지 (사용자 요구), 기본 HTML만 사용.
### UI 요소
- 선택 상태 표시 `#sel`
- 입력: `fps`(number, 기본 30), `sec`(number, 기본 0), `sc`(number, 기본 1), `name`(text, 기본 `motion-alpha`)
- 체크박스: `clear`(기본 체크), `clearAll`, `hideBg`(기본 체크)
- 실행 버튼 `#run`, 로그 영역 `<pre id="log">`
### 메시지 흐름
- `parent.postMessage`로 run 전송
- 수신: `info`, `start`, `fr`, `end`, `err`
- `fr` 수신 시 Uint8Array를 배열에 저장
- `end` 수신 시:
1. 첫/중간/끝 프레임을 canvas로 그려 알파 픽셀 존재 여부 검사 (`checkAlpha`)
2. 알파 없으면 원인·해결 안내 로그 표시하고 중단
3. 알파 있으면 FormData 구성 후 `fetch('http://localhost:8787/encode')` → Blob 다운로드 (`a.download = name + '.mov'`)
### 파일명 정제
`clean(s)` 함수에서 `\`, `/`, `:`, `*`, `?`, `"`, `<`, `>`, `|` 모두 필터. **정규식에서 `\\`로 이스케이프하는 것 잊지 말 것.**
## server.js 구현 요구사항
### 미들웨어
- `cors({ origin: '*' })` (Figma UI iframe origin이 null이라 와일드카드 필요)
- `multer({ storage: memoryStorage, limits: { files: 4000, fileSize: 30 * 1024 * 1024, fieldSize: 20 * 1024 * 1024 } })`
- files는 fps 60 × sec 60 = 3600을 상회하도록 여유 확보
### 라우트
- `GET /health` → `{ ok: true, name: 'motion-alpha-premiere-encoder', version: 'animation-rgb-alpha-2-allkeyframes' }`
- `POST /encode` (upload.array('frames')):
1. `os.tmpdir()` 아래 임시 폴더 생성
2. multer가 받은 파일들을 `originalname` 기준 정렬 후 `f%04d.png` 형식으로 디스크에 기록
3. ffmpeg 실행:
```
-y
-framerate {fps}
-i {dir}/f%04d.png
-an
-vf format=argb,pad=ceil(iw/2)*2:ceil(ih/2)*2:0:0:color=black@0,format=argb
-c:v qtrle
-g 1
-pix_fmt argb
{out}
```
4. probe로 스트림 확인 → `/(argb|rgba|bgra)/i` 매치 없으면 500 반환
5. 파일 바이트를 읽어 `hasAtom(buf, 'stss')`가 true면 500 반환 (Premiere 임포트 실패 방지)
6. `Content-Type: video/quicktime`, `Content-Disposition: attachment` 헤더로 파일 전송
7. 임시 폴더 cleanup은 성공/실패 모두에서 실행
### hasAtom 파서 (핵심)
QuickTime/MOV atom 구조를 따라가는 재귀 파서:
- 각 atom은 `[size:uint32BE][type:4바이트]` 헤더로 시작
- `size === 1`이면 이어지는 8바이트가 실제 크기 (large size)
- `size === 0`이면 end-of-buffer까지가 크기
- 컨테이너 atom(`moov`, `trak`, `mdia`, `minf`, `stbl`, `edts`)만 자식으로 재귀
- depth 상한 8로 무한 재귀 방지
- target atom을 발견하면 true 반환
**이 파서를 반드시 구현해야 한다.** 문자열 검색으로 대체하면 픽셀 데이터의 우연 일치로 오탐이 나고, `-g 1`이 정상 동작해도 500이 뜬다.
### 그 외
- `clamp(n, min, max, d)`로 fps 방어
- ffmpeg 실행은 `spawn(ffmpegPath, args, { windowsHide: true })`로 하고 stderr 로그 12KB로 rolling 유지
- `p.on('close', code)` 논-제로면 에러 메시지에 마지막 로그 포함
## 파일별 최종 검증 체크리스트
- [ ] `npm run build` → `dist/code.js`, `dist/ui.html` 생성
- [ ] `npm run server` → 콘솔에 실행 로그 + `/health` 응답 version이 `animation-rgb-alpha-2-allkeyframes`
- [ ] Figma에서 `manifest.json` import → 플러그인이 목록에 나타남
- [ ] 애니메이션이 있는 프레임 선택 → UI에 이름과 길이 표시
- [ ] 내보내기 실행 → 로그가 `PNG 프레임 생성 → PNG 알파 확인됨 → 완료` 순서로 진행
- [ ] 다운로드된 MOV의 스트림에 `qtrle`, `argb` 확인
- [ ] MOV에 `stss` atom 없음 확인 (hasAtom 검사 통과)
- [ ] Premiere Pro에 임포트 성공, 시퀀스에서 아래 트랙이 비쳐 보임
## 하지 말 것
- UI에 CSS 장식 넣지 말 것 (기본 HTML만)
- `hasAtom`을 `buf.indexOf('stss')` 같은 문자열 검색으로 대체하지 말 것
- `-g 1` 옵션 빼지 말 것 (Premiere가 열지 못함)
- multer files 상한을 1000 이하로 두지 말 것 (긴 애니메이션에서 조용히 실패)
- watch 스크립트에서 ui.html 복사 단계 빠뜨리지 말 것
- 정규식에서 백슬래시 이스케이프 (`\\`) 누락하지 말 것
## 결과물
위 파일들을 실제로 만들고, `npm install`부터 Premiere 임포트까지 흐름이 통과하는지 검증한 뒤 결과를 보고할 것.
플러그인 및 서버 시스템의 동작 구조
위 프롬프트는 피그마 샌드박스 환경의 한계를 극복하기 위해 클라이언트와 로컬 인코더 서버를 분리한 아키텍처를 채택한다. 전체 작업 흐름은 다음과 같은 단계로 진행된다.
- 프레임 샘플링 및 이미지 추출 (Figma 플러그인): 플러그인 백그라운드 스크립트(
src/code.ts)는 선택된 레이어의 애니메이션 트랙 데이터를 분석한다. 지정된 프레임레이트(FPS)에 맞춰 시간축(t)을 이동시키며 임시 노드에 변형 속성(위치, 투명도, 회전, 크기 등)을 적용한다. 이후 각 프레임을 투명 배경 PNG 이미지 바이트로 내보낸 뒤 UI iframe으로 전달한다. - 배경 투명화 및 검증 (UI 계층): 사용자 인터페이스(
src/ui.html)는 프레임 데이터를 수집하고, 첫 번째 및 중간 프레임을 캔버스에 그려 실제 알파 채널(투명 픽셀)이 존재하는지 사전 검사한다. 디자인 요소에 불필요한 배경 사각형이 남아 투명도가 상실된 경우 인코딩 전에 오류를 안내한다. - 동영상 변환 및 스트림 검증 (로컬 서버): 노드제이에스(Node.js) 기반의 로컬 인코더 서버(
server.js)는 전달받은 이미지 시퀀스를 임시 폴더에 정렬하여 기록한다. 이어 FFmpeg를 호출해qtrle(QuickTime Animation) 코덱과argb픽셀 포맷으로 영상을 인코딩하고, 완성된 MOV 바이너리를 검증한 후 클라이언트로 반환한다.
프리미어 프로 호환성을 위한 핵심 기술 요구사항
어도비 프리미어 프로 12.1 이상 버전에서는 퀵타임 애니메이션 코덱을 해석할 때 특정한 제약이 존재한다. 이 호환성 문제를 해결하기 위해 프롬프트에서는 다음 두 가지 핵심 규칙을 강제한다.
첫째, 전체 프레임 키프레임 강제(-g 1) 설정이다. 키프레임(Keyframe)은 다른 프레임의 참조 없이 완전한 한 장면의 이미지 데이터를 독립적으로 저장하는 기준 프레임이다. 반면 델타 프레임(Delta Frame)은 이전 프레임과의 차이점만 기록하여 용량을 줄이는 방식이다. FFmpeg의 qtrle 인코더는 기본적으로 델타 압축을 적용하여 파일 내부에 동기화 샘플 정보인 stss 아톰을 생성한다. 프리미어 프로는 이 델타 프레임이 포함된 QuickTime MOV 파일을 열지 못하고 임포트 오류를 일으킨다. 따라서 그룹 오브 픽처스(GOP, Group of Pictures) 간격을 1로 지정하는 -g 1 옵션을 통해 모든 프레임을 키프레임으로 생성해야 한다.
둘째, 바이너리 아톰(Atom) 트리 파서(hasAtom)의 구현이다. 퀵타임(QuickTime) 컨테이너는 데이터를 아톰(Atom)이라는 규격화된 블록 단위로 계층화하여 저장한다. 각 아톰은 크기 정보(4바이트)와 식별자(4바이트)로 구성된 헤더를 가진다. 인코딩된 파일에 stss 아톰이 존재하는지 검증할 때, 파일 버퍼를 단순 문자열로 검색하면 픽셀 데이터 내부의 임의 바이트 배열이 우연히 stss와 일치하여 오탐이 발생할 수 있다. 따라서 컨테이너 계층 구조를 순회하는 재귀 파서를 통해 실제 메타데이터 트리에 stss 블록이 없는지 정확히 검사해야 한다.
서버 구동 시 FFmpeg 바이너리 접근 권한을 확인해야 하며, 긴 애니메이션 내보내기 시 대량의 프레임 이미지가 전송되므로 멀터(Multer) 미들웨어의 수신 파일 개수 및 용량 한도를 충분히 설정해야 한다.