Communitygithub.com

ninjasweb/painter-skill

用代码画出油画风格的场景并生成动画视频——真实的笔触(鬃毛、厚涂、干刷、画布纹理)、光谱颜料混合、逐笔呈现的绘画过程以及“活画”阶段(风、流云、波光粼粼的水面、视差相机)。

painter-skill 是什么?

这是一个用于生成油画画风动画视频的AI技能(适用于Claude Code等平台)。它将绘画过程拆解为代码控制:用户先构思构图、明暗和配色,然后使用纯代码(基于WebGPU/WGSL实例化笔触在headless Chrome中渲染)逐层绘制,最后通过WebCodecs + Mediabunny导出MP4,并利用ffmpeg进行质量检查。技能内置了大量从经典大师风景画中提取的绘画规则(如明度计划、颜料混合、厚薄控制、边缘处理等),并提供成套的预置场景脚本(如落日河流、孤松、山间小屋等)和工具(参考图分析脚本)来辅助创作。工作流程强调“先看再调”:至少两轮反馈——渲染静帧、像油画一样审视、根据规则修改代码。整个技能在本地运行,无需渲染API密钥,依赖Node.js、Chrome (WebGPU) 和ffmpeg。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/ninjasweb/painter-skill/tree/HEAD

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

油画风格视频

你用代码绘画,就像画家用画笔涂抹一样:先决定画面(构图、明暗、调色板),然后从后向前放置真实的笔触,观察结果,再调整。 引擎将这些笔触转化为帧;没有任何滤镜叠加在渲染结果上——每一个标记都是你亲手放置的笔触。

该方法是从四个绘画演示(过程、松树、画意树木、日落云)以及测量13幅公有领域大师风景画中学习而来的。提炼出的经验在 references/ 中;工作代码在 engine/ 和 examples/ 中。

设置(每个项目一次)

cp -R /path/to/painterly-video/engine ./painter-engine
cd painter-engine
npm ci
node bin/paint.mjs still ../path/to/scene.js --t 8          # → PNG输出到场景文件旁边(stills/<scene>/)

将 /path/to/painterly-video 替换为包含此 SKILL.md 的文件夹(例如 ~/.claude/skills/painterly-video)。需要 Node ≥ 22.12,支持 WebGPU 的 Google Chrome(或设置 CHROME_PATH),ffmpeg 和 ffprobe。参考图分析还需要 Python 和 tools/requirements.txt。无需构建步骤或渲染 API 密钥;渲染在本地硬件上运行。新建场景时,复制 examples/ 中最接近的文件(sunset-river.js 天空/云/水面 · lone-pine.js 垂直构图,单一主体 · lake-path.js 树木/深度/路径 · sunset-study.js 暗云团带边缘光的戏剧性天空(对演示结构的分析) · mountain-hamlet.js 山峦、小屋、岩石、行走的人影、炊烟 · brush-chart.js 笔刷和颜料混合)。

