资讯动态

纯浏览器实现视频转Sprite Sheet:抽帧、绿幕抠图与拼接全流程

发布时间:2026/9/18 17:31:10 来源:尧图企业网站定制
前两天接了个小需求游戏里要加一段角色攻击动画美术那边丢给我一段 8 秒的绿幕视频让我自己去转成序列帧再用。按我以前的工作流肯定先开 ffmpeg 抽帧再拖到 Photoshop 或 GIMP 里处理绿幕最后用 TexturePacker 拼 sprite sheet。这次我实在懒得开一堆软件于是尝试了一下“纯浏览器完成抽帧 → 抠图 → 拼 sprite sheet”这条路结果比想象中顺8 秒的视频从导入到导出 PNG 加 JSON 标注全程没装东西就在一个 Chrome 标签页里跑完了。这篇文章我会把完整思路、关键代码、以及踩过的几个坑都写出来。适合游戏前端、动效开发、独立开发者也适合那些经常处理序列帧资源、但不想被桌面软件和批处理脚本绑住的人。我会尽量把每一步“为什么这么做”也讲清楚方便你改成自己的工具而不是看完一段代码就完事。1. 为什么要在浏览器里干“视频转 sprite sheet”这件事先说结论不是所有视频转序列帧场景都适合浏览器做但“8 秒绿幕短视频”这种正好卡在浏览器方案的甜区里。原因很简单素材短、格式可控、输出尺寸不大而且现代浏览器已经把 video 解码、canvas 绘制、WebGL/WASM 推理这几块拼图都补齐了。1.1 传统组合拳的尴尬我以前的做法是标准的三段式。第一步用 ffmpeg 从视频里抽帧命令本身不难ffmpeg -i input.mp4 -r 24 -f image2 frames/frame_%04d.png但抽完之后麻烦才开始。绿幕视频要抠图PS 里对单张图可以用 Color Range 加 Refine Edge可一旦有 192 张图逐张手调会让人崩溃用 PS 的动作批处理又得先录一套稳定可靠的脚本素材颜色稍有偏差整套动作就废掉。GIMP 也有类似流程效果可以但操作路径同样很繁琐。最后拼 sprite sheet 这一步TexturePacker 确实好用但它是另一个软件、另一个操作界面、另一套导出配置。整套链路下来至少要在三个工具之间切换中途任何一步素材变了比如美术重新导了一版视频你就得从头再来一遍。在浏览器里做最大的改变是流程被压缩了抽帧、抠图、拼表全在同一个环境里完成代码写好后以后每次换素材只是拖一个视频进去、点一下按钮的事。对于“快速验证角色动画”这种需求这个效率优势非常明显。1.2 浏览器方案的价值边界浏览器方案适合两类情况。第一类是“一次性批量生产”比如从绿幕视频批量提取多个动作素材要求快速、可重复执行对画质的要求没那么苛刻。第二类是“工具型场景”你本来就在做网页端编辑器、游戏素材管理后台、或在线 UI 原型需要在浏览器里直接生成序列帧资源不想额外搭一套桌面工具链。不适合用浏览器做的情况我也遇到了4K 甚至 8K 的超长视频、需要逐帧精细修图的高端影视级合成、以及需要输出无损格式给大屏用的场景。浏览器虽然有 WebCodecs但内存和编码管的限制还是比专业工具多短平快的素材才是它的主场。判断标准很简单视频时长是否在 10 秒内、输出是否需要大尺寸、是否需要高精度的调色和边缘控制。如果三个问题里有两个是“否”那浏览器方案绝对值得试。2. 环境与素材准备先想清楚三个前提动手写代码之前有几个前提条件必须确认。很多人下载了代码直接跑结果卡在这一步视频跨域、canvas 被污染、自动播放被浏览器拦然后开始怀疑代码写得不对。其实都是准备工作没做到位。2.1 视频格式与跨域、自动播放浏览器能直接解的视频格式最稳妥的是 MP4H.264和 WebMVP9。AV1 现在 Chrome 也支持但它的解码开销偏高抽帧时遇到慢设备会出现明显等待所以这个场景我不太推荐。测试素材我用的是一段 720p、H.264 编码的 MP48 秒、24fps。视频的来源有两种情况处理方式完全不同本地文件选择用input typefile拿到 File 对象然后URL.createObjectURL(videoFile)生成临时地址直接赋给 video 元素。这种方式没有跨域问题因为 blob URL 跟页面是同源的。远程地址如果视频放在 CDN 上必须保证响应头里有Access-Control-Allow-Origin: *或匹配你站点的 CORS 规则。否则drawImage画完之后canvas 会被标记为“被污染”后面调用getImageData()或toBlob()会直接抛 SecurityError这个坑非常隐蔽。为了保证程序化播放不被浏览器的自动播放策略拦截video 元素上要挂这几个属性video idvideo muted playsinline autoplay preloadauto/videomuted是关键。Chrome 等浏览器允许带声音的视频自动播放但策略比较严格静音视频自动播放基本不会被拦而我们做抽帧根本不需要声音。playsinline是为了避免 iOS Safari 把视频强行全屏播放。preloadauto让浏览器尽快开始解码能减少后面 seek 时的等待时间。2.2 抠图路线绿幕色度键还是 AI 模型标题里提到“抠图”而绿幕素材天然就有两种可选的抠图路线选哪个直接影响性能和效果。我做了个对比表格维度色度键绿幕抠图AI 模型RMBG-2.0 抠图背景要求必须绿幕且尽量均匀任意背景都可以单帧速度毫秒级可忽略不计数百毫秒到数秒不等边缘质量绿幕素材边缘容易泛绿需要 despill对非绿幕更自然但有概率产生细微空洞浏览器推理依赖无纯像素计算需要 ONNX Runtime WebGL/WebGPU 或 WASM适合场景本案例这种“本来就是绿幕视频”的情况没有绿幕的历史素材、真人实拍、复杂边缘这个案例的标题很明确8 秒绿幕视频。所以我主推色度键速度快到可以忽略不计192 帧8 秒 × 24fps处理完也就一两秒。AI 模型作为备选路线我加在后面处理那种绿幕不均匀、或者你手里的素材根本没有绿幕的情况。一个 512×512 的输入帧在 WebGL 后端跑 RMBG-2.0 大约需要 1 到 5 秒192 帧全跑完可能六到十几分钟不是不能等但没必要为了一个绿幕素材硬上。2.3 抽帧策略seek 派还是播放派抽帧是整条流水线的第一环实现思路有三派我建议你用“seek 派”。seek 派循环设置video.currentTime等待seeked事件触发再drawImage抓帧。优点是时间点精确、逻辑简单适合短视频。缺点是每跳一个时间点都可能触发解码器 seek累积等待时间不可忽略但在 8 秒视频里完全可控。播放派直接video.play()然后通过requestVideoFrameCallback拿到每一帧的 metadata 并绘制。优点是连续解码更平滑不需要反复 seek。缺点是你不好精确控制抽哪几帧如果只想每秒取 24 帧还得自己做节流和计数代码复杂度上去了。WebCodecs 派直接解码视频最灵活、性能最好但你要自己处理 MP4 的 demux把容器里的编码帧拆出来通常得配合 mp4box.js 之类的库。这个复杂度对这个体量的任务来说是过度设计了。我选 seek 派还有一个原因它跟后面的抠图逻辑天然是串行的每抽一帧就能立刻处理一帧不需要同时维护庞大的帧队列内存压力小很多。3. 抽帧 抠图 拼表的实现步骤现在进入核心实现。我会按流水线的顺序把每段的代码和关键设计理由写清楚。3.1 从 video 到 ImageData 的抽帧流程抽帧函数的核心逻辑是这样的function seekTo(video, time) { return new Promise((resolve) { const onSeeked () { video.removeEventListener(seeked, onSeeked); resolve(); }; video.addEventListener(seeked, onSeeked); video.currentTime time; }); } async function extractFrames(video, { fps 24, maxFrames 500 } {}) { const duration video.duration || 0; const totalFrames Math.min(Math.floor(duration * fps), maxFrames); const frames []; const canvas document.createElement(canvas); canvas.width video.videoWidth; canvas.height video.videoHeight; const ctx canvas.getContext(2d, { willReadFrequently: true }); for (let i 0; i totalFrames; i) { const t i / fps; await seekTo(video, t); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); const imageData ctx.getImageData(0, 0, canvas.width, canvas.height); frames.push(imageData); if (i % 30 0) { // 让出主线程避免页面长时间卡死 await new Promise((r) setTimeout(r, 0)); } } return frames; }几个关键点seekTo里我用了removeEventListener解绑不然后续每次 seek 都会叠加触发旧的onseeked事件越积越多函数就越慢。这是 event listener 泄漏的典型场景。getContext(2d, { willReadFrequently: true })是一个容易被忽略的优化。默认 2D context 会走 GPU 加速但getImageData这种频繁读像素的操作在 GPU 上下文里会拖慢性能因为每次读取都要把数据从 GPU 侧拷回 CPU。加了willReadFrequently: true后浏览器会改用 CPU 渲染在这种场景下反而更快。抽帧时我用frames数组把所有 ImageData 都先存了下来。对 8 秒视频来说没问题但如果是 30 秒、60fps 的视频1800 帧 1080p 的 ImageData 会吃满好几个 G 内存。所以我在后面的完整工具里改成了“抽一帧 → 立刻抠图 → 把结果画进 sheet → 丢弃 ImageData”的流式处理这个优化后面细说。3.2 绿幕色度键抠图与边缘处理拿到原始帧 ImageData 后第一步是绿幕抠图。色度键的核心思想并不复杂每个像素和纯绿色或者采样的背景色算一个颜色距离距离近的就置为透明距离远的保留。关键是“距离”怎么算、边界怎么柔化而不是简单判断“等于绿色就删掉”。我用的是带两个参数的色度键算法function chromaKey(imageData, options {}) { const keyColor options.keyColor || [0, 255, 0]; const similarity options.similarity ?? 0.4; const smoothness options.smoothness ?? 0.08; const k similarity * 255; const s smoothness * 255; const kR keyColor[0], kG keyColor[1], kB keyColor[2]; const data imageData.data; for (let i 0; i data.length; i 4) { const r data[i]; const g data[i 1]; const b data[i 2]; const dist Math.sqrt((r - kR) ** 2 (g - kG) ** 2 (b - kB) ** 2); let alpha 1; if (dist k) { alpha 0; } else if (dist k s) { alpha 1 - (dist - k) / s; } data[i 3] Math.round(data[i 3] * alpha); } return imageData; }similarity控制“多远算绿幕”值越大抠掉的绿色范围越大。smoothness控制过渡带的宽度值越大边缘越柔和但半透明区域也越多。绿幕打光均匀时similarity0.4 到 0.5、smoothness0.08 到 0.15 会比较稳。但这里有个问题很多绿幕素材的绿色并不均匀尤其是靠近人物边缘的地方会有背景绿色和人物颜色混在一起的现象。这时候单靠一个固定keyColor就不太稳。我在实际工具里会先采样视频四角加边缘一圈的背景像素取平均色作为keyColor而不是硬编码纯绿[0, 255, 0]。这个改动对真实素材的效果提升非常明显因为摄像机的白平衡和压缩算法会让“绿”偏掉。色度键抠完之后还有一个更头疼的问题——绿边。人物边缘通常会有 1 到 2 像素的绿色溢出直接导出会在 sprite sheet 上形成一层绿色描边。解决办法是 despill也就是把边缘像素里过高的绿色通道值压制下去。简化版思路function despill(imageData, threshold 200) { const data imageData.data; for (let i 0; i data.length; i 4) { const r data[i]; const g data[i 1]; const b data[i 2]; const a data[i 3]; if (a 0) continue; if (g r g b g threshold) { const maxRB Math.max(r, b); data[i 1] Math.min(g, maxRB 20); } } return imageData; }这个实现比较直接找到那些“绿色明显高于红蓝”的像素把绿色通道压到跟红蓝最大值差不多的水平。它不会完美解决严重的绿边但配合smoothness半透明边缘视觉上已经足够干净了。如果你想做得更精细可以把这一步放在边缘收缩erode和羽化feather之后效果更自然。3.3 调用 RMBG-2.0 做 AI 抠图如果你的素材背景不是绿幕或者你不想跟色度键的参数较劲可以走 AI 抠图路线。现在比较常用的是 RMBG-2.0 模型一个专门做人像或商品前景分割的模型ONNX 格式可以直接在浏览器里跑。在浏览器里跑 ONNX 模型需要两个基础条件ONNX Runtime Web 运行时以及一个加载模型的封装。你可以直接用现成的 npm 包也可以自己用huggingface/transformers组织 pipeline。我实际用的方式偏工程化一些大致骨架是这样的import * as ort from onnxruntime-web/webgpu; export async function loadRMBG(modelUrl) { const session await ort.InferenceSession.create(modelUrl, { executionProviders: [webgpu, webgl, wasm], }); return session; } export async function predictMask(session, inputCanvas) { // 把 canvas 缩放到模型输入尺寸比如 1024x1024 const tensor preprocess(inputCanvas); const feeds { input: tensor }; const results await session.run(feeds); // 取出 mask 输出后处理回原尺寸 return postprocess(results.output, inputCanvas.width, inputCanvas.height); }RMBG-2.0 输出的是一张 mask不是直接的 RGBA 图。你要做的是把 mask 的灰度值当作 alpha替换到原图的 alpha 通道上function applyMask(imageData, maskData) { const data imageData.data; for (let i 0; i data.length; i) { data[i * 4 3] maskData[i]; } return imageData; }这里最容易踩的坑是输入尺寸。模型内部可能用 1024×1024 的输入如果你的素材是 1920×1080那就需要先把 canvas 缩到模型输入尺寸推理拿到 mask 后再把 mask 缩回原图尺寸。缩放时一定把imageSmoothingEnabled设为 true否则 mask 边缘会出现强烈的锯齿抠出来的图边缘会非常“硬”。关于执行后端优先级我建议WebGPU 优先WebGL 其次最后才是 WASM。RMBG-2.0 是一个较大的模型WASM 在 CPU 上跑一帧可能要十几秒甚至更久WebGL 能快不少WebGPU 再快一截。具体到项目里可以先尝试创建 WebGPU session失败就降级到 WebGL再失败才去用 WASM保证老设备也能跑。另一个实践要点是模型文件的托管。ONNX 模型动辄几十 MB开发时放到静态资源目录拉起本地服务就行生产环境建议放到自己的 CDN 或对象存储避免每次都从源站拉取。浏览器首次加载会缓存后续再跑模型就快多了。3.4 网格布局拼成 sprite sheet抽帧和抠图都搞定后最后一步是把所有透明帧拼成一个大图。拼表本身很简单关键是布局策略。假设总共有totalFrames帧每帧目标宽高是frameW x frameH我一般这样计算列数和行数function buildSpriteSheet(frames, frameW, frameH, cols) { const rows Math.ceil(frames.length / cols); const sheet document.createElement(canvas); sheet.width cols * frameW; sheet.height rows * frameH; const ctx sheet.getContext(2d); frames.forEach((frameCanvas, i) { const col i % cols; const row Math.floor(i / cols); ctx.drawImage(frameCanvas, col * frameW, row * frameH, frameW, frameH); }); return sheet; }列数怎么定一个简单的原则是“让 sheet 尽量接近正方形”这样大多数游戏引擎加载和处理大图的效率更高const cols Math.ceil(Math.sqrt(totalFrames));比如 192 帧Math.ceil(Math.sqrt(192))是 14所以生成 14 列 × 14 行最后一格留空。如果你的目标平台对纹理尺寸有限制比如最多 2048×2048那就得按这个限制反推每帧的实际显示尺寸。drawImage在绘制时会把抠好的透明 canvas 缩放并绘制到目标位置所以即使原始帧尺寸很大拼进 sheet 时也可以自由缩到统一大小。这里要注意如果你的 sprite sheet 用于像素风游戏或精确碰撞检测建议直接按相同帧尺寸输出不要做非整数倍缩放否则边缘会糊。导出 PNG 非常简单sheet.toBlob((blob) { const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download sprite.png; a.click(); URL.revokeObjectURL(url); }, image/png);但导出 PNG 时有一个暗坑如果 canvas 里有半透明像素PNG 会保留 RGB 值而有些渲染引擎在读取纹理时会把半透明区域的 RGB 当成黑色或白色直接显示出来。这个问题我放在下一节的“坑”里细讲。4. 实测中的坑与对策从绿边到内存峰值再稳的代码放到真实素材上一跑也会冒出各种问题。这里把我在实测中遇到的、以及处理过的问题集中写出来每条都是我实际调过的。4.1 绿边、半透明边缘的暗边问题绿边是绿幕抠图最常见的问题我在 3.2 里写了 despill 的基本思路。但实测下来只做 despill 还不够最好配套做两步后处理边缘收缩和羽化。“边缘收缩”指的是先把 alpha 区域的边界向内收缩 1 到 2 个像素把那些混了绿幕颜色的半透明边缘像素直接去掉“羽化”是在收缩后的边界上做模糊过渡让边缘看起来不那么生硬。在 Canvas 里做羽化最简单的是对 mask 数据做一个 3x3 均值模糊或者直接用ctx.filter blur(1px)再画一遍。对于 sprite sheet 场景收缩 1px 羽化 1px 通常就够用了。更隐蔽的问题是半透明边缘的暗边。导出 PNG 后在游戏引擎白色或灰色背景下播放时角色边缘会出现一圈黑色描边在黑色背景下反而没有。原因是 Canvas 的drawImage和toBlob处理 alpha 时默认按预乘 alphapremultiplied alpha工作而很多游戏引擎读取 PNG 时用的是非预乘 alpha。半透明像素的 RGB 值如果没有被正确处理就会被引擎当成暗色甚至黑色。对策有两个方向。一是导出前手动把半透明像素的 RGB 往亮处调比如改成前景色二是让边缘完全透明或完全不透明避免半透明像素进入最终 PNG。我倾向于用后者先做 1px 边缘收缩让从外到内的过渡从 0 到 255 尽量陡峭然后再用羽化恢复一点点柔和度。这样大部分半透明像素都会被收缩掉暗边问题基本看不出来。4.2 不同浏览器的 canvas 上限与导出限制canvas 并不是无限大的。Chrome 和 Firefox 在桌面端通常支持很大的 canvas但老 WebKit 内核或移动端 Safari 会有更严格的限制。我遇到过 canvas 面积接近 GPU 纹理上限时toBlob导出的 PNG 会变成一张纯白或全透明的图不报错但肉眼一看就知道坏了。稳妥起见拼 sheet 前可以先做一个保护性检查const MAX_CANVAS_SIZE 8192; if (sheet.width MAX_CANVAS_SIZE || sheet.height MAX_CANVAS_SIZE) { // 自动降低输出分辨率或拆分多个 sheet }不同浏览器对 canvas 维度上限差别很大常见的是 16384×16384但移动端可能只有 4096×4096。所以我在工具里加了一个“探测 canvas 限制”的步骤先尝试创建一个目标尺寸的 canvas向其中画几个像素再通过getImageData读回来验证像素是否还在。如果读回来是 0 或报错就说明超限了自动降到下一档尺寸。另一个更隐蔽的导出限制是内存。超大 canvas 的toBlob会把整张未压缩像素数据编码成 PNG峰值内存可能是文件大小的几十倍。8 秒视频、192 帧、输出 1024×768 的 sheet 其实还好但如果你把帧数拉到 500、每帧又不缩放峰值会非常夸张。建议拼表之前先按目标尺寸缩放不要拿原始 4K 帧直接拼。4.3 内存、对象释放与视频解码的“延迟放射”抽帧场景最容易内存爆掉的地方就是我前面说的“把所有 ImageData 存进数组”。192 帧 1080p 的 ImageData每帧大约 1080 × 1920 × 4 字节也就是约 8.3 MB192 帧就是 1.6 GB 以上。这不是开玩笑。所以正确的做法是流式处理抽一帧 → 立即抠图 → 立即绘制到目标 sheet 的对应格子里 → 把当前帧的 ImageData 引用释放。这样整个流程中真正占用大内存的只有两处当前帧和最终的 sheet 图。async function processVideo(video, targetFrameW, targetFrameH, fps) { const totalFrames Math.min(Math.floor(video.duration * fps), 500); const cols Math.ceil(Math.sqrt(totalFrames)); const rows Math.ceil(totalFrames / cols); const sheet document.createElement(canvas); sheet.width cols * targetFrameW; sheet.height rows * targetFrameH; const sheetCtx sheet.getContext(2d); const tempCanvas document.createElement(canvas); tempCanvas.width video.videoWidth; tempCanvas.height video.videoHeight; const tempCtx tempCanvas.getContext(2d, { willReadFrequently: true }); for (let i 0; i totalFrames; i) { const t i / fps; await seekTo(video, t); tempCtx.drawImage(video, 0, 0); let imageData tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height); imageData chromaKey(imageData); imageData despill(imageData); const col i % cols; const row Math.floor(i / cols); const dx col * targetFrameW; const dy row * targetFrameH; // 用 putImageData 直接写入 sheet省去中间 canvas sheetCtx.putImageData(imageData, dx, dy); // 让引用可被 GC 回收 imageData null; } return sheet; }这个版本每一帧都直接putImageData到 sheet 上不再保留中间 canvas内存压力就是从“两百多帧全留”降到了“一帧加一张 sheet”。另外URL.createObjectURL创建的 blob URL 用完一定要revokeObjectURL特别是你在一个页面里反复导入新视频时不释放的话 devtools 里内存会不断爬升。还有一个容易被忽略的点video.src blobUrl之后浏览器不会立刻释放旧视频的解码资源。如果你在一个 SPA 里反复换视频源旧视频可能还挂在后台解码内存迟迟不降。解决方式是在换源前先video.removeAttribute(src)并调用video.load()主动告诉浏览器释放当前媒体资源。5. 验证与再加工让 sprite sheet 真正能直接用拼出来的 sheet 不是导出完就万事大吉我会在项目里做一个“回放预览”的环节直接在浏览器里模拟游戏引擎逐帧播放的效果。这一步能筛掉绝大多数边缘和透明度问题。5.1 在引擎/页面里做逐帧回放验证最朴素但有效的验证方式是把导出的 PNG 加载回一个 canvas 里按时间顺序裁剪对应的格子播放function playSpriteSheet(image, cols, frameW, frameH, fps) { const canvas document.createElement(canvas); canvas.width frameW; canvas.height frameH; const ctx canvas.getContext(2d); let index 0; const frameDuration 1000 / fps; setInterval(() { const col index % cols; const row Math.floor(index / cols); ctx.clearRect(0, 0, frameW, frameH); ctx.drawImage(image, col * frameW, row * frameH, frameW, frameH, 0, 0, frameW, frameH); index (index 1) % totalFrames; }, frameDuration); }检查时我一般会切两种背景色白色和深灰色。白色背景最容易暴露暗边深色背景最容易暴露绿边和亮边。两个背景都放一遍基本就把常见问题都看出来了。其他要核对的信息包括帧总数是否等于视频时长 × fps、最后一帧是否是完整帧、边缘是否有闪烁感、透明度是否保留、动画节奏是否和原视频一致。列成清单就是这样的检查项通过标准帧数与期望总帧数一致帧尺寸每格宽高统一无偏移边缘白背景下无暗边深背景下无绿边透明度背景完全透明不被填充底色播放节奏与原视频速度相当无明显跳帧5.2 导出配置、元数据与后续扩展方向光是导出 PNG 还不够很多游戏引擎还需要一份元数据来描述每个格子的位置尤其是非均匀布局或带 padding 的 sheet。我会在导出 PNG 的同时生成一份 JSON{ frames: { frame_0000: { frame: { x: 0, y: 0, w: 128, h: 128 } }, frame_0001: { frame: { x: 128, y: 0, w: 128, h: 128 } } }, meta: { app: browser-sprite-builder, frameWidth: 128, frameHeight: 128, fps: 24, size: { w: 1792, h: 1536 } } }这个结构基本兼容 TexturePacker 的通用 JSON 格式导出后 Cocos、Laya、PixiJS 等引擎都能直接读取。最后说两个可以继续往下做的方向。一个是把整套逻辑封装成一个页面工具支持拖入视频、实时预览抽帧阄图效果、调色度键参数、导出 ZIP 包PNG JSON。我最初就是为了这个目的才写这套流程现在每次美术更新素材我只需要把视频往浏览器里一拖几秒钟就能拿到可用的序列帧资源不用再碰 ffmpeg 和 PS。另一个方向是给流程加上 WebCodecs 支持。对于超过 10 秒、或者高帧率的视频seek 派会越来越慢这时候用 WebCodecs 直接解码内存和速度都会更好但需要额外处理 MP4 demux复杂度会明显上一个小台阶。如果只是 8 秒绿幕视频这种量级完全不需要上 WebCodecs。我实际操作下来的体会是这个流程从“临时偷懒”变成了我的默认选项最大的收获不是省了几个软件而是所有步骤变成了一段可重复运行的代码素材再怎么变我都能快速重新生成。最容易翻车的点还是那两个绿边和 canvas 上限。前者靠边缘收缩加羽化解决后者靠预检尺寸和分块策略兜底。把这两个坑避开浏览器里做视频转 sprite sheet 这条路基本就是通畅的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价