手绘动画
一种完全用代码制作短片的创作方法,源自 JohnHeibel/PDoomVideo(一部由 Claude Opus 绘制的 156 秒音乐视频)及其通用工具包 ClaudeAnimationBase(MIT 协议),该项目已作为 template/ 包含在此。
工作原理: 每一帧都是时间 t 的纯函数。studio.html 加载 p5.js + p5.brush 和您的场景文件;每个镜头使用水彩填充和沸腾墨线绘制完整的 1920×1080 帧。render.mjs 驱动无头 Chrome 生成联系表以供预览,或并行渲染所有帧,然后由 ffmpeg 编码为 MP4(如果提供音频则包含声音)。
工作流程
0. 项目脚手架
bash ~/.claude/skills/painted-animation/scripts/new_project.sh <项目目录> # 添加 --keep-demo 可保留示例场景
此命令复制模板,断开演示场景,运行 npm install 并检查 node / ffmpeg / Chrome。然后,在设计任何内容之前完整阅读 <项目目录>/ANIMATION_GUIDE.md:其中包含规则、动画原理以及完整的引擎/Clawd API。使用读取工具查看 docs/emotions.jpg 和 docs/views.jpg(角色模型表)。
对于音乐视频或任何时长超过约 20 秒的作品,还需阅读 references/music-video.md。
对于每个音乐视频,在开始故事板创作或场景实现之前,请完成以下信息收集:
- 请用户同时提供歌曲音频文件及其
.lrc歌词文件。当需要 LRC 时,不要自行编造、转写或近似缺失的歌词。 - 务必询问创作方向,即使用户已经提及部分内容。请将其作为一条自然语言消息中的一个问题提出,涵盖预期的故事、整体感觉以及任何要包含的反复出现的视觉想法或要避免的内容。不要将其拆分为多个问题、字段、清单或五个部分的表格。告知用户可以用一句话回答,也可以留空。例如:“这部 MV 应该讲述什么样的故事和传递什么样的整体感觉?有没有你想包含的反复出现的画面,或者任何你想避免的内容?如果你没有偏好,可以留空。”
- 确认收到的文件,并根据单个答案推断的创作方向进行总结。在内部提取故事、情感、氛围/意象、视觉主题和需避免的内容;不要要求用户分别回答这五个类别。将空白答案视为“无偏好”,而非缺失输入,并在故事板创作时提出合适的解读。如果音频或 LRC 文件缺失,暂停音乐视频工作流程并请求提供。
- 使用
scripts/beat_grid.py测量节奏;歌词行的节奏通常比您想象的要快。将提供的音频和 LRC 作为时间依据。
1. 先写故事板,后写代码
在项目中编写 STORYBOARD.md(格式见指南:故事梗概、世界+调色板弧线、主题、角色情感弧线,然后是与理解要点并列的镜头——观众必须理解的时间化列表)。示例:examples/xiaozhen/(一个 31 秒的歌词视频:故事板和完整场景代码)。
对于音乐视频,请将用户的故事和情绪转化为连贯的视觉叙事和情感弧线,而不是逐字逐句地诠释每条歌词。在动手制作前,向用户展示故事板并让他们做出反应——除非他们表示可以直接进行。
2. 逐镜头构建
- 在
src/config.js中设置duration、bpm、offset(以及audio)。 - 每个场景/章节一个文件,放在
src/scenes/中,使用 IIFE 包裹,以shots([[t0, fn], ...])结尾;将其<script>标签添加到studio.html。 - 首先确定关键姿势,以静态图片形式检查它们,然后添加它们之间的动作。
3. 渲染与审视——每个镜头,多次
无法通过阅读代码看到动态效果。在项目目录中运行:
node render.mjs --sheet=0.1,0.8,1.6,2.4 --cols=4 --w=480 --out=out/check/a.jpg # 关键帧
node render.mjs --strip=2.1:2.6 --out=out/check/strip.jpg # 某一时刻的每一帧
node render.mjs --sheet=2.3 --crop=760,420,500,400 --w=500 --out=out/check/face.jpg # 全分辨率细节
使用读取工具打开每张图片,并根据指南的检查清单进行实际检查:事件清晰吗?角色足够大吗?理解点的时间安排是否让首次观看的观众能够理解?有预期动作/动作延续、无突变吗?脚部和手持道具接触地面吗?每个接缝处都有过渡吗?没有文字、3D、纯黑/白、浑浊的光晕吗?修复后再次查看。预算:每个镜头至少一张联系表,每个关键动作和过渡一张条带,每个承载叙事的脸部一个裁切区域。
联系表、静态帧、短条带以及其他少量帧的预览可以在当前云环境中渲染和查看。请特意将这些预览渲染做得小巧且有针对性地服务于视觉验证,不能替代整个视频的渲染。
4. 渲染最终视频
切勿直接开始完整渲染。在预览审查完成后,告知用户完整的云端渲染可能非常缓慢,并明确询问他们希望最终视频在本地还是云端渲染。等待他们的回答;故事板批准、预览批准或之前的一般性指示都不构成渲染授权。
- 如果用户选择本地渲染,则禁止在云端进行完整渲染。除非用户之后明确改变选择,否则不要在云端启动完整序列。完成已验证的项目文件,询问(如果未知)用户的操作系统,并提供该平台的确切前提条件、
npm install、帧渲染和编码命令。保留预览图片,以便用户知道已批准的内容。 - 如果用户选择云端渲染,确认预期的时长、帧数和可能的成本/时间,然后继续进行可恢复的多工作者渲染。
- 如果用户未回答或给出模糊的答案,不要运行完整渲染。再次明确询问本地或云端的选择。
node render.mjs --clip --out=out/video.mp4 # 短视频,一个工作者
node render.mjs --frames --workers=4 && node render.mjs --encode --out=out/video.mp4 [--audio=assets/song.mp3] # 长视频,并行+可恢复
报告输出路径、时长和每帧毫秒数。提供 open out/video.mp4。
最重要的规则
以下是指南规则的浓缩版;指南具有权威性。
- 手绘媒介。 只能使用
paint()/inkLine()(p5.brush)——绝不能使用普通的 p5 的rect/ellipse/fill。仅限平面 2D:角色转向通过绘制关键视图实现(turn()、spinView()),从不使用 3D 投影。光照通过glow()(加法混合)实现,因为颜料混合会使黄色覆盖蓝色变成绿色。 - 无文字。 没有字幕、标签、标牌或对话气泡;用画面来表现。角色反应通过绘制表情符号(
!、汗滴、爱心等)来实现。唯一的例外是歌词视频中的卡拉 OK 字幕,以及最多几个大的音效文字。 - 每个镜头都有事件发生——一个事件、原因→结果、铺垫,并且要有回报。
- 给观众留出时间。 列出每个镜头的理解要点;一次只给一个理解点;动作要快,含义要停留;引导视线;理解点决定镜头长度。这是生成式动画最常失败的地方。
- 活起来。 没有静止的东西(通过
feel()产生的空闲动作、漂移的摄像机、沸腾效果);面部通过emotions()变化,而非通过替换;角色要画得大(中景头部尺寸约 20–28,特写 40–70);所有事物都踩在节拍上。 - 每个接缝处都有过渡,并选择适合故事的过渡方式(笔刷擦拭、光圈/形状光圈、甩镜头、匹配剪辑、动作剪辑、摄像机跟随)。
- 一个整体。 一个世界、一条色彩弧线、一条情感线索、相互关联的场景、一个与开头呼应的结尾。
引擎注意事项
t的纯函数。 帧是并行且无序渲染的:帧之间没有状态,没有Math.random(),没有逐帧物理。使用hash(i)实现稳定的随机性,使用jit()实现沸腾效果。- 在每个独立的背景元素/道具之前 调用
boilSeed(key),否则一个移动的物体会导致它之后绘制的所有内容每帧都重新沸腾(抖动)。 - 不要以 p5 的全局变量名命名全局变量。 p5 将
line、OVERLAY、text、color、scale……定义为全局变量;顶层function line()会失败并报Cannot redefine property,导致页面永远无法准备就绪(渲染器超时)。将辅助函数保留在场景的 IIFE 内部,或为它们起不同的名称。 - 门口、窗户和屏幕的图层顺序: 先绘制内部内容,然后在其上重新绘制周围的墙壁/框架,这样开口之外就看不到更宽的东西(例如火车的
inDoor+ 门框、电视边框部件)。 - 点列表中的
NaN会抛出Failed to construct 'OffscreenCanvas'——请保护退化几何图形。 - 当摄像机缩放值 ≳ 2 时,大形状会丢失轮廓线;轮廓线宽度随缩放比例变化,因此在特写镜头中需要缩小
sw。 - 成本 = 填充和描边的数量。 目标是 ≤ 约 1.5–2.5 秒/帧(日志会打印每帧毫秒数)。优先使用更少、更大的形状。在没有真正 GPU 的机器上,水彩填充会很慢——考虑减少
fill形状,或者作为最后手段使用--soft-gl。 - macOS:渲染器使用
--use-angle=metal并自动查找/Applications/Google Chrome.app;否则通过--chrome=<路径>传入。
适配
- Clawd 是默认主角,但并非必需。新的角色、道具和表情使用相同的工具(
paint、ribbon、through、glow)绘制,并在所有镜头中保持一致性;新表情添加到src/clawd.js中的EMO对象里。 src/karaoke.js(模板)通过core.js中的window.overlayHook钩子为歌词视频添加逐字卡拉 OK 效果。LOOPS.name = t => {...}; LOOPS.name.len = 4;+node render.mjs --loop=name --png可为 GIF 生成无缝循环(ffmpeg -i out/loop_name/f%04d.png …)。- 更高的推理努力水平往往会产生更华丽、更详细的场景。