Opus Visual Motion Engine
一个严谨的视觉交付品管线:规划 → 从模板构建 → 渲染 → 质量保证 → 修复 → 导出。质量来自于渲染前后设置的关卡,而非个人品味。请按顺序执行步骤。即使任务“看起来很简单”,也不要跳过任何一步。
以下路径是相对于此技能目录的(你被给予的 skill_resources 基础目录)。称之为 $SKILL。
0. 先阅读(无条件)
在编写任何代码之前,请阅读与交付品匹配的参考资料。它们在其主题上具有权威性:
| 交付品 | 阅读 |
|---|---|
| 视频、动态图形、解说视频、产品视频 | references/video-generation.md、references/motion-design.md、references/javascript-animation.md |
| 动态字体 / 以字体为主导的作品 | 以上视频组 + references/typography.md |
| 落地页、仪表盘、模型、UI | references/visual-design.md、references/typography.md |
| 图表、原理图、数据可视化 | references/visual-design.md §4 |
| 取景 / 镜头决策 | references/cinematography.md |
| 时长和缓动 | references/animation-primitives.md |
| 需要同步的音乐或人声 | SKILL.md §10 + references/video-generation.md §6 |
工作示例位于 examples/*.md。阅读最接近需求的那个。
1. 在代码之前制定计划(写下来)
在创建 HTML 之前,将计划写在你的回复中(或输出旁边的 PLAN.md 中)。仅存在于你脑海中的计划不算数。
UI / 页面
- 核心信息:一句话。观众必须在 3 秒内理解什么?
- 具名方向:一个名称,4–6 个具名十六进制标记及其指定含义,带有角色的字体,形态处理方式。从主题的行业、材料和行话中衍生。玩具店和金融科技仪表盘从不共享一个方向。
- 层次结构:将每个区块标记为 PRIMARY / SECONDARY / TERTIARY。只有 PRIMARY 获得那一个大胆的动效。
- 部分推进:每部分一行,说明其任务以及如何传递给下一部分。
- 预渲染计算:文本宽度 ≈
chars × 0.55 × font-size(无衬线字体)或× 0.5(衬线字体);框宽度 =max(title_chars × 8, subtitle_chars × 7) + 24以 14px 为基准;行内排列 = Σ宽度 + 间距 ≤ 容器。追踪每个连接器与每个框的关系。如果连接器穿过某个框,则规划一条 L 形路径。
视频 / 动态图形
- 核心信息:一句话。如果需要两句,那就是两个视频。
- 节拍:3–7 个。每个节拍回答“观众现在知道什么?”
- 每个节拍的场景卡:开始/结束(秒),构图的组成、镜头、字号、图元(2–4个)、退出。使用
references/motion-design.md§2 中的模板。 - 标志性时刻:命名机制被展示(而非标记)的那一帧(例如,从噪声中过滤出信号)。如果故事仅能通过其字幕工作,请重新设计该节拍。
- 全局时间线:绝对时间戳。场景的 EXIT 完成,然后下一场景才能占用该空间。将
DURATION设置为真实的结束时间(默认为 10–20 秒;仅在有旁白时使用 30–45 秒)。
没有场景卡的场景不属于视频。
2. 从模板构建(视频)
将 assets/composition-template.html 复制到交付品旁边,并填充 EDIT 块。不要重写 ENGINE 块。它们已经提供了:
- 一个 1920×1080 的
#stage(为 1080×1920 或 1080×1080 更改STAGE_W/H),适配任何窗口; window.DURATION、window.seek(t)、window.render(t)、window.SCENES:捕获/导出合约(QA 从SCENES捕获每个场景);at()、ease.*、lerp、mulberry32(种子化 PRNG)以及用于场景可见性加 EXIT 的layer();- 音频辅助函数
audioAt(t, "rms"|"low")、since(t, list)、hit(t, list, dur),这些函数读取window.AUDIO(§10),并在没有音频时返回 0; - 一个预览播放器(Space、←/→、Home/End),在捕获和减少运动模式下自动关闭;
- 在所需的 3 层(浅色、受
prefers-color-scheme保护、[data-theme])中的颜色标记。
对于页面,按照 §6 编写一个普通的单文件 HTML。
3. 确定性时间(视频)
render(t)是唯一的状态驱动程序。每个位置、透明度、大小、数字和 dashoffset 都是根据t(以秒为单位)计算得出的。seek(x)会产生完全相同的帧,无论之前渲染了什么。t以秒为单位。将t限制在[0, DURATION]内,切勿限制在[0, 1]:将秒限制在 0..1 会静默地冻结整个时间线。- 禁止:
setTimeout、setInterval、CSS 动画/过渡作为时间线、未种子化的Math.random()、状态中的Date.now()。requestAnimationFrame只能推进播放时钟并调用render(clock)。模板已经这样做了。 - 随机性:在构建时通过
mulberry32(seed)生成,切勿在render内部生成。 - SVG 描边动画需要全部三个属性:
pathLength="1"、stroke-dasharray="1"和stroke-dashoffset从 1 到 0。没有 dasharray,线条会从第 0 帧开始完全绘制出来。 inline-block单词跨度会吞掉尾随空格。将间距放在margin-right中。
4. 运动语法
五种运动,每种有一个任务。如果一个运动不属于这些类别,请删除它。
- ENTER:每个页面或场景的一个编排好的序列,按阅读顺序,兄弟元素间隔 60–120ms。每个部分都淡入是 AI 的标志。
- EMPHASIS:一次一个元素:缩放膨胀(≤1.2)、发光或具有含义的颜色变化。
- TRANSITION:一个状态变化。变化的部分移动;其他所有部分保持静止。0.2–0.6 秒。
- CAMERA:阶段级的缩放/平移,每个场景最多一次,0.8–1.2 秒,切勿在文字阅读期间进行。
- EXIT:0.3–0.5 秒,完全完成。没有元素在场景边界处处于中间状态,除非是一个有意的连续性锚点,该锚点保持不变或变形(同一对象改变状态,这是可用的最强过渡)。
过渡承载含义:硬切 = 类别变化,变形 = 同一事物变化,平移 = 遍历。切忌到处使用默认的交叉淡入淡出。持续时间和缓动函数在 references/animation-primitives.md 中。一起移动的相邻元素应设置不同的持续时间或偏移量。
5. 堆栈决策
| 情况 | 堆栈 |
|---|---|
| 页面微动效 | CSS 在 transform/opacity 上的过渡,0.2–0.6 秒;循环 ≤2 秒,位于 @media (prefers-reduced-motion: no-preference) 内部 |
| 视频 / 动态图形 / 动态字体 | 模板 + 普通 render(t)(默认) |
| >6 个交错轨迹或 SVG 变形编排 | GSAP UMD 锁定,一个暂停的主时间线,seek(t) → tl.seek(t) |
| 图表 | 内联 SVG(默认)或 Chart.js UMD 且 animation:false、十六进制颜色(Canvas 无法读取 CSS 变量) |
| 生成艺术 | p5.js 且 randomSeed/noiseSeed |
| 3D,仅当 3D 是信息本身时 | Three.js UMD 锁定;在 render(t) 中编写镜头脚本 |
使用能达到结果的最简单堆栈。外部脚本必须是来自 cdnjs.cloudflare.com 或 cdn.jsdelivr.net 的确切锁定版本。其他所有内容都内联。每种字体都需要一个真正的后备堆栈。
6. 技术底线(不可协商)
- 单个
.html文件,CSS 和 JS 内联,小于 16 MB,图像/数据作为data:URI。 - 颜色作为
:root标记;暗色层位于@media (prefers-color-scheme: dark)下,受:root:not([data-theme="light"])保护,并重复于:root[data-theme="dark"]下;显式设置body背景。 meta viewport带有viewport-fit=cover;env(safe-area-inset-*)内边距;html/body 上使用height:100%,切勿使用100vh;宽块在其自己的overflow-x:auto内滚动;网格使用minmax(0,1fr)。- 显示的计算数字需通过
Math.round、toFixed或Intl.NumberFormat。计数器应恰好落在最终值上。 - 可访问性:对比度 ≥4.5:1(≥24px 文本时为 ≥3:1)、可见焦点、尊重
prefers-reduced-motion(模板显示稳定的结束帧)。 - 可读字体:在 1080p 视频帧中,文本不低于 20px(H/54)。在移动页面上,正文不低于 14px。
- 浏览器存储绝不应持有时间线状态。如果使用了存储,将其包裹在 try/catch 中。
7. 避免通用 AI 设计
在需求留有余地的地方,绝不要将其花在这些默认项上(qa.mjs 会自动标记其中几项):
| 标志 | 修正 |
|---|---|
米色 #F4F1EA 之类 + 衬线展示字体 + 赭色 #D97757 之类强调色 | 取自主题自身材料的调色板 |
| 近乎黑色 + 一种酸性强调色,不论主题如何都使用 | 仅当方向在书面中得到证实时才允许 |
| 相同的圆角卡片,所有地方使用相同圆角,相同的柔和阴影 | 按层次结构使用圆角标记;卡片仅用于有边界对象 |
| 作为装饰的渐变、紫色渐变、玻璃态 | 纯色填充;最多一个渐变,仅用于连续的物理属性 |
| 全大写的眉毛标签、中间点元数据字符串、链接上的“→” | 使用句子大小写;删除重复内容的标签 |
| 默认使用 Inter / 系统字体 | 为此主题选择的字体系列,带有后备堆栈 |
| 标题中仅强调一个词 | 仅当强调色是一个语义标记,并在作品的其他地方用于相同含义时才允许(例如,珊瑚色 = 噪声) |
| 通用英雄区:标题 + 副标题 + 行动号召 + 截图 | 以主题世界中最具特色的东西开场 |
| 所有内容居中,每个部分使用相同的网格节奏 | 每个部分使用不同的对齐系统;变化节奏 |
| 所有元素同时进行动画,所有地方都是 300ms | 持续时间映射 + 交错(§4) |
将大胆用在一个地方,然后移除一个装饰元素。
8. 渲染 + 质量保证(交付前强制进行)
运行自动化质量保证。它会在无头模式下渲染文件并写入证据和结论。需要 Node ≥18。在技能的新副本中首次运行需要在 $SKILL/scripts 目录中执行 npm install(如果 scripts/node_modules 存在则跳过)。
node "$SKILL/scripts/qa.mjs" path/to/deliverable.html
在 Windows 的 DSH 中,使用 pwsh 工具和相同的命令运行它。选项:--step 0.25(为快速场景提供更精细的网格)、--out <dir>、--mode page|video。
视频模式(当存在 DURATION + seek 时自动启用)每步捕获一帧并检查:禁止的 API、乱序重访的确定性、冻结的时间线、无信号区间、近乎空的跨度、不稳定的结束帧、文本碰撞、文本覆盖其他图形、裁剪、对比度、最小字号、从不绘制的 dashoffset 线条、减少运动模式下的循环、控制台/页面错误以及模板痕迹。
页面模式渲染桌面 1440、移动端 390 和深色方案,并检查相同的布局规则以及移动端水平溢出。
输出位于 qa-<name>/ 目录下:report.md(结论和发现)、report.json(每帧的油墨覆盖率和变化)、contact-sheet.png(所有帧合并到一张图片上)、moments.png(取自 window.SCENES 的每个场景的进入、中间和结束)、key_000..100.png 和 frames/。
- 显式传递标志性时刻和任何同步点:
--at 5.4,9.1。 - 有意的分层(有目的地堆叠的动态字体、覆盖在素材上的标题)应放入
data-qa-allow-overlap元素中。这将仅针对该子树跳过碰撞和文本覆盖图形检查。切勿用它来掩盖意外的碰撞。 font警告意味着首选的字体家族未加载,文本处于后备字体状态。在判断布局之前修复它,因为所有宽度都是错误的。
退出代码 0 = 无 FAIL。FAIL 是阻塞性的。 每个 WARN 要么被修复,要么在你的评论中用一行文字说明理由。
9. 评论循环
- 如果你的模型可以读取图像: 打开
contact-sheet.png,然后打开任何看起来不对的key_*.png。评判像素,而非你的意图。 - 如果你的模型是纯文本的: 使用
report.md加上report.json中的每帧表格。coverage< 0.015 意味着该帧读起来是空的。场景内diff为 0 意味着没有移动。diff的峰值标记了一个剪切点。对照这些数字交叉检查每个场景卡的时间戳。 - 用具体的观察(而非“✓”)回答每个问题:
- 构图:在每个关键帧,视线首先落在哪里?那是信息所在吗?
- 层次结构:PRIMARY 是否明确无误?每一帧是否恰好有一个重要的移动元素?
- 机制:标志性时刻是否在移除字幕的情况下展示了想法?
- 连续性:所有交接是否干净,边界处没有处于中间状态的内容,锚点是否保持不变或变形?
- 时序:是否存在偏移量,没有默认持续时间,每 8 个单词的阅读时间 ≥1.2 秒?
- 排版 / 对比度 / 密度:尺寸符合计划,每帧 ≤5 个竞争元素,在深色模式下可读?
- 叙事:观看者能否在观看一次后复述核心信息?
- 修复 → 重新运行
qa.mjs→ 重新评论。重复直至 QA 通过且评论中没有未解决的项目。 - 最后一个问题,诚实地回答:“这看起来是有意设计的,还是仅仅是生成的?” 如果是“生成的”:指出哪些是通用的,修改计划(§1),然后重建。第一次渲染几乎永远不是最终交付品。预计需要 2–3 次生成。
10. 音频同步(当有音乐或人声时)
切勿凭耳朵猜测时间。将音频转换为数据,让 render(t) 读取它:
node "$SKILL/scripts/audio-data.mjs" music.mp3 --inject composition.html # 将 window.AUDIO 写入 HTML
AUDIO.beats(在估计的节拍下的均匀间隔网格)、AUDIO.onsets(重音、音符、音节)以及每帧的AUDIO.rms/AUDIO.low(0..1;low是 <150 Hz 的底鼓/低音段)。- 使用模板辅助函数驱动运动:使用
hit(t, AUDIO.beats, 0.2)进行节拍脉冲(EMPHASIS),使用audioAt(t, "low")进行跟随底鼓的缩放,使用since(t, AUDIO.onsets)进行每次重音的揭示。将场景剪切放在节拍上:从AUDIO.beats中选择场景起始点。 - 同步从定义上来说是帧精确的:脉冲落在其节拍处或之后的第一个帧上(≤1 帧,30 fps 下 ≤33 毫秒)。如果音频比 t=0 晚开始,则传递
--offset <s>。 - 在任何人监听之前检查电平。削波的样本会退出代码 1。样本峰值高于 −1 dBFS 会发出警告,因为 AAC 编码会将其削波。报告响度在 −18..−12 LUFS 之外(平台会归一化到大约 −14)。
--bpm可覆盖估计的 BPM。传递--no-inject以仅写入 JSON。