1. 从"写代码"到"做视频":为什么这条路值得走
用大模型写代码做视频,这个方向我第一次接触的时候也觉得有点绕——视频不是应该用剪辑软件做吗?但真正上手之后才发现,这条路解决的是一个非常具体的痛点:当你需要批量生成、参数化控制、或者把视频生成嵌入到自动化流程里时,传统剪辑软件的手工操作就变成了瓶颈。
Claude Opus 5.5 在这件事上的价值在于,它对 Canvas 2D、WebGL、动画时序、缓动函数这些前端图形编程的理解已经到了"你说人话它就能写出能跑的代码"的程度。你不需要是图形学专家,只要能把想要的视觉效果描述清楚,它就能帮你把代码骨架搭出来,剩下的就是调参和微调。
这篇文章适合三类人看:一是想用代码做视频但不知道从哪下手的前端开发者;二是需要批量生成营销视频、数据可视化视频的运营或产品同学;三是已经会用 AI 写代码,但还没试过把它用在视频生成场景的同行。我会把 7 类技术路线拆开讲清楚各自的适用场景和坑,再用 5 个实际案例展示从提示词到成片的完整链路,最后给出一套可以直接复用的提示词框架。
先说一个反直觉的结论:用代码做视频,最难的不是写代码,而是想清楚"哪些东西该用代码控制,哪些东西该交给后期"。我见过太多人一上来就想用 Canvas 画所有东西,结果做到一半发现文字排版、音频同步、编码导出这些环节全是坑。正确的做法是先明确视频的"可控维度",再选择对应的技术路线。
2. 七类技术路线的适用边界与选型逻辑
2.1 Canvas 2D 逐帧绘制:最直接但也最容易踩性能坑
Canvas 2D 是最容易上手的路线。核心思路很简单:用requestAnimationFrame驱动一个时间轴,每一帧根据当前时间t计算出所有元素的位置、透明度、缩放,然后重绘整个画面。Claude Opus 5.5 写这类代码非常熟练,你只要告诉它"我要一个圆形从左侧移动到右侧,用时 2 秒,带缓动",它就能给你写出带easeInOutQuad的完整实现。
但这里有个性能陷阱:如果你每帧都调用clearRect然后重绘所有元素,当元素数量超过 200 个时,帧率会明显下降。我的经验是,静态背景用离屏 Canvas 预渲染一次,动态元素才逐帧绘制。另外,ctx.save()和ctx.restore()的配对使用一定要严格,否则变换矩阵会累积,画面会莫名其妙地偏移。
// 典型的逐帧绘制骨架 const duration = 3000; // 毫秒 let startTime = null; function render(timestamp) { if (!startTime) startTime = timestamp; const elapsed = timestamp - startTime; const progress = Math.min(elapsed / duration, 1); const eased = easeInOutQuad(progress); ctx.clearRect(0, 0, width, height); drawBackground(); // 静态部分 drawMovingCircle(eased); // 动态部分 if (progress < 1) requestAnimationFrame(render); } function easeInOutQuad(t) { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; }这段代码里easeInOutQuad是缓动函数,决定了动画的"手感"。线性运动看起来很机械,加了缓动之后就有"加速再减速"的自然感。Claude 对这类函数的理解很到位,你可以直接说"我要一个先快后慢的缓动",它会给你easeOutQuad或easeOutCubic。
2.2 WebGL 着色器路线:适合粒子、流体、光效类视觉
当你要做的是粒子系统、流体模拟、光线追踪这类效果时,Canvas 2D 就不够用了,必须上 WebGL。这条路线的核心是片元着色器——你写一段 GLSL 代码,GPU 会对每个像素并行执行它,算出这个像素的颜色。
Claude Opus 5.5 写 GLSL 的能力让我挺意外的。你描述"我要一个从中心向外扩散的波纹效果,颜色从青色渐变到紫色",它能直接给你写出带smoothstep和mix的片元着色器。但要注意,GLSL 的调试非常痛苦,因为报错信息往往只有"编译失败"四个字,不告诉你哪一行错了。我的做法是先用最简单的着色器跑通,再逐步加复杂度,每加一步就验证一次。
// 片元着色器:中心扩散波纹 precision mediump float; uniform float u_time; uniform vec2 u_resolution; varying vec2 v_uv; void main() { vec2 uv = v_uv; float dist = distance(uv, vec2(0.5)); float wave = sin(dist * 20.0 - u_time * 3.0) * 0.5 + 0.5; float fade = smoothstep(0.5, 0.0, dist); vec3 color = mix(vec3(0.0, 0.8, 0.8), vec3(0.6, 0.2, 0.9), wave); gl_FragColor = vec4(color * fade, 1.0); }这里u_time是每帧更新的时间 uniform,v_uv是插值后的纹理坐标。smoothstep用来做边缘淡出,避免波纹在边界处突然截断。这套代码在 1080p 下跑 60 帧毫无压力,因为 GPU 并行计算的能力远超 CPU。
2.3 SVG + CSS 动画:适合图表、文字排版类视频
如果你的视频主要是数据图表、文字动画、图标变换这类"矢量风格"的内容,SVG + CSS 动画其实是效率最高的路线。原因是 SVG 的路径、文字、渐变都是声明式的,你改一个属性就能改变视觉效果,不需要像 Canvas 那样手动重绘。
Claude 在这条路线上的优势是它能直接生成结构清晰的 SVG 代码,并且知道stroke-dasharray和stroke-dashoffset配合可以做出"线条逐渐画出"的效果。这个技巧在做流程图、时间线动画时特别有用。
<svg viewBox="0 0 400 100"> <path d="M10,50 L390,50" stroke="#333" stroke-width="2" stroke-dasharray="380" stroke-dashoffset="380"> <animate attributeName="stroke-dashoffset" from="380" to="0" dur="2s" fill="freeze"/> </path> </svg>stroke-dasharray设为路径总长度,stroke-dashoffset从总长度变到 0,视觉上就是线条从左到右逐渐画出来。这个技巧我用了很多次,做数据增长动画、流程演示都很稳。
2.4 纯 JavaScript 时序控制:把动画逻辑和渲染解耦
不管用哪种渲染方式,时序控制层都是独立的。我的做法是写一个简单的Timeline类,管理所有动画片段的开始时间、持续时间和缓动函数。这样渲染层只需要问"当前时间 t 下,这个元素的属性是什么",不需要关心动画逻辑。
class Timeline { constructor() { this.tracks = []; } add({ start, duration, from, to, ease = t => t, onUpdate }) { this.tracks.push({ start, duration, from, to, ease, onUpdate }); } seek(time) { for (const track of this.tracks) { const local = (time - track.start) / track.duration; if (local < 0 || local > 1) continue; const eased = track.ease(Math.min(Math.max(local, 0), 1)); const value = track.from + (track.to - track.from) * eased; track.onUpdate(value); } } }这个类只有 20 行,但它是整个视频生成系统的骨架。你可以往里面加任意多条轨道,每条轨道控制一个属性。Claude 写这种"工具类"代码几乎不会出错,你只要把接口描述清楚就行。
2.5 音频驱动动画:用 Web Audio API 做可视化
做音乐可视化视频时,需要从音频里提取频谱数据来驱动画面。Web Audio API 的AnalyserNode可以实时给出频率数据,但在离线渲染场景下,你需要用OfflineAudioContext预先分析整段音频,把每帧的频谱数据存成数组,渲染时直接查表。
async function analyzeAudio(buffer) { const offlineCtx = new OfflineAudioContext(1, buffer.length, buffer.sampleRate); const source = offlineCtx.createBufferSource(); source.buffer = buffer; const analyser = offlineCtx.createAnalyser(); analyser.fftSize = 256; source.connect(analyser); analyser.connect(offlineCtx.destination); source.start(); const frames = []; const data = new Uint8Array(analyser.frequencyBinCount); // 按帧采样,每帧取一次频谱 for (let t = 0; t < buffer.duration; t += 1/30) { analyser.getByteFrequencyData(data); frames.push([...data]); } return frames; }这里fftSize决定了频率分辨率,256 对应 128 个频段。帧率我用的 30fps,和视频输出帧率保持一致。这个方案的好处是渲染时不需要实时分析音频,性能稳定。
2.6 离屏渲染 + 编码导出:把 Canvas 变成 MP4
前面五条路线都是在浏览器里"播放"动画,但你要的是视频文件。导出环节有两个方案:一是用MediaRecorder录制 Canvas 流,二是用CCapture或Whammy这类库逐帧编码。
MediaRecorder的优点是简单,缺点是它录制的是实时流,如果动画卡顿,录出来的视频也会卡。更可靠的做法是逐帧渲染 + 编码,虽然慢,但每一帧都是精确的。
const stream = canvas.captureStream(30); const recorder = new MediaRecorder(stream, { mimeType: 'video/webm;codecs=vp9', videoBitsPerSecond: 8000000 }); const chunks = []; recorder.ondataavailable = e => chunks.push(e.data); recorder.onstop = () => { const blob = new Blob(chunks, { type: 'video/webm' }); // 下载或上传 }; recorder.start(); // 播放动画... recorder.stop();videoBitsPerSecond设成 8Mbps 是我实测下来 1080p 比较平衡的值,再低会有明显压缩痕迹,再高文件太大。
2.7 服务端渲染路线:用 Node.js + FFmpeg 批量生成
如果你要批量生成几百条视频,浏览器方案就不合适了,得上服务端。核心思路是用node-canvas或puppeteer在服务端渲染每一帧,然后用fluent-ffmpeg把帧序列编码成 MP4。
这条路线最重,但也是唯一能支撑批量生产的方案。Claude 写fluent-ffmpeg的调用代码很熟练,你只要告诉它输入帧序列的路径模式和输出参数就行。
const ffmpeg = require('fluent-ffmpeg'); ffmpeg() .input('frames/frame_%04d.png') .inputFPS(30) .outputOptions('-c:v', 'libx264', '-pix_fmt', 'yuv420p') .output('output.mp4') .on('end', () => console.log('done')) .run();-pix_fmt yuv420p这个参数一定要加,否则某些播放器会显示黑屏。这是我踩过的坑,当时排查了半天才发现是像素格式的问题。
3. 五个案例拆解:从提示词到成片的完整链路
3.1 案例一:数据增长柱状图动画
需求是做一个"季度销售额增长"的柱状图动画,柱子从底部升起,数字从 0 滚动到目标值。
我给 Claude 的提示词是:"用 Canvas 2D 写一个柱状图动画,5 根柱子,高度分别是 120、200、150、280、320,柱子从底部升起用时 1.5 秒,带 easeOutCubic 缓动,每根柱子延迟 0.15 秒开始,柱子顶部显示数字,数字从 0 滚动到对应值。"
Claude 返回的代码基本可以直接用,我只改了两个地方:一是把颜色从默认的蓝色改成了品牌色;二是加了柱子的圆角,用roundRect实现。整个案例从提示词到成片大概花了 20 分钟,其中 15 分钟是在调颜色和间距。
这个案例的关键经验是:数字滚动要用Math.floor取整,否则会出现小数闪烁。另外,数字的字体要用等宽字体,不然滚动时宽度会跳。
3.2 案例二:粒子汇聚成 Logo
需求是几百个粒子从随机位置飞向中心,最终拼成一个 Logo 形状。
这个案例必须用 WebGL,因为 Canvas 2D 跑 500 个粒子就开始掉帧了。我给 Claude 的提示词是:"用 WebGL 写一个粒子系统,500 个粒子,初始位置随机分布在画布内,目标位置是 Logo 的采样点,粒子在 2 秒内从初始位置移动到目标位置,带 easeInOutQuad 缓动,粒子颜色从随机色渐变到白色。"
Claude 给出的方案是用gl.POINTS绘制粒子,顶点着色器里做位置插值。这里有个细节:Logo 的采样点需要预先从图片里提取,我用了一个离屏 Canvas 读取像素,每隔 4 个像素取一个点,最终得到约 500 个目标位置。
这个案例的坑在于:粒子的初始位置如果完全随机,会有一些粒子从画布外飞进来,视觉上很突兀。我的做法是把初始位置限制在画布内,并且让粒子的初始透明度为 0,前 0.3 秒淡入。
3.3 案例三:文字逐字打出 + 光标闪烁
需求是模拟打字机效果,文字逐字出现,末尾有光标闪烁。
这个用 SVG + CSS 动画最合适,因为文字排版交给 SVG 处理,动画交给 CSS。我给 Claude 的提示词是:"用 SVG 和 CSS 做一个打字机效果,文字是'Hello World',每个字符间隔 0.1 秒出现,光标用竖线表示,闪烁频率 1Hz。"
Claude 给出的方案是用stroke-dasharray控制每个字符的显示,但这样有个问题:中文字符的路径长度不一致,用 dasharray 会导致显示速度不均匀。后来我改成了用opacity控制,每个字符一个<tspan>,用 CSS animation 的animation-delay逐个显示。
这个案例的经验是:做文字动画时,能用 opacity 就别用路径动画,因为路径动画在不同字体下的表现差异很大。
3.4 案例四:音频频谱可视化
需求是把一段音乐做成频谱可视化视频,频谱条随音乐跳动。
这个案例用了 Web Audio API + Canvas 2D。我给 Claude 的提示词是:"用 Web Audio API 分析音频频谱,用 Canvas 2D 绘制 64 根频谱条,每根条的高度对应频段能量,颜色从绿色渐变到红色,帧率 30fps。"
Claude 给出的代码里用了getByteFrequencyData,这个是对的。但有个细节它没考虑到:频谱数据需要做平滑处理,否则条会跳得很厉害。我的做法是每帧的新值和上一帧的值做加权平均,权重 0.7 给新值,0.3 给旧值。
const smoothed = data.map((v, i) => v * 0.7 + prevData[i] * 0.3); prevData = smoothed;这个平滑处理让视觉效果从"抽搐"变成了"律动",差别非常大。
3.5 案例五:3D 旋转立方体
需求是做一个 3D 旋转立方体,六个面不同颜色。
这个案例用 WebGL 或者 Three.js 都行。我选的是原生 WebGL,因为不想引入额外依赖。我给 Claude 的提示词是:"用原生 WebGL 写一个 3D 旋转立方体,六个面不同颜色,绕 Y 轴旋转,每秒转 90 度,带透视投影。"
Claude 给出的代码里包含了顶点着色器和片元着色器,以及矩阵变换的代码。这里的关键是透视投影矩阵的计算,Claude 用的是标准的perspective矩阵,参数是fov=45度, aspect=width/height, near=0.1, far=100。
这个案例的坑在于:立方体的面需要正确的深度测试,否则会出现面片穿插。需要在 WebGL 里开启gl.enable(gl.DEPTH_TEST),并且在每帧开始时gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)。
4. 可复用提示词框架:让 Claude 稳定输出可用代码
4.1 提示词的四段式结构
经过几十次尝试,我总结出一个稳定的提示词结构,分四段:
第一段:技术栈声明。明确告诉 Claude 用什么技术,比如"用 Canvas 2D"、"用 WebGL 原生 API"、"用 SVG + CSS"。不要让它自己选,否则它可能给你一个你不熟悉的方案。
第二段:视觉描述。描述你想要的画面,包括元素、颜色、位置、大小。这里可以用"左上角"、"居中"、"占屏幕 80% 宽度"这类相对描述,Claude 能理解。
第三段:动画参数。这是最关键的一段,要明确时间、缓动、延迟。比如"用时 2 秒"、"easeOutCubic 缓动"、"每根柱子延迟 0.15 秒"。
第四段:输出要求。告诉它你要什么形式的代码,比如"输出单个 HTML 文件"、"输出一个函数,接收 canvas 元素作为参数"、"不要用外部库"。
4.2 缓动函数的命名约定
Claude 对缓动函数的命名很敏感,你用标准命名它就能给出正确的实现。常用的有:
| 缓动名称 | 效果 | 适用场景 |
|---|---|---|
| linear | 匀速 | 进度条、旋转 |
| easeInQuad | 慢起快终 | 元素入场 |
| easeOutQuad | 快起慢终 | 元素出场 |
| easeInOutQuad | 慢起慢终 | 位置移动 |
| easeOutCubic | 更快的减速 | 弹入效果 |
| easeOutBack | 超出再回弹 | 强调效果 |
我实测下来,easeOutCubic是最通用的,大部分入场动画用它都不会错。
4.3 让 Claude 自己检查代码的三个追问
Claude 第一次给出的代码往往有 bug,我通常会用三个追问让它自己修:
第一个追问:"这段代码在 60fps 下跑 10 秒,有没有性能问题?"它会检查是否有每帧创建对象、是否有内存泄漏。
第二个追问:"如果画布尺寸变成 1920x1080,这段代码还能正常工作吗?"它会检查是否有硬编码的坐标。
第三个追问:"这段代码在 Safari 上能跑吗?"它会检查是否有兼容性问题,比如roundRect在旧版 Safari 上不支持。
这三个追问能解决 80% 的常见问题。
4.4 提示词模板
下面是我常用的模板,你可以直接套:
技术栈:[Canvas 2D / WebGL / SVG+CSS] 画面描述:[元素、颜色、位置、大小] 动画参数: - 总时长:[X] 秒 - 缓动:[easeOutCubic] - 延迟:[每元素延迟 X 秒] - 循环:[是/否] 输出要求: - 输出单个 HTML 文件 - 不使用外部库 - 画布尺寸 1920x1080 - 帧率 30fps这个模板我用了很多次,Claude 的输出质量很稳定。
5. 实操中的性能陷阱与兼容性处理
5.1 Canvas 2D 的三个性能杀手
第一个是每帧创建渐变对象。createLinearGradient是有开销的,如果每帧都创建,帧率会掉。正确做法是在初始化时创建好,存成变量。
第二个是频繁的save/restore。这两个操作会操作状态栈,虽然开销不大,但如果每帧调用几百次,累积起来也很可观。我的做法是只在必要时用,比如需要临时改变换矩阵时。
第三个是大尺寸的drawImage。如果你把一张 4K 图片每帧绘制到 1080p 画布上,GPU 的上传带宽会成为瓶颈。正确做法是预先缩放图片到目标尺寸。
5.2 WebGL 的上下文丢失问题
WebGL 上下文在某些情况下会丢失,比如系统休眠、GPU 驱动更新。如果不处理webglcontextlost事件,页面会直接白屏。
canvas.addEventListener('webglcontextlost', e => { e.preventDefault(); // 停止渲染循环 cancelAnimationFrame(rafId); }); canvas.addEventListener('webglcontextrestored', () => { // 重新初始化所有资源 initBuffers(); initShaders(); startRenderLoop(); });这个处理在桌面浏览器上可能一年都遇不到一次,但在移动端很常见。如果你的视频生成工具要在移动端跑,这个必须加。
5.3 导出视频的编码参数选择
导出 MP4 时,编码参数直接影响文件大小和兼容性。我常用的参数组合是:
| 参数 | 值 | 说明 |
|---|---|---|
| 编码器 | libx264 | 兼容性最好 |
| 像素格式 | yuv420p | 必须,否则部分播放器黑屏 |
| 码率 | 8Mbps | 1080p 的平衡值 |
| 帧率 | 30fps | 和渲染帧率一致 |
| CRF | 18 | 质量优先,文件稍大 |
如果文件太大,可以把 CRF 调到 23,文件大小能减少一半,画质损失在可接受范围内。
5.4 中文字体在 Canvas 里的渲染问题
Canvas 里渲染中文时,如果字体没加载完就绘制,会 fallback 到默认字体,导致排版错乱。正确做法是用document.fonts.ready等待字体加载完成。
await document.fonts.ready; // 现在可以安全地绘制中文了 ctx.font = '48px "Noto Sans SC"'; ctx.fillText('你好世界', 100, 100);这个坑我在做中文视频时踩过,当时排查了很久才发现是字体加载时序的问题。
6. 从单次生成到批量生产的工作流
6.1 参数化模板的设计思路
当你需要生成几十条相似但数据不同的视频时,把可变部分抽成 JSON 配置是最有效的做法。比如柱状图动画,柱子高度、标签、颜色都是可变的,把这些抽出来:
{ "title": "季度销售额", "bars": [ { "label": "Q1", "value": 120, "color": "#4A90D9" }, { "label": "Q2", "value": 200, "color": "#50C878" }, { "label": "Q3", "value": 150, "color": "#F5A623" }, { "label": "Q4", "value": 280, "color": "#D0021B" } ], "duration": 3.0, "easing": "easeOutCubic" }渲染代码读取这个配置,生成对应的动画。这样你改数据不用改代码,批量生成时只需要替换 JSON 文件。
6.2 用 Puppeteer 做批量截图
批量生成的核心是"用浏览器渲染每一帧,截图,然后编码"。Puppeteer 可以精确控制渲染时机:
const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.setViewport({ width: 1920, height: 1080 }); await page.goto('file:///path/to/animation.html'); for (let frame = 0; frame < totalFrames; frame++) { await page.evaluate((t) => window.seek(t), frame / 30); await page.screenshot({ path: `frames/frame_${String(frame).padStart(4, '0')}.png` }); }这里window.seek(t)是页面里暴露的一个函数,用来把动画定位到指定时间。这个方案比MediaRecorder慢,但每一帧都是精确的,不会丢帧。
6.3 批量任务的队列管理
当你要生成几百条视频时,不能一次性全部启动,否则内存会爆。我的做法是用一个简单的队列,同时只跑 2-3 个任务:
const queue = [...tasks]; const concurrency = 2; const running = new Set(); async function runNext() { if (queue.length === 0) return; const task = queue.shift(); const promise = processTask(task).finally(() => { running.delete(promise); runNext(); }); running.add(promise); if (running.size < concurrency) runNext(); }这个模式我用了很多次,稳定可靠。并发数设成 2 是因为每个 Puppeteer 实例占内存不小,设太高反而会拖慢整体速度。
6.4 失败重试与日志记录
批量任务一定要有重试机制,因为 Puppeteer 偶尔会因为内存问题崩溃。我的做法是每个任务最多重试 3 次,每次重试前重启浏览器实例。日志记录到文件,方便排查。
async function processWithRetry(task, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { return await processTask(task); } catch (err) { console.error(`Task ${task.id} failed (attempt ${i + 1}):`, err.message); if (i === maxRetries - 1) throw err; await restartBrowser(); } } }这套机制让我在跑 500 条视频的批量任务时,最终成功率达到了 99.8%。
7. 一些踩坑之后的个人体会
用 Claude Opus 5.5 写代码做视频这件事,我最大的体会是:它把"写代码"的门槛降到了很低,但"做视频"的门槛并没有降低。你还是需要理解动画的时序、缓动的感觉、颜色的搭配、排版的节奏,这些是代码之外的东西。
另一个体会是,不要指望一次提示词就能得到完美的代码。我的平均情况是:第一次生成能跑通 70%,剩下 30% 需要 2-3 轮追问和手动调整。这个比例已经很高了,比我自己从零写快得多。
最后分享一个小技巧:当你不知道某个效果该怎么描述时,直接把你想要的画面用文字描述给 Claude,然后问它"这个效果用什么技术实现最合适"。它给出的建议往往比我自己的第一直觉更合理。比如我想做"文字逐渐模糊消失"的效果,第一反应是用 Canvas 逐帧改透明度,Claude 建议用 CSS 的filter: blur()配合transition,实现起来简单得多,效果也更平滑。