工作流程——不要跳过观察步骤

  1. 简报。 主题、情绪、时间、画幅(16:9, 9:16, 1:1——任意 --size)、时长,以及时间如何分配:绘画呈现过程、活画阶段,或两者兼有(默认:约60%呈现,40%活画)。
  2. 有参考图时先研究参考图(tools/study.py,参见 references/composition.md)。它会报告明度团块、基调、色度、调色板和焦点——学习结构,绝不复制画面。
  3. 先构图再上色。 在场景文件的顶部用注释写出构图:地平线高度(绝不在中间)、焦点(位于三分之一线上,最强明度对比和最高色度所在处)、引导线、边框,以及明度计划——每个大形状一个明度值(0–1),分为三个不等的团块。references/composition.md。
  4. 用颜料构建调色板,放置在明度计划上。 tone(mix({ ultramarine: 1, transYellowOxide: 1 }), 0.42)。除焦点强调色外,所有地方的色度都保持较低(大师作品:平均OKLab色度 0.02–0.06,高色度区域约占8%)。
  5. 从后向前绘制,使用主题绘制器(paintSky, paintCumulus, paintHills, paintMountains, paintWater, paintTreeline, paintPine, paintFir, paintTree, paintGrass, paintRocks, paintHouse, paintGlow, paintMoon, walker, boat, smoke……)以及其他内容使用原始 layer.scatter/stroke/path。每个阶段一个 p.phase('name'),图层由远及近创建。references/api.md。
  6. 观察、批评、调整——至少两轮。 渲染静帧,打开PNG,像评价一幅画一样审视它:眯眼(三个明度团块能读出来吗?),焦点是否在计划的位置,是否有任何元素等距/等大/等饱和,是否有任何标记显得机械?在代码中修正原因。在自己的静帧上运行 study.py 并与计划比较。这个循环是技能的核心;第一次渲染永远不会正确。
  7. 时间。 使用 p.timelapse() 实现呈现效果,然后是运动:sway, drift, shimmer, p.wind, p.camera(按图层深度的视差效果),p.dynamic 用于移动的物体。references/animation.md。
  8. 联系表,然后渲染。 still --n 12 --sheet → 检查时间线的每个阶段;然后 render → qa。references/pipeline.md。
  9. 诚实报告:自动检查了哪些内容,你查看了哪些内容,你没有检查哪些内容。

场景框架

import { Painting, mix, tone, mixOk, skyColor, paintSky, ridge, paintHills, paintGrass, key, ease } from 'painter';
export const meta = { duration: 16, fps: 30 };

export default function ({ aspect, seed }) {            // aspect = 渲染的宽度/高度
  const p = new Painting({ aspect, seed, ground: '#c9b89a' });   // 有色底,绝不用白色
  p.duration = 16;
  const A = aspect;                                     // 场景单位:x 0…A, y 0(顶部)… 1(底部)

  const skyTop = tone(mix({ ultramarine: 2, titaniumWhite: 2 }), 0.72), skyLow = tone(mix({ cadmiumOrange: 1, titaniumWhite: 3 }), 0.88);
  const hillCol = tone(mix({ ultramarine: 1, burntUmber: 1 }), 0.3);
  const hill = ridge({ y: 0.68, amp: 0.05, freq: 2, seed });

  p.phase('sky');
  paintSky(p.layer('sky', { depth: 0 }), { color: skyColor({ top: 0, bottom: 0.7, stops: [[0, skyTop], [1, skyLow]] }), bottom: 0.72 });
  p.phase('land');
  paintHills(p.layer('hill', { depth: 0.6 }), { top: hill, color: (x, y, k) => hillCol, size: 0.03 });
  paintGrass(p.layer('grass', { depth: 1 }), { bounds: [0, 0.8, A, 1.05], dark: hillCol, light: tone(hillCol, 0.5), blade: 0.05 });

  p.timelapse({ start: 0.3, end: 10 });                 // 画面在10秒时完成
  p.camera = (t) => ({ zoom: key(t, [[10, 1], [16, 1.07]], ease.inOut) });
  p.wind = (t) => ({ strength: key(t, [[0, 0], [10, 0.15], [12, 1]]) });
  return p;
}

使其看起来像油画的原则(简要列表)

  1. 明度做苦工,颜色得荣誉。 规划三个不等的明度团块;每张静帧都要眯眼测试。
  2. 有色底,绝不用白色。 选择底色来协调画面(绿色下用暖赭石,日落下用淡奶油色)。
  3. 先远后近。 远处=更浅、更冷、更灰、更软、更薄的颜料、更小的笔触。近处=最暗的暗部、最高的对比和色度、最厚的颜料、最大的笔触。使用 atmosphere(color, d, air)。
  4. 暗部先涂且薄,亮部后涂且厚。 thick 0.1–0.3 用于阴影和远处,0.8–1 用于高光;高光小而少。
  5. 亮衬暗,暗衬亮。 用比底层颜色一个微小明度步长(±0.05)的笔触打破每个团块;使用 p.valueAt(x, y) / p.sample(x, y) 了解底层颜色。
  6. 笔触跟随形体:围绕云块,沿斜坡,平铺在水面,沿草叶向上,沿树干向下。
  7. 混合绿色并使其沉稳(earth(mix({ ultramarine: 1, cadmiumYellow: 1 }), 0.3));将纯色留给一个强调色。
  8. 变化一切:大小、间距、高度、间隔。相等=机械。奇数分组、重叠形状、一个主导。
  9. 消失与明确的边缘:形体转向或后退处柔和(soft 0.5+),焦点处清晰。
  10. 时间的纯函数。 场景由 seed 决定;帧仅依赖于 t。永远不要使用 Math.random() 或挂钟时间——使用 layer.rand / rng(seed)。

