HyperFrames
HTML이 비디오의 진실 공급원입니다. 컴포지션은 data-* 속성으로 타이밍을 정의하고, GSAP 타임라인으로 애니메이션을 제어하며, CSS로 외형을 지정하는 HTML 파일입니다. 프레임워크는 클립의 가시성, 미디어 재생 및 타임라인 동기화를 처리합니다.
접근 방식
탐색 (탐색적 요청만 해당)
사용자가 방향을 정하지 않은 개방형 요청("제품 출시 비디오 만들어줘", "우리 브랜드용 뭔가 만들어줘")의 경우, 색상을 선택하기 전에 의도를 파악하세요:
- 대상 — 누가 시청하나요? 개발자? 임원? 일반 소비자?
- 플랫폼 — 어디서 재생되나요? 소셜 미디어(15초), 웹사이트 히어로, 제품 데모, 내부?
- 우선순위 — 무엇이 가장 중요한가요? 모션 품질? 콘텐츠 정확성? 브랜드 충실도? 속도?
- 변형 — 사용자가 여러 옵션을 원하나요, 아니면 하나의 최선의 결과물을 원하나요?
특정 요청("타이틀 카드 추가해줘", "장면 3의 타이밍 수정해줘")의 경우 탐색 단계를 건너뜁니다.
탐색적 요청의 경우, 단순히 색상만 바꾼 것이 아니라 의미 있게 다른 2~3가지 변형을 고려해 보세요. 예를 들어 템포, 에너지 수준, 구조적 접근 방식이 다른 옵션입니다. 하나는 안전하고 예상 가능한 버전, 하나는 야심찬 버전입니다. 반드시 그래야 하는 것은 아닙니다 — 적절할 때 사용할 수 있는 도구일 뿐입니다.
1단계: 디자인 시스템
프로젝트에 design.md 또는 DESIGN.md 파일이 있으면 먼저 읽으세요 (두 대소문자 모두 확인하세요 — Linux에서는 서로 다른 파일입니다). 이 파일이 브랜드 색상, 폰트, 제약 조건의 진실 공급원입니다. 정확한 값을 사용하세요 — 색상을 임의로 만들거나 폰트를 대체하지 마세요. 모든 형식(YAML 프론트매터, 산문, 표 등)이 가능합니다 — 값을 추출하기만 하면 됩니다.
로컬에서 찾을 수 없는 폰트를 지정한 경우 (fonts/ 디렉토리에 .woff2 파일이 없거나 내장 폰트가 아닌 경우), HTML을 작성하기 전에 사용자에게 경고합니다: "design.md에서 [폰트 이름]을 지정했지만 폰트 파일을 찾을 수 없습니다. fonts/에 .woff2 파일을 추가해 주세요. 그렇지 않으면 [가장 가까운 내장 대안]으로 대체하겠습니다."
design.md 파일이 없는 경우, 사용자에게 선택지를 제공합니다:
- 사용자가 스타일이나 분위기를 언급했나요? → visual-styles.md를 읽어 8개의 명명된 프리셋을 확인하고 가장 가까운 것을 선택하세요.
- 옵션을 시각적으로 탐색하고 싶나요? → 디자인 피커를 실행하세요: references/design-picker.md를 읽어 전체 워크플로우를 확인하세요. 시각적 피커 페이지를 제공합니다. 사용자가 브라우저에서 분위기, 팔레트, 타이포그래피, 모션을 구성한 다음 생성된 design.md를 복사하여 대화에 붙여넣습니다.
- 빠르게 진행하고 싶나요? → 분위기, 밝거나 어두운 배경, 브랜드 색상/폰트를 물어보세요. 그런 다음 house-style.md에서 팔레트를 선택하세요.
design.md는 브랜드를 정의합니다. 비디오 컴포지션 규칙을 정의하지 않습니다. 규칙은 references/video-composition.md와 house-style.md에서 제공됩니다. 브랜드 색상은 웹 UI 수준의 불투명도가 아닌 비디오에 적합한 규모로 사용하세요.
2단계: 프롬프트 확장
모든 컴포지션에 대해 항상 실행합니다 (단일 장면 작품 및 사소한 편집 제외). 이 단계는 사용자의 의도를 design.md 및 house-style.md에 맞춰 다운스트림 에이전트가 동일하게 읽을 수 있는 일관된 중간 결과물을 생성합니다.
전체 프로세스와 출력 형식은 references/prompt-expansion.md를 참조하세요.
3단계: 계획
HTML을 작성하기 전에 높은 수준에서 생각합니다:
- 무엇 — 시청자는 무엇을 경험해야 하나요? 내러티브 아크, 주요 순간, 감정적 비트를 식별합니다.
- 구조 — 컴포지션은 몇 개이며, 어떤 것이 하위 컴포지션이고 어떤 것이 인라인인지, 어떤 트랙이 무엇(비디오, 오디오, 오버레이, 캡션)을 담당하는지.
- 리듬 — 구현하기 전에 장면 리듬을 선언합니다. 어떤 장면이 빠른 히트인지, 어떤 장면이 홀드인지, 셰이더가 어디에 배치되는지, 에너지가 어디서 최고조에 달하는지. 패턴 이름을 지정합니다: 빠름-빠름-느림-빠름-셰이더-홀드. 리듬 템플릿은 references/beat-direction.md를 참조하세요.
- 타이밍 — 어떤 클립이 지속 시간을 결정하고, 전환이 어디에 배치되며, 페이싱은 무엇인지.
- 레이아웃 — 먼저 종료 상태를 구축합니다. 아래 "애니메이션 전 레이아웃"을 참조하세요.
- 애니메이션 — 그런 다음 아래 규칙을 사용하여 모션을 추가합니다.
요청된 것을 만드세요. "타이틀 카드" 요청은 "타이틀 카드 + 3개의 지원 장면 + 배경 음악 + 캡션"에 대한 요청이 아닙니다. 모든 장면, 모든 요소, 모든 트윈은 그 자리를 정당화해야 합니다. 추가 장면이나 요소가 작품을 진정으로 향상시킬 경우 제안하세요 — 추가하지 마세요.
작은 편집(색상 수정, 타이밍 조정, 한 요소 추가)의 경우 바로 규칙으로 건너뜁니다.
애니메이션 전 레이아웃
각 요소를 가장 잘 보이는 순간 — 완전히 진입하고 올바르게 배치되었으며 아직 종료되지 않은 프레임 — 에 배치합니다. 먼저 정적 HTML+CSS로 작성합니다. 아직 GSAP은 사용하지 않습니다.
이것이 중요한 이유: 요소를 애니메이션 시작 상태(화면 밖, 0으로 축소, 투명도 0)에 배치하고 도달해야 할 위치로 트윈하면 최종 레이아웃을 추측하는 것입니다. 비디오가 렌더링될 때까지 겹침이 보이지 않습니다. 먼저 종료 상태를 구축하면 모션을 추가하기 전에 레이아웃 문제를 확인하고 수정할 수 있습니다.
프로세스
- 각 장면의 히어로 프레임을 식별합니다 — 대부분의 요소가 동시에 보이는 순간입니다. 이것이 구축할 레이아웃입니다.
- 해당 프레임에 대한 정적 CSS를 작성합니다.
.scene-content컨테이너는width: 100%; height: 100%; padding: Npx;와display: flex; flex-direction: column; gap: Npx; box-sizing: border-box를 사용하여 전체 장면을 채워야 합니다. 패딩을 사용하여 콘텐츠를 안쪽으로 밀어 넣습니다 — 콘텐츠 컨테이너에position: absolute; top: Npx를 절대 사용하지 마세요. 절대 위치 콘텐츠 컨테이너는 콘텐츠가 남은 공간보다 클 경우 오버플로됩니다.position: absolute는 장식용으로만 예약하세요. gsap.from()으로 진입을 추가합니다 — 화면 밖/보이지 않음에서 CSS 위치로 애니메이션합니다. CSS 위치가 기본 진실이며, 트윈은 그곳에 도달하는 과정을 설명합니다. (data-composition-src를 통해 로드된 하위 컴포지션에서는gsap.fromTo()를 선호합니다 — references/motion-principles.md의 부하 지지 GSAP 규칙 참조.)gsap.to()로 종료를 추가합니다 — CSS 위치에서 화면 밖/보이지 않음으로 애니메이션합니다.
예시
/* scene-content가 장면을 채우고, 패딩이 콘텐츠를 배치합니다 */
.scene-content {
display: flex;
flex-direction: column;
justify-content: center;
width: 100%;
height: 100%;
padding: 120px 160px;
gap: 24px;
box-sizing: border-box;
}
.title {
font-size: 120px;
}
.subtitle {
font-size: 42px;
}
/* 컨테이너는 모든 장면 크기(1920x1080, 1080x1920 등)를 채웁니다.
패딩이 콘텐츠를 배치합니다. Flex + gap이 간격을 처리합니다. */
틀렸음 — 하드코딩된 크기와 절대 위치:
.scene-content {
position: absolute;
top: 200px;
left: 160px;
width: 1920px;
height: 1080px;
display: flex; /* ... */
}
// 3단계: 해당 위치로 애니메이션
lt.from(".title", { y: 60, opacity: 0, duration: 0.6, ease: "power3.out" }, 0);
tl.from(".subtitle", { y: 40, opacity: 0, duration: 0.5, ease: "power3.out" }, 0.2);
tl.from(".logo", { scale: 0.8, opacity: 0, duration: 0.4, ease: "power2.out" }, 0.3);
// 4단계: 해당 위치에서 종료 애니메이션
tl.to(".title", { y: -40, opacity: 0, duration: 0.4, ease: "power2.in" }, 3);
tl.to(".subtitle", { y: -30, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.1);
tl.to(".logo", { scale: 0.9, opacity: 0, duration: 0.3, ease: "power2.in" }, 3.2);
요소가 시간에 따라 공간을 공유하는 경우
요소 A가 요소 B가 동일한 영역에 진입하기 전에 종료되면, 둘 다 각각의 히어로 프레임에 대한 올바른 CSS 위치를 가져야 합니다. 타임라인 순서는 시각적으로 공존하지 않음을 보장합니다 — 그러나 레이아웃 단계를 건너뛰면 타이밍 오류로 인해 우발적으로 겹치는 경우를 발견할 수 없습니다.
의도적인 겹침으로 간주되는 것
레이어 효과(텍스트 뒤 글로우, 그림자 요소, 배경 패턴)와 z-스택 디자인(카드 스택, 깊이 레이어)은 의도적입니다. 레이아웃 단계는 비의도적인 겹침 — 두 헤드라인이 서로 위에 겹쳐지는 경우, 통계가 레이블을 가리는 경우, 콘텐츠가 프레임 밖으로 흘러나가는 경우 — 을 잡기 위한 것입니다.
데이터 속성
모든 클립
| 속성 | 필수 | 값 |
|---|---|---|
id | 예 | 고유 식별자 |
data-start | 예 | 초 또는 클립 ID 참조 ("el-1", "intro + 2") |
data-duration | img/div/컴포지션에 필수 | 초. 비디오/오디오는 미디어 지속 시간을 기본값으로 사용. |
data-track-index | 예 | 정수. 동일 트랙의 클립은 겹칠 수 없음. |
data-media-start | 아니요 | 소스의 트림 오프셋 (초) |
data-volume | 아니요 | 0-1 (기본값 1) |
data-track-index는 시각적 레이어링에 영향을 주지 않습니다 — CSS z-index를 사용하세요.
컴포지션 클립
| 속성 | 필수 | 값 |
|---|---|---|
data-composition-id | 예 | 고유 컴포지션 ID |
data-start | 예 | 시작 시간 (루트 컴포지션: "0" 사용) |
data-duration | 예 | GSAP 타임라인 지속 시간보다 우선함 |
data-width / data-height | 예 | 픽셀 크기 (1920x1080 또는 1080x1920) |
data-composition-src | 아니요 | 외부 HTML 파일 경로 |
data-variable-values | 아니요 | 하위 컴포지션 호스트의 인스턴스별 변수 재정의의 JSON 객체 |
루트 <html> 요소:
| 속성 | 필수 | 값 |
|---|---|---|
data-composition-variables | 아니요 | 선언된 변수의 JSON 배열 (id/type/label/default) — Studio 편집 UI를 구동하고 getVariables()의 기본값 제공 |
컴포지션 구조
data-composition-src를 통해 로드된 하위 컴포지션은 <template> 래퍼를 사용합니다. 독립 실행형 컴포지션 (메인 index.html)은 <template>를 사용하지 않습니다 — <body>에 직접 data-composition-id div를 배치합니다. 독립 실행형 파일에 <template>를 사용하면 모든 콘텐츠가 브라우저에서 숨겨져 렌더링이 깨집니다.
하위 컴포지션 구조:
<template id="my-comp-template">
<div data-composition-id="my-comp" data-width="1920" data-height="1080">
<!-- content -->
<style>
[data-composition-id="my-comp"] {
/* scoped styles */
}
</style>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
// tweens...
window.__timelines["my-comp"] = tl;
</script>
</div>
</template>
루트에서 로드: <div id="el-1" data-composition-id="my-comp" data-composition-src="compositions/my-comp.html" data-start="0" data-duration="10" data-track-index="1"></div>
변수 (매개변수화된 컴포지션)
소스 HTML을 편집하지 않고 동일한 컴포지션을 다른 콘텐츠(제목, 테마 색상, 가격, 캡션)로 렌더링합니다.
세 단계 패턴:
- 선언 — 컴포지션의
<html>루트에data-composition-variables로 변수를 선언합니다. 각 항목은id,type(string,number,color,boolean,enum중 하나),label,default가 필요합니다. Enum 항목은options: [{value, label}, ...]도 필요합니다. - 읽기 — 컴포지션의 스크립트 내에서
window.__hyperframes.getVariables()로 해결된 값을 읽습니다. 선언된 기본값 + 인스턴스별 재정의 + CLI 재정의의 병합된 결과를 반환합니다. - 재정의 —
npx hyperframes render --variables '{...}'(최상위) 또는 호스트 요소의data-variable-values='{...}'(하위 컴포지션의 인스턴스별)로 렌더링 시 재정의합니다.
<!doctype html>
<html
data-composition-variables='[
{"id":"title","type":"string","label":"제목","default":"안녕하세요"},
{"id":"theme","type":"enum","label":"테마","default":"light","options":[
{"value":"light","label":"라이트"},
{"value":"dark","label":"다크"}
]}
]'
>
<body>
<div data-composition-id="root" data-width="1920" data-height="1080">
<h1 id="hero" class="clip" data-start="0" data-duration="3"></h1>
<script>
const { title, theme } = window.__hyperframes.getVariables();
document.getElementById("hero").textContent = title;
document.body.dataset.theme = theme;
</script>
</div>
</body>
</html>
# 개발 미리보기는 선언된 기본값 사용
npx hyperframes preview
# 재정의로 렌더링
npx hyperframes render --variables '{"title":"Q4 보고서","theme":"dark"}' --output q4.mp4
# 또는 JSON 파일에서
npx hyperframes render --variables-file ./vars.json
하위 컴포지션의 인스턴스별 값: data-composition-src를 통해 로드된 하위 컴포지션 내에서도 동일한 getVariables()가 작동합니다. 각 호스트 요소는 자체 값을 전달합니다:
<div
data-composition-id="card-pro"
data-composition-src="compositions/card.html"
data-variable-values='{"title":"프로","price":"$29"}'
></div>
<div
data-composition-id="card-enterprise"
data-composition-src="compositions/card.html"
… (원본에서 잘림)