vtake-cut について
VTake は、ローカル動画をカード形式の構成に変換し、MP4 にレンダリングするスキルと CLI ワークフローです。文字起こしに基づいてカードを設計し、HTML で作成します。中間ファイルも確認できます。
主な機能
- 01音声と動画メタデータの抽出
- 02文字起こしと内容の修正
- 03文字起こしに基づくカード設計
- 04カードと構成用 HTML の作成
- 05構成を MP4 にレンダリング
活用シーン
- 01ローカル動画をカード形式の編集にする
- 02文字起こしからビジュアルな絵コンテを作る
- 03タイムスタンプ付き文字起こしを確認・修正する
- 04組み立てた HTML を MP4 にレンダリングする
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ notedit/ vtake-skills --skill vtake-cutインストール後、Claude Code で/vtake-cutを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/vendor/gsap.min.jsreferences/DESIGN_INDEX.mdreferences/frames/clean.htmlhairline.htmlpolaroid.htmlreferences/layouts/overlay.htmlpip.htmlsplit.htmlstack.htmlreferences/styles/academic.htmlaudit.htmleditorial.htmlgeom.htmlminimal.htmlspotlight.htmlswiss.htmlterminal.htmlwhiteboard.htmlxhs.html詳細
VTake ローカルワークフロー
VTake は、ローカルの入力動画をカードベースのコンポジションに変換します。エージェントがカード(タイミング+コンテンツ)を設計し、会話内に各カードの HTML を直接記述してから、単一のコンポジション HTML にまとめ、hyperframes を使って MP4 にレンダリングします。固定のアーキタイプ一覧や指定されたカード構造はありません。カードは、文字起こしの実際の内容に基づいて形作られます。
作業ディレクトリ内で確認できる中間ファイル:
metadata.json— 長さ/幅/高さ/fpsaudio.mp3— 抽出した音声transcript.json— タイムスタンプ付きのセグメントと単語storyboard.json— 軽量なカードの概要(エージェントの計画)public/cards/card-XX.html— カードごとの HTML フラグメントpublic/index.html— 最終的に組み立てたコンポジションoutput.mp4— レンダリングした動画
CLI の解決
# vtake CLI — auto-downloaded from npm on first run
npx -y @notedit/vtake@latest --help
# hyperframes — for rendering the assembled HTML to MP4
npx hyperframes render --help
以下の各
vtake …コマンドは、npx -y @notedit/vtake@latest …の省略表記です。
ワークフロー
1. 環境を確認する
npx -y @notedit/vtake@latest doctor
# confirm bundled assets:
ls "<SKILL_DIR>/assets/fonts" "<SKILL_DIR>/assets/vendor/gsap.min.js"
必須:
ffmpeg/ffprobe(システム)<SKILL_DIR>/assets/fonts/*.woff2、<SKILL_DIR>/assets/vendor/gsap.min.js(このスキルにバンドル済み。ステップ 9 で作業ディレクトリに配置)
任意:
ELEVEN_API_KEY— 設定すると、vtake transcribeは ElevenLabs に直接接続し、レート制限付きプロキシを経由しません。設定されていない場合は、https://vtake.app/api/transcribeにフォールバックします。このプロキシでは、IP ごとに1 分あたり 3 件のリクエストが許可されています。VTAKE_TRANSCRIBE_ENDPOINTを使ってプロキシ URL を上書きできます(例:ローカルの Wrangler 開発環境用)。
macOS で hyperframes render を使用する場合は、次の設定を強く推奨します。
export PRODUCER_BROWSER_GPU_MODE=hardware
2. 作業ディレクトリを作成する
VIDEO_PATH="/absolute/path/input.mp4"
WORK_DIR=".vtake-work/$(basename "$VIDEO_PATH" | sed 's/\.[^.]*$//')"
mkdir -p "$WORK_DIR"
3. 音声とメタデータを抽出する
npx -y @notedit/vtake@latest extract "$VIDEO_PATH" --out-dir "$WORK_DIR"
出力:metadata.json(長さ、幅、高さ、fps)+ audio.mp3。
4. 文字起こしする
npx -y @notedit/vtake@latest transcribe "$WORK_DIR/audio.mp3" --out-dir "$WORK_DIR" --asr elevenlabs
出力:{ segments, words, raw } を含む transcript.json。
レート制限(プロキシモードのみ — ELEVEN_API_KEY なし): サーバーでは、IP ごとに 1 分あたり 3 件のリクエストが許可されています。rate_limited: または service_busy: で始まるエラーが表示された場合は、自動で再試行しないでください。処理を止め、待機する秒数をユーザーに伝えます(メッセージには再試行の目安が含まれています)。ユーザーから再度依頼されたら、このステップから再開します。
5. 文字起こしを修正する
transcript.json を読み、明らかな ASR エラーを修正します。
- 同音異義語、製品名、専門用語、句読点
- すべての
start/endタイムスタンプを保持する - 可能な限り
segments[].textのみを編集する - 明確な一対一の置き換えに限り、個別の
words[].wordを編集する
6. 軽量なストーリーボードを作成する(チャット内)
CLI は使用しません。 transcript.json と metadata.json を読み、カードを直接設計します。storyboard.json はエージェント内部の計画用成果物です。これを使用する vtake CLI コマンドはありません。ステップ 9 で各カードの HTML を記述する前に、タイミングとコンテンツを明確に検討できるように用意されています。以下の例と同じ構造にして、同じ概要からステップ 9 で作成するコンポジションを組み立てられるようにします。
{
"schemaVersion": 3,
"composition": {
"fps": 30,
"width": 1080,
"height": 1920,
"durationSeconds": 121.2,
"layout": "portrait",
"themeId": "noir",
"seed": 42
},
"videoTrack": {
"sourcePath": "input-video.mp4",
"startSec": 0,
"endSec": 121.2,
"bounds": { "x": 0, "y": 0, "width": 1080, "height": 1920 }
},
"subtitles": { "enabled": false },
"cards": [
{
"id": "card-01",
"intent": "Hook with the speaker's anxious midnight question",
"startSec": 0.5,
"endSec": 13.0,
"accentIndex": 0,
"zone": "fullscreen",
"contentHints": {
"kicker": "AN HONEST QUESTION",
"title": "晚上 11 点的灵魂提问",
"detail": "客户六十秒语音:「人民币会升值,我的美金保单是不是亏惨了?」"
}
}
]
}
必須のカードフィールド:
| フィールド | 型 | 目的 |
|---|---|---|
id | string | カード HTML と GSAP セレクターで使用する安定した ID |
intent | string | 自然言語による説明。カードの生成に使用 |
startSec / endSec | number | 秒単位の時刻(endSec > startSec) |
accentIndex | 0 | 1 | 2 | 3 | 4 | このカードで使用する 5 色のテーマアクセントカラーの番号 |
zone | enum(下記参照) | キャンバス上でカードを配置する場所 |
contentHints | object | 自由形式のデータ一式。エージェントが kicker/title/detail/data/quote をここに記述 |
archetype(任意) | string | カードのパターンを記憶しておくために付けられる自由形式のラベル。省略時はデフォルトの自由形式 |
transition(任意) | enum:cut | fade | slide | wipe | カード間の遷移を宣言 |
5 つの zone 値:
| ゾーン | 解決後の境界 | 使用する場面 |
|---|---|---|
fullscreen | キャンバス全体を覆います | 印象的な演出、大きな数字、マントラ |
whiteboard-area | 40px の余白(または縦向きの高さの 45%)を取ります | データ量が多い内容/注釈付きコンテンツ |
lower-third | 下部 30% の帯状領域 | 表示中の動画に重ねる注釈 |
side-panel | 右側 42%(横向き)または下部 40%(縦向き) | 片側にデータ、もう片側に動画を表示 |
video-overlay | キャンバス全体を覆い、ほぼ透明なカードを想定します | 全画面動画に重ねる注釈 |
ステップ 9 でコンポジションを組み立てる際、上の表に従って各カードの zone
をカードホストのラッパー上のピクセル境界に変換します。
動画の境界はコンポジションレベル(videoTrack.bounds)で一度だけ設定します。
動画を「カード間で移動」しているように見せるには、コンポジションの <script> 内で
#video-wrap を対象に GSAP のトゥイーンを作成します(ステップ 9 を参照)。
**カードの役割や物語の展開に決まりはありません。**カードは動画で実際に語られている内容から生まれます。引用だけの場合も、データだけの場合もあり、数字から始まる場合も、物語から始まる場合もあります。トランスクリプトに合わせてリズムを作りましょう。
要点はいくつにするか? — 長さと密度から自動推定します。上限はありません。まず動画の長さから基本ペースを選び、次に情報密度に応じて調整します。固定されているのは最低数だけで、カードは最低 5 枚です。短い動画でもリズムを作れるようにします。
ステップ 1 — 長さに応じた基本ペース(中程度の密度における、カード 1 枚あたりの標準秒数):
| 動画の長さ | 基本ペース(カード 1 枚あたりの秒数) | 理由 |
|---|---|---|
| < 60s(短いリール) | 6–8s | 短尺動画ではテンポの速いカットが期待されます |
| 60s – 3 min | 8–12s | 一般的なソーシャル動画のペースです |
| 3 – 10 min | 12–20s | 余裕を持たせ、各カードに多くの内容を盛り込みます |
| 10 – 30 min | 20–35s | 長尺の講義/インタビューに適したリズムです |
| > 30 min | 30–60s | エピソード形式で、章に近い感覚になります |
ステップ 2 — 密度の乗数(基本ペースに掛けます):
| トランスクリプト内の兆候 | 乗数 | 効果 |
|---|---|---|
| 高密度 — 数字が多い、異なる主張が次々と出てくる、テンポが小刻み、列挙形式、1〜2 文ごとに新しいアイデアが出てくる | × 0.7 | カットが速くなり、カードが増えます |
| 中程度の密度 — データと物語が混在する流れ | × 1.0 | 基本ペースです |
| 低密度 — 長い物語が 1 つ続く、表現を変えて同じ内容を繰り返す、ゆっくり内省的に進む、1 つの主張を展開する | × 1.5 | カットが遅くなり、カードが減ります |
ステップ 3 — 計算:
secPerCard = basePace × densityMultiplier
cardCount = max(5, round(videoDurationSec / secPerCard))
例(上限はありません。動画が長ければ、自然にカードも増えます):
- 30s のリール、オチが 1 つ(低密度) → 7 × 1.5 = 1 カードあたり 10.5s → round(30/10.5)=3 → 最低数の 5 カードに調整
- 60s の内省的な独白(低密度) → 10 × 1.5 = 1 カードあたり 15s → 4 → 最低数の 5 カードに調整
- 121s のデータ豊富なトーク動画(高密度) → 10 × 0.7 = 1 カードあたり 7s → 17 カード
- 5 min のインタビュー(中程度の密度) → 16 × 1.0 = 1 カードあたり 16s → 19 カード
- 10 min の詳細解説(高密度) → 16 × 0.7 = 1 カードあたり 11s → 55 カード
- 30 min の講義(中程度の密度) → 28 × 1.0 = 1 カードあたり 28s → 64 カード
- 1 hr のポッドキャスト(低密度) → 45 × 1.5 = 1 カードあたり 67.5s → 53 カード
カードの表示時間が約 15s を超える場合は、内容を充実させたカード(データブロック、
複数段階の表示、段階的なアニメーションで展開する複数のサブポイントなど)を計画します。
静的な一行表示は 8s を超えると退屈になります。カードの多くが 30s を超える長い動画では、
タイムラインを複数のサブコンポジションに分割することを検討してください(章ごとに 1 つの .html を作成し、
data-composition-src でマウントします)。そうすることで、ファイルごとの GSAP タイムラインを
管理しやすくできます。timeline_track_too_dense HyperFrames lint 警告を参照してください。
content はプレーンな文字列(「标题:年化 5.69%\n说明:...」)でも、データを表現する任意の JSON
形式でもかまいません。各カードに適した形式はエージェントが決定します。
最後のカード — 必ず card-cta-vtake を追加します。
すべてのコンテンツカードの後に、固定のブランドアウトロカードを 1 枚 cards 配列に追加します:
{
"id": "card-cta-vtake",
"intent": "Powered By vTake brand outro",
"startSec": <last content card's endSec>,
"endSec": <last content card's endSec + 2.0>,
"accentIndex": 3,
"zone": "fullscreen",
"contentHints": {}
}
また、composition.durationSeconds を2.0秒延長し、card-cta-vtake.endSec と同じ値に設定します。ソース動画のトラックは動画の自然な終端で停止します。2秒の延長部分は、CTA カードの背後に動画が表示されない、純粋なグラフィックによるブランド演出です。CTA カードには、1秒のシネマティックな登場シーケンス(「Editorial Cinema」)を適用し、その後約0.7秒間表示してから、約0.3秒かけてフェードアウトします。
7. レンダリング戦略を決める
ユーザーにビジュアルの方向性を確認する(最初に実行すること)
カードのデザインや範囲の決定を始める前に、出力比率、レイアウト、スタイル、カード密度のプリセットをユーザーに選んでもらってください。フレームは、選択されたレイアウトとスタイルの組み合わせに基づいて自動選択されます(下記の「フレームの自動選択」表を参照)。質問を送る前に、次の2つを事前に計算してください。
-
ソース動画のアスペクト比(
metadata.jsonの幅 / 高さ)からrecommendedRatioを計算します。sourceAspect = width / heightsourceAspect ≥ 1.5(約3:2以上の横長)→16:9を推奨sourceAspect ≤ 0.7(約9:13以下の縦長)→9:16を推奨0.7 < sourceAspect < 1.5(正方形に近い)→4:5を推奨
推奨する選択肢のラベルに「 (推荐 · 匹配源视频 X:Y)」を付け、推奨理由をユーザーに示します。
-
Step 6 の
autoCount(max(5, round(videoSec / (basePace × densityMultiplier))))を計算し、「自動」の選択肢のラベルに具体的な数を表示できるようにします。
環境との互換性 — 最適な質問方法を選んでください。 すべての実行環境で同じ構造化質問ツールが使えるとは限りません。次の順序で適用してください。
AskUserQuestion(Claude Code、Anthropic Console)— 以下の構造化された4つの質問を使用します。- その他のネイティブな確認ツール(例:
ask_question、request_user_input、IDE 固有のプロンプト)— 同じ4つの質問文と選択肢リストを使って、そのツールで質問します。推奨マーカーと事前計算した値も維持してください。 - ネイティブツールがない場合(Codex CLI、プレーンテキストのみの実行環境)— 通常の会話で直接質問してください。このセクション末尾のプレーンテキスト用テンプレートを使います。1つのメッセージに、番号付きの質問を4つ含めてください(1ラウンドあたりの上限は2~5問です。この場合はその範囲内です)。
すべての質問方法に適用されるルール:
- 1ラウンドにつき2~5問まで質問できます。ここでは4問なので問題ありません。
- 不足情報がなくてもレンダリングを進められる場合でも、最終出力に大きく影響するパラメーター(比率、レイアウト、スタイル、cardCount)は、一度ユーザーに確認してください。
- ユーザーがデフォルトを事前承認している(「just use defaults」、「无需询问」、「auto-pick everything」など)、または質問しないよう指示している場合は、質問を完全に省略し、
recommendedRatio、layout="stack"(比率を問わず最も安全なデフォルト)、トランスクリプトのトーンに基づいて最も中立的なグループ(editorial/数据)から選んだstyle、autoCountを使用します。選択した内容を1文でユーザーに伝えてから続行してください。
チャンネルA — ネイティブの AskUserQuestion:
// Precompute before the call:
// recommendedRatio = "16:9" | "9:16" | "4:5"
// autoCount = integer (from Step 6)
AskUserQuestion({
questions: [
{
question: "输出视频比例 (画幅):",
header: "画幅",
multiSelect: false,
// Reorder so the recommended option appears FIRST (per AskUserQuestion convention).
// Append " (推荐 · 匹配源视频 W×H)" to the recommended option's label.
options: [
{ label: "16:9 (1920×1080) 横屏", description: "TV / YouTube / 电脑播放。源视频已经是横屏时最自然,画幅最宽。" },
{ label: "9:16 (1080×1920) 竖屏", description: "抖音 / 小红书 / TikTok / Reels。源视频竖屏时最自然;移动端原生体验。" },
{ label: "4:5 (1080×1350) 方屏偏竖", description: "Instagram feed / 微信朋友圈。近方形源视频或想兼顾两种平台时最稳。" }
]
},
{
question: "选择整体布局:视频和卡片在画面里如何共存?",
header: "布局",
multiSelect: false,
options: [
{ label: "左右分屏 (split)", description: "video 和 card 各占画面一半。访谈 / 数据并列时最稳,画面分隔清晰。" },
{ label: "上下分屏 (stack)", description: "video 在上方 (~52%),card 在下方。说话人头像 + 总结句的经典组合,竖屏也好用。" },
{ label: "画中画 (pip)", description: "card 满屏,video 缩成圆角小窗在右上角。内容为主、speaker 为辅时用。" },
{ label: "全屏浮层 (overlay)", description: "video 全屏播放,card 作为玻璃浮层落在画面上。情绪 / 电影感强烈。" }
]
},
{
question: "选择卡片视觉风格 (style):",
header: "风格大类",
multiSelect: false,
// NOTE: these 3 groups intentionally match the frame auto-pick matrix
// rows below, so picking a group resolves both `style` group AND the
// frame matrix column in one step. Memberships are mutually exclusive.
options: [
{ label: "温暖纸感 (warm-paper)", description: "academic 学术笔记 · editorial 大字编辑 · whiteboard 手写白板 · xhs 小红书。适合访谈反思、产品发布、生活方式、情绪故事。" },
{ label: "冷峻临床 (clinical)", description: "audit 审计杂志 · swiss 瑞士网格 · terminal CLI · minimal 现代极简。适合财报分析、调查报告、技术教程、严肃陈述。" },
{ label: "实验前卫 (experimental)", description: "geom 撞色几何 · spotlight 暗色聚光。适合短视频高光、产品发布、强烈情绪、电影质感。" }
]
},
{
question: "卡片数量 (takeaway 节奏):要切多少张?",
header: "卡片数量",
multiSelect: false,
options: [
{ label: "自动 (推荐) · 约 N 张", description: "按视频时长和信息密度自动推断 (见 Step 6 规则)。本次推断约 N 张。带 N 进 label —— N 是你刚算出的 autoCount。" },
{ label: "少量 · 约 round(N × 0.6) 张", description: "切得稀疏一点,每张卡停留更久,适合 reflective / 慢节奏。" },
{ label: "更多 · 约 round(N × 1.5) 张", description: "切得更紧凑,节奏更快,适合 staccato / 数据密集 / 短视频高光。" }
]
}
]
})
「Other」について — AskUserQuestion は「カード数」の質問に「Other」の選択肢を自動で追加します。ユーザーは数字(例:「8」、「20」)を直接入力して、目標の cardCount を指定できます。入力を整数として解析してください。
解析に成功した場合 → その値をそのまま使用します(最低5枚を下限とします)。解析に失敗した場合 → 「自動」に戻します。
チャンネルB — プレーンテキストでの代替手段(Codex CLI、ネイティブな質問ツールがない実行環境)。以下を通常のメッセージとして1つ投稿し、返信を待ちます。箇条書き形式の1/2/3/4なら、返信を解析しやすくなります。
我需要先和你确认四个视觉决策再开始切卡片:
1) 输出比例 (画幅):
A. 16:9 横屏 (1920×1080) — TV / YouTube / 电脑播放
B. 9:16 竖屏 (1080×1920) — 抖音 / 小红书 / TikTok
C. 4:5 方屏偏竖 (1080×1350) — Instagram feed / 兼顾两端
▸ 我的推荐: <recommendedRatio> (匹配源视频 W×H = <sourceW>×<sourceH>)
2) 整体布局 (video & card 怎么共存):
A. split 左右分屏 (50/50)
B. stack 上下分屏 (video 顶, card 底)
C. pip 画中画 (card 满屏, video 圆角小窗)
D. overlay 全屏浮层 (video 全屏, card 玻璃浮层)
3) 卡片风格大类 (与 frame 自动矩阵同构,3 选 1):
A. 温暖纸感 warm-paper (academic / editorial / whiteboard / xhs)
B. 冷峻临床 clinical (audit / swiss / terminal / minimal)
C. 实验前卫 experimental (geom / spotlight)
4) 卡片数量 (takeaway 节奏):
A. 自动 (推荐) — 约 <autoCount> 张
B. 少量 — 约 round(<autoCount> × 0.6) 张
C. 更多 — 约 round(<autoCount> × 1.5) 张
D. 直接给我一个数字 (如 "8"、"20")
回复格式: "1A 2C 3B 4A" 或自然语言均可。
若你想全部用推荐默认值,回复 "默认" / "auto" / "都用推荐" 即可。
プレーンテキストの返信の解析:
"1A 2C 3B 4A"、"A C B A"、"16:9 / pip / 数据 / 自动"、文章による回答、または默认など、形式は柔軟に受け付けます。- 回答に曖昧な点がある場合 → 曖昧な項目だけを再質問します(引き続き2~5問の上限内に収めます)。
- ユーザーが「默认 / auto / 都用推荐」と言った場合 → 再質問せずに省略します。
ユーザーが回答した後(いずれの方法でも):
-
比率の回答に基づいて出力キャンバスを決定します。以下の値を
storyboard.composition.width / heightに正確に書き込みます。ユーザーの選択 composition.width × height storyboard.layout フィールド 16:91920 × 1080 "landscape"9:161080 × 1920 "portrait"4:51080 × 1350 "portrait"(スキーマでは4:5をポートレートとして扱います — 高さが幅を上回ります)references/layouts/*.html内で 4:5 の範囲を指定する場合 — これらのファイルでは、横長(1920×1080)と縦長(1080×1920)のみを扱っています。4:5(1080×1350)の 範囲は、縦長から比例換算して求めます。水平方向の値はそのままにし、垂直方向の値を 1350/1920 ≈ 0.703倍します。例:overlayの縦長カードが{ x: 24, y: 1280, w: 1032, h: 564 }の場合、4:5 のカードは{ x: 24, y: round(1280 × 0.703), w: 1032, h: round(564 × 0.703) }= { x: 24, y: 900, w: 1032, h: 397 }です。 -
スタイルグループを具体的なスタイルに割り当てます。 文字起こしのトーンを 確認して、最も合うものを選びます。ただし、ユーザーが選んだグループの範囲内に 収めてください。グループ内の具体的なスタイルを 2 つに絞りきれない場合は、 その 2〜4 個の具体的なスタイル候補を提示して、2 回目の
AskUserQuestionを送ります。 -
密度の回答から最終的な cardCount を決定します。
ユーザーの選択 最終的な cardCount 自动 (推荐) すでに計算した autoCount少量 max(5, round(autoCount × 0.6))更多 round(autoCount × 1.5)(上限による制限なし)Other = ""(整数) max(5, parseInt(n))Other = その他 autoCountにフォールバック -
次の表から動画フレームを自動選択します(フレームについてユーザーには尋ねません。レイアウトとスタイルから決まります)。
レイアウト warm-paper スタイル(academic / whiteboard / editorial / xhs) clinical スタイル(audit / swiss / terminal / minimal) experimental スタイル(geom / spotlight) splitpolaroidhairlinecleanstackpolaroidhairlinecleanpipclean(pip のピルにはすでに装飾があるため)cleancleanoverlayclean(フルブリードでは装飾フレームを使用できないため)cleanclean -
選択した内容を、アスペクト比(+キャンバスサイズ)、レイアウト、具体的なスタイル、 フレーム、最終的な cardCount を含めて 1 文でユーザーに伝えます。その後、ステップ 7 の 残りの作業(カードごとのレイアウト、モーションパターン)に進みます。
-
5 つの値(ratio / layout / style / frame / cardCount)を作業メモリに記録します (スキーマフィールドは不要です)。ステップ 8 で各カードの HTML を記述する際と、 トークンおよび構造を確認するために対応する
references/<dim>/<key>.htmlを読む際に 参照します。
ユーザーが「Other」で 10 種類のスタイルライブラリにないスタイル名を自由入力した場合は、 それを新しいカードのビジュアルを独自にデザインするためのヒントとして扱います。ただし、 選択されたレイアウトの範囲を基準にしてください。
レンダリング戦略の入力
Step 7.0 で ratio / layout / style / cardCount / frame を固定したら、 カードごとに決める項目は次のとおりです。
- GSAP のターゲット内でのソース動画のフィット方法:動画要素には
object-fit: coverが設定され、#video-wrapのトゥイーン範囲に クリップされます。 トリミングしたくない場合(たとえば、縦向きのソース動画を横向きの キャンバスに配置する際に上下を切りたくない場合)は、ソースと同じ アスペクト比の矩形をトゥイーンのターゲットにし、周囲のキャンバスを 見せる(またはカード/背景で埋める)ようにします。 - カードごとの
card.zone:選択した構成レイアウトから決めます (split → side-panel、stack → lower-third、pip → fullscreen、 overlay → video-overlay)。または、個別のバリエーションとして 別のゾーンを選びます(ヒーロー/引用には fullscreen、高密度の データには whiteboard-area)。 - カードごとの
accentIndex:各カードでは、テーマのアクセントカラー 5色のうち1色を使います。リズムを生むためカードごとに変え、同じ ナラティブ上のまとまりに属するカードには同じインデックスを再利用します。 - モーションの語彙:
data-animの種類から、繰り返し使うパターンを 2~3種類選び(後述の表を参照)、構成に一貫性を持たせるため、それらを 継続して使います。
次の themeId パレットから選び、構成の <style> ブロックで
--accent-N/--bg/--text CSS 変数として使用します。
| themeId | アクセントパレット(5色) | ボードの背景 | テキスト |
|---|---|---|---|
| classic | #1971c2 #e03131 #2f9e44 #e8590c #9c36b5 | #FFF9E3(紙) | #1e1e1e |
| noir | #4cc9f0 #f72585 #4ade80 #fb923c #a78bfa | #1a1a1a | #f1f1f1 |
| mint | #0077b6 #d62828 #2d6a4f #e76f51 #7209b7 | #e8faf0 | #1b4332 |
| craft | #bf5700 #d62728 #6c757d #e9b54a #3d5a80 | #f6efe1 | #2d2d2d |
| slate | #0ea5e9 #ef4444 #22c55e #f97316 #a855f7 | #1e293b | #f1f5f9 |
| mono | #000 #555 #888 #aaa #ccc | #fff | #000 |
使用可能なフォント(<SKILL_DIR>/assets/fonts/ 内の woff2 ファイル。Step 9 で
作業ディレクトリに配置されます):Caveat(手書き風)、
LXGW WenKai TC(中国語の手書き風)、Inter(モダンなサンセリフ)、
Virgil(幾何学的な手書き風)。@font-face または font-family で
直接参照します。
ビジュアルパターンの参考として、<SKILL_DIR>/references/styles/ には
10種類の自己完結型リファレンスカード(学術/エディトリアル/ミニマル/
スポットライト/ジオメトリック/ホワイトボード/監査/ターミナル/
スイス/xhs)が用意されており、出発点としてコピーできます。ただし、
これらに合わせる必要はありません。各カードは独自にデザインしてください。
ビジュアルデザインライブラリ(<SKILL_DIR>/references/)
構成レベルの themeId に加えて、このスキルには <SKILL_DIR>/references/ に、自由に組み合わせられる3つの独立したビジュアル要素を網羅した、より充実したリファレンスライブラリが含まれています。
Style × Layout × VideoFrame
(10) (4) (3)
| 要素 | キー | 決定する内容 |
|---|---|---|
| style | academic editorial minimal spotlight geom whiteboard audit terminal swiss xhs | カードのビジュアル表現 — フォント、色、装飾、カード内のレイアウト |
| layout | split stack pip overlay | キャンバス上でソース動画とカードをどのように配置するか |
| frame | clean hairline polaroid | 動画要素の周囲の装飾的な枠 |
全体の一覧と大まかな選択ガイド(インタビュー/製品発表/データ分析/ソーシャル向け編集/技術チュートリアル/感情を伝えるストーリーなど)については、<SKILL_DIR>/references/DESIGN_INDEX.md
を参照してください。特定のスタイル/レイアウト/フレームを使うと決めたら、対応するファイルを参照してください。
references/styles/<key>.html— そのスタイルのCSSトークン(色、フォント、余白、装飾)とプレースホルダーの要点を含む、単独で使用できるカードフラグメントです。.card[data-card-id="ref-<key>"]のスタイルブロックをコピーし、data-card-idをカードのIDに変更して、プレースホルダーの内容を実際の要点に差し替えれば完了です。references/layouts/<key>.html— 横向きと縦向きの両方に対応した正確なvideoBoundsとcardBounds、およびstoryboard.jsonのカードごとのlayoutフィールドに貼り付けられるJSONスニペットです。references/frames/<key>.html—#video-wrapの兄弟要素として追加する装飾的なHTMLと、構成CSSへの配置手順です。
カードごとに style × layout × frame を選択してください。切り替えが自然に見える限り、カードごとに3つすべてを変更できます。よくある流れは、editorial × overlay × clean で始め、データカードでは audit × split × hairline に切り替え、最後は whiteboard × pip × polaroid で締めくくる方法です。
10種類のスタイルはスキル側のデザイントークンであり、構成レベルのテーマではありません。storyboard.composition で宣言する必要はなく、各カードのHTML内に記述します。themeId フィールドでは引き続き構成レベルのパレットを選択できます。このパレットはページ本文の背景と動画の枠線を制御します(上の表を参照)。
レイアウト構成(カード+動画)
各カードについて、ソース動画とキャンバスをどう共有するかを、連動する2つの判断で定義します。
card.zone(storyboard.jsonで宣言) — スキーマで定められた5つの値のいずれかです。ステップ9でカードホストのラッパーにインラインstyleを書く際、ステップ6の表に従ってピクセル単位の境界に変換します。- このカードの時間枠における
#video-wrapの境界(構成のGSAPタイムライン内で命令的に宣言) — エージェントは、レイアウト遷移ごとに#video-wrapを目標の矩形へトゥイーンします。
スキーマには、カードごとの動画境界は保存されません。videoTrack.boundsは構成レベルで一度だけ設定され(デフォルトはキャンバス全体)、カード間で動画が「移動する」のは、index.htmlに記述するGSAPアニメーションのみです。card.layoutフィールドはありません。このドキュメントの以前のバージョンでは誤って記載していましたが、実際のスキーマにあるのはcard.zoneだけです。
4つの構成レイアウト(references/layouts/にあります) — それぞれ、zoneと#video-wrapのトゥイーン目標を組み合わせたレシピです。
| 構成レイアウト | 推奨するcard.zone | #video-wrapのGSAP目標(横向き1920×1080) | #video-wrapのGSAP目標(縦向き1080×1920) | 使用する場面 |
|---|---|---|---|---|
split | side-panel | { left: 960, top: 0, width: 960, height: 1080 } | { left: 0, top: 960, width: 1080, height: 960 }(下半分) | 登壇者とデータを横並びにする/比率50:50 |
stack | lower-third | { left: 14, top: 14, width: 1892, height: 548 }(上部52%) | { left: 0, top: 0, width: 1080, height: 844 }(上部44%) | 登壇者を上、要約カードを下に配置する |
pip | fullscreen | { left: 1480, top: 760, width: 400, height: 300 } + .framedクラスを追加 | { left: 690, top: 28, width: 360, height: 203 } + .framedを追加 | コンテンツ主体のカード+隅のピクチャーインピクチャー |
overlay | video-overlay | { left: 0, top: 0, width: 1920, height: 1080 }(全画面表示) | { left: 0, top: 0, width: 1080, height: 1920 } | 映画的/ドラマチックな演出/動画全体に重ねるガラス風カード |
4:5(1080×1350)の場合は、縦向きのy/hの値に1350/1920 ≈ 0.703を掛けて拡大縮小します(ステップ7.0のChannel A/Channel BのrecommendedRatio解像度表を参照)。
単発のバリエーションで使うその他のzone値(引き続きcard.zoneを使います。「layout」フィールドをでっち上げないでください)。
zone | 変換後の境界 | 一般的な用途 |
|---|---|---|
fullscreen | キャンバス全体を覆う | ヒーローカード。動画は非表示またはピクチャーインピクチャーにトゥイーンします |
whiteboard-area | 40pxの余白(横向き)または下部45%(縦向き)を除いた範囲 | 情報量の多いデータカード、余白を確保する場合 |
lower-third | 下部30%の帯 | トーク映像への注釈 |
side-panel | 右側42%(横向き)または下部40%(縦向き) | サイドバー/「split」レシピ |
video-overlay | キャンバス全体。カードのルートは透明にする | 画面全体に表示する動画に重ねるガラス風オーバーレイ |
カードごとにレシピを組み合わせられます。その場面に合うcard.zoneを選び、カード間で#video-wrapをトゥイーンするGSAPコードを記述してください。
ストーリーボードのレンダリング契約
storyboard.json はエージェント内部で使う計画用アーティファクトです。これを解析する vtake CLI コマンドはありません。各カードの HTML を書く前に、タイミングと内容の判断を明確にするために使います。Step 9 で組み立てる構成にも同じアウトラインを使えるよう、以下の v3 形式に沿ってください。
必須の構造(完全な例は Step 6 を参照):
schemaVersion: 3composition: { fps, width, height, durationSeconds, layout, themeId, seed }—durationSeconds/fps/themeId/layoutは最上位ではなく、composition内に置きますvideoTrack: { sourcePath, startSec, endSec, bounds? }— 動画の境界は既定でキャンバス全体ですsubtitles: { enabled, ... }cards[]— 各カードには必須フィールドが 6 つあります:id、intent、startSec、endSec、accentIndex、zone、contentHints
ルール:
- カードの時間は
composition.durationSecondsの範囲内に収め、意図的でない限り重ねないでください。重ねる場合はdata-track-indexを使って z 順を制御します。 - ビジュアルの詳細はカード HTML フラグメント(Step 8)に記述し、
contentHintsには記述しないでください。contentHintsはカードのデザイン用に独自に作成する構造化プロンプトです。レンダリング後の見た目を決めるのは HTML です。 - ストーリーボードの形式は安定させてください。解析されることはありませんが、Step 8/9 の作成時に読み返すため、一貫性を保つことでカード ID とタイミングの同期が取れます。
- 「overlay × geom × clean を選んだ」といったエージェント側の判断は、
storyboard.jsonに記述しないでください。作業用メモリに保持し、カード HTML と GSAP トゥイーンの作成時に使ってください。
動画と同じキャンバスに表示するカードは、背景を透明にします。
GSAP トゥイーンによってカードの背後または隣に動画が表示される場合(overlay レシピ、pip レシピ、または card.zone = 'lower-third' | 'video-overlay' のタイミング)、カードの .root に不透明な背景を全面に描画してはいけません。描画すると動画が隠れてしまいます。次の 2 つのパターンがあります。
/* Pattern A: transparent root, page body provides the cream backdrop */
html, body { background: var(--bg); }
.card[data-card-id="card-X"] .root { background: transparent; }
/* Pattern B: explicit per-card background ONLY for fullscreen cards */
.card[data-card-id="card-hero"] .root { background: var(--bg); }
.card[data-card-id="card-overlay"] .root { background: transparent; }
side-panel ゾーンのカード(split レシピ)の場合、card-host はキャンバスの半分のみを占めるため、不透明なカード背景を使っても問題ありません。背景が覆うのはその半分だけです。
8. 各カードの HTML を作成する
各カードについて $WORK_DIR/public/cards/{card-id}.html を作成します。各ファイルには、次の契約に従うルートが 1 つだけの HTML フラグメントを記述します。
カード HTML 契約
<div class="card" data-card-id="{cardId}">
<style>
/* MUST: every rule starts with .card[data-card-id="{cardId}"] */
.card[data-card-id="card-01"] .root {
width: 100%; height: 100%;
display: flex; ...;
font-family: 'Caveat', 'LXGW WenKai TC', serif;
color: var(--text);
background: var(--bg);
}
.card[data-card-id="card-01"] .title { font-size: 84px; ... }
</style>
<div class="root">
<h1 id="card-01-title"
data-anim="kinetic-chars"
data-anim-at="0.3"
data-anim-duration="0.5"
data-anim-stagger="0.04"
data-anim-pattern="pop">
<span class="char">字</span>
<span class="char">幕</span>
</h1>
<div id="card-01-line"
data-anim="grow-x"
data-anim-at="0.65"
data-anim-duration="0.5"
data-anim-target-w="420"
style="width:0;height:8px;background:var(--accent-0);border-radius:4px;"></div>
</div>
</div>
厳守事項(hyperframes の lint で違反があると拒否されます):
- ルートの
<div class="card" data-card-id="{cardId}">は 1 つだけにします - インラインの
<style>ルールには、上記のスコープセレクターを必ず付けます <script>タグは禁止ですsrc=/href=に外部 URL を指定しないでください(CDN、リモートフォントは禁止)- インラインイベントハンドラー(
onclick=など)は禁止です - すべてのアセットは、同じ
public/ディレクトリ内への相対パスで指定します - テーマを切り替えても使えるよう、色は
var(--accent-N)などで指定します
アニメーションはコードに記述せず、宣言します。 data-anim-* 属性のみを使用し、アニメーション用に <script> を記述しないでください。すべての data-anim-* 宣言をコンパイルし、Step 9 で単一のマスター GSAP タイムラインにまとめます。
カードのサイズ設定 — 縦向きのモバイルファースト
references/styles/*.html の 10 個のファイルは、1920×1080 の横向き
プレビュー用にサイズ設定されています。storyboard.layout = "portrait"(1080×1920。ソーシャル/モバイルで主流の
ケース)の場合は、すべての視覚要素のサイズを大きくしてください。スマートフォンは画面を近くで見るため、同じピクセル数でも
横向きのテレビ風キャンバスより小さく見えます。
| トークン | 横向きの基準値 | 縦向きの目標値 | 倍率 |
|---|---|---|---|
| タイトル(ヒーロー h1/h2) | 64–96px | 88–132px | ×1.35 |
| 詳細/本文 | 24–30px | 30–40px | ×1.30 |
| キッカー/チップのラベル | 14–16px | 18–22px | ×1.30 |
| タイムコード/メタデータ | 12–14px | 16–18px | ×1.30 |
| データブロックの主数値 | 48–60px | 64–88px | ×1.40 |
| 行の高さの倍率 | 1.05–1.5 | 同じ | (拡大しない) |
目安: portraitPx = round(landscapePx × 1.3) とし、視覚的なリズムを整えるため、
近い 4px 刻みの値に切り下げます。ヒーロー見出しは最大 ×1.4 まで拡大できます。小さな
メタデータのテキストは、窮屈にならないよう ×1.2 にとどめます。
縦向きでは、パディングを少し小さくします。カードの幅が狭いため、横向きで大きな パディング(40–64px)を使うと幅を取りすぎます。縦向きでは、左右のパディングを 24–36px にしてください。
両方のレイアウトで使える単一のカードを作る場合は、サイズを固定値で指定するより、カードのルート要素に対する
@container クエリを優先してください。
.card[data-card-id="X"] .root { container-type: inline-size; }
.card[data-card-id="X"] .title { font-size: clamp(64px, 8.5cqi, 132px); }
.card[data-card-id="X"] .detail { font-size: clamp(24px, 3.2cqi, 40px); }
ただし、ほとんどのカードではレイアウトを 1 つに決めて問題ありません。ストーリーボードの layout フィールドに
一致するサイズ表の列を選んでください。
使用できる data-anim の種類
| 種類 | 用途 | 主なパラメーター |
|---|---|---|
fade-in | 表示 | at、duration、ease? |
fade-out | 非表示 | at、duration、ease? |
slide-in | スライド表示 | at、duration、from=left|right|top|bottom、distance |
kinetic-chars | 文字ごとのポップ表示 | at、duration、stagger、pattern=pop|fade — 要素には <span class="char"> の子要素が必要です |
typewriter | 文字ごとのフェード表示 | kinetic-chars と同じですが、デフォルトのスタッガーはより遅くなります |
count-up | 数値のアニメーション | at、duration、from、to、format=.0f|.1f|.2f|,d |
draw-path | SVG パスの描画表示 | at、duration — 要素は <path> にしてください |
grow-y | バーの高さ | at、duration、target-h(px)— 要素の開始時の height:0 |
grow-x | バーの幅 | at、duration、target-w(px)— 要素の開始時の width:0 |
scale-pop | ポップ表示 | at、duration |
blur-in | ぼかしからフォーカス | at、duration |
mask-reveal | クリップ表示 | at、duration、direction=left|right|top|bottom |
morph-to | 任意の CSS をトゥイーン | at、duration、props='{...JSON...}' |
data-anim-at は、カードの startSec からの相対秒数です。ステップ 9 で各宣言を GSAP タイムラインに
コンパイルする際に、カードの startSec を加算して絶対時間を求め、1/fps に量子化してください。
card-cta-vtake:固定ブランドアウトロカード
この固定テンプレートを$WORK_DIR/public/cards/card-cta-vtake.htmlとして保存してください
— 変更しないでください。デザイン言語は**「エディトリアル・シネマ」です。ビューファインダーのコーナーマーク、上下のフィルムクレジットメタストリップ、vTマークを囲む2つの回転リング、ダイヤモンドを両端に配した区切り線、イタリック体のタグライン、アンバー色のきらめくパーティクルを取り入れます。登場シーケンスは1.00s**で完了し、残り約1.0sは環境アニメーション(リングの回転、パーティクルの脈動)が続きます。
<div class="card" data-card-id="card-cta-vtake">
<style>
.card[data-card-id="card-cta-vtake"] .root {
width: 100%; height: 100%;
background:
radial-gradient(ellipse 60% 80% at 50% 50%, rgba(232,144,52,0.12) 0%, transparent 55%),
radial-gradient(ellipse 120% 40% at 50% 100%, rgba(201,169,97,0.10) 0%, transparent 60%),
#0D0B08;
position: relative; overflow: hidden;
container-type: inline-size;
font-family: 'Inter', ui-sans-serif, sans-serif;
color: #F5EFE1;
}
/* fractal noise overlay — kills the "plastic" look */
.card[data-card-id="card-cta-vtake"] .root::after {
content: '';
position: absolute; inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.06 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
mix-blend-mode: overlay;
opacity: 0.5;
pointer-events: none;
}
/* viewfinder corner marks (1.5px hairline) */
.card[data-card-id="card-cta-vtake"] .corner {
position: absolute;
width: 3.5cqi; height: 3.5cqi;
border-color: rgba(245,239,225,0.18);
pointer-events: none;
}
.card[data-card-id="card-cta-vtake"] .corner.tl { top: 3.5cqi; left: 3.5cqi; border-top: 1.5px solid; border-left: 1.5px solid; }
.card[data-card-id="card-cta-vtake"] .corner.tr { top: 3.5cqi; right: 3.5cqi; border-top: 1.5px solid; border-right: 1.5px solid; }
.card[data-card-id="card-cta-vtake"] .corner.bl { bottom: 3.5cqi; left: 3.5cqi; border-bottom: 1.5px solid; border-left: 1.5px solid; }
.card[data-card-id="card-cta-vtake"] .corner.br { bottom: 3.5cqi; right: 3.5cqi; border-bottom: 1.5px solid; border-right: 1.5px solid; }
/* top film-credit strip */
.card[data-card-id="card-cta-vtake"] .top-meta {
position: absolute; top: 4.8cqi; left: 0; right: 0;
display: flex; justify-content: center;
gap: clamp(14px, 2cqi, 36px);
font-size: clamp(9px, 0.85cqi, 16px);
font-weight: 500;
letter-spacing: 0.36em;
text-transform: uppercase;
color: rgba(245,239,225,0.18);
}
.card[data-card-id="card-cta-vtake"] .top-meta .sep { color: #E89034; opacity: 0.7; }
/* bottom film-credit strip */
.card[data-card-id="card-cta-vtake"] .bot-meta {
position: absolute; bottom: 5cqi; left: 0; right: 0;
display: flex; justify-content: space-between;
padding: 0 7cqi;
font-size: clamp(9px, 0.8cqi, 15px);
font-weight: 500;
letter-spacing: 0.32em;
text-transform: uppercase;
color: rgba(245,239,225,0.18);
}
.card[data-card-id="card-cta-vtake"] .bot-meta .right { color: #E89034; opacity: 0.85; }
/* center stage */
.card[data-card-id="card-cta-vtake"] .stage {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
display: flex; flex-direction: column; align-items: center;
z-index: 2;
}
/* "Powered by" row with short flanking lines */
.card[data-card-id="card-cta-vtake"] .powered-row {
display: flex; align-items: center;
gap: clamp(10px, 1.4cqi, 24px);
margin-bottom: clamp(24px, 3cqi, 56px);
}
.card[data-card-id="card-cta-vtake"] .powered-row .line {
height: 1px;
background: rgba(245,239,225,0.18);
width: 0;
}
.card[data-card-id="card-cta-vtake"] .powered-row .label {
font-size: clamp(11px, 1.05cqi, 20px);
font-weight: 600;
letter-spacing: 0.46em;
text-transform: uppercase;
color: rgba(245,239,225,0.40);
}
/* vT mark wrapper + dual concentric rings */
.card[data-card-id="card-cta-vtake"] .mark-wrap {
position: relative;
width: clamp(160px, 15cqi, 280px);
height: clamp(160px, 15cqi, 280px);
display: flex; align-items: center; justify-content: center;
}
.card[data-card-id="card-cta-vtake"] .mark-ring {
position: absolute; inset: 0;
border-radius: 50%;
border: 1.5px dashed rgba(232,144,52,0.22);
animation: ctaRingSpin 32s linear infinite;
}
.card[data-card-id="card-cta-vtake"] .mark-ring.inner {
inset: 8%;
border: 1px solid rgba(245,239,225,0.10);
animation: ctaRingSpin 32s linear infinite reverse;
}
.card[data-card-id="card-cta-vtake"] .vtake-mark {
width: 60%; height: auto;
color: #F5EFE1;
position: relative; z-index: 1;
overflow: visible;
}
/* main brand name */
.card[data-card-id="card-cta-vtake"] .vtake-name {
font-size: clamp(48px, 6cqi, 112px);
font-weight: 900;
letter-spacing: -0.05em;
margin-top: clamp(20px, 2.5cqi, 48px);
line-height: 1;
overflow: hidden;
clip-path: inset(0 100% 0 0);
}
.card[data-card-id="card-cta-vtake"] .name-accent { color: #E89034; }
/* decorative divider: ── ◆ ── */
.card[data-card-id="card-cta-vtake"] .divider {
display: flex; align-items: center;
gap: clamp(6px, 0.7cqi, 14px);
margin-top: clamp(18px, 2.2cqi, 40px);
}
.card[data-card-id="card-cta-vtake"] .divider .seg {
width: 0; height: 1px;
background: rgba(232,144,52,0.5);
}
.card[data-card-id="card-cta-vtake"] .divider .diamond {
width: clamp(4px, 0.45cqi, 8px);
height: clamp(4px, 0.45cqi, 8px);
background: #E89034;
transform: rotate(45deg) scale(0);
}
/* tagline below divider */
.card[data-card-id="card-cta-vtake"] .tagline {
margin-top: clamp(14px, 1.8cqi, 32px);
font-size: clamp(10px, 0.9cqi, 17px);
font-weight: 500;
letter-spacing: 0.56em;
text-transform: uppercase;
color: rgba(245,239,225,0.40);
padding-left: 0.5em;
}
/* amber sparkle particles */
.card[data-card-id="card-cta-vtake"] .spark {
position: absolute;
width: clamp(2px, 0.2cqi, 3px);
height: clamp(2px, 0.2cqi, 3px);
background: #E89034;
border-radius: 50%;
box-shadow: 0 0 14px #E89034;
opacity: 0;
}
.card[data-card-id="card-cta-vtake"] .spark.s1 { top: 32%; left: 22%; animation: ctaSparkle 3.5s ease-in-out 1.0s infinite; }
.card[data-card-id="card-cta-vtake"] .spark.s2 { top: 64%; left: 76%; animation: ctaSparkle 4.2s ease-in-out 1.4s infinite; }
.card[data-card-id="card-cta-vtake"] .spark.s3 { top: 26%; right: 22%; animation: ctaSparkle 3.8s ease-in-out 1.6s infinite; }
.card[data-card-id="card-cta-vtake"] .spark.s4 { bottom: 32%; left: 38%; animation: ctaSparkle 4.6s ease-in-out 0.8s infinite; }
.card[data-card-id="card-cta-vtake"] .spark.s5 { top: 48%; right: 14%; animation: ctaSparkle 3.2s ease-in-out 2.0s infinite; }
@keyframes ctaRingSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes ctaSparkle {
0%, 100% { opacity: 0; transform: scale(0.5); }
50% { opacity: 0.85; transform: scale(1.3); }
}
</style>
<div class="root">
<span class="corner tl" id="cta-corner-tl"
data-anim="fade-in" data-anim-at="0.20" data-anim-duration="0.50"></span>
<span class="corner tr" id="cta-corner-tr"
data-anim="fade-in" data-anim-at="0.20" data-anim-duration="0.50"></span>
<span class="corner bl" id="cta-corner-bl"
data-anim="fade-in" data-anim-at="0.20" data-anim-duration="0.50"></span>
<span class="corner br" id="cta-corner-br"
data-anim="fade-in" data-anim-at="0.20" data-anim-duration="0.50"></span>
<div class="top-meta" id="cta-top-meta"
data-anim="fade-in" data-anim-at="0.25" data-anim-duration="0.50">
<span>VTAKE™</span>
<span class="sep">♦</span>
<span>Cinematic Postproduction</span>
<span class="sep">♦</span>
<span>Est. 2026</span>
</div>
<div class="bot-meta" id="cta-bot-meta"
data-anim="fade-in" data-anim-at="0.30" data-anim-duration="0.50">
<span>© 2026 vTake Studio</span>
<span class="right">vtake.app</span>
</div>
<span class="spark s1"></span>
<span class="spark s2"></span>
<span class="spark s3"></span>
<span class="spark s4"></span>
<span class="spark s5"></span>
<div class="stage">
<div class="powered-row" id="cta-powered-row">
<span class="line" id="cta-line-l"
data-anim="grow-x" data-anim-at="0.18"
data-anim-duration="0.40" data-anim-target-w="56"></span>
<span class="label" id="cta-powered-label"
data-anim="fade-in" data-anim-at="0.15"
data-anim-duration="0.45">Powered by</span>
<span class="line" id="cta-line-r"
data-anim="grow-x" data-anim-at="0.18"
data-anim-duration="0.40" data-anim-target-w="56"></span>
</div>
<div class="mark-wrap">
<span class="mark-ring" id="cta-ring-outer"
data-anim="fade-in" data-anim-at="0.55" data-anim-duration="0.50"></span>
<span class="mark-ring inner" id="cta-ring-inner"
data-anim="fade-in" data-anim-at="0.60" data-anim-duration="0.50"></span>
<svg class="vtake-mark" viewBox="0 0 200 130" fill="none"
aria-label="vTake" role="img">
<g transform="skewX(-14) translate(16 0)"
stroke="currentColor" stroke-width="14"
stroke-linecap="round" stroke-linejoin="round">
<path id="cta-path-v" d="M24 32 L56 100 L88 32"
data-anim="draw-path" data-anim-at="0.05" data-anim-duration="0.40"/>
<path id="cta-path-tbar" d="M100 32 L164 32"
data-anim="draw-path" data-anim-at="0.30" data-anim-duration="0.25"/>
<path id="cta-path-tstem" d="M132 32 L132 100"
data-anim="draw-path" data-anim-at="0.50" data-anim-duration="0.25"/>
</g>
<circle id="cta-dot" cx="48" cy="118" r="7.2" fill="#E89034"
data-anim="scale-pop" data-anim-at="0.65" data-anim-duration="0.25"/>
</svg>
</div>
<div id="cta-vtake-name" class="vtake-name"
data-anim="mask-reveal" data-anim-at="0.40"
data-anim-duration="0.45" data-anim-direction="bottom">
<span class="name-accent">v</span>Take
</div>
<div class="divider">
<span class="seg" id="cta-seg-l"
data-anim="grow-x" data-anim-at="0.75"
data-anim-duration="0.30" data-anim-target-w="56"></span>
<span class="diamond" id="cta-diamond"
data-anim="scale-pop" data-anim-at="0.85" data-anim-duration="0.30"></span>
<span class="seg" id="cta-seg-r"
data-anim="grow-x" data-anim-at="0.75"
data-anim-duration="0.30" data-anim-target-w="56"></span>
</div>
<div class="tagline" id="cta-tagline"
data-anim="fade-in" data-anim-at="0.90" data-anim-duration="0.45">
Visual Take · Reimagined
</div>
</div>
</div>
</div>
アニメーションのタイムライン(登場は1.00s、すべてのdata-anim-at値は
card-cta-vtake.startSecを基準とします):
| At | 要素 | アニメーション | Dur |
|---|---|---|---|
| 0.05 | V字型のストローク | draw-path | 0.40 |
| 0.15 | 「Powered by」ラベル | fade-in | 0.45 |
| 0.18 | 両側のライン | grow-x(→56px) | 0.40 |
| 0.20 | ビューファインダーのコーナー ×4 | fade-in | 0.50 |
| 0.25 | 上部のフィルムクレジットストリップ | fade-in | 0.50 |
| 0.30 | 下部のフィルムクレジットストリップ | fade-in | 0.50 |
| 0.30 | Tバーのストローク | draw-path | 0.25 |
| 0.40 | 「vTake」のメインネーム | mask-reveal | 0.45 |
| 0.50 | T字の縦線のストローク | draw-path | 0.25 |
| 0.55 | 外側の破線リング | fade-in(+ 32s回転) | 0.50 |
| 0.60 | 内側の実線リング | fade-in(+ 32s逆回転) | 0.50 |
| 0.65 | アンバー色のドット | scale-pop(弾性) | 0.25 |
| 0.75 | 区切り線のセグメント | grow-x(→56px) | 0.30 |
| 0.85 | 区切り線のダイヤモンド | scale-pop | 0.30 |
| 0.90 | タグライン | fade-in | 0.45 |
| 1.00+ | 環境アニメーション | リングの回転 + きらめきのループ | — |
9. コンポジションHTMLを組み立てる
アセットを配置し、$WORK_DIR/public/index.htmlを作成します。
# SKILL_DIR is injected by the host ("Base directory for this skill: …")
SKILL_DIR="<SKILL_DIR>"
mkdir -p "$WORK_DIR/public/fonts" "$WORK_DIR/public/vendor" "$WORK_DIR/public/cards"
cp -n "$SKILL_DIR/assets/fonts/"* "$WORK_DIR/public/fonts/"
cp -n "$SKILL_DIR/assets/vendor/gsap.min.js" "$WORK_DIR/public/vendor/"
# stage the input video so the composition can reference it by relative path
ln -f "$VIDEO_PATH" "$WORK_DIR/public/input-video.mp4" 2>/dev/null \
|| cp "$VIDEO_PATH" "$WORK_DIR/public/input-video.mp4"
コンポジションテンプレート
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
@font-face { font-family: 'Caveat'; src: url('fonts/Caveat-400-latin.woff2') format('woff2'); font-weight: 400; font-display: block; }
@font-face { font-family: 'Caveat'; src: url('fonts/Caveat-700-latin.woff2') format('woff2'); font-weight: 700; font-display: block; }
@font-face { font-family: 'LXGW WenKai TC'; src: url('fonts/LXGWWenKaiTC-400-latin.woff2') format('woff2'); font-weight: 400; font-display: block; }
@font-face { font-family: 'Inter'; src: url('fonts/Inter-400-latin.woff2') format('woff2'); font-weight: 400; font-display: block; }
@font-face { font-family: 'Inter'; src: url('fonts/Inter-700-latin.woff2') format('woff2'); font-weight: 700; font-display: block; }
@font-face { font-family: 'Virgil'; src: url('fonts/Virgil.woff2') format('woff2'); font-display: block; }
:root {
/* Pick from the themeId palette table in Step 7 — example: classic */
--bg: #FFF9E3;
--text: #1e1e1e;
--accent-0: #1971c2;
--accent-1: #e03131;
--accent-2: #2f9e44;
--accent-3: #e8590c;
--accent-4: #9c36b5;
--font-family: 'Caveat', 'LXGW WenKai TC', serif;
}
* { box-sizing: border-box; }
/* Body font-family MUST list concrete font names (not just var(--font-family)) —
the HyperFrames renderer's static analyzer doesn't expand CSS variables when
resolving fonts, so a var-only chain triggers `font_family_without_font_face`
lint and falls back to a generic. Use the concrete chain here; cards that
want the theme font can still reference var(--font-family) internally. */
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #000;
font-family: 'Inter', 'Caveat', 'LXGW WenKai TC', ui-sans-serif, system-ui, sans-serif; }
#stage { position: relative; width: 100%; height: 100%; overflow: hidden; }
/* video-wrapper holds the source video. Its position / size are animated
over time by the master timeline (one tween per layout transition). */
.video-wrapper {
position: absolute;
left: 0; top: 0; width: 1920px; height: 1080px;
overflow: hidden;
border-radius: 0;
box-shadow: none;
}
.video-wrapper video { width: 100%; height: 100%; object-fit: cover; }
.card-host { position: absolute; pointer-events: none; overflow: hidden; }
.card-host .card { position: relative; width: 100%; height: 100%; overflow: hidden; }
.card-host .char { display: inline-block; visibility: visible; }
/* Subtle drop shadow + rounded corners for non-fullscreen video framings */
.video-wrapper.framed {
border-radius: 16px;
box-shadow: 0 12px 40px rgba(0,0,0,0.35);
}
</style>
</head>
<body>
<div
id="stage"
data-composition-id="vtake"
data-start="0"
data-duration="121.2"
data-fps="30"
data-width="1920"
data-height="1080"
>
<div class="video-wrapper" id="video-wrap">
<video id="bg-video"
src="input-video.mp4"
muted playsinline
data-start="0"
data-duration="121.2"
data-track-index="1"></video>
</div>
<div class="card-host clip"
data-card-id="card-01"
data-start="1.0000"
data-duration="6.5000"
data-track-index="2"
style="left:0;top:0;width:1920px;height:1080px;visibility:hidden;opacity:0;">
</div>
<div class="card-host clip"
data-card-id="card-02"
data-start="8.0000"
data-duration="12.0000"
data-track-index="2"
style="left:0;top:0;width:960px;height:1080px;visibility:hidden;opacity:0;">
</div>
<script src="vendor/gsap.min.js"></script>
<script>
(function(){
// count-up formatter helper
window.__fmt = function(v, fmt) {
if (typeof fmt === 'string' && /^\.[0-9]+f$/.test(fmt)) {
return Number(v).toFixed(Number(fmt.slice(1, -1)));
}
if (fmt === ',d') return Math.round(v).toLocaleString();
return String(Math.round(v));
};
const tl = window.gsap.timeline({ paused: true });
// ── Card lifecycle (one block per card) ──
// Example for card-01 [1.0, 7.5] with kinetic-chars at +0.3, grow-x at +0.65:
// Enter (fade in over 0.4s)
tl.set('.card-host[data-card-id="card-01"]', { visibility: 'visible' }, 1.0000);
tl.fromTo('.card-host[data-card-id="card-01"]',
{ opacity: 0 }, { opacity: 1, duration: 0.4000, ease: 'power2.out' }, 1.0000);
// Card-internal anims (compile each data-anim-* declaration here)
tl.from('.card[data-card-id="card-01"] #card-01-title .char',
{ opacity: 0, y: 8, scale: 0.8, duration: 0.5000, ease: 'power2.out', stagger: 0.0400 },
1.3000);
tl.fromTo('.card[data-card-id="card-01"] #card-01-line',
{ width: 0 }, { width: 420, duration: 0.5000, ease: 'power2.out' }, 1.6500);
// Exit (fade out over 0.35s, ending at endSec)
tl.to('.card-host[data-card-id="card-01"]',
{ opacity: 0, duration: 0.3500, ease: 'power2.in' }, 7.1500);
tl.set('.card-host[data-card-id="card-01"]', { visibility: 'hidden' }, 7.5000);
// ── Video framing transitions ──
// When the next card uses a different composition layout, animate the
// video-wrapper to its new bounds. Example: card-01 = fullscreen
// (video hidden behind), card-02 = split composition (zone="side-panel"
// → video on right, card on left).
// Card-02 enters at 8.0s with the split composition. Animate video to
// the right half during the card-01 → card-02 gap (between 7.5 and 8.0s).
tl.set('#video-wrap', { className: 'video-wrapper framed' }, 7.5);
tl.to('#video-wrap',
{ left: 960, top: 0, width: 960, height: 1080,
duration: 0.6, ease: 'power2.inOut' }, 7.5);
// Card-02 enter — same pattern as card-01
tl.set('.card-host[data-card-id="card-02"]', { visibility: 'visible' }, 8.0);
tl.fromTo('.card-host[data-card-id="card-02"]',
{ opacity: 0 }, { opacity: 1, duration: 0.4, ease: 'power2.out' }, 8.0);
// ...card-02 internal anims...
// ── repeat for each card; if the NEXT card's layout differs,
// insert another tl.to('#video-wrap', ...) tween before its enter ──
window.__timelines = window.__timelines || {};
window.__timelines["vtake"] = tl;
})();
</script>
</div>
</body>
</html>
GSAPステートメント早見表
各data-anim属性をGSAPステートメントに変換します。時刻は
絶対秒数=card.startSec + data-anim-atで、1/fps単位に量子化します。
セレクターは.card[data-card-id="X"] #elementIdです。
| data-anim | GSAPステートメントのテンプレート |
|---|---|
fade-in | tl.fromTo(SEL, { opacity: 0 }, { opacity: 1, duration: D, ease: 'power2.out' }, T); |
fade-out | tl.to(SEL, { opacity: 0, duration: D, ease: 'power2.in' }, T); |
slide-in (from=left, dist=80) | tl.fromTo(SEL, { opacity: 0, x: -80 }, { opacity: 1, x: 0, duration: D, ease: 'power2.out' }, T); |
kinetic-chars (pop) | tl.from(SEL + ' .char', { opacity: 0, y: 8, scale: 0.8, duration: D, ease: 'power2.out', stagger: S }, T); |
count-up | (function(){const o={v:FROM};tl.to(o,{v:TO,duration:D,ease:'power2.out',onUpdate:function(){const el=document.querySelector(SEL);if(el)el.textContent=__fmt(o.v,'FMT');}},T);})(); |
draw-path | (function(){const el=document.querySelector(SEL);if(!el)return;const L=el.getTotalLength();tl.set(SEL,{strokeDasharray:L,strokeDashoffset:L},T);tl.to(SEL,{strokeDashoffset:0,duration:D,ease:'power2.inOut'},T);})(); |
grow-x (target-w=W) | tl.fromTo(SEL, { width: 0 }, { width: W, duration: D, ease: 'power2.out' }, T); |
grow-y (target-h=H) | tl.fromTo(SEL, { height: 0 }, { height: H, duration: D, ease: 'power2.out' }, T); |
scale-pop | tl.fromTo(SEL, { opacity: 0, scale: 0.6 }, { opacity: 1, scale: 1, duration: D, ease: 'back.out(1.6)' }, T); |
mask-reveal (direction=left) | tl.fromTo(SEL, { clipPath: 'inset(0 100% 0 0)' }, { clipPath: 'inset(0 0 0 0)', duration: D, ease: 'power2.inOut' }, T); |
量子化:T = Math.round(absSec * fps) / fps。30fpsでは最小ステップは
1/30 ≈ 0.0333sです。JSリテラル内では小数点以下4桁に丸める(.toFixed(4))方法で問題ありません。
動画のフレーミングリファレンス(layout の値ごと)
動画コンテナーのセレクターは #video-wrap です。カード間で境界をアニメーションさせるには、tl.to('#video-wrap', { ...bounds }, T) を使用します。初期の境界は、card-01 のレイアウトに合わせて要素にインラインで設定してください。トランジションの長さは 0.5~0.7s とし、ease: 'power2.inOut' を指定します。
装飾フレーム(clean/hairline/polaroid)は #video-wrap の兄弟要素として配置し、レイアウトのトランジション中も追従させます。各フレームの配置用 HTML、推奨 CSS、対応するレイアウトについては、references/frames/ を参照してください。簡単なルール:overlay レイアウトでは装飾フレームを表示しません(フルブリード動画と装飾が干渉するため)。PiP レイアウトには独自のピル状スタイル(border-radius + 白いリング + 影)がすでにあるため、装飾フレームは split/stack の上にのみ追加します。
#video-wrap の GSAP ターゲット参照表(コンポジションの各レイアウトに対応)
(横長 1920×1080 — 縦長と 4:5 については、3 つの比率すべてを記載した references/layouts/*.html を参照してください):
| コンポジションのレイアウト | 典型的な card.zone | #video-wrap の GSAP ターゲット | 追加の css クラス |
|---|---|---|---|
split | side-panel | { left: 960, top: 0, width: 960, height: 1080 } | — |
stack | lower-third | { left: 14, top: 14, width: 1892, height: 548 }(上から 52%) | — |
pip(右下) | fullscreen | { left: 1480, top: 760, width: 400, height: 300 } | pip-pill(border-radius + ring + shadow) |
pip(左上) | fullscreen | { left: 40, top: 40, width: 400, height: 300 } | pip-pill |
overlay(動画が全面表示) | video-overlay | { left: 0, top: 0, width: 1920, height: 1080 }(デフォルトから変更なし) | — |
| 動画を非表示(純粋なグラフィックの場面) | fullscreen | { opacity: 0 }(またはキャンバスの外に移動) | — |
PiP の場面に入るとき、または PiP の場面から出るときに、pip-pill の装飾(border-radius + 白いリング + ドロップシャドウ)を切り替えるには:
// Enter pip — add chrome
tl.set('#video-wrap', { className: 'video-wrapper pip-pill' }, T);
tl.to('#video-wrap', { left: 1480, top: 760, width: 400, height: 300,
duration: 0.6, ease: 'power2.inOut' }, T);
// Leave pip — back to clean full-bleed
tl.set('#video-wrap', { className: 'video-wrapper' }, T_NEXT);
tl.to('#video-wrap', { left: 0, top: 0, width: 1920, height: 1080,
duration: 0.6, ease: 'power2.inOut' }, T_NEXT);
カードホストの境界はゾーンに合わせます。Step 6 の冒頭にある表を使って、カードの zone をピクセル単位の境界に変換し、その値をカードホストのインライン style="left:Xpx;top:Ypx;width:Wpx; height:Hpx;..." に記述します。video-overlay ゾーン(overlay レシピ)の場合、カードホストはキャンバス全体を埋めます。実際に見えるカードをどこに配置するかは、.card .root 内の CSS で決まります。
HyperFrames のレイアウト/アニメーション QA ルール
- 各カードの静止ヒーローフレーム(カード全体が表示され、読みやすい状態)を最初に作成します。
- 動画、カード、字幕/キャプション、図が意図せず重なっていないことを確認します。
- 非表示の動画領域がフレームでクリップされ、意図した境界の外に表示されていないことを確認します。
- 一時停止状態のマスタータイムラインを
window.__timelines["vtake"]として登録します。 - ページ読み込み時にタイムラインを同期的に作成します。
async、setTimeout、Promises、メディアのplay()呼び出しは使用しないでください。 - レンダリング処理内で
Math.random()やDate.now()を使用しないでください。 repeat: -1は使用せず、動画の長さをもとに有限回の繰り返し数を計算してください。- モーションには、レイアウトプロパティ(
top、left、width、height)よりも、GSAP のトランスフォームと不透明度(x、y、scale、rotation、opacity)を優先してください。 - 動画要素のサイズを直接アニメーションするのではなく、
#video-wrapなどのラッパーをアニメーションしてください。 - 同じ要素の同じプロパティを、複数のタイムラインで同時にアニメーションしないでください。
data-layerではなくdata-track-indexを、data-endではなくdata-durationを使用してください。- すべての時間指定要素(
card-host、サブコンポジションなど)には、独自のクラスとともにclass="clip"を必ず含めてください。例:class="card-host clip"。HyperFrames ランタイムは.clipを使用して、data-start … data-start+data-durationの範囲内でのみ要素を表示します。これがない場合、要素は動画全体に表示されます(lint:timed_element_missing_clip_class)。 - body/グローバルの
font-familyには、'Inter', 'Caveat', …のような具体的なフォント名を指定してください。var(--font-family)のような CSS 変数は使用しないでください。HyperFrames のフォントリゾルバーは静的解析時に CSS 変数を展開しません(lint:font_family_without_font_face)。カードでは@font-face宣言が読み込まれるため、内部でvar(--font-family)を使用できます。
card-cta-vtake:固定 GSAP アニメーションブロック
GSAP タイムラインブロックの末尾(最後の window.__timelines 登録の直前)に、この固定コードブロックを追加してください。CTA_START を card-cta-vtake.startSec に、CTA_END を card-cta-vtake.endSec に置き換えてください。
// ── card-cta-vtake: Editorial Cinema brand outro (2.0s total) ──
// Sequence: 1.0s entrance · 0.7s hold (CSS ambient breathing) · 0.3s fade-out
const PREFIX = '.card[data-card-id="card-cta-vtake"]';
// Fade source video out at the start of the CTA
tl.to('#video-wrap', { opacity: 0, duration: 0.30, ease: 'power2.in' }, CTA_START);
// Card host enter
tl.set('.card-host[data-card-id="card-cta-vtake"]', { visibility: 'visible' }, CTA_START);
tl.fromTo('.card-host[data-card-id="card-cta-vtake"]',
{ opacity: 0 }, { opacity: 1, duration: 0.30, ease: 'power2.out' }, CTA_START);
// V-shape stroke draw
(function(){
const sel = PREFIX + ' #cta-path-v';
const el = document.querySelector(sel); if(!el) return;
const L = el.getTotalLength();
tl.set(sel, { strokeDasharray: L, strokeDashoffset: L }, CTA_START + 0.05);
tl.to(sel, { strokeDashoffset: 0, duration: 0.40, ease: 'power2.inOut' }, CTA_START + 0.05);
})();
// "Powered by" label
tl.fromTo(PREFIX + ' #cta-powered-label',
{ opacity: 0 }, { opacity: 1, duration: 0.45, ease: 'power2.out' }, CTA_START + 0.15);
// Flanking lines grow-x
tl.fromTo(PREFIX + ' #cta-line-l',
{ width: 0 }, { width: 56, duration: 0.40, ease: 'power2.out' }, CTA_START + 0.18);
tl.fromTo(PREFIX + ' #cta-line-r',
{ width: 0 }, { width: 56, duration: 0.40, ease: 'power2.out' }, CTA_START + 0.18);
// Viewfinder corners ×4 fade-in
['tl','tr','bl','br'].forEach(pos => {
tl.fromTo(PREFIX + ' #cta-corner-' + pos,
{ opacity: 0, scale: 0.7 },
{ opacity: 1, scale: 1, duration: 0.50, ease: 'power2.out' },
CTA_START + 0.20);
});
// Top/bottom film-credit meta strips
tl.fromTo(PREFIX + ' #cta-top-meta',
{ opacity: 0, y: 6 }, { opacity: 1, y: 0, duration: 0.50, ease: 'power2.out' }, CTA_START + 0.25);
tl.fromTo(PREFIX + ' #cta-bot-meta',
{ opacity: 0, y: -6 }, { opacity: 1, y: 0, duration: 0.50, ease: 'power2.out' }, CTA_START + 0.30);
// T-bar stroke draw
(function(){
const sel = PREFIX + ' #cta-path-tbar';
const el = document.querySelector(sel); if(!el) return;
const L = el.getTotalLength();
tl.set(sel, { strokeDasharray: L, strokeDashoffset: L }, CTA_START + 0.30);
tl.to(sel, { strokeDashoffset: 0, duration: 0.25, ease: 'power2.inOut' }, CTA_START + 0.30);
})();
// "vTake" main name mask-reveal (left → right)
tl.fromTo(PREFIX + ' #cta-vtake-name',
{ clipPath: 'inset(0 100% 0 0)' },
{ clipPath: 'inset(0 0% 0 0)', duration: 0.45, ease: 'power2.inOut' },
CTA_START + 0.40);
// T-stem stroke draw
(function(){
const sel = PREFIX + ' #cta-path-tstem';
const el = document.querySelector(sel); if(!el) return;
const L = el.getTotalLength();
tl.set(sel, { strokeDasharray: L, strokeDashoffset: L }, CTA_START + 0.50);
tl.to(sel, { strokeDashoffset: 0, duration: 0.25, ease: 'power2.inOut' }, CTA_START + 0.50);
})();
// Dual concentric rings fade-in (CSS @keyframes handles the slow spin)
tl.fromTo(PREFIX + ' #cta-ring-outer',
{ opacity: 0 }, { opacity: 1, duration: 0.50, ease: 'power2.out' }, CTA_START + 0.55);
tl.fromTo(PREFIX + ' #cta-ring-inner',
{ opacity: 0 }, { opacity: 1, duration: 0.50, ease: 'power2.out' }, CTA_START + 0.60);
// Amber dot scale-pop (elastic)
tl.fromTo(PREFIX + ' #cta-dot',
{ opacity: 0, scale: 0.4, transformOrigin: '48px 118px' },
{ opacity: 1, scale: 1, duration: 0.25, ease: 'back.out(1.7)' },
CTA_START + 0.65);
// Decorative divider segments grow-x
tl.fromTo(PREFIX + ' #cta-seg-l',
{ width: 0 }, { width: 56, duration: 0.30, ease: 'power2.out' }, CTA_START + 0.75);
tl.fromTo(PREFIX + ' #cta-seg-r',
{ width: 0 }, { width: 56, duration: 0.30, ease: 'power2.out' }, CTA_START + 0.75);
// Divider diamond pop
tl.fromTo(PREFIX + ' #cta-diamond',
{ rotate: 45, scale: 0 },
{ rotate: 45, scale: 1, duration: 0.30, ease: 'back.out(1.7)' },
CTA_START + 0.85);
// Tagline fade-in
tl.fromTo(PREFIX + ' #cta-tagline',
{ opacity: 0, y: 4 }, { opacity: 1, y: 0, duration: 0.45, ease: 'power2.out' }, CTA_START + 0.90);
// Card exit — fade-out in the last 0.30s of the 2.0s window
tl.to('.card-host[data-card-id="card-cta-vtake"]',
{ opacity: 0, duration: 0.30, ease: 'power2.in' }, CTA_END - 0.30);
tl.set('.card-host[data-card-id="card-cta-vtake"]', { visibility: 'hidden' }, CTA_END);
10. MP4 にレンダリングする
cd "$WORK_DIR"
PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \
-o output.mp4 \
--fps 30
hyperframes render <dir> は <dir>/index.html を読み込み、MP4 を生成します。
macOS では PRODUCER_BROWSER_GPU_MODE=hardware(または --browser-gpu)フラグの使用を強くおすすめします。ソフトウェアのみでの Chrome レンダリングは、ほとんどのノートパソコンでタイムアウトします。
本番のレンダリング前に動作を確認するため、特定のタイムスタンプのフレームを 1 枚キャプチャします。
npx hyperframes snapshot public --at 5 --out snapshot-5s.png
11. 結果を報告する
ユーザーに以下を伝えてください。
- 作業ディレクトリのパス
storyboard.json(作成したカードのアウトライン)public/cards/*.html(カードごとに 1 つの HTML ファイル)public/index.html(組み立てたコンポジション)output.mp4(最終動画)- 使用した ASR プロバイダー
- カード数と選定方法(1 文で)
- 不足しているキーや品質に関する注意事項
ユーザーから依頼がない限り、作業ディレクトリを削除しないでください。