容易出现的问题(构建过程中学到的)

  • 白色吞噬混合色。 光谱混合器按亮度加权颜料;1:9 的颜色:白色混合几乎就是白色。混合色调用少量或不加白色,然后使用 tone(c, value) 放置。用 chroma(c, 0.5–0.8) 削减刺眼的结果。
  • 云朵像球上插棍子 → 底部平坦,多个小叶片(cumulusShape lobes 40–60),仅朝向太阳的一侧有光,圆形柔软的轻拍,适中的 thick。
  • 松树像棕榈树 → 针叶是圆形簇状(短扇形笔触的爆发)栖息在树枝上,之间有天空空隙,底部暗,顶部亮;树枝低垂并向上延伸。
  • 树枝呈伞骨状/黑色树干的树木 → 枝条必须结束在树冠内部;树干暗部值约为0.22,不是黑色,并随距离增加空气感(atmosphere)。
  • 整个区域使用饱和色(橙色地面,青绿色天空)→ 使其沉稳;将纯色作为几个小笔触散布。
  • 路径看起来像河流 → 暖浅灰,笔触沿行进方向,阴影中只有冷紫色。
  • 由短虚线组成的水面看起来像数字的 → 水面是相互融合的长水平拖拽,只在靠近观察者处有少数涟漪标记(paintWater 所做的);不要手动添加成排的虚线。
  • 海面看起来像玻璃镜面 → 使用 paintWater({ chop: 1 }) 添加波浪背面和白色浪花;湖面和河流保持 chop: 0。
  • 灯/窗户光是一个平坦的色块 → 在暗背景上用 paintGlow,然后是小的炽热核心;一个长的柔和笔触不是发光。
  • 光线像白色尖刺 → 少而宽,非常柔和,低不透明度的光束(paintRays 默认值)。
  • 在浅色薄颜料上,画布纹理变成网格 → 保持 look.weave ≤ 0.2。
  • 山脉像鳞片墙或斑马纹 → 使用 peaks() 构建天际线,保持远处(薄颜料,size ≈ 0.02,窄明度范围)并让 paintMountains 将其分割成几个大的受光与投影面。
  • 平坦的带状不是云层/山丘/林线 → 变化顶部边缘、内部值并留有空隙。
  • 场景因模糊的浏览器错误而失败 → CLI首先进行语法检查;运行时错误会打印场景行。局部变量与颜色名冲突(leaf, sky, hill)是 c.map is not a function 的常见原因。
  • 读取过期的静帧:在打开PNG之前,始终检查命令输出是否有错误。
  • WebGPU 和 WebCodecs 仅在安全源上存在——CLI服务于 http://127.0.0.1;永远不要通过 file:// 打开场景。

参考文件

  • references/painting-lessons.md —— 每个演示教授的内容以及如何映射到引擎调用。
  • references/composition.md —— 场景构图、明度计划、来自大师作品的数据、study.py。
  • references/api.md —— Painting / layer / brush / colour / subject-painter 参考,编写新的主题绘制器。
  • references/animation.md —— 呈现节奏、活画运动、相机和视差、动态图层、沸腾效果、音频。
  • references/pipeline.md —— CLI、渲染设置、性能、QA、故障排除、交付物。

相关技能