引人入胜的解说视频
一次拍摄的视觉故事。一个单一的英雄元素(在参考风格中,是一个作为开关旋钮的白色小球)穿过一系列小型界面“站点”,每个站点都会对其做出反应:它沿着滑块滚动并将其推至100,从末端掉下来并在经过时弹跳出一堆通知,降落在键盘上并在按键之间弹跳输入一个单词,按下回车键后被弹射回空中。相机从不剪辑:它让英雄元素保持在画面中心附近,并为每次交互进行放大,在高潮时它会拉远镜头,让观众一次性看到整个路线布局,然后再重新回到起点。
对于产品来说,站点是其工作流程的步骤,按空间布局在一个画布上,英雄元素则是贯穿该工作流程的事物。拉远镜头是“现在你明白它是如何工作的”时刻。
你将其编写为一个HTML页面,根据时间值渲染任何帧,然后将其渲染为带运动模糊和音效的MP4文件。
第一步:获取简报(发送一条消息,然后独立工作)
用一条消息询问所有内容,然后独立工作。只有产品是必需的;对于任何留空的内容,自行决定并说明你的选择。
- 产品:名称以及用几句话描述其功能。提供网站URL更好。
- 观众必须记住的一件事(例如:“输入想法,获得实时应用”)。
- 品牌:颜色、字体、Logo文件,或者“使用网站”。如果没有,则使用下面的默认调色板。
- 格式:16:9(1920×1080)或9:16(1080×1920)。默认16:9。
- 时长:默认时长如下所述。
- 音乐:用户自己的音轨(文件路径),或者让你合成一个。默认:合成。
- 方向(可选):他们想要的场景、标语、行动号召或其他任何需要避免的内容。
如果他们提供了URL,请在写任何内容之前阅读它(网页抓取)以获取产品的实际措辞、功能、颜色和字体名称。在UI标签和标语中使用他们的真实措辞。
切勿捏造事实。 不得编造客户数量、收入、评级、推荐或引用。数字仅当用户或网站明确说明时才可使用;否则使用相对条形图、进度和占位符骨架线条。UI内部的内容示例(如提示、文件名、项目名称、收据上的价格)是可以的,并且应该是可信的;但关于产品本身的声明(如统计数据、用户数量、评级、“免费”等优惠)则不允许。行动号召仅使用用户或网站提供的URL或优惠;如果没有,则使用名称和标签结尾。
Logo。 仅当用户提供文件或这是他们自己的产品且他们要求时才使用Logo。否则,以品牌字体文本形式书写名称。第三方Logo(如集成)仅当提供时才使用;否则使用带名称的中性圆角方块。
另外,询问(可选):3-6个步骤的工作流程(用户做什么 → 产品做什么 → 结果)。如果他们提供了URL,则从网站推导出来。
此风格的默认时长:12秒,无缝循环。范围10-30秒(更长 = 更多站点,而不是更慢的站点)。
第二步:设计路线,然后构建
1. 选择英雄元素
英雄元素是一个简单、可读的物体,代表产品故事的核心,并且它永远不会消失或被替换(它可能会暂时改变形状,然后恢复):一个小球/旋钮(默认,适用于任何产品)、一个文本光标或插入符、一个文件或文档方块、一个消息气泡、一枚硬币或订单、一个数据点。它应该自然地融入站点的界面(如一个旋钮、一个拇指滑块、一个键帽、一个引脚)。
2. 将工作流程映射到站点
每个步骤成为一个站点:一个紧凑的真实界面元素,英雄元素与之物理交互,并且该站点因为交互而明显变化。从这些机制中汲取灵感:
| 机制 | 英雄元素... | 站点反应 |
|---|---|---|
| 开关/按钮 | 是旋钮,或按下它 | 开启,颜色填充 |
| 滑块/进度条 | 沿着它滚动作为拇指 | 工具提示中数值增加,轨道填充 |
| 通知堆栈 | 落在每张卡片上并弹跳 | 每张卡片在被击中时弹出,并带有叙述故事的信息 |
| 键盘/输入框 | 在按键之间弹跳 | 每个按键按下,字母出现在上方输入框中 |
| 列表/表格 | 沿着行滑动 | 每行高亮、打勾或排序 |
| 图表 | 成为线条的前导点 | 线条在它后面绘制,条形图在它经过时升起 |
| 聊天 | 作为气泡进入 | 回复打字输出 |
| 部署/发送按钮 | 被它发射 | 出现toast或URL |
站点的标签和信息用几个词讲述故事,并带有一点诙谐(参考风格使用滑块 · 达到100,重力 · 有东西在坠落,消息 · 现在呢?,然后输入重试)。对于产品,根据其实际工作流程编写(例如编码工具:输入提示 → 代理 · 正在编写12个文件 → 进度 → 预览 · 准备就绪 → 按下部署 → 已上线 myapp.app)。
3. 布局世界
将站点放置在一个大的画布上(约为帧大小的3倍),按英雄元素访问的顺序排列,路径呈Z字形(顶部向右,中间向下,底部横向)。重力应合理:坠落向下,发射向上。每个站点的位置在整个视频中固定不变。世界是一个带有微弱点状网格的深色画布,该网格随相机移动(网格使相机移动清晰可读)。
4. 编写英雄元素的路径
英雄元素的位置是时间的纯函数,由接触点之间的线段构建而成(每个接触点对应一个节拍):
- 滚动/滑动沿站点:位置使用
ease或弹簧沿轨道运动;站点的值从英雄元素的位置派生。 - 跳跃和坠落在接触点之间:抛物线(
x随时间线性变化,y = y0 + v·t + ½·g·t²,求解v使其在节拍时降落在下一个接触点)。重力在整个视频中是常数。 - 撞击: 接触时压缩(缩放1.25 × 0.8,持续约60毫秒,然后弹簧回弹),飞行时沿速度方向拉伸(最多1.15 × 0.9),以及被撞击物体的轻微下压。
- 发射: 最后一个站点将英雄元素向上并向后朝向起点发射,形成一条长弧线,同时每个站点以相反顺序重置(数值递减,通知取消,输入框清除),以便循环闭合。以慢动作运行发射弧线:首先选择所需的顶点高度(路线顶部),该高度在重力下确定实际飞行时间,然后扭曲时间,使飞行填满揭示窗口,在顶点处最慢(通常为5-10倍;例如速度
1 − 0.9·sin(πu)^0.6在弧线上)。在揭示保持期间(而非发射瞬间)调度站点重置,在观众看到完整路线之后。 - 无缝: 将循环点设置在英雄元素降落在第一个站点时。
seek(duration)必须看起来与seek(0)完全一样;着陆压缩和按键按下也显示在时间0。对于声音,在buildAudio内部将音轨渲染到你自己稍长的OfflineAudioContext中,将末尾超出duration的部分折叠回开头,并将该缓冲区播放到给定的上下文中。 - 重力是侧向的。 布局站点时使坠落从侧面看合理:通知卡片形成楼梯(每张卡片水平偏移超过其宽度,以便英雄元素永远不会穿过它们),键盘正面朝上,英雄元素以微小弧线在按键之间跳跃。
- 空槽: 当英雄元素曾是站点的一部分(如旋钮、拇指)并离开时,站点保留一个其离开的槽位的暗淡轮廓,当英雄元素返回时该轮廓再次填充。
5. 相机
相机是难点;在优化其他内容之前先确保其正确。
- 跟随: 相机目标 = 英雄元素位置(加上约0.15秒速度的前置量)与当前站点的固定锚点的混合,通过临界阻尼弹簧(
w≈ 6)平滑处理,以便在撞击时永不抖动。在交互期间权重偏向锚点约0.7(站点保持在画面中),在坠落和跳跃期间权重偏向前置量0.15(英雄元素领先),在揭示保持期间权重为0.85。平滑处理需要历史记录,因此在加载时烘焙(参见页面约定),从时间0处第一个站点的固定构图开始,并在前0.8秒内过渡到跟随。英雄元素永远不会离开帧的中间一半。 - 变焦: 每次交互期间变焦紧密:站点填充约一半的帧宽,英雄元素在屏幕上50-70像素(在1080p下)。在坠落和跳跃期间略微拉宽,以便下一个站点已经可见。将站点居中构图,而非空白画布。
- 揭示保持: 在发射之前,在最终结果(toast、实时URL)上保持0.4-0.6秒,完全在帧内。
- 揭示: 在发射时,相机停止跟随并缓动至适合整个路线的固定构图(约0.3倍),带有轻微的3D倾斜(
perspective+rotateX15-25°)以增加深度。保持约1秒,同时英雄元素在其间弧线运动;放大英雄元素并为其添加柔和光晕,使其保持最亮、最显眼(在屏幕上至少24像素)。然后俯冲至第一个站点的固定构图,时机同步使英雄元素在相机到达时着陆,并混合回跟随模式。 - 真实运动模糊: 英雄元素和世界在快速移动时产生模糊;这是外观的一部分。
外观
- 默认调色板(深色iOS风格):画布
#0C0C0E,点状网格#1E1E22,表面#1C1C1F/#2C2C2E,文本#F2F2F7,次要#8E8E93,强调色#0A84FF,英雄元素白色#FFFFFF。品牌颜色替换强调色;英雄元素保持屏幕上最亮。浅色变体(画布#F2F2F4,英雄元素 = 强调色)在品牌是浅色时也可以。 - 逼真的系统UI: 开关、带刻度标记的滑块、带有应用图标方块、标题、正文行和
现在的通知卡片;带有键帽和回车键的键盘。字体:类似SF(Inter或Geist)。 - 光标可以启动链条(点击第一个站点)然后让开。
- 禁止: 剪辑、淡入黑屏、解释正在发生什么的文本叠加(站点自己的标签是唯一文本)、多个英雄元素、不反应的站点、看起来轻飘飘的物理效果。
声音
紧凑、极简的节拍,节奏为110-120 BPM(柔和的底鼓、咔嗒响的镲片、柔和的拨弦贝斯),并且英雄元素的接触是主要乐器:每次撞击播放一个短促的音高嘀嗒声或马林巴音符(随着故事推进音高上升),按键按下发出咔嗒声,滑块移动发出随数值增加而加速的细微嘀嗒声,通知发出柔和的“噗”声,发射发出上升呼啸声,俯冲发出下降呼啸声。每个声音恰好落在其视觉接触点上。
页面构建方式
一个自包含的HTML文件(内联CSS和JS;图片仅当用户提供时才包含)。使用DOM + CSS变换、SVG或一个全屏画布进行绘制,选择适合的方式。
页面约定
window.VIDEO = {w, h, duration}和window.seek(t),一个时间的纯函数:为时间t(秒)设置每个元素。没有CSS过渡或动画,没有setTimeout,没有从前一帧继承的状态。任何帧必须能够独立、按任意顺序正确渲染。在seek内部测量布局(例如offsetLeft、measureText)是可以的;它是确定性的。- 随机性(颗粒、粒子、音频中的噪声)来自种子生成器,绝不使用
Math.random。 - 任何需要历史记录的内容(滞后于移动物体的相机、模拟)在加载时烘焙一次:以1/240秒的步长从0到
duration进行逐步模拟,存储值,并使seek通过插值读取它们。对于循环,从循环开始的初始状态开始模拟(参见风格的缝合规则)。 - 循环:
seek(t)使用t % duration;跨越缝合线的效果(着陆压缩、声音尾部)在两端都绘制或混合。 - 当URL中包含
?render时:没有控件,没有预览缩放;舞台正好是w × h,位于左上角。没有它时:舞台使用CSS缩放以适应窗口,并显示预览控件。 - 字体:嵌入它们。 下载woff2文件(Google Fonts或品牌的字体)并将其内联为base64
@font-face,以便渲染永远不会无声地回退。对于字重变化,使用带有范围的变量字体([email protected])。 - 图层: 为场景图层提供透明背景,让一个舞台背景在其后提供基色,这样后面的图层就不会意外隐藏前面的图层。绝对定位的文本获得
white-space: nowrap。 - 速度: 大的CSS
filter: blur()和大的模糊渐变在截图时非常慢。在加载时在画布上绘制渐变和光晕一次(或作为图像),然后移动它们。
辅助工具:粘贴这些并在其上构建所有内容
// 阻尼弹簧阶跃响应 0 -> 1。w = 速度(8慢,14正常,22快),z = 阻尼(1 = 无过冲)
function spring(t, w = 14, z = 0.82) {
if (t <= 0) return 0;
if (z >= 1) return 1 - Math.exp(-w * t) * (1 + w * t);
const wd = w * Math.sqrt(1 - z * z);
return 1 - Math.exp(-z * w * t) * (Math.cos(wd * t) + (z * w / wd) * Math.sin(wd * t));
}
// 一个多次更改目标的值,对每个目标进行弹簧运动:track(start, [[time, value, w?, z?], ...]) -> f(t)
const track = (v0, keys) => t => { let v = v0, p = v0; for (const [tk, val, w, z] of keys) { v += (val - p) * spring(t - tk, w, z); p = val; } return v; };
// 精确缓动关键帧(最适合相机移动):kf([[t0, v0], [t1, v1, easeFn?], ...]) -> f(t)
const kf = keys => t => { if (t <= keys[0][0]) return keys[0][1]; for (let i = 1; i < keys.length; i++) { const [a, va] = keys[i - 1], [b, vb, e = ease] = keys[i]; if (t <= b) return lerp(va, vb, e((t - a) / (b - a))); } return keys[keys.length - 1][1]; };
const clamp = (x, a = 0, b = 1) => Math.min(b, Math.max(a, x));
const ease = x => (x = clamp(x), x < .5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2); // easeInOutCubic
const lerp = (a, b, k) => a + (b - a) * k;
const seg = (t, a, b) => clamp((t - a) / (b - a)); // t在[a, b]中的0..1进度
const rng = s => () => (s = (s +
…
[为翻译而截断]