handdrawn-3d は何をしますか?
対象物を3Dでいくつかの単純な立体にモデル化し、キットがそれを手描きで描きます。面には左上からの光源による3段階のフラットなマーカートーンが適用されます。同じトーンの隣接面は1つの塗りとして融合します。シルエットは太いインクで縁取られ、折り目は細く、ゆがんで先細りの線で描かれます。紙はクリーム色です。シーンはtが1ループを回るごとに1フレームずつ描画されます。出力はループアニメーションSVGです。フレームはCSSで切り替えられ、スクリプトは不要なので、ブラウザや<img>タグで動作します。テキストにはベトナム語を完全サポートした組み込みの手描きフォントを使用します。
キット: assets/kit.js · レンダラー: assets/render.mjs · すべての関数とオプション: references/kit.md(シーンを書く前に読むこと) · 実装例: examples/(カップ、ティーポット、トースター、風車:最も近い例を読むこと)
セットアップ(1回のみ)
PNG/GIF出力には@resvg/resvg-jsが必要です。render.mjsが見つからないと表示された場合は、このスキルのフォルダでnpm installを実行してください。SVG自体には何も必要ありません。
描く前に
- 描画可能か? 抽象的なテーマ(「希望」「生産性」など)の場合は、漠然としたものを描かないでください。具体的なシーンを1〜2つ提案し、ユーザーが選んだものを描いてください。
- 図表、グラフ、UI、ボタン、インタラクティブなもの: このスキルは対象外です。
- 何が動くか? すべての絵は物体自体をアニメーションさせます。カメラは決して動きません。対象物にふさわしい1〜2つの動きを選んでください。やかんの蓋は揺れて湯気を出す。トーストは飛び出す。風車の羽根は回る。扇風機は回る。猫の尻尾は振る。ランプは揺れる。植物はたわむ。ユーザーが動きを指定した場合はそれを使ってください。どうしても動かせるものがなければ、湯気、穏やかな上下動、揺れ、あるいは線自体のうねりを使ってください。
ワークフロー
- パーツ。 対象物の実際のパーツを列挙し、それぞれがどのように立体で構成されるかを記述します。シーンの先頭にコメントとして表にします:パーツ · 呼び出し · サイズ/位置 · 色。実際のパーツ(縁、蓋、つまみ、取っ手、脚、スロット、蝶番)に基づいてモデル化し、名前だけの塊にしないでください。わからない比率は調べてください。
- モデリング を
scene.jsで行います(世界: x右、y奥、z上、地面z=0; 単位はmm程度; フレーミングは自動):- ろくろで回せるような丸いもの →
k.lathe(容器にはwall:を追加)、k.cylinder、k.sphere - 箱状のもの →
k.box(柔らかい家電の角にはround:)、変形した輪郭 →k.extrude - 取っ手、注ぎ口、パイプ、脚、ケーブル、茎 →
k.tube、本体の内部から始める(表面で切断されます) - 容器の中身 →
{ in: container }; 面上のフラットなマーク →k.panel/k.line({ on: solid }付き) - 対象物全体を
k.group({ rotate: { z: 45 } }, …)で回転させると、「側面図」が視聴者に向くようにできます(ティーポットの注ぎ口が左、取っ手が右)
- ろくろで回せるような丸いもの →
- アニメーション は
t∈ [0, 1)で行います。動くパーツはk.group({ about, rotate, at }, …)の中に入れ、k.wave()、k.pulse()、または360 * tで駆動します。すべての動きはt = 1で元の位置に戻らなければなりません。 - レンダリングと確認:
node <skill>/assets/render.mjs scene.js -o out/scene.svg --png out/scene.png --strip --thumbout/scene.png(静止画)とout/scene-frames.png(ループ全体の6フレーム)をReadツールで開いて確認してください。--gridを追加するとシートの座標を読み取れます。out/scene-128.pngも確認してください。128pxでも対象物が何か判別できなければなりません。 - 修正して再レンダリング。 確認項目:
- 一目で対象物とわかるか? まずシルエット。
- 描画順序が間違っていないか: 本体を通して部品が見える、遠くの部品が近くのものより手前に描かれる、中身が隠れたり突き抜けたりする。
references/kit.mdの「立体の順序付け」のルールで修正してください:立体を分離する、in:、on:、開口部から出ている部分だけをモデル化する、最後の手段としてlayerを使う。 - 部品はそれを支えるものに接触している。浮いていてはいけない(跳ねている蓋は除く)。
- 動きがフレームストリップで読み取れ、ループで途切れない。
- ラベルは余白に配置され、描画を必要以上に横切らない。
手順4〜5を正しくなるまで繰り返します(通常2〜4回)。最終出力は
--gridなしでレンダリングします。
静止画とフレームストリップを確認せずに納品してはいけません。 SVGパス(アニメーション画像)とPNGを納品し、PNGを表示してください。ユーザーがGIFを希望する場合や、SVGアニメーションを再生できない環境(チャットアプリ、一部のメールクライアント)に出力する場合は、--gif out/scene.gifを追加してください。
スタイルルール
- ひとつの見た目: インク+紙上のフラットなマーカートーン。グラデーション、グロー、ぼかし、不透明度のテクニック、SVGの
<text>や生のSVGは使わないでください。キットのみを使用します。 - 色: パレット名(
references/kit.mdを参照)、1枚あたり3〜5色+インク。パレット名が最も美しく見えます。16進数指定の色は柔らかくなります。明、中、暗のトーンは自動的に各塗りから生成されます。余分な立体で陰影を偽装しないでください。 - カメラ: デフォルトは等角投影です。
sheet.cameraを変更するのは理由がある場合のみにしてください。ピッチを低く(20〜25)、ヨーを25〜35にすると、背の高いもの(塔、ボトル、ランプ、人など)に対してより親しみやすい「商品撮影」のような見え方になります。 - シンプルで塊感のある: 細かく複雑な立体よりも、いくつかのきれいな塊の方がよく見えます。小さな部品(つまみ、脚、ボタン)を追加することでリアルさが増します。
- 地面に接地: 影は自動的に生成されます。板、テーブル、受け皿の上に置かれたものは、その上に影を落とします。小さな物体の下に板、トレイ、土台、テーブルがあると、空間に固定されているように見えます。
- テキスト: オプションで左上に
k.title(...)、さらに3D点を指す短い小文字の余白メモ(k.label)を追加できます。ユーザーの言語で書いてください。描画の上にボックスや吹き出しは置かないでください。 - シート: デフォルトは1000×1000。ワイドなシーンには
export const sheet = { width: 1200, height: 900 }を使用します。--transparentで紙を透明にできます。 - アニメーションサイズ: デフォルトは16フレーム、12fps(約1.3秒のループ)。典型的なSVGのサイズは0.5〜1MBです。スローモーションには
sheet.fps = 8を使用します。framesは24以下にしてください。
シーンテンプレート
// 三脚台の上のティーポット。蓋が揺れます。
// パーツ 呼び出し サイズ / 位置 色
// 三脚台 cylinder r 120, h 10 wood
// 本体 lathe { smooth } r 55→94→50, h 118, on the trivet teal
// 注ぎ口 tube [14, 7] 本体の内側から左側へ teal
// 取っ手 tube 8 両端とも本体の内側、右側 teal
// 蓋+つまみ lathe + sphere 後ろ端を支点とするグループ内 teal, ochre
export const sheet = { seed: 3 };
export function draw(k, t = 0) {
k.cylinder([0, 0, 0], 120, 10, { fill: 'wood' });
k.group({ rotate: { z: 45 } }, () => { // 注ぎ口を画面左、取っ手を画面右に
k.lathe([0, 0, 10], [[55, 0], [78, 10], [94, 45], [90, 85], [68, 112], [50, 118]], { fill: 'teal', smooth: true });
k.tube([[-78, 0, 50], [-112, 0, 66], [-128, 0, 100], [-146, 0, 118]], [14, 7], { fill: 'teal' });
k.tube([[70, 0, 112], [112, 0, 108], [122, 0, 72], [84, 0, 40]], 8, { fill: 'teal' });
const hop = Math.max(0, k.wave(0, 2 * t)); // 0..1、1ループあたり2回の跳ね
k.group({ about: [44, 0, 128], rotate: { y: -18 * hop }, at: [0, 0, 16 * hop] }, () => {
k.lathe([0, 0, 127], [[52, 0], [50, 5], [30, 13], [8, 18], [0, 19]], { fill: 'teal', smooth: true });
k.sphere([0, 0, 152], 10, { fill: 'ochre' });
});
k.steam([-150, 0, 125], { h: 90, n: 2, spread: 18 });
});
k.label('rattling lid', { to: [0, 0, 140], side: 'right' });
}