01 / OVERVIEW

方案总览:我们要做什么

一句话概括:把一段"人物转身"的视频当作可拖动的胶片,鼠标左右移动 = 用手搓动胶片,从而实现"鼠标往哪移,人物往哪看"的丝滑交互。本教程覆盖从 AI 图像生成 → 首尾帧视频生成 → 网页部署的完整工作流。

效果是什么样的

页面背景是一幅山巅侠客的画面。它不自动播放——视频完全静止,时间轴由你的鼠标接管:鼠标向左滑,人物上半身、头部和眼神缓缓转向画面左侧;向右滑,则转向右侧。天空、云彩、机位、站位全程不变。

鼠标在左 → 人物朝左
鼠标居中 → 人物背影
鼠标在右 → 人物朝右

完整工作流:三步走

本方案采用图像先行、首尾帧驱动、网页收尾的三段式工作流:

① 生成静态图像 Midjourney / 可灵 / SD 出图 质量控制点:构图、光影一致性 ② 首尾帧生成视频 静态图 → 动态转身 质量控制点:运动幅度、背景静止 ③ 网页交互部署 HTML + JS + FFmpeg 转码 质量控制点:关键帧密度、seek 丝滑 IMAGE → VIDEO → WEBPAGE 图像先行保证画面质量,首尾帧驱动保证运动可控,网页层保证交互体验
图 1-1 · 三段式工作流:每段都有独立的质量控制点

为什么不能直接"播放"

普通视频是"放电影":时间轴自己往前走。而我们要的是"拉胶片":时间轴完全听鼠标指挥,鼠标不动画面就静止。所以技术上有三个关键词:

核心 1

不自动播放
视频 muted + playsInline + preload="auto",并显式 pause()。

核心 2

鼠标映射时间轴
鼠标 X 坐标 ÷ 屏幕宽度 = 视频进度百分比。

核心 3

平滑 seek
用插值 + 队列防抖动,杜绝机械感和卡顿。

✓
给小白的一句话你不需要懂视频编码原理,只要记住:AI 负责出图,首尾帧工具负责让图动起来,FFmpeg 负责"翻译"成浏览器喜欢的格式,JS 负责把鼠标动作翻译成时间轴位置。本教程每一环都给了可直接复制的命令和代码。
02 / DESIGN PHILOSOPHY

设计思路:为什么要这样做

技术方案不是凭空产生的。理解"为什么图像先行"、"为什么首尾帧驱动"、"为什么不用 GIF/Canvas 序列帧",才能在遇到问题时做出正确判断。

为什么"图像先行"而不是直接生成视频

当前 AI 视频生成工具在直接文生视频时有一个共同弱点:画面细节不可控。人物长相、服装纹理、背景元素每一帧都在微妙变化,做成交互视频后来回 seek 会出现"脸部漂移""衣服变色"等诡异现象。

而图像先行的策略是:先用文生图工具生成一张高质量的静态画面,确保构图、人物、光影、风格都达到满意标准。然后以这张图作为视频生成的起点(首帧),再生成一张转身后的姿态作为终点(尾帧)。这样:

优势 1

画面质量可控
静态图可以反复抽卡、局部重绘、upscale,直到满意为止。

优势 2

首尾一致性
首帧和尾帧都是静态图,中间过渡由 AI 补全,不会出现人物"变形"。

优势 3

成本更低
文生图比文生视频便宜一个数量级,试错成本大幅降低。

为什么用"首尾帧"而不是"图生视频"单帧驱动

很多工具支持"上传一张图,让图中内容动起来"(图生视频)。这种方式的问题是:AI 会自由发挥运动方向和幅度。你可能想要"人物从右往左转",AI 却理解成"人物向前走一步"或"镜头推进"。

首尾帧模式(First Frame + Last Frame)相当于给 AI 两个锚点:起点姿态和终点姿态都已锁定,AI 只需要填补中间的运动过渡。这大大减少了"AI 自由发挥"的空间,让转身方向、幅度、速度都更加可控。

首尾帧模式 vs 单帧图生视频 ✓ 首尾帧模式 首帧:人物朝右 尾帧:人物朝左 AI 只补中间过渡 ✗ 单帧图生视频 只给首帧 AI 自由决定运动 方向/幅度不可控 → 可控性大幅提升
图 2-1 · 首尾帧模式把 AI 的发挥空间压缩到"过渡补间",结果更可控

为什么用 HTML5 Video 而不是 GIF / Canvas 序列帧

方案优点缺点本方案选择
GIF兼容性好,实现简单色深只有 256 色,文件巨大(10MB 视频 → 50MB+ GIF),不支持透明控制不推荐
Canvas 序列帧逐帧精确控制,可叠加特效需预加载几十到几百张 PNG,首次加载极慢,内存占用高不推荐
HTML5 Video浏览器原生解码,硬件加速,文件小,支持 seek需要处理关键帧密度、编码格式兼容性推荐
WebM + AV1压缩率极高,画质好Safari 支持差,编码慢,生态不成熟未来考虑

设计哲学:少即是多

这个方案的核心设计哲学可以概括为三个"少":

少变动

