gfx-html은(는) 무엇을 하나요?
그래픽은 Chrome 헤드리스가 한 프레임씩 렌더링하는 HTML 페이지이며, 알파 무비로 인코딩됩니다. 전체 요청 텍스트는 $ARGUMENTS에 있으며, 첫 번째 토큰은 보통 프로젝트 슬러그(slug)입니다. 동사 이름은 빌드 계약을 따릅니다. 충돌 시 python engine/ve.py --help가 우선합니다.
이 작업 방식을 가능하게 하는 규칙
모든 그래픽은 ?t=<초>의 순수 함수입니다. CSS 애니메이션, GSAP 타임라인, requestAnimationFrame, 트랜지션은 사용하지 않습니다. 페이지는 location.search에서 t를 읽고, 레이아웃을 계산한 후 하나의 정적 프레임을 렌더링합니다.
이는 스타일 선호도가 아닙니다. chrome --headless --screenshot은 "페이지가 로드된 것으로 보일 때" 실행됩니다. 이 빌드에는 가상 클록, Emulation.setVirtualTimePolicy, beginFrame이 없습니다. 그런 방식으로 샘플링된 CSS 애니메이션 페이지는 실행할 때마다 다른 픽셀을 반환합니다. 이는 콘텐츠 주소 기반 캐싱을 망가뜨리고(같은 키지만 다른 픽셀), 눈에 띄는 떨림을 유발합니다.
동일한 단순 루프로 샘플링된 t의 순수 함수는 결정론적이며 캐시 가능합니다. 이것이 전부입니다.
미리 알려야 할 예산
Chrome은 정상 상태에서 프레임당 약 430-470ms로 렌더링하며, 첫 시작 시 7.9초가 소요됩니다. 30fps에서 5초 분량의 그래픽은 150프레임으로, 단일 스레드에서 약 65초가 걸립니다. 엔진은 4개의 Chrome 프로세스를 병렬화하고 프레임별 PNG를 캐시하므로, 변경되지 않은 그래픽은 무료로 재렌더링됩니다. 하지만 첫 렌더링은 즉각적이지 않습니다.
긴 그래픽을 시작하기 전에 프레임 수와 예상 시간을 사용자에게 알리세요. 짧은 그래픽을 선호하세요: 6초 동안 유지되는 하단 자막은 6초 전체가 움직일 필요 없이, 0.4초 인, 정적 유지, 0.4초 아웃이면 충분합니다.
1단계 - 템플릿 선택 또는 작성
템플릿은 presets/graphics/*.html에 있습니다. 새로 작성하기 전에 기존 템플릿을 재사용하세요. 템플릿은 다음을 따르는 단일 자체 포함 HTML 파일입니다:
location.search에서t(초)와 해당 속성(props)을 읽습니다.- 일반 산술 연산으로
t를 기반으로 모든 위치/불투명도를 계산합니다. 이지(ease) 함수도 괜찮습니다. 순수 함수이기 때문입니다. - 투명한 바디(body) 위에 렌더링합니다. 배경색을 절대 칠하지 마세요.
assets/fonts/의 폰트를file:///URL과 함께@font-face를 사용하여 불러옵니다. 시스템 폰트에 의존하지 마세요. 이 상자에서 Chrome이 해석하는 폰트는 QC 컨트롤 렌더가 가정하는 것과 다릅니다.- 1920x1080에서 읽을 수 있어야 하며 플랫폼의 캡션 안전 영역 안에 있어야 합니다.
output.platform을 확인하세요: 하단 1/3에 위치하는 하단 자막은 TikTok UI 아래에 있습니다.
레이아웃 세부 사항, 이징(easing) 규칙 및 속성(props) 계약은 docs/graphics.md에 있습니다. 여기서 다시 설명하지 않습니다.
2단계 - 프레임 렌더링
python engine/ve.py gfx <slug>
프레임별로 엔진은 Chrome을 argv 목록으로 호출합니다. 대략 다음과 같습니다:
chrome.exe --headless=new --disable-gpu --no-sandbox --hide-scrollbars
--user-data-dir=<win-path>\prof --default-background-color=00000000
--window-size=1920,1080 --screenshot=<win-path>\frame_0007.png
"file:///C:/.../template.html?t=0.233&title=..."
두 가지 플래그가 전체 작업 방식을 결정합니다:
--default-background-color=00000000은 진정한 알파를 생성합니다. 확인됨: 모서리 픽셀이0 0 0 0으로 읽힙니다. 이 플래그가 없으면 불투명한 검정이 되어 오버레이가 비디오를 덮습니다.file:///URL은 실제 Windows 경로여야 합니다. Git Bash의$PWD경로는 조용히 실패합니다. 세 개의 다른 HTML 파일이 빈 다크모드 오류 페이지의 동일한 md5를 생성하며, exit 0, 그럴듯한 파일 크기를 냅니다. 이것이 QC 검사(g)가 알려진 빈 해시와 비교하는 이유입니다.
프레임은 .cache/html/ 아래에 sha256(html_content + t + params + renderer + renderer_version)를 키로 하여 캐시됩니다. 템플릿을 변경하면 모든 프레임이 무효화되며, 이는 올바른 동작입니다.
3단계 - 알파를 유지하며 인코딩
동일한 명령어입니다. ve gfx는 프레임을 렌더링하고 한 번에 인코딩합니다. 별도의 인코딩 동사는 없습니다. PNG 시퀀스 옆에 무비가 없으면 하류에서 사용할 수 없는 반쯤 완성된 아티팩트이기 때문입니다.
PNG 시퀀스는 **QuickTime RLE (qtrle, argb)**로 인코딩합니다. ProRes 4444 및 원시 PNG 시퀀스도 알파를 올바르게 전달합니다. 이 빌드에서 VP9-in-WebM은 -metadata alpha_mode=1을 사용하더라도 알파를 조용히 버립니다. 사용하지 말고, 사용자의 에셋에서도 프로빙 없이 받아들이지 마세요.
출력은 <key>.json 사이드카와 함께 .cache/ovl/<xx>/<key>.mov로 저장됩니다.
4단계 - 오버레이 고정
overlays[] 항목을 작성합니다: id, kind: "html", template, props, anchor, duration_frames, layer, render.
anchor는 항상 {segment_id, offset_frames}입니다. 절대 타임라인 위치를 사용하지 마세요. 타임라인 위치는 활성화된 세그먼트의 누적 합계에서 파생됩니다. 누군가 상류에서 세그먼트를 비활성화하는 순간 모든 절대 위치가 틀어지고 모든 그래픽이 표류합니다. 고정된 오버레이는 자동으로 해당 세그먼트와 함께 이동하며, 이는 편집자가 기대하는 동작입니다.
layer는 합성 순서를 결정합니다. 숫자가 높을수록 위에 표시됩니다. 캡션은 마지막에 렌더링되며 그래픽 위에 위치합니다.
5단계 - 완료 전 확인
세 가지 검사가 있으며, 모두 비용이 저렴합니다:
- 알파 프로브. 모서리 프레임에서 원시 rgba 픽셀 하나를 읽고
a = 0인지 확인합니다. QC (d). - 빈 페이지 확인. 프레임이 하나의 단색이 아님을 확인합니다. QC (g). 이는 잘못된
file:///URL, 로드 실패한 폰트, JS 오류를 잡아냅니다. 이 모든 경우 exit 0으로 정상 종료되고 그럴듯한 파일을 생성합니다. 첫 번째, 중간, 3/4 지점 프레임을 샘플링하여 모두 균일할 때만 실패합니다. 모든 템플릿은 불투명도 0에서 시작하므로, 작동하는 그래픽의 프레임 0은 정당하게 비어 있으며, 이것만으로 판단하면 올바른 렌더를 실패로 처리합니다.
ve gfx는 생성된 결과에 대해 두 검사를 자동으로 실행하며, 실패 시 exit 2로 종료합니다.
3. 결정론. 하나의 프레임을 두 번 렌더링하고 해시를 비교합니다. 다르면 템플릿의 무언가가 t의 순수 함수가 아닙니다. 찾으세요. 이 검사가 존재하는 이유는, 그것이 잡아내는 실패가 캐시된 렌더가 새 렌더와 일치하지 않을 때까지 보이지 않기 때문입니다.
그런 다음 중간 지점의 스틸 이미지를 사용자에게 보여주세요. 스크린샷이 한 명령어 거리에 있을 때 그래픽을 글로 설명하지 마세요.
실패 모드
| 증상 | 원인 | 수정 방법 |
|---|---|---|
| 오버레이가 비디오를 검정으로 덮음 | --default-background-color 누락, 또는 템플릿이 배경을 그림 | 플래그 추가; 바디를 투명하게 만듦 |
| 모든 프레임이 동일하고, 파일 크기가 그럴듯함 | 잘못된 file:/// URL로 인한 빈 오류 페이지 | 실제 Windows 경로 사용; 빈 해시와 비교 확인 |
| 실행 간에 같은 키지만 다른 픽셀 | 템플릿이 t의 순수 함수가 아님 | 모든 CSS 애니메이션과 rAF 제거 |
| 텍스트가 잘못된 서체로 렌더링됨 | 시스템 폰트 대체 | assets/fonts/에서 file:/// URL로 @font-face 사용 |
| 인코딩 후 알파 소실 | VP9/WebM | qtrle argb 또는 ProRes 4444 사용 |
| 상류 편집 후 그래픽이 표류 | 절대 타임라인 앵커 | {segment_id, offset_frames}로 앵커 지정 |
| 렌더링에 수 분 소요 | 430 ms/프레임이 최소치 | 그래픽을 짧게 하거나 수용; 캐시로 두 번째 실행은 무료 |