絵画風動画生成
あなたは画家が筆で描くようにコードで描きます。まず絵を決め(構成、バリュー、パレット)、実際のストロークを遠景から近景へと配置し、結果を見て調整します。 エンジンはそれらのストロークをフレームに変換します。フィルターやレンダリングの後処理ではなく、すべてのマークはあなたが置いた筆のストロークです。
この手法は、4つのペインティングデモンストレーション(プロセス、松の木、絵画的な木々、夕焼けの雲)と、13点のパブリックドメインの名作風景画の計測から学ばれました。抽出された教訓は references/ に、動作コードは engine/ と examples/ にあります。
セットアップ(プロジェクトごとに1回)
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 ブラシと混合)。
ワークフロー — 観察ステップを飛ばさないこと
- 概要。 被写体、雰囲気、時間帯、フォーマット(16:9、9:16、1:1 — 任意の
--size)、再生時間、および時間の分割方法:ペイントオン・リビール、生きた絵画、またはその両方(デフォルト:約60%リビール、約40%生きた絵画)。 - 参照がある場合は研究する(
tools/study.py、references/composition.md参照)。バリューマス、キー、クロマ、パレット、焦点を報告します — 構造をコピーし、絵そのものは決してコピーしないでください。 - 彩色の前に構成。 シーンの先頭にコメントとして構成を記述します:地平線の高さ(中央は不可)、焦点(三分割線上、最も強いバリューコントラストと最も高いクロマが来る場所)、誘導線、それを囲むもの、そして バリュープラン — 各大きな形状に対して1つのバリュー(0–1)、不等な3つのマスに分けて。
references/composition.md。 - 顔料からパレットを、バリュープランに配置。
tone(mix({ ultramarine: 1, transYellowOxide: 1 }), 0.42)。焦点のアクセント以外ではクロマを低く保ちます(巨匠たち:平均OKLabクロマ0.02–0.06、高クロマ領域は約8%未満)。 - 後ろから前に描く 被写体ペインター(
paintSky、paintCumulus、paintHills、paintMountains、paintWater、paintTreeline、paintPine、paintFir、paintTree、paintGrass、paintRocks、paintHouse、paintGlow、paintMoon、walker、boat、smoke…)と、その他のものには生のlayer.scatter/stroke/pathを使用します。段階ごとに1つのp.phase('name')、レイヤーは遠景から近景へ作成。references/api.md。 - 観察し、批評し、調整 — 少なくとも2回。 静止画をレンダリングし、PNGを開いて絵画のように評価します:目を細めて(3つのバリューマスが読めるか?)、焦点は計画どおりか、等間隔・等サイズ・等彩度のものはないか、機械的に見えるマークはないか?コード内の原因を修正します。自身の静止画に対して
study.pyを実行し、計画と比較します。このループがスキルです。最初のレンダリングが正しいことは決してありません。 - 時間。
p.timelapse()でリビール、次にモーション:sway、drift、shimmer、p.wind、p.camera(レイヤーの深度によるパララックス)、移動するものにはp.dynamic。references/animation.md。 - コンタクトシート、そしてレンダリング。
still --n 12 --sheet→ タイムラインの各段階を確認。その後render→qa。references/pipeline.md。 - 正直に報告: 自動でチェックしたこと、自分で確認したこと、確認しなかったこと。
シーンの骨格
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;
}
絵画らしく見せるためのルール(簡易リスト)
- バリューが仕事をし、色が功労を得る。 不等な3つのバリューマスを計画し、すべての静止画を目を細めてテストすること。
- 色付きの下地、白は避ける。 インプリマトゥーラ(下塗り)を選んで絵を調和させる(緑の下には暖色のシエナ、夕焼けの下には淡いクリーム色)。
- 遠景から手前に。 遠く=明るく、冷たく、灰色がかり、柔らかく、薄い絵の具、小さなマーク。近く=最も暗い暗部、最も高いコントラストとクロマ、最も厚い絵の具、最も大きなマーク。
atmosphere(color, d, air)を使用。 - 暗部を先に薄く、明部を最後に厚く。
thickは影と遠景で0.1–0.3、ハイライトで0.8–1。ハイライトは小さく、数も少なく。 - 明るいものは暗いものの隣に、暗いものは明るいものの隣に。 すべてのマスを、その下にあるものよりわずかに(±0.05)異なるバリューのマークで分割。
p.valueAt(x, y)/p.sample(x, y)で現在の状態を確認。 - ストロークは形状に沿って: 雲の塊の周り、斜面に沿って、水面に水平に、草の葉に沿って上向きに、幹に沿って下向きに。
- 緑色は混色し、土の色で落ち着かせる(
earth(mix({ ultramarine: 1, cadmiumYellow: 1 }), 0.3))。純粋なクロマは1箇所のアクセントに取っておく。 - すべてを変化させる: サイズ、間隔、高さ、間隔。均一=機械的。奇数のグループ、重なり合う形状、1つの支配的な要素。
- 失われたエッジと見つかったエッジ: 形状が奥に曲がるか後退する場所は柔らかく(
soft0.5以上)、焦点ではくっきりと。 - 時間の純粋関数。 シーンは
seedから決定論的に決まり、フレームはtにのみ依存。Math.random()や実時間は決して使用しない —layer.rand/rng(seed)を使用。
よくある失敗(これを構築する中で学んだこと)
- 白が混色を飲み込む。 スペクトルミキサーは顔料を輝度で重み付けするため、1:9の色:白の混色はほぼ白になります。色相を白なしまたは少量で混色し、その後
tone(c, value)で配置します。派手な結果はchroma(c, 0.5–0.8)で抑えます。 - 雲が棒のついた球に。 → 平らな土台の上に多数の小さなローブ(
cumulusShapeローブ数40–60)、太陽側のみ明るく、丸く柔らかいダブ、控えめなthick。 - 松の木がヤシの木に。 → 針葉は丸みを帯びた房(短い扇形ストロークのバースト)で、枝の上にあり、間に空の隙間がある。下部は暗く、上部は明るい。枝は低く垂れ、高く伸びる。
- 傘の骨のような枝 / 黒い幹の木。 → 枝は樹冠の中で終わらせる。幹の暗部はバリュー約0.22で、黒ではない。距離に応じて
atmosphereを適用。 - エリア全体が彩度の高い色に。(オレンジの地面、ターコイズの空)→ 土の色で落ち着かせ、純色を少数の小さなマークとして散らす。
- 道が川のように見える。 → 暖かい明るいグレー、進行方向に沿ったストローク、影のみに冷たいバイオレット。
- 短いダッシュで作られた水面がデジタル的に見える。 → 水面は長く水平な引きで互いにブレンドされ、手前のごくわずかなさざ波のマークだけがある(
paintWaterの動作)。手動でダッシュの列を追加しない。 - 海が鏡のように見える。 →
paintWater({ chop: 1 })で波の裏側と白波を追加。湖や川ではchop: 0を維持。 - ランプ / 窓の明かりが平らなパッチに。 → 暗い背景の上に
paintGlowを、次に小さく熱いコアを。1本の長く柔らかいストロークはグローにならない。 - 光線が白いとげに。 → 数本、幅広く、非常に柔らかく、低い不透明度の光線(
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、トラブルシューティング、成果物。