背景完全静止
只有人物转身,云不飘、草不动、光不变。这样 seek 时不会有倒放鬼畜感。

少文件

单文件 H.264
不依赖外部库,不加载序列帧,一个 MP4 文件 + 几十行 JS 搞定全部。

少干扰

交互即内容
不需要播放按钮、进度条、音量控制。用户移动鼠标,画面自然响应。

i
设计决策树当你想做一个类似的交互视频背景时,先问自己三个问题:① 画面细节是否重要?(重要 → 图像先行)② 运动方向是否明确?(明确 → 首尾帧)③ 目标平台是否包含 Safari?(是 → H.264)。这三个问题的答案就是本方案的全部设计依据。
03 / TOOLKIT

工具选择:每个环节用什么

工欲善其事,必先利其器。本方案涉及四个环节——图像生成、视频生成、视频转码、网页开发——每个环节都有多种工具可选。下面是经过实战验证的最佳组合。

环节一:静态图像生成

这是整个工作流的起点,画面质量决定了最终效果的上限。推荐以下工具,按优先级排列:

MJ

Midjourney

艺术感最强,光影和氛围感一流。
适合需要电影感、史诗感的场景。
需订阅,支持 --ar 16:9 比例控制。

首选推荐
可灵

可灵 AI(图生图)

国产工具,对中文提示词理解好。
支持高清放大和局部重绘。
免费额度充足,适合国内用户。

国内首选
SD

Stable Diffusion

本地部署,完全免费,可控性最高。
需一定技术基础,适合有 GPU 的用户。
ControlNet 可精确控制人物姿态。

进阶玩家
FL

Flux

2024 年最强开源模型,文字渲染好。
细节丰富,人物手指等难点表现优秀。
可本地或在线使用。

新兴力量

环节二:首尾帧视频生成

将两张静态图(首帧 + 尾帧)交给 AI,让它生成中间的过渡动画。这是本方案的核心创意环节。

工具首尾帧支持特点推荐度
可灵 AI(视频版)✓ 原生支持国产,对亚洲人脸和古装理解好;运动自然;免费额度多强烈推荐
Runway Gen-3✓ 原生支持国际标杆,运动幅度大,画质高;需订阅,价格较贵推荐
Pika 2.0✓ 原生支持风格化强,特效丰富;适合创意类项目;免费额度有限可选
Luma Dream Machine✓ 支持物理模拟好,3D 感强;人物转身效果自然;新用户免费可选
Stable Video Diffusion△ 需配合工作流开源可本地运行;效果不如商业工具;适合技术玩家不推荐新手

环节三:视频转码

AI 生成的视频不能直接用于网页,必须经过转码处理。只有一个工具是行业标准:

FF

FFmpeg(免费开源)

视频处理领域的"瑞士军刀"。编码、转码、裁剪、合并、提取帧、分析元数据——全部一条命令搞定。Windows/Mac/Linux 全平台支持。

唯一选择

环节四:网页开发

本方案的网页层极其轻量,不需要框架,原生 HTML + CSS + JS 即可:

H5

HTML5 Video API

浏览器原生支持,无需任何库。
currentTime、seeked、progress 等事件直接可用。

核心依赖
JS

原生 JavaScript

不需要 React/Vue/jQuery。
requestAnimationFrame + addEventListener 足够。

核心依赖
VS

VS Code + Live Server

本地开发调试最佳组合。
保存即刷新,实时看效果。

开发工具

完整工具链速查表

环节第一推荐备选费用
静态图像Midjourney / 可灵Stable Diffusion / Flux订阅制 / 免费额度
首尾帧视频可灵 AI / Runway Gen-3Pika / Luma订阅制 / 免费额度
视频转码FFmpegHandBrake(GUI)完全免费
网页开发VS Code + Live ServerCodePen / CodeSandbox免费
部署上线Vercel / NetlifyGitHub Pages / 腾讯云免费额度充足
✓
最低成本组合可灵(免费额度出图) + 可灵(免费额度出视频) + FFmpeg(免费) + VS Code(免费) + Vercel(免费) = 零成本完成整个项目。付费订阅只是在追求更高画质和更快出图速度时的可选项。
04 / ARCHITECTURE

架构与流程

看懂两张图,就理解了整个方案 80% 的内容:一张讲"系统由哪几块组成",一张讲"鼠标移动的一瞬间发生了什么"。

架构图:页面里的四层

呈现层 UI Layer 标题文案 · 导航 · 卡片 · 加载进度条 · 鼠标光斑(纯 HTML/CSS,与视频解耦) 交互层 Interaction mousemove 监听 → 计算位移/坐标 → 换算目标时间 targetTime(含方向映射与 clamp) 平滑引擎 Smooth Engine ★ rAF 指数插值(去机械感) + seek 串行队列(防 seek-flooding) + seeked 回调追赶 媒体层 Media <video muted playsinline preload="auto"> · H.264 密集关键帧 · 浏览器解码器 ★ 平滑引擎是本方案的灵魂,第 8 页有完整代码
图 4-1 · 四层架构:上层做任何改动都不影响下层

流程图:一次鼠标移动的旅程

