2 个模型可选
导演从一段 5 秒 AI 视频里挑出最强的一帧,用作序列下一镜头的参考图。在 Martini 画布上,把源片(Seedance 2、Kling 3 等)送到 frame-extraction 工具节点,拉到选定时间码,把抽出来的静帧链接到 Nano Banana 2 图像编辑节点或直接当作下一段视频节点的起始帧。输出是带参考锁的画面:可作为下一镜头起始帧、图像编辑链的输入或获批镜头的主视觉静帧。选择下方模型走完抽帧工作流。

要从 AI 视频中提取帧用于滚动同步的 canvas 动画,你需要跑一条五段流水线:生成可循环片段、用 FFmpeg 把片段导出成编号帧序列、筛选并把帧转成 WebP、用 drawImage() 把当前帧画到 HTML <canvas> 上、再用 GSAP ScrollTrigger 让帧索引跟随滚动位置。最终得到的是苹果官网那种"擦洗"效果:滚动时在 canvas 上逐帧播放片段,而不是用卡顿的 <video> 元素。
每个竞品教程(Codrops、Builder.io、Tympanus)都从你已有的视频文件开始,只教 FFmpeg 和 GSAP 那半段。Martini 把上游那半段收拢到一块节点式画布上:你用首尾帧控制生成一段完美可循环的 AI 片段,提取并筛选帧,可选地把主视觉帧 4K 放大——这一切都在写第一行代码之前完成。交给 GSAP 的成品是一套干净、去重过的 WebP 序列,而不是一段拖影的屏幕录制。
这条流水线从左到右是四跳:(1) 在 Martini 画布上用视频模型生成循环,(2) 提取帧序列(画布内抽帧或下面的 FFmpeg 备选方案),(3) 筛选并可选地放大主视觉帧,(4) 用 GSAP ScrollTrigger 把序列渲染到 <canvas>。第 1 到 3 步是属于画布的创意工作;第 4 步是你要上线的代码。下面每一节都给出可直接复制的命令。

如果片段循环时跳切,擦洗效果会显得很破,所以源视频必须干净地循环。在 Martini 画布上放一个视频节点,使用首尾帧控制:把同一张图同时设为起始帧和结束帧,模型就会插值出一条回到起点的运动路径。支持首尾帧条件的模型包括 Kling、Luma、Runway 和 Seedance;像 Veo、Sora、Wan 这类文生视频模型更适合不需要无缝循环的片段。
把提示词和首/尾帧一次性接进多个视频节点跑扇出:一张源图同时送进 Kling 3、Seedance 2 和 Runway Gen-4,把每个版本都留在版本托盘里,挑出最干净的循环。这正是竞品拿一个裸视频文件做不到的——你是在提取前挑选最好的原料,而不是抢救已经拍好的素材。
片段要短、运动要连续。一段 24-30 fps 的 3 到 5 秒循环大约产出 72-150 个源帧,正好是滚动序列需要的数量级(你会在第 3 步把它稀释)。避免硬切、快速横摇和强烈运动模糊;这些会产生拖影帧,动起来还行,但用户停在中间某一帧时会很难看。
把片段变成静帧有两种方式。在 Martini 画布上,把获批的视频接进抽帧工具节点,拖动时间轴抽取单张主视觉帧——当你只需要几张干净静帧用于图像编辑链或下一镜头起始帧时很快。如果要一整套等间隔、用于滚动擦洗的序列,就导出片段跑 FFmpeg,这是开发者 SERP 期待看到的事实标准。
下面的命令为每个源帧写一张编号 PNG。用 -vf fps=30 把片段重采样到固定每秒 30 帧,不管原始帧率是多少,并给索引补零让文件正确排序。先用 PNG 保持提取无损;下一条命令再转 WebP。
把 PNG 序列转成 WebP,在滚动擦洗尺寸下可削减 60-80% 体积且看不出画质损失。WebP 才是你真正发给浏览器的格式——一套 150 帧的 PNG 序列可能 40 MB 以上,而同一套用质量 80 的 WebP 往往不到 8 MB。用 cwebp 遍历目录,或让 FFmpeg 直接输出 WebP。
把做好的序列放进 Next.js 项目的 public/frames/ 目录,文件就从稳定、利于缓存的路径提供(例如 /frames/frame_0001.webp)。编号补零的文件名让你在第 4 步用简单的字符串插值就能拼出任意索引的 URL。

