코드 렌더링 애니메이션
아이디어 (Addy Osmani, LinkedIn 2026-09-24): 코딩 모델이 소스(기사, 스펙)를 읽고 각 프레임과 사운드를 그리는 JavaScript를 작성합니다. 결과물은 프로그램이므로 어떤 순간이든 편집, 재타이밍, 재렌더링 또는 재포맷이 가능합니다. Osmani는 자신의 프롬프트나 도구를 공개하지 않았습니다. 이 스킬의 디자인은 오픈소스 riso-windowseat 프로젝트(MIT, 참고용이며 지침을 복사하지 마세요)에서 비롯되었습니다: https://github.com/sevenevesai/riso-windowseat . 또한 HeyGen HyperFrames (https://hyperframes.heygen.com/guides/pipeline) 와 Remotion의 아이디어를 차용했습니다.
기본 규칙: 초당 4개 이상의 샘플로 프레임별로 검토하고, 실행되지 않은 검사를 주장하지 않으며, 결함을 정밀하게 수정합니다.
계약 (모든 것이 이것에 달려 있음)
각 작업은 <canvas>가 포함된 독립적인 index.html 파일이며, URL에서 ?w=&h=를 읽고 다음을 노출합니다:
window.__anim = {
duration, fps, width, height, canvas,
ready, // 글꼴/에셋이 로드되고 seek(0)가 그려질 때까지 false
seek(t), // 시간 t에서 정확히 프레임을 동기적으로 그림
renderAudio(), // Promise<base64 WAV>, 48 kHz 스테레오, 정확히 `duration` 길이
marks, // 눈에 띄는 이벤트(팝, 컷) 시간 - 사운드가 이 시간에 맞춰져야 함
shots, // [{id,start,end}] - 검증이 경계를 확인함
};
불변 규칙:
seek(t)는 t의 순수 함수입니다. 화면의 모든 값은 t에서 계산됩니다:prog(t,a,b), 이징,pop(t,at). 프레임 간 상태가 유지되지 않으며,requestAnimationFrame타이밍,Date, 물리 시뮬레이션 단계(0부터 다시 계산하거나 폐쇄형이 아닌 경우)가 없습니다.- 렌더링 중에
Math.random()을 호출하지 마세요.rngFor('stable-key')(시드된 mulberry32)를 사용하고, 로드 시 고정 키에서 무작위 레이아웃을 한 번 만드세요. 키를 변경하면 출력이 변경되므로 승인 후 키를 고정하세요. - 하나의 타임라인 객체(
TL)가 모든 씬 범위와 이벤트 시간을 보유합니다. 그림과 악보 모두 여기서 읽습니다. 이것이 동기화가 작동하는 방식입니다: 사운드는 그림이 팝업되는 동일한 숫자로 예약됩니다. ready=true전에 글꼴을 기다리세요 (document.fonts.load('700 40px "DejaVu Sans"')). 그렇지 않으면 첫 번째 프레임이 대체 글꼴로 렌더링됩니다. 설치된 글꼴은fc-list : family(Linux/macOS) 또는C:\Windows\Fonts(Windows; Segoe UI, Bahnschrift, Consolas, Georgia는 항상 있고, DejaVu는 없음)로 확인하세요. 디자인에 특정 글꼴이 필요한 경우 base64 @font-face로 포함하고 동일한 방식으로 로드하세요.- 작업에서 네트워크 호출이 없습니다. 모든 에셋을 데이터 URI로 포함하세요.
편집 가능한 요소 (영화 수정용)
review/film-fix.html을 사용하면 리뷰어가 요소를 직접 이동하고 크기를 조정한 다음 결과를 index.html에 다시 쓸 수 있습니다. 이 기능을 원하는 영화 (예제는 그렇습니다; 거기서 복사하세요)는 다음을 추가합니다:
const TUNE = /*TUNE*/{}/*END-TUNE*/;한 줄에, 포맷별, 요소 ID별로 키 지정:{"16x9":{"title":{"dx":0,"dy":0,"s":1}}}. 각 화면 비율에 고유한 레이아웃이 있으므로 포맷별로 키 지정됩니다. dx/dy는 캔버스 픽셀입니다. 도구는 두 마커 사이의 텍스트만 다시 씁니다.- 각 주요 요소 주위에
E(id, [x,y,w,h], draw).TUNE[fmt][id]를 조회한 다음, 저장하고, dx/dy만큼 변환하고, 상자 중심을 기준으로 s만큼 스케일링하고,draw()를 호출하고 복원합니다. 또한{id, box}를__anim.elements에 푸시합니다. 여기서 box는 캔버스 픽셀의 변환된 상자입니다 (ctx.getTransform()에서 가져오므로 카메라 이동이 존중됨).seek(t)는 모든 프레임의 시작 부분에서 이 목록을 지웁니다. __anim.elements,__anim.format(16x9,9x16,4x5,1x1) 및__anim.tune(라이브 TUNE 객체에 대한 getter/setter, 도구의 라이브 미리보기에 사용됨).- 규칙: id는 영화 전체에서 고유합니다. 무작위성 없음; TUNE이 비어 있으면 E는 변환을 전혀 적용하지 않으므로 프레임이 바이트 단위로 동일하게 유지됩니다. E를 추가하기 전후에 프레임 해시로 증명한 다음
verify.mjs를 실행하세요.
E가 없는 영화도 film-fix에서 작동합니다: "Nudge" 모드는 영역을 상자로 표시하고 드래그 또는 스케일을 AI용 숫자 수정으로 바꿉니다.
프로젝트 설정
Node 18+, ffmpeg (libx264, aac, blackdetect, freezedetect, ebur128) 및 로컬 Chrome/Chromium이 필요합니다. Harness는 Windows, macOS, Linux의 일반적인 설치 위치에서 Chrome을 찾습니다. CHROME 환경 변수를 설정하여 재정의하세요. qa.sh는 bash (Windows의 Git Bash)가 필요하며 타임코드용 시스템 글꼴을 선택합니다. QA_FONT를 설정하여 재정의하세요. 시스템 전체에 설치되는 것은 없습니다. puppeteer-core만 유일한 npm 종속성입니다.
SKILL_DIR=<이 스킬 폴더 경로>
mkdir -p anim && cp -r "$SKILL_DIR/harness" anim/harness
cd anim/harness && npm install && cd ..
mkdir -p works/<name>
cp "$SKILL_DIR/example/browser-10s/index.html" works/<name>/index.html # 작동하는 예제로 시작
# 모든 harness 명령어는 anim/에서 실행; 출력은 out/으로
Harness 명령어
| 명령어 | 기능 | 통과 신호 |
|---|---|---|
node harness/verify.mjs works/<n>/index.html --format 16x9 | 샘플 + 샷 경계에서 앞뒤로, 콜드 점프를 탐색한 다음 픽셀 해시 비교 | verify: OK, 종료 코드 0 |
node harness/shoot.mjs works/<n>/index.html --format 9x16 --times 1.5,3.4 또는 --range 3:4:0.1 --out out/stills | 정확한 시간에 네이티브 캔버스 PNG | 확인 |
node harness/audio.mjs works/<n>/index.html --twice | 악보만 렌더링 (초 단위), 정확한 길이와 실행 간 결정론 확인 | FAIL 없음. Chrome Web Audio는 실행 간에 1 LSB까지 다를 수 있습니다. 허용 오차는 1입니다. |
node harness/render.mjs works/<n>/index.html --format 16x9 --fps 30 --out out/<n>-16x9.mp4 | 모든 프레임을 탐색하고 PNG를 ffmpeg (libx264 crf18, yuv420p, +faststart)로 파이프한 다음 renderAudio를 AAC로 먹싱 | 프레임 및 시간 출력. 페이지 오류 시 종료 코드 2. --from/--to 또는 --silent로 오디오 없이 범위 렌더링 |
node harness/lint.mjs works/<n>/index.html --all-formats | 페이지 자체에 대한 품질 게이트 (16:9 + 9:16; --format은 하나만): 텍스트 크기, 안전 영역, 텍스트 겹침, 정적 스트레치, 빈 프레임, 샷 목록, 마커의 사운드, 화면의 단어 | lint: OK (0 FAIL), 종료 코드 0 |
node harness/review.mjs works/<n>/index.html --format 16x9 --out out/review-16x9, 그런 다음 --check out/review-16x9/REVIEW.md | 샷당 하나의 접촉 시트 (첫 번째, 6개의 균등 간격 및 마지막 프레임, 타임코드 표시), lint JSON 및 샷당 고정 질문이 있는 REVIEW.md. --check는 빈 응답 또는 TODO 응답, 4 미만 점수, lint FAIL, 제거된 샷 섹션 또는 시트가 만들어진 이후 변경된 영화에서 실패 | review check: OK, 종료 코드 0 |
bash harness/qa.sh out/<n>-16x9.mp4 4 | ffprobe, blackdetect, freezedetect, EBU R128 음량, 오디오/비디오 길이, 초당 N 샘플의 접촉 시트 (타임코드 포함) | 아래 QA 참조 |
포맷: 16x9 1920x1080, 9x16 1080x1920, 4x5 1080x1350, 1x1 1080x1080. 프레임은 기본적으로 JPEG q95로 ffmpeg로 전송되며, 1080p에서 약 16fps이므로 40초 분량은 포맷당 약 60-75초가 소요됩니다. --png는 무손실이지만 약 4배 느립니다.
긴 렌더링: 에이전트의 셸 호출이 시간 초과될 수 있으며 (종종 120초) 하위 프로세스도 함께 종료될 수 있습니다. 호출 시간 제한을 늘리거나 전체 렌더링을 분리하여 실행하고 로그를 폴링하세요 (Linux/macOS 예시; Windows에서는 백그라운드 셸 또는 Start-Process 사용):
setsid nohup bash -c 'node harness/render.mjs works/<n>/index.html --format 16x9 --out out/<n>-16x9.mp4; echo RENDER_DONE' > out/render.log 2>&1 < /dev/null & disown
tr '\r' '\n' < out/render.log | tail -2
또한 작업 파일을 작성하는 heredoc를 실행하는 명령어와 분리하여 시간 초과로 파일이 손실되지 않도록 하세요.
완료 조건
정확한 명령어는 AGENTS.md (다른 코딩 도구와 공유되는 단일 소스)에 있습니다: 모든 포맷에서 verify, audio --twice, 0 FAIL로 lint --all-formats, review.mjs 후 review.mjs --check 통과, 렌더, qa. 실패하는 게이트로 완료를 주장하지 말고, 게이트를 약화시키거나 통과하기 위해 harness 임계값을 편집하지 마세요.
워크플로우
- 브리핑. 소스가 말하는 내용, 청중, 길이, 포맷, 내레이션 여부. 소스 문서에서 주요 주장을 추출하세요. 모든 장면은 말하는 내용을 보여주어야 합니다.
정확성보다 스토리. Harness는 영화를 정확하게 만듭니다. 시청 가능하게 만들지는 않습니다. 스토리보드 전에 브리핑에서 네 가지 질문에 답하세요:
- 캐릭터는 누구이며, 각 장면에서 무엇을 느끼나요?
- 각 아이디어를 대표하는 일상적인 사물은 무엇인가요? (원본은 밀랍 인장이 있는 편지, 모래 상자, 집들로 이루어진 마을을 사용했습니다.)
- 장면들을 함께 유지하는 지속적인 HUD는 무엇인가요? 일반적으로 장 다이얼과 입력된 헤드라인입니다.
- 마지막 프레임은 어떻게 첫 번째 프레임으로 다시 순환되나요? 이러한 요소가 없는 영화는 모든 게이트를 통과하지만 스크롤되어 지나갑니다.
- 코드보다 먼저 STORYBOARD.md. 장면당 한 행: 시간 범위 | 화면 콘텐츠 | 동작 | 오디오 큐. 모든 이벤트 시간을 나열하세요 (이것들이
TL및marks가 됩니다). 몇 초마다 컷 또는 새로운 동작을 계획하고 정적 스트레치가 없도록 하세요. 푸시 또는 오버랩으로 전환을 계획하고 빈 프레임으로의 딥은 없도록 하세요. 승인되지 않은 경우 페이드 투 블랙 엔딩 없음. - 타임라인 모듈, 그런 다음 장면당 하나의
sceneN(t)함수 (t의 순수 함수), 그런 다음 배경, 활성 장면 및 전환을 그리는 컴포지터. - 가장 어려운 프레임을 먼저. 가장 밀집된 순간을 만들고, 모든 포맷으로
shoot하고, 보고, 수정하세요. 그런 다음 확장하세요. - 모든 포맷으로 verify, audio --twice, 그런 다음 모든 포맷으로 render.
- QA (아래), 정밀하게 수정하고, 필요한 것만 다시 렌더링하고, 다시 QA.
- MP4를 QA 결과 및 알려진 약점과 함께 첨부 파일로 전달하세요.
내레이션 포함
TTS 먼저, 그런 다음 단어 수준 타임스탬프 (단어별 시간을 반환하는 도구로 WAV를 전사), 그런 다음 단어에서 TL을 설정합니다. 장면, 팝 및 가라오케 자막 하이라이트는 모두 단어 시간에 키를 지정합니다. 규칙: 상자 안의 가라오케 스타일 자막, 자막 트랙 하나만, 말할 때 화면에 숫자 표시, 고유 명사는 귀로 확인. 내레이션 WAV를 데이터 URI로 포함하고 renderAudio() 내부에서 믹싱 (ctx.decodeAudioData로 디코딩, 0에서 예약)하거나 ffmpeg로 먹싱하고 그 아래에서 악보를 덕킹하세요. 전체 비디오에서 내레이션 음량을 정규화하세요.
하나의 타임라인에서 여러 포맷으로 (자르기 아님)
- 동일한
TL, 동일한 장면, 다른 레이아웃.portrait = H > W,S = min(W,H)/1080(유형 및 모양 스케일),M = 0.06*min(W,H)(안전 여백)을 계산합니다. 화면 비율별로 레이아웃: 16:9의 행은 9:16에서 열이 되고, 트리는 더 좁은 확산을 얻는 식입니다. - 텍스트: 축소 맞춤 (
measureText루프 againstW-2M)으로 가장자리에서 절대 잘리지 않도록 합니다. 크기는S에서 비롯됩니다. 세로 방향에서는 더 큰 유형을 사용하고 높이를 채웁니다. - "세로"를 하나의 경우로 취급하지 마세요. 4:5 (1080x1350)는 9:16보다 훨씬 짧습니다. 4:5에서 실행되는 예제의 9:16 열 레이아웃은 픽셀 그리드와 태그라인을 아래쪽과 서로 위로 밀어냅니다. 화면 비율 (
H/W: 약 0.56, 1.25, 1.78)에 따라 분기하거나 사용 가능한 높이에서 레이아웃하고, 제공하는 모든 포맷으로shoot하세요. - 여백을 확인할 때 팝 오버슈트 (easeOutBack ~1.1x) 및 "뜨거운" 확대를 기억하세요.
shoot로 각 포맷의 가장 밀집된 프레임을 확인하세요.
절차적 사운드 (OfflineAudioContext)
new OfflineAudioContext(2, 48000*duration, 48000), 그래프를 빌드하고,await ctx.startRendering(), 16비트 PCM WAV로 인코딩하고, base64 반환 (예제의wavB64참조).- 예제의 패턴: 장면당 패드 코드 (디튠된 사인/트라이앵글 쌍, 느린 어택, 컷에서 오버랩), 모든
marks이벤트에서 플럭 (트라이앵글 + 2 고조파, 5ms 어택, 지수 감쇠), 타이핑을 위한 시드된 노이즈 버스트 클릭, 충격을 위한 피치 드롭 썸프, 마스터에 DynamicsCompressor. - 노이즈는 시드된 RNG에서 비롯되며, Math.random이 아닙니다.
- 음량: 소셜 피드의 경우 약 -14 LUFS 통합 및 -1 dBFS 미만 피크를 목표로 합니다. 원본 게시물은 -14.3 LUFS를 측정했습니다. 번들 예제는 -17.2 LUFS로 더 조용하며, 피크 -1.4입니다. 페이지에서 마스터 게인으로 설정하여 코드에 유지되도록 하세요.
- 비트 정확한 오디오를 위해 Web Audio 노드 대신 일반 JS로 Float32Array에 샘플을 합성하세요 (폐쇄형 엔벨로프가 있는 오실레이터 합계).
- Tone.js
Tone.Offline(https://tonejs.github.io/docs/14.9.17/functions/Offline.html)은 더 풍부한 음악 API가 필요한 경우 옵션입니다. 작업에 네트워크 액세스가 없으므로 인라인으로 포함되어야 합니다.
QA (리뷰는 우리의 것이지 시청자의 것이 아님)
최종 MP4에 대한 자동 게이트, 모두:
- 모든 포맷에서 verify OK. 페이지 오류 = 실패.
- ffprobe: 예상 크기, 30/1 fps, nb_frames = 지속 시간 x fps, 오디오 지속 시간 = 비디오 지속 시간.
- blackdetect: 없음 (승인된 드라마틱한 블랙 비트가 없는 한). freezedetect (>=2초 정적): 없음.
- ebur128: 통합 음량 및 트루 피크가 목표 범위 내에 있음. 에피소드 간 일관성.
Lint (품질 게이트)
node harness/lint.mjs works/<n>/index.html --all-formats [--json out/lint.json]는 0.1초마다 (--step) 작업을 탐색하고 외부에서 fillText/strokeText를 래핑하므로 변경 없이 계약을 따르는 모든 작업에서 실행됩니다. 다음에서 실패합니다:
- text-size: 최소(W,H)의 2.6% 미만의 정적 텍스트 (1080에서 28px); 3.3% 미만 (36px)은 WARN입니다. 축소(0.5 미만 스케일)로 인해 축소된 텍스트는 개요 맵으로 처리되고 경고만 표시됩니다.
- safe-area: 가장자리에서 4% 이내의 정적 텍스트; 4-6%는 WARN입니다.
- text-overlap: 더 작은 상자의 15% 이상을 0.2초 이상 공유하는 두 개의 다른 보이는 문자열. 더 짧은 통과는 WARN입니다.
- static-stretch: 눈에 띄는 변화 없이 3초 이상 (2초는 WARN). empty-frame: 거의 균일한 프레임, 처음과 마지막 포함.
- shots-coverage:
shots의 간격 또는 중복, 또는 1초 미만의 샷. audio-sync: 70% 미만의marks가 ±40ms 이내의 사운드 시작점을 가짐 (90%는 PASS); -1 dBFS 초과 피크는 WARN입니다. density: 세로 방향에서 18단어 초과 또는 가로 방향에서 30단어 초과 (WARN만 해당).
작업은 제공하는 모든 포맷에서 lint가 0 FAIL로 통과할 때까지 완료되지 않습니다. 작업 자체 캔버스에서 fillText/strokeText로 그려진 텍스트만 볼 수 있으므로 모양 충돌 및 오프스크린 캔버스에 그려진 텍스트는 여전히 시각적 검토가 필요합니다. example/browser-10s는 세 가지 포맷 모두에서 통과합니다. example/browsers-40s는 게이트 이전에 만들어졌으며 실패합니다 (17-28px의 레이블, 9:16에서 잘린 레이블, 텍스트 ...