1. 从"Claude Opus 5.5 做视频"这个说法说起
第一次看到"Claude Opus 5.5 是怎么做出视频的"这个标题,我脑子里冒出来的第一个念头是:它到底是在"生成视频",还是在"生成能播放视频的东西"?这两个概念差得很远,很多人一看到"AI 做视频"就自动脑补成 Sora 那种逐帧渲染像素的画面生成模型,但如果你真去拆解一个对话式模型在视频这件事上的能力边界,会发现它压根不碰像素,它碰的是代码——具体来说,是 JavaScript 加 Canvas 这一套浏览器里就能跑起来的东西。
这个区别非常关键。像素级视频生成需要扩散模型在潜空间里一帧一帧去噪,算力开销巨大,而且你几乎没法精确控制每一帧里某个元素的位置、颜色、运动曲线。而"用代码做视频"走的是另一条路:模型输出一段可执行的程序,程序在 Canvas 上按时间轴绘制图形、文字、动画,浏览器负责把它渲染出来。你得到的不是一段 mp4,而是一个"活的"、可交互、可参数化的动画程序。想改颜色?改一行变量。想换文案?改一个字符串。想导出成视频?用录制工具把 Canvas 的帧抓下来合成即可。
所以这篇东西我想聊的,不是玄学层面的"AI 觉醒了视觉能力",而是一个非常工程化的问题:当一个语言模型被要求"做一个视频"时,它实际交付的是什么形态的产物,这套产物背后的技术栈长什么样,以及我们自己怎么复现这套流程。关键词里出现的JS、Canvas、canvas 绘图、canvas 2d vue、canvas 绘图引擎、html5 视频倍速这些词,其实已经把答案摆在桌面上了——核心就是浏览器端的 Canvas 2D 绘制加上一套时间轴驱动逻辑。
适合谁看?如果你是想搞清楚"AI 生成视频"到底怎么回事的技术好奇者,或者是想自己动手用代码做动画、做数据可视化视频、做产品演示短片的前端/全栈开发者,再或者你只是被"Claude Opus 5.5 做视频"这个热搜勾起了兴趣想弄明白原理,那接下来的内容应该都对你有用。我会尽量把每一步为什么这么做讲透,而不是甩一段代码让你自己猜。
2. 为什么"代码生成视频"这条路反而更靠谱
2.1 像素生成和代码生成,本质是两种完全不同的活
先把两种路线摆清楚,不然后面很多设计选择你会看不懂。
像素级视频生成,模型直接输出的是每一帧每个像素的 RGB 值,或者更准确说是潜空间里的特征,再解码成画面。它的优势是能生成真实感极强的连续画面,比如一个人走路的自然姿态、光影变化。但它的软肋也很明显:不可控、不可编辑、不可复现。你让它"把标题往左移 20 像素",它大概率做不到精确;你想改一个字,得重新生成整段,而且每次结果都不一样。
代码生成视频,模型输出的是程序逻辑。它不描述"第 37 帧第 200 行第 300 列的像素是 #FF5733",它描述的是"在画布中央画一个圆,半径随时间从 10 变到 100,颜色从红渐变到蓝"。渲染交给浏览器,浏览器是确定性的——同样的代码,同样的时间点,画出来的一定是同一帧。这就带来了三个像素路线给不了的东西:精确可控、随时可改、完全可复现。
我个人的判断是,对于"信息传达型"的视频——比如数据动画、产品介绍、教学演示、文字动效——代码路线在效率和可控性上碾压像素路线。而对于"真实感叙事型"的视频,像素路线才有优势。Claude Opus 5.5 被拿来"做视频",走的显然是前者。
2.2 Canvas 为什么是这套方案的天选载体
浏览器里能画东西的技术有好几套:DOM + CSS 动画、SVG、WebGL、Canvas 2D。为什么偏偏是 Canvas?
DOM + CSS 做动画,元素一多就卡,而且做逐帧精确控制很别扭,CSS 的动画曲线是声明式的,你想在某一帧插入一个基于数学函数的形变,写起来很痛苦。SVG 适合矢量图形,但节点数量上去之后性能同样吃紧,而且它本质还是 DOM。WebGL 性能最强,但上手门槛高,写个简单的文字动画都要搞一堆 shader,对"快速产出内容"这个目标来说太重了。
Canvas 2D 刚好卡在中间:命令式 API,逐帧完全可控,性能足够,学习成本低。你有一个requestAnimationFrame循环,每一帧你清空画布、重新画一遍所有元素,元素的位置由当前时间决定。这套模型和"视频"的心智模型几乎是一一对应的——视频不就是一帧一帧画面按时间顺序播放吗?Canvas 的渲染循环天然就是这个结构。
关键词里还有canvas 2d vue,说明很多人是在 Vue 项目里用 Canvas。这也很合理,Vue 负责 UI 和状态管理,Canvas 负责渲染区域,两者通过 ref 拿到 canvas 元素、在onMounted里启动渲染循环即可。这个组合我在好几个项目里用过,配合得相当顺。
2.3 时间轴:把"动画"变成"视频"的那根主轴
光有 Canvas 还不够,Canvas 本身只会画"当前这一帧"。要让它变成"视频",你需要一个**时间轴(timeline)**概念。
最朴素的做法是用performance.now()或者Date.now()记录起始时间,每一帧算出已经过去了多少毫秒,然后所有元素的状态都是这个"已过去时间"的函数。比如:
const start = performance.now(); function frame(now) { const t = (now - start) / 1000; // 已过去秒数 ctx.clearRect(0, 0, W, H); // 元素位置是 t 的函数 const x = 100 + t * 50; ctx.fillRect(x, 200, 40, 40); requestAnimationFrame(frame); } requestAnimationFrame(frame);这段代码里,t就是时间轴。所有动画本质上都是"某个属性 = f(t)"。想让方块 3 秒后停下?把x写成100 + Math.min(t, 3) * 50。想让它来回弹?用Math.sin(t)。这就是代码做视频最爽的地方——运动曲线完全由你(或模型)用数学表达,精确到帧。
理解了这一层,你就明白 Claude Opus 5.5 "做视频"的本质了:它生成的就是这样一段带时间轴逻辑的 Canvas 程序。它不需要理解"视频"这个媒体格式,它只需要理解"如何用代码描述随时间变化的画面"。
3. 拆解一段"AI 生成的视频程序"应该长什么样
3.1 骨架:初始化、时间轴、渲染循环、结束条件
一段合格的、能当"视频"用的 Canvas 程序,结构上通常分四块,我按重要性排一下。
第一块是画布初始化。拿到 canvas 元素、设置宽高、处理高分屏(devicePixelRatio)缩放。高分屏这块特别容易翻车,很多人写出来的动画在普通屏上清晰,一到 Retina 屏就糊成一片,原因就是没做 DPR 适配。正确做法是把 canvas 的width/height属性设成CSS 尺寸 × dpr,然后用ctx.scale(dpr, dpr)把坐标系缩回来。
第二块是时间轴管理。记录起始时间,提供t(秒)、progress(0 到 1 的进度)、当前处于第几个"场景"等信息。如果视频有多个片段(比如先出标题、再出图表、最后出结尾),时间轴还要负责场景切换。
第三块是渲染循环。用requestAnimationFrame驱动,每一帧清屏、按当前t绘制所有可见元素。这里有个性能要点:不要每帧都重建对象,能复用的路径、渐变、文字测量结果都缓存起来。
第四块是结束与循环控制。视频总时长到了之后,是停在最后一帧、还是循环播放、还是触发一个回调(比如通知外部"可以导出了")。这块决定了这个程序是"一次性播放"还是"可循环的动画"。
function createVideo(canvas, { duration = 10, loop = false } = {}) { const dpr = window.devicePixelRatio || 1; const cssW = canvas.clientWidth; const cssH = canvas.clientHeight; canvas.width = cssW * dpr; canvas.height = cssH * dpr; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); let startTime = null; let rafId = null; function render(now) { if (startTime === null) startTime = now; let elapsed = (now - startTime) / 1000; if (elapsed > duration) { if (loop) { startTime = now; elapsed = 0; } else { elapsed = duration; } } const progress = Math.min(elapsed / duration, 1); ctx.clearRect(0, 0, cssW, cssH); drawScene(ctx, { t: elapsed, progress, w: cssW, h: cssH }); if (elapsed < duration || loop) { rafId = requestAnimationFrame(render); } } rafId = requestAnimationFrame(render); return () => cancelAnimationFrame(rafId); }这段骨架就是"视频程序"的通用模板。Claude Opus 5.5 生成的东西,剥掉花哨的绘制逻辑,内核就是这个结构。你把这个模板记住,以后看任何 AI 生成的 Canvas 动画代码,都能一眼看穿它在干嘛。
3.2 场景编排:把"一个动画"升级成"一支片子"
单个动画和一支"视频"的区别,在于编排。一支片子通常有多个场景,每个场景有自己的入场、停留、退场,场景之间有转场。
实现方式有两种。一种是时间区间法:给每个场景定义[start, end]时间区间,渲染时判断当前t落在哪个区间,只画那个场景。另一种是状态机法:维护一个当前场景索引,时间到了就切到下一个。前者更适合"总时长固定、场景时间写死"的情况,后者更适合"场景时长动态"的情况。
转场效果是编排里最出彩的部分。淡入淡出最简单,用ctx.globalAlpha控制透明度即可。滑动转场就是把两个场景分别画在偏移的位置,用t驱动偏移量。更花哨的还有遮罩转场、粒子消散,但说实话,做信息传达型视频,淡入淡出加轻微位移就够了,过度炫技反而干扰信息接收。
这里有个经验:场景切换的时间点要留缓冲。我见过太多 AI 生成的动画,场景 A 刚消失场景 B 就硬切进来,观感很生硬。正确做法是让 A 的退场和 B 的入场有 0.3 到 0.5 秒的重叠,视觉上就顺滑多了。
3.3 缓动函数:让运动"像那么回事"的关键
线性运动(x = t * speed)看起来非常机械,像机器人在动。真实世界里没有东西是匀速启动匀速停止的,都有加速和减速。这就是**缓动函数(easing function)**存在的意义。
最常用的是easeInOutCubic:
function easeInOutCubic(x) { return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; }它的输入是 0 到 1 的进度,输出也是 0 到 1,但曲线是 S 形的——开头慢、中间快、结尾慢。把元素位置写成startX + (endX - startX) * easeInOutCubic(progress),运动立刻就有了"重量感"。
我实测下来,一套视频里 80% 的动画用easeInOutCubic和easeOutBack(带一点回弹)就够了。easeOutBack特别适合元素入场,会稍微冲过头再弹回来,很有活力:
function easeOutBack(x) { const c1 = 1.70158; const c3 = c1 + 1; return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2); }缓动函数是"AI 做视频"里最容易被忽略、但最影响观感的细节。模型如果只给你线性运动,你手动套一层缓动,质感立刻上一个台阶。
4. 从零复现:我实际跑通的一套流程
4.1 环境准备里那些容易忽略的坑
先说环境。这套东西纯前端,理论上一个 HTML 文件就能跑,但真要做成"项目",你会遇到几个坑。
第一个坑是包管理器。关键词里出现了pnpm : 无法将"pnpm"项识别为 cmdlet和pnpm' 不是内部或外部命令,这是典型的环境变量没配好。pnpm 装完之后,如果没把它的安装目录加进 PATH,命令行就找不到它。Windows 上一般是%LOCALAPPDATA%\pnpm,macOS/Linux 上通常是~/.local/share/pnpm或者通过 corepack 管理。我的建议是直接用corepack enable让 Node 自带的 corepack 接管,省得手动配 PATH。
第二个坑是 Node 版本。Canvas 相关的构建工具链对 Node 版本有要求,太老的版本跑不起来 Vite 之类的新工具。用node -v确认一下,建议 18 以上。
第三个坑是高分屏适配,前面提过,这里再强调一次,因为它太容易翻车了。你本地开发机可能是普通屏,测试同事的 Mac 是 Retina,一上去就糊。所以 DPR 适配要从第一行代码就做对,别等出问题再补。
提示:如果你在 Vue 项目里用 Canvas,记得在
onMounted之后再拿 canvas 元素,onMounted之前 DOM 还没挂载,clientWidth会是 0,画布尺寸算出来全是错的。
4.2 一个能跑的最小视频程序
下面这段是我实际跑通的、能当"视频"用的最小完整示例。它做一个 6 秒的短片:标题淡入、三个方块依次弹入、最后整体淡出。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> html, body { margin: 0; background: #111; } #stage { width: 100vw; height: 100vh; display: block; } </style> </head> <body> <canvas id="stage"></canvas> <script> const canvas = document.getElementById('stage'); const dpr = window.devicePixelRatio || 1; const W = canvas.clientWidth; const H = canvas.clientHeight; canvas.width = W * dpr; canvas.height = H * dpr; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); const DURATION = 6; function easeOutBack(x) { const c1 = 1.70158, c3 = c1 + 1; return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2); } function easeInOutCubic(x) { return x < 0.5 ? 4*x*x*x : 1 - Math.pow(-2*x+2, 3)/2; } // 把全局时间映射到某个区间内的 0~1 进度 function seg(t, from, to) { return Math.max(0, Math.min(1, (t - from) / (to - from))); } const start = performance.now(); function draw(t) { ctx.clearRect(0, 0, W, H); // 标题:0~1.2s 淡入,5~6s 淡出 const titleIn = easeInOutCubic(seg(t, 0, 1.2)); const titleOut = 1 - easeInOutCubic(seg(t, 5, 6)); ctx.globalAlpha = titleIn * titleOut; ctx.fillStyle = '#fff'; ctx.font = 'bold 48px system-ui, sans-serif'; ctx.textAlign = 'center'; ctx.fillText('用代码做视频', W/2, H/2 - 80); // 三个方块:1.5s 起依次弹入 const colors = ['#ff6b6b', '#4ecdc4', '#ffe66d']; colors.forEach((c, i) => { const p = easeOutBack(seg(t, 1.5 + i*0.4, 2.5 + i*0.4)); const out = 1 - easeInOutCubic(seg(t, 5, 6)); const x = W/2 - 180 + i * 180; const y = H/2 + 40; const size = 80 * p; ctx.globalAlpha = out; ctx.fillStyle = c; ctx.fillRect(x - size/2, y - size/2, size, size); }); ctx.globalAlpha = 1; } function loop(now) { const t = (now - start) / 1000; if (t <= DURATION) { draw(t); requestAnimationFrame(loop); } else { draw(DURATION); } } requestAnimationFrame(loop); </script> </body> </html>把这段存成.html双击打开就能看到效果。它麻雀虽小五脏俱全:DPR 适配、时间轴、缓动、分段进度、淡入淡出全都有。你把这个骨架里的draw函数换掉,就能做出完全不同的片子。这就是"AI 做视频"交付物的真实形态——一段结构清晰、可读可改的 Canvas 程序。
4.3 把 Canvas 动画导出成真正的 mp4
Canvas 跑起来是"活的",但很多时候你需要一个能发出去的 mp4 文件。导出有两条路。
第一条是录制法。用MediaRecorder配合canvas.captureStream(),把 Canvas 的画面流录下来:
const stream = canvas.captureStream(60); // 60fps const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' }); const chunks = []; recorder.ondataavailable = e => chunks.push(e.data); recorder.onstop = () => { const blob = new Blob(chunks, { type: 'video/webm' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'output.webm'; a.click(); }; recorder.start(); // 播放你的动画,结束后 recorder.stop()这条路简单直接,缺点是录制依赖实时播放,6 秒的视频就得录 6 秒,而且录出来的帧率受浏览器调度影响,可能不稳。另外它默认出的是 webm,要 mp4 还得转码。
第二条是逐帧渲染法。不依赖实时播放,而是手动控制时间轴,一帧一帧地画,每画一帧就抓一次canvas.toDataURL()或者toBlob(),最后把所有帧交给编码器合成。这条路的好处是帧率绝对稳定、可以渲染比实时更慢或更快的动画,适合对质量要求高的场景。缺点是慢,而且需要额外的编码环节(比如用 ffmpeg 把图片序列合成视频)。
我的选择是:快速预览用录制法,正式出片用逐帧法。录制法几行代码搞定,逐帧法质量可控。关键词里那个html5 视频倍速其实也提示了另一个思路——如果你只是想让 Canvas 动画"快放慢放",直接在时间轴上乘一个倍率系数就行,t * speed,比处理视频文件的倍速简单多了。
5. 那些真正会绊住你的坑
5.1 文字渲染的度量陷阱
Canvas 里画文字,最容易出问题的是文字宽度测量。你想让一段文字水平居中,得先知道它多宽,用ctx.measureText(text).width。但这个测量结果依赖字体是否加载完成——如果字体还没加载好你就测量,量出来的是 fallback 字体的宽度,等真字体加载完,文字宽度变了,居中就歪了。
解决办法是用document.fonts.ready或者FontFace.load()等字体就绪后再开始渲染。这个坑我在做文字动效时踩过不止一次,表现就是"本地看着好好的,换个网络环境字体加载慢,标题就偏了"。
另一个陷阱是多行文字。Canvas 没有自动换行,你得自己按宽度切分字符串。中文还好,按字符切;英文得按单词切,不然会把单词从中间劈开。这块逻辑不复杂但很琐碎,建议封装成一个wrapText工具函数复用。
5.2 性能:什么时候 Canvas 会卡
Canvas 2D 性能其实不错,但有几个操作是性能杀手。
一是频繁的shadowBlur。阴影效果很漂亮,但每帧对大量元素加阴影,GPU 压力陡增。我的经验是阴影能不用就不用,非要用就只给少数几个关键元素加。
二是每帧重建渐变对象。createLinearGradient这类调用有开销,如果渐变参数不变,应该缓存起来复用,而不是每帧新建。
三是大尺寸画布。4K 画布每帧清屏加重绘,数据量是 1080p 的四倍。如果只是做演示,1080p 足够,别盲目上 4K。
四是**clearRect之后的全量重绘**。这是 Canvas 的固有模式,没法避免,但可以通过"只重绘变化区域"来优化——不过对于全屏动画,全量重绘通常还是最简单可靠的方案,优化收益有限,别过早优化。
5.3 高分屏、跨域和导出失败
高分屏适配前面说过了,这里补充一个细节:DPR 不是恒定的。用户把窗口从主屏拖到副屏,两块屏 DPR 可能不同,devicePixelRatio会变。严谨的做法是监听resize事件,重新计算画布尺寸。虽然大多数场景下用户不会这么干,但做产品级的播放器时得考虑。
跨域问题主要出现在导出环节。如果你的 Canvas 上画了来自其他域的图片,toDataURL()和toBlob()会因为画布被"污染"而抛安全错误。解决办法是给图片设置crossOrigin = 'anonymous',并且确保图片服务器返回了正确的 CORS 头。这个坑在导出视频时特别致命——动画播放一切正常,一点导出就报错,排查半天才发现是图片跨域。
导出失败的另一个常见原因是MediaRecorder的 mimeType 不被支持。不同浏览器支持的编码格式不一样,Chrome 支持 webm/vp8、webm/vp9,Safari 对 webm 支持有限。稳妥的做法是先MediaRecorder.isTypeSupported()检测,挑一个支持的格式,别写死。
6. 这套思路还能往哪儿延伸
6.1 数据可视化视频:把图表变成动态叙事
Canvas 做视频最实用的延伸方向之一是数据可视化动画。静态图表谁都会画,但"柱状图一根根长出来、折线图从左往右画出来、数字滚动增长"这种动态呈现,信息传达效率高得多。
实现上,柱状图的高度就是targetHeight * easeOutCubic(progress),折线图的绘制用ctx.setLineDash配合lineDashOffset做"描边动画",数字滚动用Math.floor(target * progress)。这些技巧组合起来,几分钟就能做出一支像模像样的数据短片。关键词里的canvas 绘图、canvas 绘图引擎指向的正是这个方向。
6.2 交互式内容:视频的下一形态
Canvas 程序做出来的东西,天生就是可交互的。你可以在动画播放到某个节点时暂停,等用户点击再继续;可以让用户拖动滑块改变某个参数,实时看到画面变化;可以做成"分支叙事",用户的选择决定接下来播哪个片段。
这是 mp4 永远做不到的。mp4 是死的,Canvas 程序是活的。如果你的内容需要用户参与,比如教学演示、产品配置器、互动广告,那代码路线几乎是唯一选择。关键词里的canvas 小人形象、canvas 文字 3d 效果说明大家已经在往更丰富的视觉表现上探索了,而交互性是这个探索的自然延伸。
6.3 和 AI 协作的正确姿势
回到标题本身。Claude Opus 5.5 这类模型在"做视频"这件事上的真正价值,不是替你完成全部工作,而是帮你跨过从想法到可运行代码的那道坎。
我的实际用法是这样的:先想清楚这支片子要表达什么、分几个场景、每个场景停留多久,然后用自然语言把需求描述给模型,让它生成 Canvas 代码骨架。拿到代码后,我自己调缓动曲线、调颜色、调时间节奏——这些"审美"层面的东西,模型给的是起点,最终效果还得靠人把关。
模型生成的代码常见问题是:时间节奏偏快、缓动用得少、场景切换生硬。这三点几乎是通病。你拿到代码后,先把所有duration拉长 30%,再给每个入场动画套上easeOutBack,最后给场景切换加 0.3 秒重叠,观感立刻不一样。这套"后期处理"流程我用了很多次,屡试不爽。
注意:模型生成的 Canvas 代码有时会漏掉 DPR 适配,或者把
canvas.width和 CSS 宽度搞混。拿到代码第一件事就是检查画布初始化那几行,这是最容易埋雷的地方。
7. 我在实际项目里攒下的几条经验
做这类"代码生成视频"的东西做多了,有些体会是文档里不会写的,这里掏心窝子分享几条。
第一条:先把时间轴画出来再写代码。我早期习惯边写边调,结果就是时间点全乱,改一个场景的时长,后面全得跟着挪。后来我改成先在纸上(或者一个简单的数组里)把每个场景的[start, end]列清楚,再照着写渲染逻辑,效率高了一大截。时间轴是这支片子的"骨架",骨架定了,肉怎么长都好说。
第二条:颜色别超过五个。这是设计常识,但在代码里特别容易失控——每个元素随手给个颜色,最后花花绿绿像打翻了调色盘。我的做法是定义一组主题色常量,所有元素从这组里取,视觉上立刻统一。
第三条:给动画留"呼吸"。元素入场后别马上开始下一个动作,停个 0.3 到 0.5 秒,让观众的眼睛跟上。AI 生成的动画往往节奏太赶,信息还没看清就切走了。这个"呼吸感"是区分业余和专业的关键。
第四条:导出前先在多个浏览器里过一遍。Canvas 的 API 兼容性总体很好,但MediaRecorder、字体渲染、DPR 处理在不同浏览器上还是有差异。Chrome 上完美的效果,Safari 上可能字体就变了。正式出片前,Chrome、Safari、Firefox 各跑一遍,能省掉很多返工。
第五条:把可调参数抽出来。别把时长、颜色、文案硬编码在绘制逻辑里。抽成一个config对象放最上面,改起来一目了然。这个习惯在需要反复调整的创作场景里,能帮你省下大量时间。
这套东西说到底,核心就一句话:"AI 做视频"做的是程序,不是像素。理解了这一点,剩下的都是工程问题——时间轴怎么设计、缓动怎么调、性能怎么优化、导出怎么搞。而这些问题,恰恰是我们这些写代码的人最擅长的。模型帮你把想法翻译成代码,你把代码打磨成作品,这个分工,我觉得挺合理。