paint-video
用代码做手绘风格的动画短片:水彩、水墨用 p5.js + p5.brush,蜡笔绘本用自写的 2D canvas 蜡笔引擎,皮影用自写的皮影引擎,打斗用自写的设色水墨引擎;逐帧画,无头 Chrome 渲染,ffmpeg 合成 MP4。用户说「做个短片 / 动画 / MV / 开场动画 / 十几秒的视频」时用这份;配整首歌的长片或 MV 另看同目录的 mv-workflow.md,蜡笔画风看 cra
SKILL.mdpaint-video/SKILL.md
GitHub手绘动画短片
整条路线:每一帧都是一个 t(秒)的纯函数,用笔刷画出来(墨线、平涂、水彩晕染、排线)。无头 Chrome 并行把帧渲染成图片,ffmpeg 再合成 MP4。例外是流体和沙画:画面有跨帧的模拟状态,只能从头顺序渲染。画面里没有一张图是 AI 生图,全部是代码画的。
八套引擎:
| 画风 | 引擎 | 从哪起步 |
|---|---|---|
| 水彩、水墨(排线可做局部版画感) | p5.js + p5.brush,速度随镜头复杂度波动较大 | ClaudeAnimationBase(MIT) |
| 蜡笔、绘本、连环画 | 纯 2D canvas,每帧 10–50ms | 本仓库的 crayon-kit/,见 crayon.md |
| 皮影、剪影、戏台 | 纯 2D canvas,每帧 2–10ms,锣鼓用代码合成 | 本仓库的 piying-kit/,见 piying.md |
| 设色水墨打斗(雾山五行风) | 纯 2D canvas,每帧 20–100ms | 本仓库的 inkfight-kit/,见 wushan.md |
| 科普讲解片(图示、字幕、配音) | 纯 2D canvas,每帧 60–130ms,声效代码合成,旁白 edge-tts | 本仓库的 explainer-kit/,见 explainer.md |
| 版画系列(孔版、铜版、蓝晒、网点等) | SVG,每帧约 1 秒,多路并行渲染;配乐用 Tone.js 加真乐器采样 | 本仓库的 prints-kit/,见 prints.md、music.md |
| 流体彩墨(音符驱动、抽象) | WebGL2 稳定流体,每帧约 65ms,只能顺序渲染 | 本仓库的 fluid-kit/,见 fluid.md |
| 沙画(灯箱透光、一幅抹成下一幅) | WebGL2 高度场,每帧约 90ms 起,只能顺序渲染 | 本仓库的 sand-kit/,见 sand.md |
参照作品:PDoomVideo,一支两分半的 MV,由 Claude 在 Claude Code 里生成。那个仓库没有许可证,只能学,不能拿代码去发布;ClaudeAnimationBase 是同一作者后来整理出的 MIT 底座。
环境
- 这份 skill 来自 paint-video-skill 仓库。文中的
crayon-kit/、piying-kit/、inkfight-kit/、explainer-kit/、prints-kit/、fluid-kit/、sand-kit/、examples/、tools/都在那个仓库里,不在 skill 目录下;本机找不到就问用户仓库 clone 在哪,或者重新 clone 一份。 - Node 22.12 以上,Chrome 或 Chromium(找不到时给
render.mjs传--chrome=<路径>,或设CHROME_PATH),ffmpeg 在 PATH 里。 - 字幕工具要 Python 3 和 numpy、Pillow。
npm install会下载并执行第三方代码,装之前先问用户。开新项目时把已经装好的模板目录连node_modules一起复制,就不用每次重装。
开工步骤(水彩 / 水墨)
- 复制底座到新项目目录,排除
out、docs、.git。Windows 上用robocopy <底座> <新目录> /E /XD out docs .git /XJ,退出码 1 表示复制成功。 - 先通读新目录里的
ANIMATION_GUIDE.md。那是完整的规则和 API:paint()、inkLine()、相机、转场、clawd()的全部参数,都在那份里,这里不重复。 - 先写
STORYBOARD.md,给用户过目,再写代码。 格式按 guide 里的来:一句话梗概、世界与配色、贯穿母题、情绪弧、逐镜头的「读点」时间表。写之前先读同目录的directing.md(导演手册:景别、轴线、剪辑点、节奏、构图、表演),逐镜表按它的格式写,写完按它末尾的检查表过一遍。 - 改
src/config.js里的时长和 bpm。场景写成新文件src/scenes/<名字>.js,用 IIFE 包起来,最后调用shots([...])。在studio.html里用它替换demo.js那一行。 - 一个镜头一个镜头地做,每做完一个就用下面的检查循环看一遍。
- 出片:
node render.mjs --clip --out=out/video.mp4。时长较长的用--frames --workers=4并行渲染,再--encode合成。
本仓库 examples/brush/ 有两支完整可跑的场景:stars.js(《拾星》,14.5 秒,bpm 90)和 ink.js(《落款》,水墨,16.8 秒,bpm 80)。放进 src/scenes/,在 studio.html 里替换 demo.js,再把 config.js 的时长和 bpm 改成上面括号里的值。只参考写法,别照抄故事。
给底座打的两个补丁(render.mjs 里两处 ffmpeg 参数和一处 Chrome 参数):
- 输出改成标准 tv range:把
'-pix_fmt', 'yuv420p'换成'-vf', 'scale=out_range=tv,format=yuv420p', '-color_range', 'tv'。否则有的播放器会放成全黑。 - Windows 双显卡笔记本加
--force_high_performance_gpu,否则 Chrome 落在集显上越跑越慢。crayon-kit/render.mjs已经带了这两处。
最要紧的规则
各套画风都适用
模型做动画最容易翻车的地方:
- 帧是 t 的纯函数:帧会乱序并行渲染,所以不能有跨帧状态,不能用
Math.random(),随机值全部由时间和元素编号算出来。 - 纯 2D,不做 3D 投影:纵深靠遮挡、大小和冷暖。
- 不写字:画面里不要标签、招牌、带字的对话气泡。要表达什么就画出来,用 emote(
!、?、汗滴、灯泡)。MV 的歌词字幕是例外:单独做一层叠上去,做法见mv-workflow.md的字幕一节。 - 每个镜头都要发生一件事,一次只让观众看一件事,先有原因,再有反应。
- 时间感是最常翻车的地方:观众只看一遍。给每个「读点」留出找到、看懂、停一下的时间。快的是动作,慢的是意义。第一版几乎总是太快。
- 每个接缝都要转场:包括开头和结尾,不能硬切开始、戛然而止。转场要跟故事有关,别每次都用同一种。打斗片例外:镜头之间直接切在动作中间,不加装饰转场,见
wushan.md。 - 角色要大:按 1080p 画面里的高度看,中景的角色约占画高 1/3–1/2,特写的脸要占到画高一半以上,小于 1/10 只用于远景建立镜头。不同引擎的尺寸参数含义不同,数值不能混用。
p5.brush 专用(水彩 / 水墨)
来自底座的 ANIMATION_GUIDE.md:
- 固定的随机值用
hash(i),线条抖动用jit()。每个独立元素开头调用一次boilSeed(key),否则一个动的东西会让后面所有静止的东西跟着乱抖。 - 全部用笔刷画:只用
paint()和inkLine(),不要 p5 原生的rect、ellipse、fill(),那样看起来像 2000 年代的 Flash。 - 转身靠画好的几个关键视角切换(
turn())。 clawd()的u:中景 20–28,特写 40–70,小于 12 只用于远景。
蜡笔引擎的 API、随机数和角色尺寸是另一套,见 crayon.md。
默认审美
用户没给风格时,按下面这套来。它偏「纸和墨」:低饱和的暖色、手作痕迹、克制的动效。用户有自己的风格说明时以用户的为准。
配色:在场景文件开头覆盖调色板。底座默认的调色板偏饱和(天蓝、紫、玫红):
Object.assign(PAL, {
paper: '#f7f2e6', cream: '#faf7f0', ink: '#2e2a24',
clay: '#bb5f3c', clayDk: '#8f4428', clayLt: '#d98a6c', // 陶土色;Clawd 的身体用的就是 clay
sky: '#a9bfc4', sap: '#8a9a6a', ochre: '#c9a25a', teal: '#6f9590',
rose: '#c98a8a', indigo: '#4a5670', night: '#262a38', violet: '#7d6f8f'
});
每支片从里面挑 3–5 个颜色就够,不要全用上。颜色要有一条随剧情变化的弧线,比如冷暗的开头走向暖亮的结尾。
质感:底座自带纸张底纹和颗粒,保留。排线(hatch)适合版画感,但只用在阴影和局部,别铺满全画面。
丰富 ≠ 廉价:
- 可以热闹:一个镜头里可以有很多东西在动,这是「表演型」画面。
- 不能廉价:不要为了热闹撒彩纸、星星、火花来填空。特效必须是剧情里的东西,比如星星掉下来、灯亮起来。
- 自检方法:去掉这个元素,画面会不会少了意思?只少了热闹,就删掉。
- 克制不等于空:留白要有层次,画面里要有环境、前中后景和质感,不是一大片纸色上摆两三个孤零零的物件。一个镜头只看一件事,但这件事要放在一个有生活气的世界里。
笔触要像手画的:角色按 guide 用平涂加墨线没问题,但墨线别全片一个粗细:inkLine(pts, sw, colour, brush) 的 sw 按远近和主次变化,细节换 'inkfine'、'dry' 笔刷。背景和环境用水彩 fill、wash 铺出干湿层次。所有东西都是同一根粗黑线加平涂,就会像剪贴画。
一条不变的约束:每支片先定一个贯穿全片的不变量,比如同一个构图位置、同一个形状、同一个角色姿势,转场和匹配剪辑都围着它做,结尾呼应开头。
音乐和节奏:底座里的一切都卡 bpm。没有配乐时 bpm 取慢一点(80–100),节奏沉稳些。
测试片学到的
《拾星》(examples/brush/stars.js)
Clawd 捧着空罐子接住掉下来的星星,一罐星光把夜色一点点焐暖。
- emote 要手动关掉:底座里的 excited、proud 表情会在头边自动冒
spark火花,是廉价装饰。用emotions(t, [[0, 'excited', { emote: null }], ...])关掉。!、?、爱心这类有意义的反应可以留着。 - 「变暖」别整片天换成暖色:夜空的底色直接混向棕黄色,整个画面会变成一片土褐色,发脏。天还是夜色,只把冷色减弱(混向
#4b4356),暖意放在地平线的晕染和发光物体上。 - 道具要一眼认出来:暗背景上的玻璃罐一开始画成灰色,看着像垃圾桶。改成偏冷的浅色玻璃,加一道高光才认得出。
- 光圈收尾要分三段:先收到主体上,停一下,再合上。一口气收到 0 会像突然黑屏。
- Clawd 的手臂很短,够不到头顶上的东西,举起来的效果像顶在头上。要「捧着」的话得自己画抱的动作。
- 实测速度:14.5 秒、348 帧,4 个进程并行约 6 分钟,每帧约 1 秒(RTX 4060 笔记本),ffmpeg 编码 10 秒。联系表每帧 0.1–1.7 秒,很便宜,多看几轮。
《借露》(反面教材)
蜗牛背露水去救一株缺水的幼苗。规矩全守住了(没字、没火花、色板对、角色没跑形),片子还是没戏:
- 故事靠的状态变化必须夸张到一眼看出:幼苗从头到尾差不多精神,「缺水低头 → 喝水挺起」对比不出来,整支片就没了因果。关键状态的前后两帧并排放,外行一眼分不出差别就是不够,要改到夸张。
- 推动剧情的道具至少要有角色的头那么大:露水比蜗牛的眼睛还小、颜色又淡,落到壳上就看不见了。道具小就推近镜头,或者加亮、加轮廓。
- 分镜写了移动,画面就得真移动:分镜写蜗牛背水走到根边,实际位移很小,大部分时间像静图。对照分镜逐条核对「谁从哪到哪」。
- 画面太空:整片只有一株苗、一只蜗牛、一片叶子,见上面的「克制不等于空」。
- 转场要有来由:一条和剧情无关的绿色色带横扫全屏,只是换了个样子的硬切。
写完的自检:把每个镜头的首帧和尾帧并排看,说不出这一镜发生了什么变化,就是没戏,回去放大事件。
《落款》,水墨(examples/brush/ink.js)
- 换画风只需要换调色板和画法,底座不用动:全片只用墨的三档浓淡加 Clawd 的朱红。「万墨丛中一点红」是水墨的经典手法,Clawd 正好就是那一点红。
- 墨晕开:形状的每个点从落墨点出发,按距离先后铺开(
bloom()),比「整个形状淡入」更像墨在纸上化开。 - 水墨的层次:先铺浅色底,再在山头叠一层重墨往下淡,山脚压一道纸色的雾,远山配合视差移动得慢一些。
- 世界跟着角色一笔一笔长出来:它走到哪里,哪里才画出芦苇、河和石头,事件本身就是在画画。这个做法也保证了每个镜头的首尾帧有明显变化。
- 构图约束也要考虑终点:角色要看得见前方的路,就得站在行进方向的后侧,走到最后自然落在画面那一侧的角上。
第二套画风:蜡笔绘本
用户要童趣、绘本、蜡笔、连环画,或者不想再要水彩时,读同目录的 crayon.md。这套不用 p5.brush,是自写的 2D canvas 蜡笔引擎(crayon-kit/),比水彩快一个数量级。里面有:引擎原理和 API、角色参数、变身梗和分格砸格的做法、蜡笔字幕、用探针让字幕避开人物。
第三套画风:皮影
用户要皮影、剪影、戏台,或者想做一支不配歌、只用锣鼓的短片时,读同目录的 piying.md。引擎在 piying-kit/:每片皮单独刻好镂空再乘到透光的幕上,角色是带操纵杆的关节小人,锣鼓点用 numpy 合成。里面有:引擎原理和 API、角色参数、动作砸在锣鼓点上的做法、幕后机位的镜像和剪影轮廓光、合成器。
第四套画风:设色打斗(雾山五行风)
用户要打斗、武侠、动作戏,或者点名雾山五行、设色水墨时,读同目录的 wushan.md。引擎在 inkfight-kit/:人物用毛笔勾线加平涂和暗面,动作走穿过关键帧的平滑曲线,脚单独按踩点走,冲击帧用反相和朱红,最后火墨炸满屏。里面有:引擎和人体的 API、怎么编一场打斗(先定接触点再倒推站位,用 probe.mjs 拿数字对)、镜头安排、踩过的坑。
第五套:科普讲解片
用户要科普视频、讲清楚某个发现或原理时,读同目录的 explainer.md。底座是 explainer-kit/(《用光,打开大脑》,2026 诺奖光遗传学,3:05)。每场一种画风,允许字幕、章节和图示标注;画面、声效、旁白共用一份 timeline.js;旁白读得比画面长时,按句子时长把那一段画面拉长。
第六套:版画系列短片
用户要有材质感、不走扁平简约风的系列画面,或者点名版画、孔版、铜版、蓝晒、网点时,读同目录的 prints.md。底座是 prints-kit/(《远光》,93 秒,一颗蒲公英种子的六段旅程)。里面讲了:
- 地平线和光源六段不动,工艺每段都换;
- 六种工艺各自的 SVG 做法;
- 用固定随机数让逐帧重画不闪;
- 让画面「讲事」的三样东西:主角和伙伴、一天的时间、铅笔题签;
- 五种换场印法;
- 多路并行渲染(每路约占 2–3GB 内存,默认 4 路)。
第七套:流体彩墨 MV(音符驱动)
想要画面一直在动、不讲具体故事、跟着音乐一下一下变的 MV 时,读同目录的 fluid.md。底座是 fluid-kit/(《一滴墨》,91 秒,古典 + 摇滚)。WebGL2 稳定流体,颜料按吸光度叠色;乐谱导出音符表,每种乐器有固定画法(小提琴是笔、底鼓是冲击、镲是开花),每一段是一种「水性」;高潮的开花有 7 种炸法轮换。不画人,没有镜头,帧只能顺序渲染。
第八套:沙画(灯箱透光,一幅抹成下一幅)
要讲一个小故事、但不想画人,画面又要一直在变、不像动态 PPT 时,读同目录的 sand.md。底座是 sand-kit/(《沙漏》,75 秒,一粒沙看见的一年,九幅首尾相接)。WebGL 高度场模拟沙子:撒、指尖划、手掌推、休止角、沙帘、按平、抹亮三角、开月亮;底光按三个通道分别透射,薄处一粒一粒。规矩是光是主角、转场靠形状接上、留白。画面先定,配乐后写,60 拍每分钟,一拍等于一秒。帧只能顺序渲染。
自己写配乐
没有现成的歌、要给短片写配乐时,读同目录的 music.md。用 Tone.js 加真乐器采样,不用合成音色。里面有编曲规矩、怎么去掉机器味、VSCO2 管弦乐采样和它八度标法的坑、换音源后怎么对齐响度、本地采样缓存、无头 Chrome 离线导出加两遍响度标准化,以及怎么用「拍」把画面和音乐对齐。古典 + 摇滚的曲子另有一节:多力度轮换的真鼓(DRSKit)、失真吉他、分轨导出,以及自写的 Python 混音台(均衡、压缩、饱和、卷积混响、按段对齐响度、限幅)。
检查循环(每个镜头都要做,别省)
只读代码看不出动画效果,必须渲染出来看:
node render.mjs --sheet=0.1,0.8,1.6,2.4 --cols=4 --w=480 --out=out/check/sheet.jpg # 联系表:每个镜头的首、中、尾帧
node render.mjs --strip=2.1:2.6 --cols=6 --w=320 --out=out/check/strip.jpg # 连续帧:转身、起跳、转场
node render.mjs --sheet=2.3 --crop=760,420,500,400 --w=500 --out=out/check/face.jpg # 局部放大:表情、手、接触点
联系表的 ms/frame 全是 0,说明页面根本没画东西(多半是场景文件有语法错误),先看终端里的 [page error]。
用读图工具打开,逐条检查:
- 事件读不读得出来,角色够不够大,和背景分不分得开;
- 有没有瞬移、突变的帧;
- 身体和支撑物:坐下、站上、靠上某个物体,要有抬腿、屈膝、落座的过渡,身体不能贴着物体平移上去,也不能和它重叠;
- 肢体长度不变:伸手够东西时胳膊不能拉长,够不着就让角色走近或弯腰;
- 挥动、翻转的道具不能穿过场景里的物体;
- 动作有没有预备和跟随,是不是左右对称地同时在动;
- 接缝处有没有转场;
- 有没有字;
- 颜色有没有发脏(黄色叠在蓝色上会变绿,发光要用
glow()); - 配色是否守住定好的色调。
性能与已知坑(p5.brush)
- 续渲只看帧文件在不在:改了代码再跑
--frames,已经存在的帧会原样保留,成片里混进旧画面,而且不报错。底座的render.mjs就是这样,改代码后先把out/frames挪走(别删)再渲。crayon-kit的render.mjs会给帧记代码签名,代码变了就停下,让你选--force(旧帧挪到out/frames_old_*)或--keep-frames(已经手动挪走改过的那几镜)。 - 页面报错:
crayon-kit的render.mjs遇到页面报错会失败退出;底座的只打印一行[page error]然后照常出图,要自己留意。 - 联系表会打印每帧耗时,目标 ≤1.5 秒/帧。水彩
fill最贵:大面积的形状用wash,fill少用而且边数要少。 - 有独显时每帧约 1 秒。明显变慢时用
node gpu_probe.mjs <chrome路径>查 Chrome 落在了哪块显卡上。 studio.html会从 Google Fonts 加载 Permanent Marker 字体,只有letter()用得到。不写字的片直接删掉那一行<link>,免得网络不通时卡在加载上。- 点坐标里出现 NaN 时,报错信息是
Failed to construct 'OffscreenCanvas',堆栈指向场景代码而不是 NaN 本身,要回头查几何计算。 - 相机放大到 2 倍以上时,离原点很远的长线条会塌缩成一个点。超长的边用
inkLine分段画。 - 偶尔出现 5 条
WebGL: INVALID_OPERATION警告是无害的,其他页面报错是真错误。 - 大形状只用
fill几乎是透明的:远山、水面这种上千像素宽的形状,只给fill会淡得看不见。要先用wash(washOp: 255,颜色调浅)铺底,再叠一层fill做晕染和边缘。 inkLine只给两个点时什么都不画,而且不报错:直线也要给 3 个点以上,最好带一点起伏,更像手画。涟漪、水痕、地面短线最容易中招。- 同一个形状别分段画:山头的暗部分成几段画,重叠处会出现竖条接缝。整条一次画完,边缘的起伏用
hash(i)做。 inkLine分段时检查最后一段的点数:切片切到末尾可能只剩 1 个点,会直接报错spline() requires at least 2 points。
长片或 MV
先读同目录的 mv-workflow.md:音乐分析、拍号驱动的逐镜表、零件库、分批做、分段渲染、局部返修、成片调色、歌词字幕。
另一种做法是由主代理写好分镜和风格说明,按章节分给多个子代理,各改各的章节文件。用户没要求就一个人从头做到尾,全片更统一。
歌词对时、音频节拍分析还可以参考 paint-mv-skills(MIT,中文)。
交付
- 交付物是
out/video.mp4,再附一张联系表截图。 - 汇报时说清三件事:总时长、每帧平均耗时、自己检查时发现并修掉了什么。没看过的部分要明说没看过。