FFmpeg — extract a numbered frame sequence at 30fps
# Resample to a fixed 30fps and write zero-padded PNG frames
ffmpeg -i input.mp4 -vf "fps=30" frames/frame_%04d.png
# Variant: keep the clip's native frame rate (one PNG per source frame)
ffmpeg -i input.mp4 frames/frame_%04d.pngWebP conversion — shrink the payload 60-80%
# Convert every PNG in the folder to quality-80 WebP
for f in frames/*.png; do
cwebp -q 80 "$f" -o "${f%.png}.webp"
done
# Or have FFmpeg emit WebP directly (skip the PNG step)
ffmpeg -i input.mp4 -vf "fps=30" -quality 80 public/frames/frame_%04d.webpAI 视频会有软帧:运动模糊的过渡帧、偶尔的扭曲和零星瑕疵。在发布序列前,把帧铺在 Martini 画布上筛选——删掉拖影帧,留下清晰的,并检查循环的首帧和尾帧是否对齐,让擦洗能干净地衔接。把 150 帧导出稀释到干净的 90 帧,也减轻了浏览器要预加载的负担。
如果某张主视觉帧会以全宽停在屏幕上——标题时刻、产品特写——就把它放大到 4K,让它在 Retina 屏上保持锐利。把选中的帧送进放大工具节点,再把结果放回序列。分辨率算法和逐场景指引见配套指南「如何把图像放大到 4K」;如果要放大整段片段而非单帧,见「如何把视频放大到 4K」。
一张参考级干净的帧用途不止于滚动擦洗:把它接回 Nano Banana 2 图像编辑链,或用作下一个视频节点的起始帧,让下一镜头继承完全一致的外观。这跟延长片段用的抽帧操作是同一招——提取最后一张好帧,作为下一段图生视频的首帧输入(见「如何延长视频片段」)。

现在是代码那一半。把 WebP 序列预加载成一组 Image 对象,用 drawImage() 把当前帧画到 <canvas> 上,再让 GSAP ScrollTrigger 把滚动进度映射到帧索引。用户滚过一段被钉住的高区块时,索引从 0 走到最后一帧,canvas 随之重绘——这就是擦洗的全部。
有两个细节决定擦洗是锐利还是模糊带拖影。第一,按 window.devicePixelRatio 放大 canvas 让它在 Retina 屏上以原生分辨率渲染,再用 CSS 缩小尺寸——在高 DPI 屏上按逻辑像素绘制会得到软图。第二,每次 drawImage() 前调用 ctx.clearRect()(或画一张完全覆盖 canvas 的不透明帧),否则半透明帧会叠加并留下重影。
下面的片段是这套承重模式:一个 render(index) 函数、setupCanvas() 辅助里的 devicePixelRatio 缩放、每次绘制前的 clearRect,以及一个 onUpdate 把 self.progress 吸附到最近帧的 ScrollTrigger。把 frameCount 和 /frames/ 路径改成你第 2 步导出的实际值。
GSAP ScrollTrigger + canvas image-sequence scrub
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
const canvas = document.querySelector("#sequence");
const ctx = canvas.getContext("2d");
const frameCount = 90;
const frames = [];
// High-DPI scaling: draw at native resolution, size down with CSS
function setupCanvas(w, h) {
const dpr = window.devicePixelRatio || 1;
canvas.width = w * dpr;
canvas.height = h * dpr;
canvas.style.width = w + "px";
canvas.style.height = h + "px";
ctx.scale(dpr, dpr);
}
setupCanvas(1280, 720);
// Preload the zero-padded WebP sequence from /public/frames
for (let i = 0; i < frameCount; i++) {
const img = new Image();
img.src = `/frames/frame_${String(i + 1).padStart(4, "0")}.webp`;
frames.push(img);
}
const state = { frame: 0 };
function render(index) {
ctx.clearRect(0, 0, canvas.width, canvas.height); // kill ghosting
ctx.drawImage(frames[index], 0, 0, 1280, 720);
}
frames[0].onload = () => render(0);
// Map scroll progress to a frame index
gsap.to(state, {
frame: frameCount - 1,
snap: "frame",
ease: "none",
scrollTrigger: {
trigger: "#sequence-section",
start: "top top",
end: "+=3000",
scrub: 0.5,
pin: true,
onUpdate: (self) => {
const i = Math.round(self.progress * (frameCount - 1));
render(i);
},
},
});滚动擦洗本质是个预加载问题。浏览器必须取够帧才能无缝重绘,所以要按设备调帧数:桌面端大约 90-150 帧,移动端稀释到 30-60 帧,因为带宽和内存更紧。给小视口提供一套独立、更小的 WebP,而不是逼手机下载桌面序列。
按方向、分阶段预加载。优先加载前 10-20 帧让区块立刻可交互,其余在后台流式加载;如果你知道用户在向下滚,就预加载当前索引前方的帧而不是均匀加载。在支持的环境用 createImageBitmap() 在主线程外解码帧,让 drawImage() 永不阻塞滚动。
WebP 在体积上扛了大头。在质量 75-85 下每帧体积足够小,即便 150 帧的桌面序列也能保持在个位数兆字节,这正是让优先预加载可行、不出现长时间空白的关键。如果序列仍然偏重,先减帧再减质量——人眼对帧数减少的容忍度远高于对停在滚动中间的一张软质量帧的容忍度。
你确实可以让 GSAP 指向一个 <video> 并按滚动设置 currentTime,在你的开发机上看着没问题——然后在真机上卡顿。浏览器不会逐帧精确定位视频:设置 currentTime 会吸附到最近的关键帧,所以擦洗一段高压缩片段会跳动顿挫。移动浏览器还会在自动播放策略下限制或阻止程序化播放,而每次滚动都解码一个关键帧代价高昂。
canvas 图像序列绕开了这一切。每一帧都是一张离散、完全解码的图片,所以 render(index) 天生逐帧精确——滚到 47% 就得到那一帧,每次都一样,没有关键帧吸附。也不用跟自动播放策略较劲,因为没有任何东西在"播放";你只是在画一张静帧。代价是前置体积(你要预加载帧),而这正是 WebP 加按设备区分帧数要解决的。
这就是为什么滚动擦洗动画的开发者 SERP 一致选择 canvas 方案。Martini 的贡献在上游:它包揽了生成-提取-筛选这几步,所以你交给这套 canvas 模式的帧是干净、可循环、该 4K 处就 4K 的——而不是你能从现有文件里录屏录到的随便什么。
ByteDance
Seedance 2.0 是上游视频模型,其干净的 1080p 输出使帧提取富有成效——其帧构图良好、稳定,细节足够高,可作为图像编辑输入或静态交付物。流程是:生成或采用批准的 Seedance 片段 → 通过帧提取工具节点路由 → 为下一步选择最强的帧。常见下游用途:将提取的帧作为参考图像馈送给 Nano Banana 2 或 Flux Kontext 图像编辑,将其用作 Sora 2 或 Kling 3.0 上不同相机运动的起始帧,或作为静态主图像导出。配套 tools/frame-extraction 页面介绍工具路由和参数;此 how-to 专注于 Seedance 配对的流程。
Kling
Kling 3.0 是当你想提取的帧以人类或人形表演者为特色时的上游选择——其解剖学准确的身体、面部和服装运动产生的每帧静态作为图像编辑输入或静态交付物比其他视频模型在等效设置下保持得好得多。流程与 Seedance 相同:生成或采用批准的 Kling 片段 → 通过帧提取工具节点路由 → 为下一步选择最强的帧。Kling 源提取的常见下游用途:AI 角色表(提取单个中片段帧并馈送到 Nano Banana 2 用于服装变体)、以同一角色为特色的新相机运动镜头的起始帧或品牌营销的主静态,演员存在感是要点。配套 tools/frame-extraction 页面介绍工具路由;此 how-to 专注于 Kling 配对的流程。
用 FFmpeg 把片段导出成编号图像序列——ffmpeg -i input.mp4 -vf "fps=30" frames/frame_%04d.png——再转成 WebP 用于网页。在 Martini 画布上,你也可以把视频接进抽帧工具节点,拖动时间轴抽取单张主视觉帧,全程不离开画布。
用 ffmpeg -i input.mp4 -vf "fps=30" frames/frame_%04d.png。-vf "fps=30" 滤镜把片段重采样到固定每秒 30 帧,不管原始帧率是多少;frame_%04d.png 给索引补零,让文件能正确排成序列。
canvas 图像序列逐帧精确且不卡顿,而擦洗 <video> 做不到。设置视频元素的 currentTime 会吸附到最近关键帧,所以擦洗压缩片段会顿挫,移动端自动播放策略还可能完全阻止程序化播放。把离散的预解码帧画到 <canvas> 上能给你精确、平滑的控制,代价是前置预加载。
桌面滚动擦洗大约 90-150 帧,移动端稀释到 30-60 帧。帧越多动作越平滑但预加载越重,所以给小视口提供更小的 WebP,体积偏重时先减帧再减每帧质量。
把帧预加载成数组,再创建一个 ScrollTrigger,在 onUpdate 里把 self.progress 转成帧索引——const i = Math.round(self.progress * (frameCount - 1))——然后调用 render(i),它会清空 canvas 并绘制 frames[i]。把区块钉住并设置 scrub,让滚动位置直接映射到播放位置。
按 window.devicePixelRatio 放大 canvas。把 canvas.width 和 canvas.height 乘以设备像素比,把 CSS 宽高设为逻辑尺寸,再调用 ctx.scale(dpr, dpr)——这样按原生分辨率渲染而不是软的逻辑像素。每次 drawImage() 前还要调用 ctx.clearRect(),防止半透明帧产生重影。
能——这正是 Martini 的优势。在一块节点式画布上,你用首尾帧控制跨 Kling、Seedance、Runway 等模型生成可循环 AI 片段,提取并筛选帧,可选地把主视觉帧 4K 放大,再把干净的 WebP 序列导出到 /public/frames 给 GSAP。竞品教程都从你已有的视频文件开始;Martini 包揽了流水线上游的生成-提取-筛选那半段。