用户移动鼠标 读取 mouseX 坐标 换算 targetTime 并 clamp rAF 插值出 smoothTime 正在 seek 中? 差值 > 半帧才执行 否 → video.currentTime 赋值 是 → 等 seeked 再追赶
图 4-2 · 鼠标事件 → 时间轴的完整决策流
✓
为什么要"等 seeked"浏览器的 seek 是异步的,一帧 seek 没完成就塞下一个,会排队拥堵(叫 seek-flooding),画面反而卡。所以我们一次只发一个 seek,完成后再追最新目标——就像等电梯,而不是狂按按钮。
05 / IMAGE GENERATION

图像生成与提示词:工作流的第一步

高质量静态图是整个工作流的基石。本章给出可直接复制的提示词模板,以及从"抽卡"到"定稿"的完整流程。

为什么图像质量决定最终效果上限

视频生成工具在接收首帧图像后,会尽量保持图像的风格、色调、人物特征不变。如果首帧图本身模糊、构图失衡、光影混乱,那么生成的视频只会继承这些缺陷。反之,一张构图精准、光影统一、细节丰富的高质量图像,能让后续视频生成的成功率大幅提升。

× 低质量首帧

人物五官不自然、背景元素杂乱、光影方向不一致 → 视频生成后出现脸部漂移、背景扭曲、色调突变

√ 高质量首帧

人物姿态明确、背景简洁、光影统一、构图留有安全区 → 视频生成后画面稳定,转身过渡自然

图像生成的四条铁律

铁律原因提示词关键词
机位锁定首帧和尾帧必须是同一机位,否则首尾帧视频会强行"矫正"导致画面撕裂locked tripod shot / static camera / fixed camera
人物占安全区网页会用 cover 模式铺满,边缘会被裁切;人物必须放在画面中央偏右的 2/3 区域subject positioned right of center / rule of thirds composition
背景简洁静止复杂的动态背景元素(飘动的旗帜、摇曳的树枝)在视频生成后会成为"鬼畜"源头still background / frozen environment
高分辨率出图低分辨率图像放大到 1920×1080 会模糊;建议原生出图至少 1024×576 以上8K / ultra detailed / high resolution

首帧图像提示词模板(可直接复制)

中文版(适合可灵、Midjourney 中文模式):

一位古装侠客立于山巅岩石之上,晚霞漫天,云海翻涌于远方山谷。
固定三脚架机位,镜头完全静止,无推拉摇移。
人物位于画面右侧三分之一处,面向画面右侧,手持长剑,衣袂微扬。
天空、云彩、地面、草叶全部静止,整体构图保持不变。
电影感,史诗氛围,暖橙色调,8K 超高清,极致细节。

英文版(适合 Midjourney / Runway / Pika 等):

A lone swordsman in flowing robes standing on a mountain peak at sunset,
sea of clouds in the valley below. Locked tripod shot, completely static
camera, no camera movement. Subject positioned on the right third of the
frame, facing right, holding a long sword, robes gently flowing.
Sky, clouds, ground and grass remain frozen. Composition unchanged.
Cinematic, epic atmosphere, warm orange tones, 8K ultra detailed.

尾帧图像提示词模板

尾帧必须与首帧保持完全一致的机位、背景、光照、服装,只有人物朝向发生变化。最简单的方法是用首帧图作为参考,修改人物姿态:

[与首帧完全相同的场景描述],
固定三脚架机位,镜头完全静止。
人物位于画面右侧三分之一处,面向画面左侧,手持长剑,衣袂微扬。
天空、云彩、地面、草叶全部静止,整体构图保持不变。
电影感,史诗氛围,暖橙色调,8K 超高清,极致细节。
i
高效做法在 Midjourney 中,先用首帧提示词生成一张满意的图,然后使用 --cref [首帧图URL] 或 --sref [首帧图URL] 保持人物和风格一致,只修改朝向关键词(facing right → facing left)。在可灵中,可以使用"参考图"功能保持人物一致性。

负向提示词(防止 AI 自由发挥)

camera movement, zoom, pan, dolly, walking, footsteps moving,
blowing grass, moving clouds, motion blur, flickering, morphing face,
extra limbs, scene change, cut, transition, text, watermark, signature,
low quality, blurry, deformed hands, bad anatomy

Midjourney 参数建议

参数推荐值说明
--ar16:9与最终视频比例一致,避免裁切问题
--v6 或 6.1最新版本,人物和场景理解最好
--s250~750风格化程度,越高艺术感越强但可能偏离提示词
--q2最高质量,出图时间稍长但值得
--nocamera movement, text简写版负向提示词
✓
抽卡策略不要指望一次出图就完美。建议每次生成 4 张变体,挑选构图最好的一张,用 Vary (Strong) 或 Vary (Region) 局部重绘调整不满意的部分。人物朝向不精确没关系——首尾帧视频生成工具会处理过渡。
06 / VIDEO GENERATION

首尾帧视频生成:让静态图动起来

这是整个工作流中最"魔法"的一步:把两张静态图交给 AI,它自动补全中间的转身动画。但魔法需要约束——提示词和参数设置决定了最终效果。

首尾帧模式的工作机制

