1. 从"Claude Opus 5.5 是怎么做出视频的"这个标题说起
第一次看到"Claude Opus 5.5 是怎么做出视频的"这个标题,我脑子里冒出来的第一个念头是:模型本身并不直接输出视频文件,它输出的是代码。所谓"做出视频",本质上是让模型生成一段可运行的程序,再由这段程序在浏览器里把画面一帧一帧画出来,最后通过录制或导出变成视频。这个链路里真正干活的是JavaScript + Canvas,模型扮演的是"写代码的人"。
这个认知很关键,因为很多人对"AI 做视频"有误解,以为模型内部有个视频生成器,输入一句话就吐出一个 mp4。实际路径要朴素得多:模型理解你的需求,规划出动画的时间轴、元素、运动曲线,然后把这些翻译成 Canvas 的绘制指令。视频只是这些指令连续执行后的视觉结果。
所以这篇文章我想聊的不是"某个模型有多神",而是这条链路到底怎么搭、每一环容易在哪里翻车、以及怎么把它做得稳定可复现。适合两类人看:一类是想用模型辅助做动画/可视化 demo 的前端同学,另一类是好奇"AI 生成视频"背后到底发生了什么的技术爱好者。哪怕你只会一点点 JS,跟着思路也能跑通一个最小版本。
关键词里出现的canvas、canvas绘图、canvas绘图引擎、javascript canvas、canvas 2d vue、canvas文字3d效果、canvas小人形象,其实都指向同一件事:用 Canvas 2D 上下文做逐帧绘制。下面我就按真实项目里会遇到的顺序,一层层拆开讲。
2. 模型"做视频"的真实链路:它到底产出了什么
2.1 模型输出的是代码,不是视频文件
先把最容易混淆的一点讲透。当你对模型说"帮我做一个 10 秒的粒子动画视频",它给你的通常是一段 HTML 或 JS 代码,里面包含<canvas>元素、requestAnimationFrame循环、以及一堆ctx.arc、ctx.fillRect、ctx.fillText之类的绘制调用。这段代码在浏览器里跑起来,你看到的就是动画。
那"视频"从哪来?两个途径:
- 屏幕录制:用系统的录屏工具或
MediaRecorderAPI 把 canvas 的画面录下来,导出 webm 或 mp4。 - 逐帧导出:把动画的时间轴离散成 N 帧,每帧调用一次绘制函数,用
canvas.toDataURL()或toBlob()存成图片序列,再用 ffmpeg 合成视频。
我实测下来,短动画(10 秒以内)用 MediaRecorder 最省事,长动画或者需要精确帧率的场景,逐帧导出更可控。模型一般会默认给你 MediaRecorder 方案,因为它代码短、依赖少。
提示:模型生成的代码里如果出现
canvas.captureStream(),那是配合 MediaRecorder 用的,不是所有浏览器行为都一致,导出前一定要在目标浏览器里实测一遍。
2.2 时间轴、状态与绘制三件套
不管动画多复杂,拆开看都是三样东西:
- 时间轴(timeline):当前是第几秒、进度百分比是多少。
- 状态(state):这一刻每个元素的位置、透明度、缩放、旋转。
- 绘制(render):把状态翻译成 Canvas 的像素。
模型写代码时,如果它靠谱,会帮你把这三者分开。比如用一个t表示归一化时间(0 到 1),然后x = lerp(startX, endX, easeInOut(t))算出位置,最后ctx.fillRect(x, y, w, h)画出来。这种结构的好处是:改动画曲线不用动绘制代码,改绘制不用动时间逻辑。
我见过不少模型生成的"一坨"代码,把时间计算和绘制混在一个大函数里,改一个参数就全乱。遇到这种情况,别急着用,先让它重构:明确要求"把时间轴、状态计算、绘制分成三个函数"。这一步能省掉后面大量返工。
2.3 为什么 Canvas 而不是 DOM 或 SVG
有人会问,做动画用 CSS 或 SVG 不也行吗?行,但 Canvas 有两个不可替代的优势:
- 元素数量大时性能稳:几百上千个粒子,DOM 节点会卡,Canvas 只是多画几个圆。
- 像素级控制:做粒子、拖尾、滤镜、像素操作,Canvas 直接操作
ImageData,DOM 做不到。
代价是 Canvas 没有"对象"概念,你画完就忘了,下一帧要重新画。所以每一帧都要清屏重绘(ctx.clearRect或铺一层背景),这也是新手最容易忘的一步——忘了清屏,画面就会糊成一团拖影。
关键词里的canvas 2d vue说明很多人是在 Vue 项目里用 Canvas。这里有个坑:别把 canvas 的绘制逻辑写进 Vue 的响应式数据里,否则每次data变化都触发重渲染,性能直接崩。正确做法是把 canvas 相关的变量放在setup外的普通变量或shallowRef里,Vue 只负责挂载和卸载。
3. 搭一个最小可跑的 Canvas 动画骨架
3.1 环境准备:一个 HTML 文件就够
不需要 webpack、不需要 vite,先跑通再说。新建一个index.html:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <style> body { margin: 0; background: #111; display: flex; justify-content: center; align-items: center; height: 100vh; } canvas { background: #000; border-radius: 8px; } </style> </head> <body> <canvas id="stage" width="800" height="450"></canvas> <script src="main.js"></script> </body> </html>注意width和height是 canvas 的绘制分辨率,和 CSS 尺寸是两回事。如果你用 CSS 把 canvas 拉大,画面会模糊。想要高清,得按devicePixelRatio放大绘制分辨率,再用 CSS 缩回去。这个细节模型经常漏,导出视频时就会糊。
3.2 主循环与时间归一化
main.js的核心就是一个循环:
const canvas = document.getElementById('stage'); const ctx = canvas.getContext('2d'); const DURATION = 10000; // 10 秒 let startTime = null; function easeInOut(t) { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; } function lerp(a, b, t) { return a + (b - a) * t; } function render(progress) { ctx.clearRect(0, 0, canvas.width, canvas.height); // 背景 ctx.fillStyle = '#0a0a12'; ctx.fillRect(0, 0, canvas.width, canvas.height); // 一个从左到右移动的圆 const x = lerp(100, 700, easeInOut(progress)); const y = canvas.height / 2; ctx.beginPath(); ctx.arc(x, y, 40, 0, Math.PI * 2); ctx.fillStyle = '#4af'; ctx.fill(); } function loop(now) { if (startTime === null) startTime = now; const elapsed = now - startTime; const progress = Math.min(elapsed / DURATION, 1); render(progress); if (progress < 1) requestAnimationFrame(loop); } requestAnimationFrame(loop);这段代码里,progress是 0 到 1 的归一化时间,easeInOut是缓动函数,lerp是线性插值。把时间归一化是让动画可复用的关键——不管动画是 5 秒还是 30 秒,逻辑都不用改,只改DURATION。
3.3 用 MediaRecorder 把画面录成视频
在上面基础上加录制:
const stream = canvas.captureStream(60); // 60fps const recorder = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=vp9' }); 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 = 'animation.webm'; a.click(); }; recorder.start(); requestAnimationFrame(loop); // 动画结束后调用 recorder.stop()这里有几个实测经验:
captureStream(60)的帧率是上限,实际帧率取决于你的绘制速度。如果每帧绘制超过 16ms,录出来就会掉帧。mimeType优先选vp9,体积小画质好;Safari 对 webm 支持有限,需要 mp4 的话得换方案。- 录制开始和动画开始要对齐,否则开头会有一段静止画面。
注意:MediaRecorder 录的是"实时"画面,如果你的动画依赖
requestAnimationFrame,而浏览器标签页切到后台,rAF 会被节流甚至暂停,录出来就是断的。所以录制时别切标签页。
4. 让画面"像视频"的几个关键技巧
4.1 缓动曲线决定观感
线性运动看起来最"假",因为现实里没有东西是匀速启动匀速停止的。模型如果只给你lerp,画面会很生硬。加缓动之后立刻不一样:
| 缓动类型 | 公式思路 | 适用场景 |
|---|---|---|
| easeInOut | 两头慢中间快 | 通用位移 |
| easeOut | 开头快结尾慢 | 元素入场 |
| easeIn | 开头慢结尾快 | 元素退场 |
| 弹性 | 带过冲回弹 | 强调、Q 弹感 |
| 回弹 | 超出后回位 | 按钮、卡片 |
我一般会让模型"给每个元素配一个缓动函数",而不是全局一个。不同元素用不同曲线,画面才有层次。
4.2 拖尾、残影与运动模糊
视频之所以"顺滑",一部分来自运动模糊。Canvas 里模拟很简单:不清屏,而是铺一层半透明背景。
ctx.fillStyle = 'rgba(10, 10, 18, 0.15)'; ctx.fillRect(0, 0, canvas.width, canvas.height);这样上一帧的圆会留下淡淡的痕迹,形成拖尾。透明度越低,拖尾越长。这个技巧做粒子、光轨、流星效果特别好用。但要注意:拖尾和清屏是互斥的,用了半透明覆盖就别再clearRect。
4.3 文字与 3D 感的营造
关键词里有canvas文字3d效果,Canvas 2D 本身没有 3D,但可以用多层偏移 + 渐变伪造:
function draw3DText(text, x, y) { for (let i = 8; i > 0; i--) { ctx.fillStyle = `rgba(0, 0, 0, ${0.1 + i * 0.05})`; ctx.fillText(text, x + i, y + i); } ctx.fillStyle = '#fff'; ctx.fillText(text, x, y); }先画多层深色偏移当"厚度",最后画一层亮色当"正面"。配合ctx.shadowBlur还能加发光。这套做法在标题动画里非常常见,成本低效果好。
4.4 小人形象与角色动画
canvas小人形象这类需求,本质是用基本图形拼角色:圆当头、矩形当身体、线段当四肢,然后用关节角度驱动。核心是维护一个"骨骼状态":
const skeleton = { head: { x: 400, y: 150, r: 30 }, body: { x: 400, y: 200, w: 20, h: 80 }, leftArm: { angle: 0 }, rightArm: { angle: 0 }, };走路动画就是让leftArm.angle和rightArm.angle按正弦波反相摆动。模型生成这类代码时,容易把角度写死,你要主动要求"用时间驱动关节角度",这样才有连续动画。
5. 模型生成代码时最容易翻车的几个点
5.1 坐标系与 DPR 处理
模型经常忽略devicePixelRatio,导致高分屏上画面模糊。正确做法:
const dpr = window.devicePixelRatio || 1; canvas.width = 800 * dpr; canvas.height = 450 * dpr; canvas.style.width = '800px'; canvas.style.height = '450px'; ctx.scale(dpr, dpr);这样绘制坐标还是按 800x450 来写,但实际像素翻倍,画面清晰。导出视频前一定要确认这一步,否则录出来是糊的。
5.2 时间依赖与帧率解耦
新手(和模型)常犯的错:用"每帧移动 5 像素"来驱动动画。这样在 60Hz 屏上是 300px/s,在 120Hz 屏上就变成 600px/s,速度翻倍。
正确做法是用时间差驱动:
const dt = (now - lastTime) / 1000; // 秒 x += speed * dt; // speed 单位是 px/s这样不管帧率多少,速度都一致。模型生成的代码如果用了固定步长,一定要让它改成基于dt。
5.3 内存与性能陷阱
- 每帧 new 对象:在
render里new Particle()会产生大量垃圾,GC 一卡画面就顿。粒子要预先分配好,循环复用。 - 频繁改 canvas 尺寸:改
canvas.width会清空画布并重置上下文状态,别在循环里改。 - 阴影和模糊滥用:
shadowBlur和filter很吃性能,元素多的时候能关就关。
我实测过一个 500 粒子的场景,开shadowBlur帧率从 60 掉到 25,关掉立刻恢复。所以发光效果优先用叠加半透明圆模拟,而不是 shadowBlur。
5.4 导出格式与兼容性
MediaRecorder的mimeType支持情况因浏览器而异。稳妥的探测方式:
const types = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm']; const mimeType = types.find(t => MediaRecorder.isTypeSupported(t));如果目标平台只收 mp4,浏览器端录制就比较麻烦,通常的做法是导出图片序列 + 服务端 ffmpeg 合成。图片序列方案虽然慢,但帧率精确、画质无损,适合正式产出。
6. 从 demo 到可交付:我的实操流程
6.1 先定分镜再写代码
直接让模型"做个视频"效果通常很一般,因为它不知道你想要什么。我的做法是先自己列一个分镜表:
| 时间段 | 画面内容 | 关键动作 |
|---|---|---|
| 0-2s | 标题淡入 | 透明度 0→1,上移 20px |
| 2-5s | 粒子汇聚成 logo | 粒子从四周飞入 |
| 5-8s | logo 发光脉冲 | 缩放 1→1.1→1 |
| 8-10s | 整体淡出 | 透明度 1→0 |
有了这张表,再让模型"按这个分镜生成 Canvas 代码",产出质量会高一个档次。模型擅长执行明确指令,不擅长猜你的审美。
6.2 分段验证,别一次性生成
我一般让模型一次只生成一个分镜的代码,跑通、调好,再生成下一段。最后用一个总时间轴把它们串起来。这样出问题容易定位,不会一坨代码里到处是 bug。
串联时用一个"场景调度器":
const scenes = [ { start: 0, end: 2, render: renderTitle }, { start: 2, end: 5, render: renderParticles }, { start: 5, end: 8, render: renderLogo }, { start: 8, end: 10, render: renderFadeOut }, ]; function render(progress) { const time = progress * DURATION; const scene = scenes.find(s => time >= s.start * 1000 && time < s.end * 1000); if (scene) scene.render((time - scene.start * 1000) / ((scene.end - scene.start) * 1000)); }每个场景函数只关心自己的局部进度,互不干扰。
6.3 参数外置,方便调优
把颜色、时长、缓动、尺寸这些抽成一个config对象,改起来不用翻代码:
const config = { duration: 10000, bg: '#0a0a12', primary: '#4af', accent: '#f4a', titleSize: 64, };模型生成代码时经常把魔法数字散落各处,你要主动要求"所有可调参数集中到 config"。这一步对后期迭代帮助巨大。
6.4 录制与后期
录制我一般用MediaRecorder出 webm,再用 ffmpeg 转 mp4:
ffmpeg -i animation.webm -c:v libx264 -pix_fmt yuv420p -crf 18 output.mp4-crf 18是接近无损的画质,yuv420p保证兼容性。如果要做 GIF,加一步调色板生成,否则颜色会脏。
提示:录制时把浏览器窗口固定大小,别缩放,否则 canvas 的 CSS 尺寸变化会影响录制画面比例。
7. 一些容易被忽略的细节与我的经验
7.1 关于"模型版本"这件事
标题里提到具体版本号,但我想说的是:版本号对"能不能做出视频"这件事影响没那么大,真正决定成败的是你的提示词结构和验证流程。同一个模型,你给"做个视频"和给"按这张分镜表生成 Canvas 代码,每段独立函数,参数外置",产出差距是数量级的。
所以与其纠结用哪个版本,不如把精力花在把需求拆清楚上。分镜表、参数表、函数结构,这三样给到位,任何像样的模型都能产出可用的代码。
7.2 调试 Canvas 的笨办法
Canvas 没有 DOM 检查器,画错了很难查。我的土办法是:
- 画坐标网格:临时在背景画 50px 间隔的网格线,一眼看出元素位置对不对。
- 打印状态:在
render里console.log关键变量,看数值是否符合预期。 - 单帧模式:把
progress固定成某个值(比如 0.5),反复调这一帧,调好再放开。
这三招比盲目改代码高效得多。
7.3 性能监控
用performance.now()测每帧耗时:
const t0 = performance.now(); render(progress); const cost = performance.now() - t0; if (cost > 16) console.warn('掉帧', cost);超过 16ms 就意味着达不到 60fps。定位到具体是哪个绘制调用慢,再针对性优化。
7.4 关于"视频流量预测"这类周边需求
关键词里出现了视频流量预测、短视频平台这类词,说明有人关心的是"做出来的视频能不能火"。这块我不展开,但一个基本判断是:技术产出和传播效果是两回事。Canvas 动画做得再顺滑,内容没意思照样没人看。技术是下限,创意是上限。把技术链路跑通,至少保证你的创意不会被实现能力拖后腿。
7.5 跨平台与工具链的坑
关键词里混进了不少环境问题,比如pnpm 不是内部或外部命令、claude.exe 无法运行、vmware 无法连接到虚拟机。这些和"做视频"本身没关系,但反映了一个现实:很多人的卡点不在核心逻辑,而在环境配置。
我的建议是:做 Canvas 动画这种前端任务,优先用浏览器直接跑,别一上来就上构建工具。一个 HTML 文件能解决的事,不要引入 node_modules。等 demo 跑通了,再考虑工程化。环境问题消耗的时间,往往比写动画本身还多。
8. 把这条链路固化成自己的模板
跑通一次之后,我建议把它固化成一个可复用的模板,下次直接改参数就行。模板包含:
- 一个
index.html,处理 DPR 和 canvas 挂载。 - 一个
main.js,包含主循环、场景调度、录制逻辑。 - 一个
config.js,所有可调参数。 - 一个
scenes/目录,每个分镜一个文件。
这样下次要做新动画,只需要写新的场景函数,其余全部复用。模型也只需要生成场景函数那部分代码,上下文更聚焦,产出更稳。
我自己的模板里还加了几个开关:DEBUG控制是否画网格、AUTO_RECORD控制是否自动开始录制、FIXED_PROGRESS用于单帧调试。这几个开关在开发阶段能省大量时间。
最后分享一个我踩过的坑:别在动画里用setTimeout或setInterval驱动。它们和requestAnimationFrame不同步,会导致画面撕裂或节奏错乱。所有时间相关的逻辑,统一走requestAnimationFrame的时间戳,这是唯一可靠的时间源。