paint-video

用代码做手绘风格的动画短片:水彩、水墨用 p5.js + p5.brush,蜡笔绘本用自写的 2D canvas 蜡笔引擎,皮影用自写的皮影引擎,打斗用自写的设色水墨引擎;逐帧画,无头 Chrome 渲染,ffmpeg 合成 MP4。用户说「做个短片 / 动画 / MV / 开场动画 / 十几秒的视频」时用这份;配整首歌的长片或 MV 另看同目录的 mv-workflow.md,蜡笔画风看 cra

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 一起复制,就不用每次重装。

开工步骤(水彩 / 水墨)

  1. 复制底座到新项目目录,排除 out、docs、.git。Windows 上用 robocopy <底座> <新目录> /E /XD out docs .git /XJ,退出码 1 表示复制成功。
  2. 先通读新目录里的 ANIMATION_GUIDE.md。那是完整的规则和 API:paint()、inkLine()、相机、转场、clawd() 的全部参数,都在那份里,这里不重复。
  3. 先写 STORYBOARD.md,给用户过目,再写代码。 格式按 guide 里的来:一句话梗概、世界与配色、贯穿母题、情绪弧、逐镜头的「读点」时间表。写之前先读同目录的 directing.md(导演手册:景别、轴线、剪辑点、节奏、构图、表演),逐镜表按它的格式写,写完按它末尾的检查表过一遍。
  4. 改 src/config.js 里的时长和 bpm。场景写成新文件 src/scenes/<名字>.js,用 IIFE 包起来,最后调用 shots([...])。在 studio.html 里用它替换 demo.js 那一行。
  5. 一个镜头一个镜头地做,每做完一个就用下面的检查循环看一遍。
  6. 出片: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,再附一张联系表截图。
  • 汇报时说清三件事:总时长、每帧平均耗时、自己检查时发现并修掉了什么。没看过的部分要明说没看过。