首尾帧模式(First Frame + Last Frame)的核心逻辑是:AI 以首帧为起点、尾帧为终点,生成一段连接两者的过渡视频。这类似于传统动画中的"中间帧补间",但由 AI 自动完成。

首尾帧视频生成:AI 自动补间 首帧图像 人物朝右 你提供的静态图 AI 生成过渡 4~6 秒转身动画 自动补全中间帧 尾帧图像 人物朝左 你提供的静态图
图 6-1 · 首尾帧模式:首帧和尾帧由你控制,中间交给 AI

各平台首尾帧操作指南

平台操作路径关键参数注意事项
可灵 AI视频生成 → 图生视频 → 上传首帧 → 开启"尾帧" → 上传尾帧时长 5s,运动幅度"中"或"低"国内访问稳定,免费额度充足,古装人物理解好
Runway Gen-3Generate → Image to Video → Upload First Frame → Upload Last FrameDuration 5s,Motion 建议 3~4需订阅,效果最稳定,运动最自然
Pika 2.0Create → Image + Image (首尾帧模式)Duration 4s,Motion 建议 Low~Medium风格化强,有时运动幅度过大需多次尝试
Luma Dream MachineCreate → 上传首帧 → 上传尾帧默认 5s,无需额外参数物理模拟好,但人物面部一致性有时不稳定

视频生成提示词模板(约束 AI 的运动)

即使使用了首尾帧,仍然需要提示词来约束 AI 的"补间方式"。以下是经过验证的模板:

中文版:

人物从面向右侧匀速缓慢转身到面向左侧。
只有上半身、头部和眼神转动,双脚完全不动。
机位固定,无镜头运动,无推拉摇移。
背景天空、云彩、地面、草叶完全静止不动。
整体构图保持不变,无场景切换。

英文版:

The character slowly and smoothly turns from facing right to facing left.
Only the upper body, head and gaze rotate; feet stay completely still.
Locked camera, no camera movement, no zoom, no pan.
Background sky, clouds, ground and grass remain completely frozen.
Composition unchanged, no scene transitions.

运动幅度控制:为什么建议"低"或"中"

AI 视频工具通常有一个"运动幅度"(Motion Strength)参数。本方案强烈建议设置为"低"或"中",原因有三:

高幅度

问题
人物可能多走了半步,或衣袂飘动幅度过大,导致来回 seek 时背景元素"抽搐"。

中幅度

推荐
转身自然,背景稳定,是本方案的最佳平衡点。

低幅度

保守
转身可能略显僵硬,但背景绝对稳定。适合对背景静止要求极高的场景。

生成后检查清单

检查背景是否静止

播放视频时,目光锁定远处的云或山。如果它们在飘动或抖动,说明背景不够"死",需要重新生成或降低运动幅度。

检查人物转身是否匀速

转身过程应该是平滑的,没有突然的加速或停顿。如果有,说明 AI 的补间不够均匀,可以尝试更换平台或调整提示词。

检查首尾帧衔接

视频的第一帧和最后一帧应该与你上传的首尾帧图几乎一致。如果有明显差异(色调变化、人物变形),说明 AI 的保持力不够,需要更换工具或增加 --cref 约束。

检查时长

推荐 4~6 秒。太短转身仓促,太长文件变大且鼠标要滑很久才能转完。帧率 24fps 或 30fps 均可。

!
最常见的翻车AI 很爱"加戏":让衣袂大幅飘动、让云缓慢流动。单看成片很美,但做成交互视频后,来回 seek 时这些元素会来回倒放,非常诡异。背景越"死",交互越真。如果背景有轻微运动,建议重新生成或换平台。
07 / TRANSCODE

视频规格与转码

AI 生成的原始视频不能直接上线——编码格式、关键帧密度、体积都不对。这一步用一条 FFmpeg 命令全部搞定。

目标规格一览

项目推荐值说明
分辨率1920×1080(16:9)桌面主流比例;人物放右侧 2/3 安全区
编码H.264 (libx264)全浏览器通吃;不要用 HEVC/H.265,Chrome/Firefox 多数放不了
像素格式yuv420p浏览器兼容性最好的色彩格式
关键帧间隔-g 5(每 5 帧一个)本方案的生命线,详见下方坑位说明
B 帧-bf 0(关闭)避免双向预测帧导致的 seek 延迟
音频-an(剔除)静音视频不需要音轨,省体积
moov 前置+faststart元数据放文件头,边下边播
质量-crf 15~19数字越小越清晰体积越大;背景视频 18 左右即可

一条命令完成转码

# 把 input.mp4 转成本方案可用的网页视频
ffmpeg -y -i input.mp4 \
  -c:v libx264 -pix_fmt yuv420p \
  -crf 18 -preset medium \
  -g 5 -keyint_min 5 -sc_threshold 0 -bf 0 \
  -movflags +faststart \
  -an \
  output.mp4

关键帧:本方案最大的隐形坑

视频 seek(跳转)时,浏览器只能精确落到关键帧,普通帧需要从最近的关键帧逐帧推算。AI 生成的视频往往整个文件只有 1 个关键帧——这意味着每次 seek 都要从第 0 帧算到目标帧,鼠标一动就卡成 PPT。

