方案总览:我们要做什么
一句话概括:把一段"人物转身"的视频当作可拖动的胶片,鼠标左右移动 = 用手搓动胶片,从而实现"鼠标往哪移,人物往哪看"的丝滑交互。本教程覆盖从 AI 图像生成 → 首尾帧视频生成 → 网页部署的完整工作流。
效果是什么样的
页面背景是一幅山巅侠客的画面。它不自动播放——视频完全静止,时间轴由你的鼠标接管:鼠标向左滑,人物上半身、头部和眼神缓缓转向画面左侧;向右滑,则转向右侧。天空、云彩、机位、站位全程不变。



完整工作流:三步走
本方案采用图像先行、首尾帧驱动、网页收尾的三段式工作流:
为什么不能直接"播放"
普通视频是"放电影":时间轴自己往前走。而我们要的是"拉胶片":时间轴完全听鼠标指挥,鼠标不动画面就静止。所以技术上有三个关键词:
不自动播放
视频 muted + playsInline + preload="auto",并显式 pause()。
鼠标映射时间轴
鼠标 X 坐标 ÷ 屏幕宽度 = 视频进度百分比。
平滑 seek
用插值 + 队列防抖动,杜绝机械感和卡顿。
设计思路:为什么要这样做
技术方案不是凭空产生的。理解"为什么图像先行"、"为什么首尾帧驱动"、"为什么不用 GIF/Canvas 序列帧",才能在遇到问题时做出正确判断。
为什么"图像先行"而不是直接生成视频
当前 AI 视频生成工具在直接文生视频时有一个共同弱点:画面细节不可控。人物长相、服装纹理、背景元素每一帧都在微妙变化,做成交互视频后来回 seek 会出现"脸部漂移""衣服变色"等诡异现象。
而图像先行的策略是:先用文生图工具生成一张高质量的静态画面,确保构图、人物、光影、风格都达到满意标准。然后以这张图作为视频生成的起点(首帧),再生成一张转身后的姿态作为终点(尾帧)。这样:
画面质量可控
静态图可以反复抽卡、局部重绘、upscale,直到满意为止。
首尾一致性
首帧和尾帧都是静态图,中间过渡由 AI 补全,不会出现人物"变形"。
成本更低
文生图比文生视频便宜一个数量级,试错成本大幅降低。
为什么用"首尾帧"而不是"图生视频"单帧驱动
很多工具支持"上传一张图,让图中内容动起来"(图生视频)。这种方式的问题是:AI 会自由发挥运动方向和幅度。你可能想要"人物从右往左转",AI 却理解成"人物向前走一步"或"镜头推进"。
首尾帧模式(First Frame + Last Frame)相当于给 AI 两个锚点:起点姿态和终点姿态都已锁定,AI 只需要填补中间的运动过渡。这大大减少了"AI 自由发挥"的空间,让转身方向、幅度、速度都更加可控。
为什么用 HTML5 Video 而不是 GIF / Canvas 序列帧
| 方案 | 优点 | 缺点 | 本方案选择 |
|---|---|---|---|
| GIF | 兼容性好,实现简单 | 色深只有 256 色,文件巨大(10MB 视频 → 50MB+ GIF),不支持透明控制 | 不推荐 |
| Canvas 序列帧 | 逐帧精确控制,可叠加特效 | 需预加载几十到几百张 PNG,首次加载极慢,内存占用高 | 不推荐 |
| HTML5 Video | 浏览器原生解码,硬件加速,文件小,支持 seek | 需要处理关键帧密度、编码格式兼容性 | 推荐 |
| WebM + AV1 | 压缩率极高,画质好 | Safari 支持差,编码慢,生态不成熟 | 未来考虑 |
设计哲学:少即是多
这个方案的核心设计哲学可以概括为三个"少":
背景完全静止
只有人物转身,云不飘、草不动、光不变。这样 seek 时不会有倒放鬼畜感。
单文件 H.264
不依赖外部库,不加载序列帧,一个 MP4 文件 + 几十行 JS 搞定全部。
交互即内容
不需要播放按钮、进度条、音量控制。用户移动鼠标,画面自然响应。
工具选择:每个环节用什么
工欲善其事,必先利其器。本方案涉及四个环节——图像生成、视频生成、视频转码、网页开发——每个环节都有多种工具可选。下面是经过实战验证的最佳组合。
环节一:静态图像生成
这是整个工作流的起点,画面质量决定了最终效果的上限。推荐以下工具,按优先级排列:
Midjourney
艺术感最强,光影和氛围感一流。
适合需要电影感、史诗感的场景。
需订阅,支持 --ar 16:9 比例控制。
可灵 AI(图生图)
国产工具,对中文提示词理解好。
支持高清放大和局部重绘。
免费额度充足,适合国内用户。
Stable Diffusion
本地部署,完全免费,可控性最高。
需一定技术基础,适合有 GPU 的用户。
ControlNet 可精确控制人物姿态。
Flux
2024 年最强开源模型,文字渲染好。
细节丰富,人物手指等难点表现优秀。
可本地或在线使用。
环节二:首尾帧视频生成
将两张静态图(首帧 + 尾帧)交给 AI,让它生成中间的过渡动画。这是本方案的核心创意环节。
| 工具 | 首尾帧支持 | 特点 | 推荐度 |
|---|---|---|---|
| 可灵 AI(视频版) | ✓ 原生支持 | 国产,对亚洲人脸和古装理解好;运动自然;免费额度多 | 强烈推荐 |
| Runway Gen-3 | ✓ 原生支持 | 国际标杆,运动幅度大,画质高;需订阅,价格较贵 | 推荐 |
| Pika 2.0 | ✓ 原生支持 | 风格化强,特效丰富;适合创意类项目;免费额度有限 | 可选 |
| Luma Dream Machine | ✓ 支持 | 物理模拟好,3D 感强;人物转身效果自然;新用户免费 | 可选 |
| Stable Video Diffusion | △ 需配合工作流 | 开源可本地运行;效果不如商业工具;适合技术玩家 | 不推荐新手 |
环节三:视频转码
AI 生成的视频不能直接用于网页,必须经过转码处理。只有一个工具是行业标准:
FFmpeg(免费开源)
视频处理领域的"瑞士军刀"。编码、转码、裁剪、合并、提取帧、分析元数据——全部一条命令搞定。Windows/Mac/Linux 全平台支持。
环节四:网页开发
本方案的网页层极其轻量,不需要框架,原生 HTML + CSS + JS 即可:
HTML5 Video API
浏览器原生支持,无需任何库。
currentTime、seeked、progress 等事件直接可用。
原生 JavaScript
不需要 React/Vue/jQuery。
requestAnimationFrame + addEventListener 足够。
VS Code + Live Server
本地开发调试最佳组合。
保存即刷新,实时看效果。
完整工具链速查表
| 环节 | 第一推荐 | 备选 | 费用 |
|---|---|---|---|
| 静态图像 | Midjourney / 可灵 | Stable Diffusion / Flux | 订阅制 / 免费额度 |
| 首尾帧视频 | 可灵 AI / Runway Gen-3 | Pika / Luma | 订阅制 / 免费额度 |
| 视频转码 | FFmpeg | HandBrake(GUI) | 完全免费 |
| 网页开发 | VS Code + Live Server | CodePen / CodeSandbox | 免费 |
| 部署上线 | Vercel / Netlify | GitHub Pages / 腾讯云 | 免费额度充足 |
架构与流程
看懂两张图,就理解了整个方案 80% 的内容:一张讲"系统由哪几块组成",一张讲"鼠标移动的一瞬间发生了什么"。
架构图:页面里的四层
流程图:一次鼠标移动的旅程
图像生成与提示词:工作流的第一步
高质量静态图是整个工作流的基石。本章给出可直接复制的提示词模板,以及从"抽卡"到"定稿"的完整流程。
为什么图像质量决定最终效果上限
视频生成工具在接收首帧图像后,会尽量保持图像的风格、色调、人物特征不变。如果首帧图本身模糊、构图失衡、光影混乱,那么生成的视频只会继承这些缺陷。反之,一张构图精准、光影统一、细节丰富的高质量图像,能让后续视频生成的成功率大幅提升。
× 低质量首帧
人物五官不自然、背景元素杂乱、光影方向不一致 → 视频生成后出现脸部漂移、背景扭曲、色调突变
√ 高质量首帧
人物姿态明确、背景简洁、光影统一、构图留有安全区 → 视频生成后画面稳定,转身过渡自然
图像生成的四条铁律
| 铁律 | 原因 | 提示词关键词 |
|---|---|---|
| 机位锁定 | 首帧和尾帧必须是同一机位,否则首尾帧视频会强行"矫正"导致画面撕裂 | 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 超高清,极致细节。
--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 参数建议
| 参数 | 推荐值 | 说明 |
|---|---|---|
| --ar | 16:9 | 与最终视频比例一致,避免裁切问题 |
| --v | 6 或 6.1 | 最新版本,人物和场景理解最好 |
| --s | 250~750 | 风格化程度,越高艺术感越强但可能偏离提示词 |
| --q | 2 | 最高质量,出图时间稍长但值得 |
| --no | camera movement, text | 简写版负向提示词 |
首尾帧视频生成:让静态图动起来
这是整个工作流中最"魔法"的一步:把两张静态图交给 AI,它自动补全中间的转身动画。但魔法需要约束——提示词和参数设置决定了最终效果。
首尾帧模式的工作机制
首尾帧模式(First Frame + Last Frame)的核心逻辑是:AI 以首帧为起点、尾帧为终点,生成一段连接两者的过渡视频。这类似于传统动画中的"中间帧补间",但由 AI 自动完成。
各平台首尾帧操作指南
| 平台 | 操作路径 | 关键参数 | 注意事项 |
|---|---|---|---|
| 可灵 AI | 视频生成 → 图生视频 → 上传首帧 → 开启"尾帧" → 上传尾帧 | 时长 5s,运动幅度"中"或"低" | 国内访问稳定,免费额度充足,古装人物理解好 |
| Runway Gen-3 | Generate → Image to Video → Upload First Frame → Upload Last Frame | Duration 5s,Motion 建议 3~4 | 需订阅,效果最稳定,运动最自然 |
| Pika 2.0 | Create → Image + Image (首尾帧模式) | Duration 4s,Motion 建议 Low~Medium | 风格化强,有时运动幅度过大需多次尝试 |
| Luma Dream Machine | Create → 上传首帧 → 上传尾帧 | 默认 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 生成的原始视频不能直接上线——编码格式、关键帧密度、体积都不对。这一步用一条 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。
参数逐行解读
| 参数 | 作用 | 不写的后果 |
|---|---|---|
-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
加载与交互代码
本页给出完整可运行的核心代码,逐段讲解。直接复制即可用,小白也能看懂每一行在干嘛。
第一步: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 帧,毫秒级完成 |
多端适配方案
同一个视频要在宽屏显示器、笔记本、平板、手机上都好看,核心矛盾只有一个:窗口比例 ≠ 视频比例。
桌面端:cover 铺满 + 安全区构图
桌面用 object-fit: cover 让视频铺满全屏,多余部分裁掉。裁切不可避免,所以构图时就要留安全区:
移动端:三个必须处理的差异
| 差异 | 对策 |
|---|---|
| 没有鼠标 | 改用 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,改用原比例完整呈现 + 模糊延展填充:中间是严格原始比例的画面,四周空隙用同一张图的放大模糊版填满,视觉连续无黑边。
踩坑指南
以下 8 个坑全部是真实项目中踩过的,按"踩坑概率"从高到低排列。每个坑都给出症状、原因和解药。
-g 5 -keyint_min 5 -sc_threshold 0 -bf 0(见第 7 页)。ratio 换成 1 - ratio,一行搞定,不用重新生成视频。playsinline 和 webkit-playsinline。scale=1920:1080:flags=lanczos 高质量缩放 + 低 CRF;真要 AI 超分,找有 GPU 的机器离线跑。小白实操教程: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 秒看是否假死。全部通过即可部署。
效果展示
以下素材全部来自本方案的实际项目:晚霞山巅 + 古装侠客的鼠标跟随转身主页。
最终上线效果(高清静态帧)
转身过程三态



关键指标
5.1s
视频时长
122 帧 @24fps
25+
关键帧数量
每 5 帧一个
~10MB
1080p 成片体积
CRF 15 高质量
动态网站 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 配置项速查
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
smooth | number | 0.14 | 插值系数,0~1 之间。越大越跟手,越小越平滑 |
reverse | boolean | false | 是否反转映射方向。方向反了设为 true |
eps | number | 1/48 | seek 阈值(秒)。小于此值不触发 seek,防空转 |
touch | boolean | true | 是否监听 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. 打开浏览器,移动鼠标——搞定。