手繪動畫短片
整條路線:每一幀都是一個 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 |
| 作業本塗鴉(原子筆、色鉛筆、拍照紙) | 純 2D canvas,整頁預渲成貼圖,每幀 2–10ms | 本倉庫的 doodle-kit/,見 doodle.md |
| 共點蒙太奇(畫風接力) | Node + @napi-rs/canvas 直接出幀,不開瀏覽器 | 本倉庫的 montage-kit/,見 montage.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/、doodle-kit/、montage-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 拍每分鐘,一拍等於一秒。幀只能順序渲染。
第九套:作業本塗鴉(原子筆 + 色鉛筆 + 拍照紙)
要「紙」「本子」「上課亂畫」的感覺、主角是一隻活過來的塗鴉小東西時,讀同目錄的 doodle.md。底座是 doodle-kit/(《頁邊》,38 秒,頁邊的小團子趁筆不在溜了一圈)。原子筆線會斷墨、色鉛筆是斜排線加紙齒顆粒、整頁帶拍照光;一支帶影子的實物筆立住照片感。活著的東西線條在抖,定住就回到紙上。一鏡到底,配樂跟著動作寫。後半份還記了用這套畫風拍整首歌劇情 MV 的經驗:不畫人、用兩支筆和滑動的本子演兩個人。
第十套:共點蒙太奇(同一個形狀落在同一點)
很多不相干的畫面快切、最後急煞,每一刀都讓同一個形狀落在畫面同一點時,讀同目錄的 montage.md。底座是 montage-kit/(《一口》,25.8 秒):甜甜圈鋪滿畫面,孔裡牛仔騎馬狂奔,每一拍孔裡換一種畫風、外圈晚一拍接過來,24 種畫風接力,最後咬一口。連續動畫靠一套解剖級的共用骨架,每種畫風只管怎麼畫。另一種打法「一刀一個世界」也寫在裡面。
自己寫配樂
沒有現成的歌、要給短片寫配樂時,讀同目錄的 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,再附一張連繫表截圖。 - 彙報時說清三件事:總時長、每幀平均耗時、自己檢查時發現並修掉了什麼。沒看過的部分要明說沒看過。