✗ 原始视频:只有开头 1 个关键帧,seek 到任意位置都要从头推算 ✓ 转码后:每 5 帧一个关键帧,任何 seek 最多推算 4 帧,毫秒级完成 深色 = 关键帧(可直接跳转) 浅色 = 普通帧(需推算)
图 7-1 · 关键帧密度对比:-g 5 是丝滑 seek 的前提

参数逐行解读

参数作用不写的后果
-c:v libx264使用 H.264 编码器可能输出 HEVC,Safari/Chrome 不兼容
-pix_fmt yuv420p像素格式兼容所有浏览器某些浏览器无法解码,黑屏
-g 5每 5 帧插入一个关键帧只有 1 个关键帧,seek 卡顿
-keyint_min 5最小关键帧间隔也是 5编码器可能自作主张少插关键帧
-sc_threshold 0关闭场景切换自动插关键帧编码器可能在非预期位置插帧
-bf 0关闭 B 帧(双向预测帧)seek 时需要等 B 帧解码,延迟增加
-movflags +faststart把 moov atom 移到文件头视频必须完全下载才能播放,首屏慢
-an去除音频轨道多余音轨增加体积,无意义
!
验证方法转码后用 ffprobe -select_streams v -show_entries frame=pict_type -of csv output.mp4 | grep I | wc -l 数一下 I 帧(关键帧)数量,5 秒视频应有 30 个以上;如果只有 1~2 个,说明参数没生效。

进阶:多分辨率输出

如果你的网站需要同时服务桌面端和移动端,可以一次转码出多个版本:

# 1080p 桌面版
ffmpeg -y -i input.mp4 -c:v libx264 -pix_fmt yuv420p -crf 18 -preset medium \
  -g 5 -keyint_min 5 -sc_threshold 0 -bf 0 -movflags +faststart -an \
  -vf "scale=1920:1080:flags=lanczos" output-1080p.mp4

# 720p 移动版
ffmpeg -y -i input.mp4 -c:v libx264 -pix_fmt yuv420p -crf 20 -preset medium \
  -g 5 -keyint_min 5 -sc_threshold 0 -bf 0 -movflags +faststart -an \
  -vf "scale=1280:720:flags=lanczos" output-720p.mp4
08 / CODE

加载与交互代码

本页给出完整可运行的核心代码,逐段讲解。直接复制即可用,小白也能看懂每一行在干嘛。

第一步:HTML 视频标签

<video id="heroVideo"
       muted            // 静音(自动播放策略 & 用户体验双重要求)
       playsinline      // iOS Safari 必须,否则强制全屏播放
       preload="auto"   // 提前缓冲,鼠标一动就能响应
       src="turn.mp4">
</video>
// 注意:不写 autoplay!我们不需要它播放。

第二步:鼠标坐标 → 目标时间

let targetTime = 0;   // 鼠标想去的时间点
let smoothTime = 0;   // 平滑后的实际渲染时间

window.addEventListener("mousemove", (e) => {
  if (!video.duration) return;
  // 鼠标 X 占比 0~1,映射到整条时间轴;方向反了就写 1 - ratio
  const ratio = 1 - e.clientX / window.innerWidth;
  targetTime = Math.max(0, Math.min(video.duration, ratio * video.duration));
}, { passive: true });
✓
方向反了怎么办如果实际效果是"鼠标往左人却往右",把 ratio 改成 1 - ratio 即可——这取决于你的视频是"从左往右转"还是"从右往左转"拍的。

第三步:平滑引擎(灵魂代码)

let seeking = false;
const SEEK_EPS = 1 / 48;  // 小于半帧不动,防空转

function tick() {
  // ① 指数平滑:每帧向目标靠近 14%,快而不冲,无机械感
  smoothTime += (targetTime - smoothTime) * 0.14;

  // ② 串行 seek:上一个没完成就不发下一个
  if (!seeking && Math.abs(smoothTime - video.currentTime) > SEEK_EPS) {
    seeking = true;
    video.currentTime = smoothTime;
  }
  requestAnimationFrame(tick);
}

// ③ seek 完成放行;期间目标变了,下一帧自动追赶最新值
video.addEventListener("seeked", () => seeking = false);
requestAnimationFrame(tick);

第四步:加载进度条

// 监听真实缓冲进度
video.addEventListener("progress", () => {
  if (video.buffered.length && video.duration) {
    real = video.buffered.end(video.buffered.length - 1) / video.duration;
  }
});
// 展示进度平滑追赶真实进度,到 100% 隐藏加载层
shown += (real - shown) * 0.12;
barFill.style.width = Math.round(shown * 100) + "%";

