Witch's Hut · pumpkin_patch.js
かぼちゃ畑
48×32 の床置きの小物で、上面に畝を切り手前の縁が濃い、耕した土の盛り床に、大きさの違う彫っていないかぼちゃが 3 つ、手のひら形の大きな葉、巻きひげのあるつるが伸びています。g.depth の中で手前のものから描くので、後ろのものに重なっても一つずつ輪郭線が保たれます。idle(12 コマ)は葉がそれぞれの拍子でそよぎ、巻きひげが巻いたりほどけたりします。grow(10 コマの一回再生)は真ん中のかぼちゃが緑のつぼみから大きく育ちます。harvested(12 コマ)は切った茎とくぼみが残り、小さなかぼちゃが 1 つだけあります。
6×
動くスプライト素材: Witch's Hut(itch.io)Vol.5・ハロウィンの小物 12 点・42 アニメ
冒頭のコメント。これが指示書です。サイズ、視点、光の向き、すべての変種が書かれています。
pumpkin_patch.js · 1–8
// Pumpkin patch — 48x32 floor prop, top-down 3/4, key light top-left.// A raised bed of tilled earth (furrowed top face, darker front lip) with three uncarved pumpkins of different sizes,// broad palmate leaves and curling vines with tendrils.// Variants:// idle 12f loop: the leaves rustle in a breeze (each on its own phase), the tendrils curl and uncurl// grow 10f once: the middle pumpkin swells from a green bud to full size, the leaves unfurl; ends on idle f0// harvested 12f loop: cut stem stumps and hollows where pumpkins were, one small pumpkin left, leaves rustle// Objects are drawn nearest-first inside g.depth so each one keeps its own ink outline against the ones behind it.最後の数行。submit でキャンバスの大きさと、描画エンジンが書き出す変種の一覧を指定します。
pumpkin_patch.js · 239–249
submit(PumpkinPatch, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: CX, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "grow", args: ["grow"], animate: { fps: 10, duration: N_GROW / 10, loop: false } }, { name: "harvested", args: ["harvested"] }, ],});プログラム全体を表示 (249 行)
pumpkin_patch.js · 249 行
// Pumpkin patch — 48x32 floor prop, top-down 3/4, key light top-left.// A raised bed of tilled earth (furrowed top face, darker front lip) with three uncarved pumpkins of different sizes,// broad palmate leaves and curling vines with tendrils.// Variants:// idle 12f loop: the leaves rustle in a breeze (each on its own phase), the tendrils curl and uncurl// grow 10f once: the middle pumpkin swells from a green bud to full size, the leaves unfurl; ends on idle f0// harvested 12f loop: cut stem stumps and hollows where pumpkins were, one small pumpkin left, leaves rustle// Objects are drawn nearest-first inside g.depth so each one keeps its own ink outline against the ones behind it.const W = 48, H = 32, GROUND = 29, CX = 24;const N_IDLE = 12, N_GROW = 10;const BED = { cx: 24, cy: 20, rx: 21, ry: 6.5 };const PUMP = ["r0", "r1", "r2", "r3", "g2"];const BUD = ["e0", "e1", "e2", "e3", "e3"];// ---- the bed ------------------------------------------------------------------------------------------------function bed(g) { const { cx, cy, rx, ry } = BED; g.part("bed", () => { // front lip of the raised bed (the earth wall facing the camera) g.field({ x: [cx - rx, cx + rx], y: [cy - ry, cy + ry + 3] }, (x, y) => { const nx = (x + 0.5 - cx) / rx; for (let d = 0; d <= 3; d++) { const ny = (y + 0.5 - d - cy) / ry; if (nx * nx + ny * ny <= 1) { if (d === 0) return null; // top face drawn below return nx < -0.55 ? "w1" : "w0"; } } return null; }); // top face: furrows running across, ridges catch the light, left side warmer g.field({ x: [cx - rx, cx + rx], y: [cy - ry, cy + ry] }, (x, y) => { const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry; const r2 = nx * nx + ny * ny; if (r2 > 1) return null; // furrows run across the bed, gently wavy so they read as hand-hoed earth, not planks const wav = Math.round(0.6 * Math.sin(x * 0.55 + Math.floor(y / 3) * 2.1)); const row = ((y + wav - Math.round(cy - ry) + 30) % 3); const lit = -0.6 * nx - 0.4 * ny; if (r2 > 0.8 && ny > 0.3) return chk(x, y) ? "w2" : "w1"; // crumbly near rim if (row === 0) return lit > 0.3 ? "w3" : "w2"; // ridge top if (row === 1) return "w1"; return lit > 0.35 && chk(x, y) ? "w1" : "w0"; // furrow (softened on the lit side) }); // clods and stones: countable marks for (const [x, y, k] of [[7, 19, "w3"], [11, 24, "n3"], [19, 25, "w3"], [30, 24, "n2"], [40, 20, "w2"], [43, 21, "w3"], [5, 21, "n3"], [36, 15, "w3"]]) g.pixel(x, y, k); });}// ---- pumpkin --------------------------------------------------------------------------------------------------// k: 0..1 growth (1 = ripe orange; small k = a green bud that ripens through the ramp swap point)function pumpkin(g, name, cx, base, rx, ry, lobes, opts) { const o = opts || {}; const ramp = o.green ? BUD : PUMP; const cy = base - ry; g.part(name, () => { g.field({ x: [Math.floor(cx - rx - 1), Math.ceil(cx + rx + 1)], y: [Math.floor(cy - ry - 1), Math.ceil(base)] }, (x, y) => { const nx = (x + 0.5 - cx) / rx, ny = (y + 0.5 - cy) / ry; // squat body with ribbed bumps along the silhouette const s = nx / Math.sqrt(Math.max(0.08, 1 - Math.min(0.95, ny * ny))); const lob = (Math.asin(Math.max(-1, Math.min(1, s))) / (Math.PI / 2) + 1) * lobes / 2; const fr = lob - Math.floor(lob); const bump = 1 + 0.07 * Math.sin(Math.PI * fr); if (nx * nx + ny * ny > bump * bump) return null; let i = litIndex(x, y, cx, cy, rx * bump, ry * bump, 5, 0.12); if (i < 0) i = 0; // each lobe is a little barrel: its left half catches more light if (fr < 0.38 && i < 4) i += 0; else if (fr > 0.62 && i > 0) i -= 1; // groove between lobes if ((fr < 0.14 || fr > 0.9) && Math.abs(ny) < 0.85) i = Math.max(0, i - 2); // dimple at the crown if (ny < -0.55 && Math.abs(nx) < 0.22) i = Math.max(0, i - 1); // specular on the upper-left lobe if (!o.green && rx > 4) { const sx = nx + 0.42, sy = ny + 0.42; if (sx * sx * 4 + sy * sy * 9 < 0.12) i = 4; } return ramp[Math.max(0, Math.min(4, i))]; }); }); // stem if (o.stem !== false) { const sx = Math.round(cx), sy = Math.round(cy - ry); g.part(name + "Stem", () => { if (rx >= 5) { g.pixel(sx, sy, "w1"); g.pixel(sx - 1, sy, "w2"); g.pixel(sx, sy - 1, "w2"); g.pixel(sx + 1, sy - 1, "w1"); g.pixel(sx + 1, sy - 2, "w3"); g.pixel(sx + 2, sy - 2, "w2"); } else { g.pixel(sx, sy, "w2"); g.pixel(sx + 1, sy - 1, "w3"); } }); }}// cut stem stump left in the bed after harvest, with a shallow hollow where the fruit satfunction hollow(g, name, cx, base, rx) { // the shallow dent the fruit left in the earth: dark far wall, lit near lip g.part(name, () => { g.ellipse({ x: [cx - rx, cx + rx], y: [base - 3, base] }, "w0", { fill: "w0" }); g.line([cx - rx + 2, base - 3], [cx + rx - 2, base - 3], "ink"); g.line([cx - rx + 1, base], [cx + rx - 1, base], "w3"); g.pixel(cx - rx + 1, base - 1, "w2"); g.pixel(cx + rx - 1, base - 1, "w1"); });}function stump(g, name, cx, base) { // the cut stem: a short woody stub with a pale fresh-cut face, a curl of vine still attached g.part(name, () => { g.pixel(cx, base, "w1"); g.pixel(cx + 1, base, "w0"); g.pixel(cx, base - 1, "w2"); g.pixel(cx + 1, base - 1, "w1"); g.pixel(cx, base - 2, "w4"); g.pixel(cx + 1, base - 2, "w3"); });}// ---- leaves ---------------------------------------------------------------------------------------------------// a palmate pumpkin leaf seen from above-front: 5 rounded lobes, three veins, lit top-leftfunction leaf(g, name, cx, cy, R, ang, scale, wilt) { const r = R * scale; if (r < 0.9) return; const ry = 0.72; // foreshortened by the 3/4 view g.part(name, () => g.field({ x: [Math.floor(cx - r - 1), Math.ceil(cx + r + 1)], y: [Math.floor(cy - r - 1), Math.ceil(cy + r + 1)] }, (x, y) => { const dx = x + 0.5 - cx, dy = (y + 0.5 - cy) / ry; const rho = Math.hypot(dx, dy) / r; const th = Math.atan2(dy, dx) - ang; const edge = 0.86 + 0.14 * Math.cos(5 * th); const notch = Math.cos(th) < -0.9 ? 0.55 : 1; // the stalk notch at the base if (rho > edge * notch) return null; // veins from the base if (r >= 3 && rho < 0.85) for (const va of [0, 1.15, -1.15]) { let d = th - va; d = Math.atan2(Math.sin(d), Math.cos(d)); if (Math.abs(d) * rho * r < 0.5 && rho > 0.15) return "e1"; } const l = (-0.65 * dx - 0.55 * dy * ry) / r; if (wilt && rho > 0.62) return l > 0 ? "g1" : "w2"; // late-season leaf: browned, curling edge // the leaf is a shallow cup: its far rim is lit, its near half falls in shade return l > 0.25 ? "e3" : l > -0.25 ? "e2" : rho > 0.55 ? "e1" : "e2"; }));}// ---- vines and tendrils -------------------------------------------------------------------------------------function vine(g, name, path) { g.part(name, () => g.sweep(path, { w0: 0.7, w1: 0.45 }, (u, v) => (v < -0.3 ? "e2" : "e1")));}function tendril(g, x, y, dir, turns, curl, key) { // a spiral that tightens toward its tip; curl (0..1) winds it up for (let s = 0; s <= 1.001; s += 0.08) { const rad = 2.6 * (1 - s * 0.7); const a = dir * (s * TAU * turns * (0.65 + 0.35 * curl)) - Math.PI / 2; g.pixel(Math.round(x + dir * s * 2 + Math.cos(a) * rad - Math.cos(-Math.PI / 2) * 2.6), Math.round(y + Math.sin(a) * rad * 0.8 + 2), s > 0.7 ? "e3" : key); }}// ---- scene ------------------------------------------------------------------------------------------------------// leaf table: [cx, cy, R, ang, phase, depthOrder] (bigger y = nearer)const LEAVES = [ [6, 15, 4.6, -2.3, 0.0], [17, 13, 4.2, -1.6, 0.3], [33, 13, 4.6, -1.2, 0.55], [42, 16, 4.0, -0.6, 0.8], [20, 25, 3.8, 1.9, 0.15], [31, 24, 4.4, 1.2, 0.65], [43, 23, 3.4, 0.6, 0.4],];function scene(g, t, mode, grow) { const gk = grow === undefined ? 1 : grow; // growth of the middle pumpkin (1 = ripe) const unf = grow === undefined ? 1 : 0.35 + 0.65 * Math.min(1, grow * 1.25); const harvested = mode === "harvested"; contactShadow(g, CX, GROUND, 21, 1); // layer depths: nearer = more negative; objects are drawn front-to-back const Z = (y) => -Math.round(y); // front leaves first (they overlap the pumpkins) LEAVES.forEach(([x, y, R, a, ph], i) => { if (y < 20) return; const sw = 0.22 * Math.sin(TAU * (t + ph)); const s = harvested ? (i === 4 ? 0.7 : 0.9) : (i === 4 ? unf : 1); g.depth(Z(y + 6), () => g.outlined("ink", () => leaf(g, "leafF" + i, x, y + Math.round(sw * 0.6), R, a + sw, s, harvested && i !== 6))); }); if (!harvested) { // big pumpkin, front left g.depth(Z(27), () => g.outlined("ink", () => pumpkin(g, "pumpkinBig", 12, 27, 7.5, 5.2, 5))); // middle pumpkin, back centre — it is the one that grows const k = gk; const rx = 2 + 4.6 * k, ry = 1.6 + 3.6 * k; g.depth(Z(22), () => g.outlined("ink", () => pumpkin(g, "pumpkinMid", 26, 21, rx, ry, k > 0.5 ? 5 : 3, { green: k < 0.45, stem: true }))); } else { g.depth(Z(27), () => g.outlined("ink", () => stump(g, "stumpBig", 12, 24))); g.depth(Z(22), () => g.outlined("ink", () => stump(g, "stumpMid", 26, 19))); g.depth(Z(11), () => { hollow(g, "hollowBig", 12, 26, 6); hollow(g, "hollowMid", 26, 21, 4); }); } // small pumpkin, right (the one left after the harvest) g.depth(Z(25), () => g.outlined("ink", () => pumpkin(g, "pumpkinSmall", 37, 25, 4.6, 3.4, 3))); // vines: thin runners lying on the earth (no outline; dark green reads against the brown) g.depth(Z(14), () => { vine(g, "vineL", [[8, 22], [4, 20], [4, 17], [7, 15]]); vine(g, "vineM", [[17, 22], [20, 24], [24, 24], [28, 22], [31, 20]]); vine(g, "vineR", [[31, 20], [35, 18], [39, 18], [43, 19]]); vine(g, "vineB", [[22, 18], [20, 15], [16, 14]]); }); // back leaves LEAVES.forEach(([x, y, R, a, ph], i) => { if (y >= 20) return; const sw = 0.2 * Math.sin(TAU * (t + ph)); const s = harvested ? (i === 1 ? 0.75 : 0.95) : (i === 1 || i === 2 ? unf : 1); g.depth(Z(y), () => g.outlined("ink", () => leaf(g, "leafB" + i, x, y + Math.round(sw * 0.6), R, a + sw, s, harvested && (i === 0 || i === 2)))); }); g.depth(Z(10), () => g.outlined("ink", () => bed(g))); // tendrils: thin spirals drawn on top, no outline g.depth(-60, () => g.detached("tendrils", () => { const c1 = 0.5 + 0.5 * Math.sin(TAU * t), c2 = 0.5 + 0.5 * Math.sin(TAU * (t + 0.5)); tendril(g, 3, 12, 1, 1.1, c1, "e2"); tendril(g, 44, 12, -1, 1.0, c2, "e2"); if (!harvested) tendril(g, 22, 21, -1, 0.9, c2, "e2"); }));}const PumpkinPatch = sprite(pal, (g, t, mode) => { t = ((t % 1) + 1) % 1; if (mode === "grow") { const f = Math.min(N_GROW - 1, Math.round(t * N_GROW)); const p = f / (N_GROW - 1); const k = 1 - Math.pow(1 - p, 2.2); // fast swell, settles into full size // a small overshoot near the end gives the swell some weight, back to exactly 1 on the last frame const ko = f === N_GROW - 2 ? 1.06 : f === N_GROW - 1 ? 1 : k; scene(g, 0, "idle", ko); return; } const f = Math.round(t * N_IDLE) % N_IDLE; scene(g, f / N_IDLE, mode);});submit(PumpkinPatch, { size: [W, H], origin: "top-left", kind: "icon", scale: 1, animate: { fps: 10, duration: N_IDLE / 10, loop: true }, anchor: { x: CX, y: GROUND }, args: ["idle"], variants: [ { name: "idle", args: ["idle"] }, { name: "grow", args: ["grow"], animate: { fps: 10, duration: N_GROW / 10, loop: false } }, { name: "harvested", args: ["harvested"] }, ],});掲載されたコードでは、ビルドスクリプトが自動付与する共通部分を省略しています。具体的には 1 行のランタイムヘッダー、共有パレット、および汎用ヘルパー関数(contactShadow、litIndex、flame、hash、chk、TAU)です。表示されている行番号は実際のファイルと一致しています。これらのプログラムは AIエージェントが生成し、描画されたコマの仕上がりを確認しながら修正を加えたものです。
エージェントに、こうしたプログラムを書かせる。
Tessarune は描画 API のリファレンス、25 冊の制作マニュアル、結果を確かめるための描画エンジンをエージェントに渡します。