Loading...
Loading...
Compare original and translation side by side
hyperframesConfirm the route before you build. This skill packages an existing talking-head clip with designed graphic cards (titles, lower-thirds, data callouts, quotes, side panels, PiP). If the user wants plain captions / subtitles (the spoken words as text) →; a single short unnarrated element (one logo sting / lower-third) →/embedded-captions. The clip plays untouched — re-timing, recoloring, reframing, reordering, or audio is NLE editing and out of scope. Building from a URL / topic / PR → the creation workflows. Unsure overlays-vs-captions? Read/motion-graphicsfirst./hyperframes
Graphic-packaging sibling of. Captions add the spoken words as a readable subtitle; this adds designed graphics on top of the playing video. Plain subtitles →embedded-captions. Build a video from scratch → the creation workflows (embedded-captions/product-launch-video/ …).faceless-explainer
metadata.jsonaudio.mp3transcript.json[{ text, start, end }, …]segmentswordsstoryboard.jsonpublic/cards/card-XX.htmlpublic/index.htmloutput.mp4hyperframes开始制作前请确认流程。此技能用于为现有访谈类视频片段添加设计好的图形卡片(标题、下三分之一字幕、数据标注、引用、侧边栏、画中画)。如果用户需要纯字幕/对白字幕(将语音转为文本)→ 使用;如果需要单个简短无旁白元素(如单个logo动画/下三分之一字幕)→ 使用/embedded-captions。源视频会完整播放——调整时长、调色、重构图、重新排序或音频编辑属于非线性编辑(NLE)范畴,不在此技能范围内。从URL/主题/公关材料创建视频→ 使用创建工作流。不确定是叠加层还是字幕?先阅读/motion-graphics。/hyperframes
的图形包装姊妹技能。字幕是将语音内容转为可读的文本;此技能是在播放的视频上添加设计好的图形。纯字幕→使用embedded-captions。从零开始制作视频→使用创建工作流(embedded-captions/product-launch-video/ …)。faceless-explainer
metadata.jsonaudio.mp3transcript.json[{ text, start, end }, …]segmentswordsstoryboard.jsonpublic/cards/card-XX.htmlpublic/index.htmloutput.mp4undefinedundefined
This skill runs entirely on the **hyperframes** CLI plus system `ffmpeg` / `ffprobe`.
Transcription is local **Whisper** via `hyperframes transcribe` — no third-party
service, API key, or rate-limited proxy.
此技能完全依赖**hyperframes** CLI以及系统中的`ffmpeg` / `ffprobe`。转录通过`hyperframes transcribe`使用本地**Whisper**完成——无需第三方服务、API密钥或限速代理。npx hyperframes doctor # ffmpeg, headless browser, render depsnpx hyperframes doctor # 检查ffmpeg、无头浏览器、渲染依赖
Required:
- `ffmpeg` / `ffprobe` (system)
- `<SKILL_DIR>/assets/fonts/*.woff2`, `<SKILL_DIR>/assets/vendor/gsap.min.js` (bundled inside this skill, staged to work dir in Step 9)
Transcription needs no key — `hyperframes transcribe` runs Whisper locally (Step 4).
Strongly recommended on macOS for `hyperframes render`:
```bash
export PRODUCER_BROWSER_GPU_MODE=hardware
必需依赖:
- `ffmpeg` / `ffprobe`(系统级)
- `<SKILL_DIR>/assets/fonts/*.woff2`, `<SKILL_DIR>/assets/vendor/gsap.min.js`(捆绑在此技能中,步骤9会复制到工作目录)
转录无需密钥——`hyperframes transcribe`会在本地运行Whisper(步骤4)。
在macOS上运行`hyperframes render`时强烈建议设置:
```bash
export PRODUCER_BROWSER_GPU_MODE=hardwarevideos/<project-name>/product-launch-videofaceless-explainerpr-to-videoVIDEO_PATH="/absolute/path/input.mp4"
WORK_DIR="videos/$(basename "$VIDEO_PATH" | sed 's/\.[^.]*$//')"
mkdir -p "$WORK_DIR"videos/<project-name>/product-launch-videofaceless-explainerpr-to-videoVIDEO_PATH="/absolute/path/input.mp4"
WORK_DIR="videos/$(basename "$VIDEO_PATH" | sed 's/\.[^.]*$//')"
mkdir -p "$WORK_DIR"undefinedundefined
Outputs: `metadata.json` (read `width`/`height`/`duration`; fps = the `r_frame_rate`
fraction evaluated, e.g. `30000/1001 → 29.97`) + `audio.mp3`.
输出文件:`metadata.json`(包含`width`/`height`/`duration`;帧率为`r_frame_rate`的分数计算值,例如`30000/1001 → 29.97`) + `audio.mp3`。npx hyperframes transcribe "$WORK_DIR/audio.mp3" -d "$WORK_DIR" --json --model small.entranscript.jsontextstartendendendSeccomposition.durationSecondsmetadata.jsonnpx hyperframes transcribe "$WORK_DIR/audio.mp3" -d "$WORK_DIR" --json --model small.entranscript.jsontextstartendendendSeccomposition.durationSecondsmetadata.jsontranscript.json[{ "text": "...", "start": s, "end": s }, …]segmentswordstexttextstartendsegmentstranscript.json[{ "text": "...", "start": s, "end": s }, …]segmentswordstexttextstartendsegmentstranscript.jsonmetadata.jsonstoryboard.json{
"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": "The soul-searching question at 11 PM",
"detail": "Client's 60-second voice message: 'If the RMB appreciates, does that mean my USD policy is a terrible loss?'"
}
}
]
}| field | type | purpose |
|---|---|---|
| string | stable id used in card HTML & GSAP selectors |
| string | natural-language description; fed to card synthesis |
| number | times in seconds (endSec > startSec) |
| 0 | 1 | 2 | 3 | 4 | which of the 5 theme accent colors this card pulls |
| enum (see below) | where on the canvas the card lives |
| object | free-form bag; agent puts kicker/title/detail/data/quote here |
| string | free-form label you may attach to remember a card's pattern; absent = free-form, which is the default |
| enum: | declarative card-to-card transition |
zone| zone | resolved bounds | when to use |
|---|---|---|
| covers whole canvas | hero moments, big numbers, mantras |
| inset 40px margin (or 45% of portrait height) | dense data / annotated content |
| bottom 30% band | annotation over visible video |
| right 42% (landscape) or bottom 40% (portrait) | data side, video other side |
| full canvas, expects mostly-transparent card | annotation overlays on full-bleed video |
zonevideoTrack.bounds#video-wrap<script>| video duration | base pace (sec per card) | rationale |
|---|---|---|
| < 60s (short reel) | 6–8s | viewers expect fast cuts in short-form |
| 60s – 3 min | 8–12s | normal social pace |
| 3 – 10 min | 12–20s | give breathing room; each card carries more |
| 10 – 30 min | 20–35s | long-form lecture / interview rhythm |
| > 30 min | 30–60s | episodic, near-chapter feel |
| signal in the transcript | multiplier | effect |
|---|---|---|
| High density — many numbers, distinct claims, staccato pacing, list-like enumeration, every 1–2 sentences is a new idea | × 0.7 | cuts faster, more cards |
| Medium density — mixed flow with both data and narrative | × 1.0 | base pace |
| Low density — one extended story, repeated reframing, slow reflective pacing, single argument unfolding | × 1.5 | cuts slower, fewer cards |
secPerCard = basePace × densityMultiplier
cardCount = max(5, round(videoDurationSec / secPerCard))data-composition-srctimeline_track_too_densecontentcards[]composition.durationSecondsendSectranscript.jsonmetadata.jsonstoryboard.json{
"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": "用演讲者深夜的焦虑问题吸引观众",
"startSec": 0.5,
"endSec": 13.0,
"accentIndex": 0,
"zone": "fullscreen",
"contentHints": {
"kicker": "一个坦诚的问题",
"title": "深夜11点的灵魂拷问",
"detail": "客户60秒语音消息:‘如果人民币升值,是不是意味着我的美元策略亏大了?’"
}
}
]
}| 字段 | 类型 | 用途 |
|---|---|---|
| 字符串 | 用于卡片HTML和GSAP选择器的稳定ID |
| 字符串 | 自然语言描述;用于卡片内容生成 |
| 数字 | 时间(秒,endSec > startSec) |
| 0 | 1 | 2 | 3 | 4 | 此卡片使用的主题强调色索引(共5种) |
| 枚举(见下表) | 卡片在画布上的位置 |
| 对象 | 自由格式内容;Agent可在此添加标题/副标题/详情/数据/引用等 |
| 字符串 | 用于标记卡片模式的自由格式标签;无此字段则为自由设计(默认) |
| 枚举: | 卡片间的过渡效果声明 |
zone| zone | 解析后的边界 | 使用场景 |
|---|---|---|
| 覆盖整个画布 | 核心时刻、大数字、关键口号 |
| 内边距40px(或竖屏高度的45%) | 密集数据/带注释的内容 |
| 底部30%区域 | 视频上的注释内容 |
| 右侧42%(横屏)或底部40%(竖屏) | 数据在一侧,视频在另一侧 |
| 整个画布,卡片需大部分透明 | 全屏视频上的注释叠加层 |
zonevideoTrack.bounds<script>#video-wrap| 视频时长 | 基础节奏(每张卡片秒数) | 理由 |
|---|---|---|
| < 60秒(短视频) | 6–8秒 | 观众期望短视频节奏快 |
| 60秒 – 3分钟 | 8–12秒 | 正常社交平台节奏 |
| 3 – 10分钟 | 12–20秒 | 留出呼吸空间;每张卡片承载更多内容 |
| 10 – 30分钟 | 20–35秒 | 长时讲座/访谈节奏 |
| > 30分钟 | 30–60秒 | 章节式节奏,接近分段感受 |
| 字幕中的信号 | 乘数 | 效果 |
|---|---|---|
| 高密度 — 大量数字、不同观点、急促节奏、列表式列举、每1–2句话就是新观点 | × 0.7 | 切换更快,卡片更多 |
| 中等密度 — 数据与叙事混合 | × 1.0 | 基础节奏 |
| 低密度 — 单一长篇故事、重复重构、缓慢反思节奏、单一论点展开 | × 1.5 | 切换更慢,卡片更少 |
每张卡片时长 = 基础节奏 × 密度乘数
卡片数量 = max(5, round(视频时长秒数 / 每张卡片时长))data-composition-srctimeline_track_too_densecontentcards[]composition.durationSecondsendSecrecommendedRatiometadata.jsonsourceAspect = width / heightsourceAspect ≥ 1.516:9sourceAspect ≤ 0.79:160.7 < sourceAspect < 1.54:5autoCountmax(5, round(videoSec / (basePace × densityMultiplier)))AskUserQuestionask_questionrequest_user_inputrecommendedRatiolayout="stack"styleautoCountAskUserQuestion// Precompute before the call:
// recommendedRatio = "16:9" | "9:16" | "4:5"
// autoCount = integer (from Step 6)
AskUserQuestion({
questions: [
{
question: "Output video aspect ratio (canvas):",
header: "Aspect ratio",
multiSelect: false,
// Reorder so the recommended option appears FIRST (per AskUserQuestion convention).
// Append " (recommended · matches source video W×H)" to the recommended option's label.
options: [
{ label: "16:9 (1920×1080) landscape", description: "TV / YouTube / desktop playback. Most natural when the source video is already landscape; widest canvas." },
{ label: "9:16 (1080×1920) portrait", description: "TikTok / Reels / short-form mobile. Most natural for portrait source; native mobile experience." },
{ label: "4:5 (1080×1350) near-portrait", description: "Instagram feed / WeChat Moments. Best when source is near-square or you want to cover both platforms." }
]
},
{
question: "Choose the overall layout: how should the video and cards coexist on the canvas?",
header: "Layout",
multiSelect: false,
options: [
{ label: "side-by-side (split)", description: "Video and card each take half the canvas. Most stable for interview / data side-by-side; clear visual separation." },
{ label: "top-bottom (stack)", description: "Video on top (~52%), card below. Classic combo of speaker face + summary card; works well in portrait too." },
{ label: "picture-in-picture (pip)", description: "Card fills the canvas, video shrinks to a rounded corner window. Use when content is primary and speaker is secondary." },
{ label: "full-screen overlay (overlay)", description: "Video plays full-bleed, card floats as a glass layer on top. Strong cinematic / emotional feel." }
]
},
{
question: "Choose the card visual style (style):",
header: "Style group",
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 (warm-paper)", description: "academic notebook · editorial big-type · whiteboard hand-drawn · xhs social. Best for interview reflections, product launches, lifestyle, emotional stories." },
{ label: "clinical / cold (clinical)", description: "audit magazine · swiss grid · terminal CLI · minimal modern. Best for financial analysis, investigative reports, technical tutorials, serious presentations." },
{ label: "experimental / avant-garde (experimental)", description: "geom color-clash geometry · spotlight dark-background. Best for short-form highlights, product launches, strong emotion, cinematic feel." }
]
},
{
question: "Card count (takeaway pacing): how many cards to cut?",
header: "Card count",
multiSelect: false,
options: [
{ label: "Auto (recommended) · approx N cards", description: "Inferred automatically from video duration and information density (see Step 6 rules). This run estimates approx N cards. Substitute the real N (your autoCount) into the label." },
{ label: "Fewer · approx round(N × 0.6) cards", description: "Sparser cuts, each card holds longer — suits reflective / slow-paced content." },
{ label: "More · approx round(N × 1.5) cards", description: "Tighter cuts, faster rhythm — suits staccato / data-dense / short-form highlight content." }
]
}
]
})AskUserQuestionI need to confirm four visual decisions with you before I start cutting cards:
1) Output aspect ratio (canvas):
A. 16:9 landscape (1920×1080) — TV / YouTube / desktop playback
B. 9:16 portrait (1080×1920) — TikTok / Reels / short-form mobile
C. 4:5 near-portrait (1080×1350) — Instagram feed / works for both platforms
▸ My recommendation: <recommendedRatio> (matches source video W×H = <sourceW>×<sourceH>)
2) Overall layout (how video & card coexist):
A. split side-by-side (50/50)
B. stack top-bottom (video top, card bottom)
C. pip picture-in-picture (card full canvas, video rounded corner window)
D. overlay full-screen glass overlay (video full-bleed, card glass layer)
3) Card style group (maps to frame auto-pick matrix, pick 1 of 3):
A. warm paper (warm-paper) (academic / editorial / whiteboard / xhs)
B. clinical / cold (clinical) (audit / swiss / terminal / minimal)
C. experimental (experimental) (geom / spotlight)
4) Card count (takeaway pacing):
A. Auto (recommended) — approx <autoCount> cards
B. Fewer — approx round(<autoCount> × 0.6) cards
C. More — approx round(<autoCount> × 1.5) cards
D. Give me a specific number (e.g. "8", "20")
Reply format: "1A 2C 3B 4A" or natural language is fine.
If you want all recommended defaults, reply "default" / "auto" / "use all recommendations"."1A 2C 3B 4A""A C B A""16:9 / pip / data / auto"defaultstoryboard.composition.width / height| user choice | composition.width × height | storyboard.layout field |
|---|---|---|
| 1920 × 1080 | |
| 1080 × 1920 | |
| 1080 × 1350 | |
references/layouts/*.html1350/1920 ≈ 0.703overlay{ x: 24, y: 1280, w: 1032, h: 564 }{ x: 24, y: round(1280 × 0.703), w: 1032, h: round(564 × 0.703) }{ x: 24, y: 900, w: 1032, h: 397 }AskUserQuestion| user choice | final cardCount |
|---|---|
| Auto (recommended) | the |
| Fewer | |
| More | |
| Other = "<n>" (integer) | |
| Other = anything else | fall back to |
| layout | warm-paper styles (academic / whiteboard / editorial / xhs) | clinical styles (audit / swiss / terminal / minimal) | experimental styles (geom / spotlight) |
|---|---|---|---|
| | | |
| | | |
| | | |
| | | |
references/<dim>/<key>.htmlrecommendedRatiometadata.jsonsourceAspect = width / heightsourceAspect ≥ 1.516:9sourceAspect ≤ 0.79:160.7 < sourceAspect < 1.54:5autoCountmax(5, round(视频时长秒数 / (基础节奏 × 密度乘数)))AskUserQuestionask_questionrequest_user_inputrecommendedRatiolayout="stack"styleautoCountAskUserQuestion// 调用前预先计算:
// recommendedRatio = "16:9" | "9:16" | "4:5"
// autoCount = 整数(来自步骤6)
AskUserQuestion({
questions: [
{
question: "输出视频宽高比(画布):",
header: "宽高比",
multiSelect: false,
// 重新排序,让推荐选项排在最前面(遵循AskUserQuestion约定)。
// 在推荐选项的标签后添加“(推荐 · 匹配源视频W×H)”。
options: [
{ label: "16:9 (1920×1080) 横屏", description: "电视/YouTube/桌面播放。源视频为横屏时最自然;画布最宽。" },
{ label: "9:16 (1080×1920) 竖屏", description: "TikTok/Reels/短视频移动端。源视频为竖屏时最自然;原生移动端体验。" },
{ label: "4:5 (1080×1350) 近竖屏", description: "Instagram朋友圈/微信朋友圈。源视频接近正方形或需要覆盖多平台时最佳。" }
]
},
{
question: "选择整体布局:视频和卡片如何在画布上共存?",
header: "布局",
multiSelect: false,
options: [
{ label: "左右分栏(split)", description: "视频和卡片各占画布一半。访谈/数据并列时最稳定;视觉分隔清晰。" },
{ label: "上下堆叠(stack)", description: "视频在上(约52%),卡片在下。演讲者面部+摘要卡片的经典组合;竖屏也适用。" },
{ label: "画中画(pip)", description: "卡片填满画布,视频缩小为圆角窗口。内容为主、演讲者为辅时使用。" },
{ label: "全屏叠加(overlay)", description: "视频全屏播放,卡片作为玻璃层悬浮在上方。强烈的电影感/情感氛围。" }
]
},
{
question: "选择卡片视觉风格(style):",
header: "风格组",
multiSelect: false,
// 注意:这3组与下方的帧自动选择矩阵行完全匹配
// 选择一组即可同时确定`style`组和帧矩阵列。各组互斥。
options: [
{ label: "温暖纸张风(warm-paper)", description: "学术笔记本·大字体编辑风格·手绘白板·小红书社交风。访谈反思、产品发布、生活方式、情感故事最佳。" },
{ label: "冷峻专业风(clinical)", description: "审计杂志·瑞士网格·终端CLI·极简现代风。财务分析、调查报告、技术教程、正式演示最佳。" },
{ label: experimental / avant-garde (experimental)", description: "几何撞色·暗背景聚光灯。短视频高光、产品发布、强烈情感、电影感内容最佳。" }
]
},
{
question: "卡片数量(重点内容节奏):需要制作多少张卡片?",
header: "卡片数量",
multiSelect: false,
options: [
{ label: "自动(推荐)· 约N张卡片", description: "根据视频时长和信息密度自动推断(见步骤6规则)。本次运行预计约N张卡片。将实际N值(你的autoCount)替换到标签中。" },
{ label: "更少· 约round(N × 0.6)张卡片", description: "切换更稀疏,每张卡片停留更长——适合反思/慢节奏内容。" },
{ label: "更多· 约round(N × 1.5)张卡片", description: "切换更紧凑,节奏更快——适合急促/数据密集/短视频高光内容。" }
]
}
]
})AskUserQuestion开始制作卡片前,我需要与你确认四个视觉决策:
1) 输出宽高比(画布):
A. 16:9横屏(1920×1080)——电视/YouTube/桌面播放
B. 9:16竖屏(1080×1920)——TikTok/Reels/短视频移动端
C. 4:5近竖屏(1080×1350)——Instagram朋友圈/适配多平台
▸ 我的推荐: <recommendedRatio> (匹配源视频W×H = <sourceW>×<sourceH>)
2) 整体布局(视频与卡片如何共存):
A. split 左右分栏(50/50)
B. stack 上下堆叠(视频在上,卡片在下)
C. pip 画中画(卡片填满画布,视频为圆角窗口)
D. overlay 全屏玻璃叠加(视频全屏,卡片为玻璃层)
3) 卡片风格组(对应帧自动选择矩阵,3选1):
A. warm paper(warm-paper) (学术/编辑/白板/小红书)
B. clinical / cold(clinical) (审计/瑞士风格/终端/极简)
C. experimental(experimental) (几何/聚光灯)
4) 卡片数量(重点内容节奏):
A. 自动(推荐)——约<autoCount>张卡片
B. 更少——约round(<autoCount> × 0.6)张卡片
C. 更多——约round(<autoCount> × 1.5)张卡片
D. 指定具体数字(如“8”“20”)
回复格式:“1A 2C 3B 4A”或自然语言均可。
如果要使用所有推荐默认值,回复“default”/“auto”/“使用所有推荐选项”。"1A 2C 3B 4A""A C B A""16:9 / pip / data / auto"defaultstoryboard.composition.width / height| 用户选择 | 合成文件宽×高 | storyboard.layout字段 |
|---|---|---|
| 1920 × 1080 | |
| 1080 × 1920 | |
| 1080 × 1350 | |
references/layouts/*.html1350/1920 ≈ 0.703overlay{ x: 24, y: 1280, w: 1032, h: 564 }{ x: 24, y: round(1280 × 0.703), w: 1032, h: round(564 × 0.703) }{ x: 24, y: 900, w: 1032, h: 397 }AskUserQuestion| 用户选择 | 最终卡片数量 |
|---|---|
| 自动(推荐) | 你已计算的 |
| 更少 | |
| 更多 | |
| 其他 = "<n>"(整数) | |
| 其他 = 其他内容 | fallback到 |
| 布局 | warm-paper风格(学术/白板/编辑/小红书) | clinical风格(审计/瑞士/终端/极简) | experimental风格(几何/聚光灯) |
|---|---|---|---|
| | | |
| | | |
| | | |
| | | |
references/<dim>/<key>.htmlobject-fit: cover#video-wrapcard.zoneaccentIndexdata-animthemeId--accent-N--bg--text<style>| themeId | accent palette (5 colors) | board bg | text |
|---|---|---|---|
| classic | | | |
| noir | | | |
| mint | | | |
| craft | | | |
| slate | | | |
| mono | | | |
<SKILL_DIR>/assets/fonts/CaveatLXGW WenKai TCInterVirgil@font-facefont-family<SKILL_DIR>/references/styles/object-fit: cover#video-wrapcard.zoneaccentIndexdata-animthemeId<style>--accent-N--bg--text| themeId | 强调色调色板(5种颜色) | 背景色 | 文本色 |
|---|---|---|---|
| classic | | | |
| noir | | | |
| mint | | | |
| craft | | | |
| slate | | | |
| mono | | | |
<SKILL_DIR>/assets/fonts/CaveatLXGW WenKai TCInterVirgil@font-facefont-family<SKILL_DIR>/references/styles/themeId<SKILL_DIR>/references/Style × Layout × VideoFrame
(10) (4) (3)| dimension | keys | what it decides |
|---|---|---|
| style | | the card's visual language — fonts, colors, ornament, layout-within-card |
| layout | | how the source video and the card share the canvas |
| frame | | the decorative chrome around the video element |
<SKILL_DIR>/references/DESIGN_INDEX.mdreferences/styles/<key>.html.card[data-card-id="ref-<key>"]references/layouts/<key>.htmlvideoBoundscardBoundsstoryboard.jsonlayoutreferences/frames/<key>.html#video-wrapstyle × layout × frameeditorial × overlay × cleanaudit × split × hairlinewhiteboard × pip × polaroidstoryboard.compositionthemeIdthemeId<SKILL_DIR>/references/风格 × 布局 × 视频帧
(10) (4) (3)| 维度 | 取值 | 决定内容 |
|---|---|---|
| 风格 | | 卡片的视觉语言——字体、颜色、装饰、卡片内布局 |
| 布局 | | 源视频和卡片如何共享画布 |
| 帧 | | 视频元素周围的装饰性边框 |
<SKILL_DIR>/references/DESIGN_INDEX.mdreferences/styles/<key>.html.card[data-card-id="ref-<key>"]references/layouts/<key>.htmlvideoBoundscardBoundsstoryboard.jsonlayoutreferences/frames/<key>.html#video-wrapeditorial × overlay × cleanaudit × split × hairlinewhiteboard × pip × polaroidstoryboard.compositionthemeIdcard.zonestoryboard.jsonstyle#video-wrap#video-wrapvideoTrack.boundsindex.htmlcard.layoutcard.zonereferences/layouts/zone#video-wrap| composition layout | recommended | GSAP target for | GSAP target for | when to use |
|---|---|---|---|---|
| | | | speaker + data side-by-side / 50:50 weight |
| | | | speaker on top + summary card below |
| | | | content-heavy card + corner pip |
| | | | cinematic / dramatic / glass card on full video |
1350/1920 ≈ 0.703recommendedRatiocard.zone | resolved bounds | common use |
|---|---|---|
| covers whole canvas | hero card, video tweens to hidden/pip |
| inset 40px margin (landscape) or bottom 45% (portrait) | dense data card, free margins |
| bottom 30% band | talking-head annotation |
| right 42% (landscape) or bottom 40% (portrait) | sidebar / "split" recipe |
| full canvas; expect transparent card root | glass overlay on full-bleed video |
card.zone#video-wrapcard.zonestoryboard.jsonstyle#video-wrap#video-wrapvideoTrack.boundsindex.htmlcard.layoutcard.zonereferences/layouts/zone#video-wrap| 合成布局 | 推荐 | | | 使用场景 |
|---|---|---|---|---|
| | | | 演讲者+数据并列/权重50:50 |
| | | | 演讲者在上+摘要卡片在下 |
| | | | 内容密集卡片+角落画中画 |
| | | | 电影感/戏剧性/玻璃卡片在全屏视频上 |
1350/1920 ≈ 0.703recommendedRatiocard.zone | 解析后的边界 | 常见使用场景 |
|---|---|---|
| 覆盖整个画布 | 核心卡片,视频动画到隐藏/画中画 |
| 内边距40px(横屏)或底部45%(竖屏) | 密集数据卡片,自由边距 |
| 底部30%区域 | 访谈视频注释 |
| 右侧42%(横屏)或底部40%(竖屏) | 侧边栏/“split”布局 |
| 整个画布;卡片根元素需透明 | 全屏视频上的玻璃叠加层 |
card.zone#video-wrapstoryboard.jsonschemaVersion: 3composition: { fps, width, height, durationSeconds, layout, themeId, seed }durationSecondsfpsthemeIdlayoutcompositionvideoTrack: { sourcePath, startSec, endSec, bounds? }subtitles: { enabled, ... }cards[]idintentstartSecendSecaccentIndexzonecontentHintscomposition.durationSecondsdata-track-indexcontentHintscontentHintsstoryboard.jsoncard.zone = 'lower-third' | 'video-overlay'.root/* 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-panelstoryboard.jsonschemaVersion: 3composition: { fps, width, height, durationSeconds, layout, themeId, seed }durationSecondsfpsthemeIdlayoutcompositionvideoTrack: { sourcePath, startSec, endSec, bounds? }subtitles: { enabled, ... }cards[]idintentstartSecendSecaccentIndexzonecontentHintscomposition.durationSecondsdata-track-indexcontentHintscontentHintsstoryboard.jsoncard.zone = 'lower-third' | 'video-overlay'.root/* 模式A:透明根元素,页面背景提供米色背景 */
html,
body {
background: var(--bg);
}
.card[data-card-id="card-X"] .root {
background: transparent;
}
/* 模式B:仅全屏卡片设置明确背景 */
.card[data-card-id="card-hero"] .root {
background: var(--bg);
}
.card[data-card-id="card-overlay"] .root {
background: transparent;
}side-panel$WORK_DIR/public/cards/{card-id}.html$WORK_DIR/public/cards/{card-id}.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">S</span>
<span class="char">u</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<div class="card" data-card-id="{cardId}"><style><script>src=href=onclick=public/var(--accent-N)data-anim-*<script>data-anim-*<div class="card" data-card-id="{cardId}">
<style>
/* 必须:每个规则以.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">S</span>
<span class="char">u</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<div class="card" data-card-id="{cardId}"><style><script>src=href=onclick=public/var(--accent-N)data-anim-*<script>data-anim-*references/styles/*.htmlstoryboard.layout = "portrait"| token | landscape baseline | portrait target | scale |
|---|---|---|---|
| title (h1/h2 hero) | 64–96px | 88–132px | ×1.35 |
| detail / body | 24–30px | 30–40px | ×1.30 |
| kicker / chip label | 14–16px | 18–22px | ×1.30 |
| timecode / meta | 12–14px | 16–18px | ×1.30 |
| data block primary number | 48–60px | 64–88px | ×1.40 |
| line-height multiplier | 1.05–1.5 | same | (don't scale) |
portraitPx = round(landscapePx × 1.3)@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);
}layoutreferences/styles/*.htmlstoryboard.layout = "portrait"| 变量 | 横屏基准 | 竖屏目标 | 缩放比例 |
|---|---|---|---|
| 标题(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 | 相同 | (不缩放) |
竖屏像素 = round(横屏像素 × 1.3)@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);
}layoutdata-animdata-anim| kind | use for | key params |
|---|---|---|
| enter | |
| exit | |
| slide enter | |
| per-char pop | |
| per-char fade | same as kinetic-chars but slower default stagger |
| animate number | |
| SVG path reveal | |
| bar height | |
| bar width | |
| pop entrance | |
| unfocused → focused | |
| clip reveal | |
| tween any CSS | |
data-anim-atstartSec| 类型 | 使用场景 | 关键参数 |
|---|---|---|
| 入场 | |
| 退场 | |
| 滑动入场 | |
| 逐字符弹出 | |
| 逐字符淡入 | 与kinetic-chars参数相同,但默认延迟更慢 |
| 数字动画 | |
| SVG路径展示 | |
| 高度增长 | |
| 宽度增长 | |
| 缩放入场 | |
| 失焦到聚焦 | |
| 遮罩展示 | |
| 任意CSS动画 | |
data-anim-atstartSec$WORK_DIR/public/index.htmlundefined$WORK_DIR/public/index.htmlundefinedundefinedundefined<!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="talking-head-recut"
data-start="0"
data-duration="121.2"
data-fps="30"
data-width="1920"
data-height="1080"
>
<!-- Layer 1: source video — initial position matches card-01's layout -->
<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>
<!-- Layer 2: each card-host sits at the bounds dictated by its layout. -->
<!-- IMPORTANT: every card-host MUST carry BOTH "card-host" and "clip" classes. -->
<!-- - "card-host" → our positioning + pointer-events styles -->
<!-- - "clip" → HyperFrames runtime uses this to enforce visibility -->
<!-- only during data-start … data-start+data-duration. -->
<!-- Without "clip" the host stays visible the whole video -->
<!-- (lint: timed_element_missing_clip_class). -->
<!-- Example: card-01 with zone="fullscreen" → card-host covers (0,0,1920,1080) -->
<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;"
>
<!-- paste the contents of public/cards/card-01.html here -->
</div>
<!-- Example: card-02 with zone="side-panel" (split composition layout) → card on left half -->
<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;"
>
<!-- card-02 HTML -->
</div>
<!-- ...one "card-host clip" per card with inline bounds matching resolveZoneBounds(card.zone)... -->
<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.0);
tl.fromTo(
'.card-host[data-card-id="card-01"]',
{ opacity: 0 },
{ opacity: 1, duration: 0.4, ease: "power2.out" },
1.0,
);
// 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.5, ease: "power2.out", stagger: 0.04 },
1.3,
);
tl.fromTo(
'.card[data-card-id="card-01"] #card-01-line',
{ width: 0 },
{ width: 420, duration: 0.5, ease: "power2.out" },
1.65,
);
// Exit (fade out over 0.35s, ending at endSec)
tl.to(
'.card-host[data-card-id="card-01"]',
{ opacity: 0, duration: 0.35, ease: "power2.in" },
7.15,
);
tl.set('.card-host[data-card-id="card-01"]', { visibility: "hidden" }, 7.5);
// ── 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["talking-head-recut"] = tl;
})();
</script>
</div>
</body>
</html><!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 {
/* 从步骤7的themeId调色板表格中选择——示例: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;
}
/* 正文font-family必须列出具体字体名称(不能仅使用var(--font-family))——
HyperFrames渲染器的静态分析器解析字体时不会展开CSS变量,仅使用变量会触发`font_family_without_font_face`
lint并回退到通用字体。此处使用具体字体链;卡片内部仍可使用var(--font-family)引用主题字体。 */
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包含源视频。其位置/尺寸由主时间线动画控制(每个布局过渡一个动画)。 */
.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;
}
/* 非全屏视频的细微阴影+圆角 */
.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="talking-head-recut"
data-start="0"
data-duration="121.2"
data-fps="30"
data-width="1920"
data-height="1080"
>
<!-- 第一层:源视频——初始位置匹配card-01的布局 -->
<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>
<!-- 第二层:每个card-host位于其布局指定的边界。 -->
<!-- 重要:每个card-host必须同时包含"card-host"和"clip"类。 -->
<!-- - "card-host" → 我们的定位+指针事件样式 -->
<!-- - "clip" → HyperFrames运行时使用此类控制可见性 -->
<!-- 仅在data-start … data-start+data-duration期间可见。 -->
<!-- 没有"clip"类的话,宿主会在整个视频中可见 -->
<!-- (lint错误:timed_element_missing_clip_class)。 -->
<!-- 示例:zone="fullscreen"的card-01 → card-host覆盖(0,0,1920,1080) -->
<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;"
>
<!-- 粘贴public/cards/card-01.html的内容到这里 -->
</div>
<!-- 示例:zone="side-panel"的card-02(split合成布局)→ 卡片在左半部分 -->
<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;"
>
<!-- card-02的HTML -->
</div>
<!-- ...每个卡片对应一个"card-host clip",内联边界匹配resolveZoneBounds(card.zone)... -->
<script src="vendor/gsap.min.js"></script>
<script>
(function () {
// count-up格式化工具
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-01 [1.0, 7.5],kinetic-chars在+0.3,grow-x在+0.65:
// 入场(0.4秒淡入)
tl.set('.card-host[data-card-id="card-01"]', { visibility: "visible" }, 1.0);
tl.fromTo(
'.card-host[data-card-id="card-01"]',
{ opacity: 0 },
{ opacity: 1, duration: 0.4, ease: "power2.out" },
1.0,
);
// 卡片内部动画(将每个data-anim-*声明编译到这里)
tl.from(
'.card[data-card-id="card-01"] #card-01-title .char',
{ opacity: 0, y: 8, scale: 0.8, duration: 0.5, ease: "power2.out", stagger: 0.04 },
1.3,
);
tl.fromTo(
'.card[data-card-id="card-01"] #card-01-line',
{ width: 0 },
{ width: 420, duration: 0.5, ease: "power2.out" },
1.65,
);
// 退场(0.35秒淡出,在endSec结束)
tl.to(
'.card-host[data-card-id="card-01"]',
{ opacity: 0, duration: 0.35, ease: "power2.in" },
7.15,
);
tl.set('.card-host[data-card-id="card-01"]', { visibility: "hidden" }, 7.5);
// ── 视频布局过渡 ──
// 当下一张卡片使用不同合成布局时,将video-wrapper动画到新边界。示例:card-01=全屏
// (视频在卡片后隐藏),card-02=split合成布局(zone="side-panel"
// → 视频在右侧,卡片在左侧)。
// card-02在8.0秒入场,使用split合成布局。在card-01→card-02的间隙(7.5到8.0秒)将视频动画到右半部分。
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入场——与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内部动画...
// ── 为每张卡片重复上述步骤;如果下一张卡片布局不同,
// 在其入场前插入另一个tl.to('#video-wrap', ...)动画 ──
window.__timelines = window.__timelines || {};
window.__timelines["talking-head-recut"] = tl;
})();
</script>
</div>
</body>
</html>data-anim.card[data-card-id="X"] #elementId| data-anim | GSAP statement template |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
T = Math.round(absSec * fps) / fps1/30 ≈ 0.0333s.toFixed(4)data-anim.card[data-card-id="X"] #elementId| data-anim | GSAP语句模板 |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
T = Math.round(absSec * fps) / fps1/30 ≈ 0.0333s.toFixed(4)layoutlayout#video-wraptl.to('#video-wrap', { ...bounds }, T)ease: 'power2.inOut'cleanhairlinepolaroid#video-wrapreferences/frames/overlaysplitstack#video-wrapreferences/layouts/*.html| composition layout | typical card.zone | | extra css class |
|---|---|---|---|
| | | — |
| | | — |
| | | |
| | | |
| | | — |
| hide video (pure-graphic moment) | | | — |
// 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,
);zonestyle="left:Xpx;top:Ypx;width:Wpx; height:Hpx;..."video-overlay.card .root#video-wraptl.to('#video-wrap', { ...bounds }, T)ease: 'power2.inOut'cleanhairlinepolaroid#video-wrapreferences/frames/overlaysplitstack#video-wrapreferences/layouts/*.html| 合成布局 | 典型card.zone | | 额外CSS类 |
|---|---|---|---|
| | | — |
| | | — |
| | | |
| | | |
| | | — |
| 隐藏视频(纯图形时刻) | | | — |
// 进入PiP——添加样式
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,
);
// 退出PiP——回到全屏
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,
);zonestyle="left:Xpx;top:Ypx;width:Wpx;height:Hpx;..."video-overlay.card .rootwindow.__timelines["talking-head-recut"]asyncsetTimeoutplay()Math.random()Date.now()repeat: -1xyscalerotationopacitytopleftwidthheight#video-wrapdata-track-indexdata-layerdata-durationdata-endcard-hostclass="clip"class="card-host clip".clipdata-start … data-start+data-durationtimed_element_missing_clip_classfont-family'Inter', 'Caveat', …var(--font-family)font_family_without_font_facevar(--font-family)@font-facewindow.__timelines["talking-head-recut"]asyncsetTimeoutplay()Math.random()Date.now()repeat: -1xyscalerotationopacitytopleftwidthheight#video-wrapdata-track-indexdata-layerdata-durationdata-endcard-hostclass="clip"class="card-host clip".clipdata-start … data-start+data-durationtimed_element_missing_clip_classfont-family'Inter', 'Caveat', …var(--font-family)font_family_without_font_facevar(--font-family)@font-facecd "$WORK_DIR"
PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \
--skill=talking-head-recut \
-o output.mp4 \
--fps 30hyperframes render <dir><dir>/index.htmlPRODUCER_BROWSER_GPU_MODE=hardware--browser-gpunpx hyperframes snapshot public --at 5 # → public/snapshots/frame-00-at-5s.png (a single --at ignores --out)cd "$WORK_DIR"
PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \
--skill=talking-head-recut \
-o output.mp4 \
--fps 30hyperframes render <dir><dir>/index.htmlPRODUCER_BROWSER_GPU_MODE=hardware--browser-gpunpx hyperframes snapshot public --at 5 # → public/snapshots/frame-00-at-5s.png(单个--at忽略--out)storyboard.jsonpublic/cards/*.htmlpublic/index.htmloutput.mp4public/index.html(cd "$WORK_DIR/public" && npx hyperframes preview) # or `npx hyperframes play` for a shareable linkstoryboard.jsonpublic/cards/*.htmlpublic/index.htmloutput.mp4public/index.html(cd "$WORK_DIR/public" && npx hyperframes preview) # 或`npx hyperframes play`获取可分享链接