完整 HTML 骨架(可直接保存运行)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>动态视频背景</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box}
  body{height:100vh;overflow:hidden;background:#000}
  video{position:fixed;inset:0;width:100%;height:100%;object-fit:cover}
  .ui{position:relative;z-index:10;color:#fff;padding:60px;font-family:sans-serif}
  .load{position:fixed;inset:0;background:#000;z-index:100;display:grid;
        place-items:center;color:#fff;transition:opacity .6s}
  .load.done{opacity:0;pointer-events:none}
  .bar{width:200px;height:3px;background:#333;border-radius:2px;overflow:hidden}
  .bar div{height:100%;background:#fca311;width:0%;transition:width .1s}
</style>
</head>
<body>
<div class="load" id="loader">
  <div><div class="bar"><div id="bar"></div></div><p style="margin-top:12px;font-size:12px">加载中...</p></div>
</div>
<video id="v" muted playsinline preload="auto" src="turn.mp4"></video>
<div class="ui"><h1>移动鼠标体验交互</h1></div>
<script>
const video = document.getElementById('v');
const bar = document.getElementById('bar'');
const loader = document.getElementById('loader');
let targetTime=0, smoothTime=0, seeking=false, real=0, shown=0;
const EPS=1/48;

video.pause();
window.addEventListener('mousemove',e=>{
  if(!video.duration)return;
  targetTime = (1-e.clientX/window.innerWidth)*video.duration;
  targetTime = Math.max(0,Math.min(video.duration,targetTime));
},{passive:true});

function tick(){
  smoothTime += (targetTime-smoothTime)*0.14;
  if(!seeking && Math.abs(smoothTime-video.currentTime)>EPS){
    seeking=true; video.currentTime=smoothTime;
  }
  requestAnimationFrame(tick);
}
video.addEventListener('seeked',()=>seeking=false);
requestAnimationFrame(tick);

video.addEventListener('progress',()=>{
  if(video.buffered.length&&video.duration)
    real=video.buffered.end(video.buffered.length-1)/video.duration;
});
function loadLoop(){
  shown+=(real-shown)*0.12;
  bar.style.width=Math.round(shown*100)+'%';
  if(shown>0.98) loader.classList.add('done');
  else requestAnimationFrame(loadLoop);
}
requestAnimationFrame(loadLoop);
</script>
</body>
</html>

为什么这样就"丝滑"了

手段解决的问题
rAF 插值 0.14鼠标事件是离散的,直接赋值会一格一格跳;插值让时间像水一样流过去
串行 seek 队列防止 seek 堆积(seek-flooding),浏览器不会卡顿假死
SEEK_EPS 半帧阈值目标已到位就不再发 seek,CPU 解码器不空转
密集关键帧(第 7 页)每次 seek 最多推算 4 帧,毫秒级完成
09 / RESPONSIVE

多端适配方案

同一个视频要在宽屏显示器、笔记本、平板、手机上都好看,核心矛盾只有一个:窗口比例 ≠ 视频比例。

桌面端:cover 铺满 + 安全区构图

桌面用 object-fit: cover 让视频铺满全屏,多余部分裁掉。裁切不可避免,所以构图时就要留安全区:

视频画面 16:9 安全区:关键内容(人物)必须放这里 人物 · 右侧 2/3 处 窄屏会裁掉 ← → 窄屏会裁掉 超宽屏裁上下,窄屏裁左右 —— 四周各留 ~10% 冗余
图 9-1 · 安全区示意:重要内容放中央,边缘留给裁切

移动端:三个必须处理的差异

差异对策
没有鼠标改用 touchmove / 陀螺仪 deviceorientation,或退化为"轻点屏幕左右半区转身"
iOS 强制全屏必须写 playsinline + webkit-playsinline,否则视频跳出页面全屏播放
流量与性能移动端提供 720p 小体积版本,用 <source media> 或 JS 按屏宽切换

移动端交互代码

// 同时支持鼠标和触摸
function handleInput(x) {
  if (!video.duration) return;
  const ratio = 1 - x / window.innerWidth;
  targetTime = Math.max(0, Math.min(video.duration, ratio * video.duration));
}

window.addEventListener("mousemove", e => handleInput(e.clientX), { passive: true });
window.addEventListener("touchmove", e => {
  handleInput(e.touches[0].clientX);
}, { passive: true });

// iOS 全屏拦截(双重保险)
video.setAttribute("playsinline", "true");
video.setAttribute("webkit-playsinline", "true");
video.setAttribute("x5-playsinline", "true"); // 微信内置浏览器

响应式视频源切换

<video id="heroVideo" muted playsinline preload="auto">
  <source media="(max-width: 768px)" src="turn-720p.mp4" type="video/mp4">
  <source src="turn-1080p.mp4" type="video/mp4">
</video>

// 或者用 JS 动态切换(更灵活)
const src = window.innerWidth < 768
  ? "turn-720p.mp4"
  : "turn-1080p.mp4";
video.src = src;

比例兜底:不想裁切时

如果画面一像素都不想被裁(例如客户提供的成品图),可以放弃 cover,改用原比例完整呈现 + 模糊延展填充:中间是严格原始比例的画面,四周空隙用同一张图的放大模糊版填满,视觉连续无黑边。

✓
怎么选追求沉浸感选 cover 铺满(推荐);追求构图完整选原比例 + 模糊延展。两者不要混用。
10 / PITFALLS

踩坑指南

以下 8 个坑全部是真实项目中踩过的,按"踩坑概率"从高到低排列。每个坑都给出症状、原因和解药。

1
鼠标一动就卡成 PPT(概率 ★★★★★)症状:鼠标滑动时画面 1 秒才动一下。原因:视频只有 1 个关键帧,每次 seek 都从头推算。解药:转码加 -g 5 -keyint_min 5 -sc_threshold 0 -bf 0(见第 7 页)。
2
视频在浏览器里黑屏/无法解码(★★★★☆)症状:本地能播,网页里黑屏。原因:AI 工具常导出 HEVC(H.265),Chrome/Firefox 多数设备不支持。解药:必须转码为 H.264 + yuv420p。
3
转身方向和鼠标相反(★★★★☆)症状:鼠标往左,人往右看。原因:视频拍摄方向与映射方向不一致。解药:把 ratio 换成 1 - ratio,一行搞定,不用重新生成视频。
4
画面一顿一顿、有机械感(★★★★☆)症状:人物跟随很"硬",像机器人。原因:直接把鼠标事件映射到 currentTime,事件是离散的。解药:加 rAF 指数插值(系数 0.1~0.15),让时间"流"过去而不是"跳"过去。
5
快速甩鼠标后页面假死(★★★☆☆)症状:疯狂晃动鼠标几秒后页面卡住。原因:seek-flooding——seek 请求堆积。解药:串行 seek 队列 + seeked 回调放行 + 120ms 兜底解锁。
6
iPhone 上视频突然全屏(★★★☆☆)症状:iOS Safari 里视频跳出页面全屏播放。原因:缺少 playsinline。解药:同时写 playsinline 和 webkit-playsinline。
7
背景云/草在来回倒放(★★★☆☆)症状:转身正常,但云和草随鼠标来回倒放,很诡异。原因:生成视频时背景有运动元素。解药:只能重新生成——提示词里强调 frozen background(见第 5~6 页)。
8
AI 超分提清晰度失败(★★☆☆☆)症状:想用 Real-ESRGAN 逐帧高清化,跑不动。原因:无 GPU 环境跑 153 帧要几十分钟甚至装不上依赖。解药:优先用 FFmpeg 的 scale=1920:1080:flags=lanczos 高质量缩放 + 低 CRF;真要 AI 超分,找有 GPU 的机器离线跑。
✓
排查口诀卡 → 查关键帧;黑 → 查编码;反 → 改 ratio;硬 → 加插值;死 → 查队列;全屏 → 加 playsinline。
11 / TUTORIAL

小白实操教程:30 分钟从零到上线

不需要任何编程基础,跟着步骤一步步来。所有命令和代码在第 5~8 页都有成品可复制。

生成首帧静态图像

打开 Midjourney / 可灵 / Stable Diffusion,粘贴第 5 页的提示词模板,生成一张"人物朝右"的山巅侠客图。抽卡 2~3 次,挑选构图最好、背景最干净的一张。

生成尾帧静态图像

用同样的工具,保持人物、场景、光影完全一致,只把朝向改为"朝左"。推荐使用 --cref 或参考图功能保持人物一致性。

首尾帧生成视频

打开可灵 AI / Runway / Pika,上传首帧和尾帧,粘贴第 6 页的提示词,生成 4~6 秒的转身视频。运动幅度选"中"或"低"。

安装 FFmpeg

Windows:官网下载解压,把 bin 目录加入环境变量;Mac:brew install ffmpeg;Linux:apt install ffmpeg。装好后命令行输入 ffmpeg -version 有输出即成功。

转码视频

把视频重命名为 input.mp4,打开命令行进入该目录,粘贴第 7 页的转码命令,得到 output.mp4。用 ffprobe 验证关键帧数量(第 7 页底部有命令)。

搭建页面骨架

新建 index.html,写入视频标签(第 8 页第一步),视频文件放在同目录的 assets 文件夹。用浏览器打开,能看到第一帧画面即成功。

接入鼠标交互

把第 8 页第二、三步的 JS 代码粘进 <script> 标签。刷新页面,左右移动鼠标——人物应该开始转身了。方向反了就改 ratio(第 8 页有说明)。

加上加载进度条

粘入第 8 页第四步代码,配一条 CSS 进度条样式。这样首次访问时用户看到加载进度,而不是黑屏等待。

三端自测

按第 9、10 页 checklist 过一遍:Chrome/Safari/Edge 各开一次;手机扫码打开;把窗口拉宽拉窄看裁切;疯狂甩鼠标 10 秒看是否假死。全部通过即可部署。

✓
完成标准鼠标轻移人物缓缓转头、急甩不卡、方向一致、加载有进度、手机不全屏——五条全中,恭喜毕业。
12 / SHOWCASE

效果展示

以下素材全部来自本方案的实际项目:晚霞山巅 + 古装侠客的鼠标跟随转身主页。

最终上线效果(高清静态帧)

图 12-1 · 主页背景高清帧:人物位于右侧 2/3 安全区,左侧留给标题与卡片

转身过程三态

时间轴前段 · 朝右
时间轴中段 · 背影
时间轴后段 · 朝左

关键指标

5.1s

视频时长
122 帧 @24fps

25+

关键帧数量
每 5 帧一个

~10MB

1080p 成片体积
CRF 15 高质量

✓
想看活的?翻到第 13 页「动态网站 Skill」,那里有一个可交互的演示站,体验鼠标控制转身的完整效果。
13 / SKILL

动态网站 Skill:可复用的交互组件

把本方案封装成一个可复用的 Skill,以后任何项目需要"鼠标/触摸驱动视频时间轴"的效果,直接复制这个组件即可。

Skill 设计目标

一个优秀的 Skill 应该满足三个条件:零依赖(不依赖 jQuery/React/Vue)、即插即用(复制粘贴就能跑)、可配置(通过参数调整行为而不改源码)。

零依赖

纯原生 JS,不引入任何外部库。
文件体积 < 2KB(gzip 后)。

即插即用

一个构造函数 + 一行初始化代码。
自动处理鼠标、触摸、加载、平滑。

可配置

通过配置对象调整灵敏度、方向、缓动系数。
无需阅读源码。

Skill 完整代码

/**\n * VideoScrubSkill - 鼠标/触摸驱动视频时间轴\n * @param {HTMLVideoElement} video - video 元素\n * @param {Object} opts - 配置项\n */\nclass VideoScrubSkill {\n  constructor(video, opts = {}) {\n    this.video = video;\n    this.opts = {\n      smooth: opts.smooth ?? 0.14,       // 插值系数 (0~1),越大越跟手\n      reverse: opts.reverse ?? false,  // 是否反转方向\n      eps: opts.eps ?? 1 / 48,           // 半帧阈值\n      touch: opts.touch ?? true,       // 是否启用触摸\n      ...opts\n    };\n    this.targetTime = 0;\n    this.smoothTime = 0;\n    this.seeking = false;\n    this.boundTick = this.tick.bind(this);\n    this.boundSeeked = () => this.seeking = false;\n    this.init();\n  }\n\n  init() {\n    this.video.pause();\n    this.video.muted = true;\n    this.video.setAttribute('playsinline', 'true');\n    this.video.setAttribute('webkit-playsinline', 'true');\n\n    window.addEventListener('mousemove', e => this.handleInput(e.clientX), { passive: true });\n    if (this.opts.touch) {\n      window.addEventListener('touchmove', e => this.handleInput(e.touches[0].clientX), { passive: true });\n    }\n    this.video.addEventListener('seeked', this.boundSeeked);\n    requestAnimationFrame(this.boundTick);\n  }\n\n  handleInput(x) {\n    if (!this.video.duration) return;\n    let ratio = x / window.innerWidth;\n    if (this.opts.reverse) ratio = 1 - ratio;\n    this.targetTime = Math.max(0, Math.min(this.video.duration, ratio * this.video.duration));\n  }\n\n  tick() {\n    this.smoothTime += (this.targetTime - this.smoothTime) * this.opts.smooth;\n    if (!this.seeking && Math.abs(this.smoothTime - this.video.currentTime) > this.opts.eps) {\n      this.seeking = true;\n      this.video.currentTime = this.smoothTime;\n    }\n    requestAnimationFrame(this.boundTick);\n  }\n\n  destroy() {\n    window.removeEventListener('mousemove', this.handleInput);\n    window.removeEventListener('touchmove', this.handleInput);\n    this.video.removeEventListener('seeked', this.boundSeeked);\n  }\n}\n\n// 使用方式:一行代码初始化\nconst skill = new VideoScrubSkill(document.getElementById('heroVideo'), {\n  smooth: 0.14,\n  reverse: true,   // 如果方向反了,设为 true\n  touch: true\n});

Skill 配置项速查

配置项类型默认值说明
smoothnumber0.14插值系数,0~1 之间。越大越跟手,越小越平滑
reversebooleanfalse是否反转映射方向。方向反了设为 true
epsnumber1/48seek 阈值(秒)。小于此值不触发 seek,防空转
touchbooleantrue是否监听 touchmove 事件

加载进度条 Skill(附加组件)

class VideoLoadSkill {\n  constructor(video, barEl, doneCb) {\n    this.video = video;\n    this.bar = barEl;\n    this.doneCb = doneCb;\n    this.real = 0;\n    this.shown = 0;\n    this.video.addEventListener('progress', () => {\n      if (video.buffered.length && video.duration)\n        this.real = video.buffered.end(video.buffered.length - 1) / video.duration;\n    });\n    this.loop();\n  }\n  loop() {\n    this.shown += (this.real - this.shown) * 0.12;\n    this.bar.style.width = Math.round(this.shown * 100) + '%';\n    if (this.shown > 0.98) { this.doneCb && this.doneCb(); }\n    else requestAnimationFrame(() => this.loop());\n  }\n}

🚀 快速开始

1. 复制上方 VideoScrubSkill 和 VideoLoadSkill 代码到你的项目

2. 准备一段经过 FFmpeg 转码的视频(见第 7 页)

3. HTML 里放一个 <video id="heroVideo" src="turn.mp4"></video>

4. JS 里执行:new VideoScrubSkill(document.getElementById('heroVideo'));

5. 打开浏览器,移动鼠标——搞定。

✓
扩展思路这个 Skill 不仅可以做"人物转身",还可以做:产品 360° 展示、建筑空间漫游、艺术装置交互、数据可视化时间轴……只要是一段"固定机位、内容变化"的视频,都可以用这个 Skill 实现鼠标/触摸驱动。